diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,26 @@
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [PVP](https://pvp.haskell.org/) versioning.
 
+## [0.2.0.1] - 2026-08-13
+
+### Changed (multi-package split)
+- hanalyze is now a multi-package project. Published packages: `hanalyze`
+  (umbrella — re-exports every module, existing imports keep working
+  unchanged), `hanalyze-core` / `-frame` / `-bayes` / `-models` / `-design` /
+  `-viz`, and `hanalyze-cli`. All share version 0.2.0.1.
+- The `Hanalyze.*.Plot` integration layer moved from the umbrella's
+  `plot-integration` cabal flag to its own package `hanalyze-plot`
+  (published separately, after the hgg 0.2 release). The `plot-integration`
+  flag no longer exists; depend on `hanalyze-plot` instead.
+- Version compatibility: hanalyze 0.2.0.1+ pairs with hgg 0.2.x
+  (`hanalyze-plot` 0.2.0.1 / `hgg-analyze-bridge` 0.2).
+
+### Fixed
+- Test-suite `other-modules` completeness (HBMSummarySpec, Stat.MCMCSpec,
+  Stat.SummarySpec are now compiled into the test build).
+- Documentation tree is English-default (`docs/**/*.md` English,
+  `*.ja.md` Japanese originals).
+
 ## [0.2.0.0] - 2026-07-18
 
 ### Added (NUTS streaming callback for live MCMC progress)
@@ -141,18 +161,20 @@
 candidate and never published; the multi-output GP API was rearranged
 before publication — see below.)
 
-### Multi-output GP — API のデフォルトを shared-HP に変更
-- `Hanalyze.Model.MultiGP.fitMultiGP` / `fitMultiGPMV` の **挙動を sklearn 流
-  shared-HP 版に置き換え**。1 回の HP 最適化で全 q 出力の合算周辺尤度を
-  最大化し、`Ky = K + σ_n² I` の Cholesky を再利用する (RBF 専用、
-  `q > 1` で旧版比 ~q× 速い)。
-- 旧来の per-output 独立 HP 版 (任意カーネル対応) は
-  `fitMultiGPIndep` / `fitMultiGPMVIndep` に **改名**。
-- 旧 `fitMultiGPMVSharedHP` は新しい `fitMultiGPMV` に統合済 (削除)。
-- 既存ユーザーは `fitMultiGP kern ...` を `fitMultiGPIndep kern ...` に
-  置き換えれば従来の挙動を維持できる。
+### Multi-output GP — default API switched to shared hyperparameters
+- `Hanalyze.Model.MultiGP.fitMultiGP` / `fitMultiGPMV` **now use the
+  sklearn-style shared-HP behaviour**: a single hyperparameter optimisation
+  maximises the summed marginal likelihood of all q outputs and reuses the
+  Cholesky factor of `Ky = K + σ_n² I` (RBF only; ~q× faster than the old
+  version for `q > 1`).
+- The previous per-output independent-HP version (arbitrary kernels) was
+  **renamed** to `fitMultiGPIndep` / `fitMultiGPMVIndep`.
+- The old `fitMultiGPMVSharedHP` was merged into the new `fitMultiGPMV`
+  (removed).
+- Existing users can keep the old behaviour by replacing
+  `fitMultiGP kern ...` with `fitMultiGPIndep kern ...`.
 
-### LM diagnostics + Taguchi/Quality 拡張
+### LM diagnostics + Taguchi/Quality extensions
 - `Hanalyze.Model.LM.Diagnostics` (new module): inference and residual diagnostics
   for OLS — `ciTValue`, `lmStdErrors[Multi]`, `CoefStats` /
   `lmCoefStats[Multi]` (SE / t / two-sided p), `FStat` / `lmFStatistic`
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 BSD 3-Clause License
 
-Copyright (c) 2026, Toshiaki Honda
+Copyright (c) 2026, Aelysce Project (Toshiaki Honda)
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
diff --git a/README.ja.md b/README.ja.md
new file mode 100644
--- /dev/null
+++ b/README.ja.md
@@ -0,0 +1,379 @@
+# hanalyze
+
+> 🌐 [English](README.md) | **日本語**
+
+[![License: BSD-3](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](LICENSE)
+[![GHC](https://img.shields.io/badge/GHC-9.6.7-blueviolet.svg)](https://www.haskell.org/ghc/)
+
+**hanalyze** は Haskell-native な統計解析エンジニアリング基盤です。回帰 / GLMM / ベイズ推論 (HMC/NUTS/Gibbs/ADVI/SMC) / ガウス過程 / 機械学習 (SVM / gradient boosting / ニューラルネット) / 生存解析 (KM / Cox / AFT / 競合リスク) / 時系列 (ARIMA / GARCH / 状態空間) / 因果探索 (LiNGAM) と処置効果推定 / 実験計画 (古典的 + カスタム最適設計) / 多目的最適化 / native plotting / HTML レポートを 1 つの API で統合します。
+モデリング・最適化の主要ロジックは Haskell で実装し、線形代数計算は hmatrix/BLAS/LAPACK を利用します。**R/Stan/Python ブリッジは不要**。
+ベンチマーク済みの範囲では Python/R 実装と概ね同等の結果を確認しています。性能は領域により異なり、最適化系や中小規模 MCMC では速いケースが多い一方、大規模 ML/GLM では sklearn より遅いケースがあります (詳細は下記)。
+
+---
+
+## 特徴
+
+- **Haskell-native**: 型で dtype/API の取り違えを減らし、必要な形状チェックは実行時に行う
+- **算法は Haskell で実装、数値計算は BLAS**: 線形代数は hmatrix/BLAS/LAPACK 経由。R/Stan/Python ブリッジ不要
+- **Native plotting**: [hgg](https://github.com/frenzieddoll/hgg) 統合で 90+ の図種を実装 (別パッケージ `hanalyze-plot`、`cabal build --project-file=cabal.project.plot` でビルド) — 純 Haskell SVG 出力、ブラウザ不要 ([Gallery](#gallery) 参照)
+- **HTML レポート統合**: MathJax/Mermaid + Vega-Lite 可視化を 1 関数で生成。PNG/SVG 出力は対応プロットで利用可
+- **汚いデータ防衛**: 8 種類の警告 + 自動推論 (delim/header/encoding) + クリーニング DSL
+- **Hackage `dataframe`**: Polars-like DF を直接利用。CSV ネイティブ、Parquet/JSON は `dataframe` 経由
+
+---
+
+## Gallery
+
+下のすべての図 (および [`docs/`](docs/) に 90+ の図) は解析結果から hgg
+統合を通じて直接生成されます — 純 Haskell、SVG 出力。
+
+| | |
+|:--:|:--:|
+| ![Linear regression with CI band](docs/images/lm-scatter-ci.svg)<br>線形回帰 — fit + 95% CI ([docs](docs/regression/01-lm.md)) | ![HBM MCMC dashboard](docs/images/hbm-dashboard.svg)<br>ベイズ MCMC dashboard — trace / density / R̂ / ESS ([docs](docs/bayesian/viz-diagnostics.md)) |
+| ![Gaussian process mean and credible band](docs/images/gp-mean-ci.svg)<br>ガウス過程 — mean + credible band ([docs](docs/regression/04-gp.md)) | ![Kernel SVM decision boundary](docs/images/svm-rbf-boundary.svg)<br>Kernel SVM (RBF) — decision boundary + support vectors ([docs](docs/ml/usage-ml-extensions.md)) |
+| ![DOE prediction profiler](docs/images/doe-profiler.svg)<br>DOE prediction profiler — response vs 各因子 + CI ([docs](docs/api-guide/09-doe.md)) | ![RSM 3D response surface](docs/images/rsm-surface-3d.svg)<br>RSM response surface (3D) ([docs](docs/doe/01-doe.md)) |
+| ![DirectLiNGAM causal DAG](docs/images/lingam-dag.svg)<br>DirectLiNGAM 因果探索 — estimated DAG ([docs](docs/api-guide/08-causal.md)) | ![Kaplan-Meier survival curves](docs/images/km-survival.svg)<br>Kaplan-Meier 生存曲線 ([docs](docs/regression/10-survival.md)) |
+| ![Time-series forecast](docs/images/ts-forecast.svg)<br>時系列 forecast ([docs](docs/regression/09-timeseries.md)) | ![k-means clusters with 95% ellipses](docs/images/kmeans-ellipse.svg)<br>k-means clusters + 95% ellipses ([docs](docs/stat/05-cluster.md)) |
+
+---
+
+## できること
+
+機能はジャンル別に整理し、**詳細は各ジャンルの docs と package README へ委譲**している。
+全体の目次は [`docs/README.ja.md`](docs/README.ja.md)、API の網羅的な辞書は
+[`docs/api-guide/`](docs/api-guide/README.md) (12 章) を参照。
+
+| ジャンル | 主なもの | 使い方 | API |
+|---|---|---|---|
+| 統計推測 | 仮説検定 12 種・多重比較補正・Bootstrap CI・効果量 + Power・交差検証 | [stat/](docs/stat/) | [10 stat](docs/api-guide/10-stat.md) |
+| 回帰 | LM / GLM / GLMM / ロバスト / 分位点 / 罰則付き (Ridge〜SCAD) / スプライン / GAM / GP / RFF | [regression/](docs/regression/) | [02 regression](docs/api-guide/02-regression.md) |
+| 機械学習 | RandomForest / GBM / 決定木 / k-NN / Naive Bayes / SVM / MLP / MDS / PDP・ICE | [ml/](docs/ml/) | [05 ml](docs/api-guide/05-ml.md) |
+| 多変量 | PCA / PLS / RRR / CCA / 判別分析 / クラスタリング / FDA | [fda/](docs/fda/) | [04 multivariate](docs/api-guide/04-multivariate.md) |
+| 因果 | 傾向スコア / IPW / DR / CATE / LiNGAM 全 7 variant | [causal/](docs/causal/) | [08 causal](docs/api-guide/08-causal.md) |
+| ベイズ | HBM DSL (plate・階層) / MH・HMC・NUTS・Gibbs・ADVI / 収束診断 / 事後予測 | [bayesian/](docs/bayesian/) | [03 bayesian-hbm](docs/api-guide/03-bayesian-hbm.md) |
+| 時系列・生存 | AR / VAR / GARCH / カルマン / Kaplan-Meier / 競合リスク / AFT / Cox | [timeseries/](docs/timeseries/) | [06](docs/api-guide/06-timeseries.md) / [07](docs/api-guide/07-survival.md) |
+| 最適化 | Nelder-Mead / L-BFGS / DE / CMA-ES / NSGA-II / ベイズ最適化 / 拡張ラグランジュ | [optim/](docs/optim/) | — |
+| 実験計画 | 要因計画 / RSM / D・A・I・G 最適計画 / 直交表 / タグチ / Custom Design / 検出力 | [doe/](docs/doe/) | [09 doe](docs/api-guide/09-doe.md) |
+| データ I/O | CSV / Parquet / JSON 読込・クリーニング・整形 (`Data.Transform` / `Data.Wrangle`) | [io/](docs/io/) | [11 data](docs/api-guide/11-data.md) |
+| 可視化 | Vega-Lite ベースの図・統合 HTML レポート・HBM の DAG 描画 | [visualization/](docs/visualization/) | [12 plot](docs/api-guide/12-plot.md) |
+
+**統一エントリーポイント**: どのモデルも `df |-> spec` で当てはめ、`toPlot` で描ける。
+plot 連携は別 package `hanalyze-plot` にあり
+(`cabal build --project-file=cabal.project.plot`)。
+
+plot エコシステムとの版数対応:
+
+| hgg | hanalyze | 連携 package |
+|---|---|---|
+| 0.2.x | 0.2.0.1+ | `hanalyze-plot` 0.2.0.1 (analyze → plot) / `hgg-analyze-bridge` 0.2 (plot → analyze) |
+
+## インストール
+
+### 動作環境
+
+| 項目 | 要件 |
+|---|---|
+| GHC | **9.6.7** (全 package の `tested-with`) |
+| cabal | 3.14.2 以上 (3.16.1 で動作確認) |
+| BLAS / LAPACK | **必須**。`hmatrix` が要求する (Debian/Ubuntu: `libblas-dev liblapack-dev gfortran` / Arch: `blas lapack gcc-fortran`)。OpenBLAS は `--constraint='hmatrix +openblas'` |
+| Graphviz | 任意。`ModelGraphDot` の DOT 出力を画像化する場合のみ |
+
+### ライブラリとして使う
+
+本リポジトリは **10 package の multi-package 構成**で、まだ package として配布して
+いない。clone して自分の project の `cabal.project` に並べる。
+
+```bash
+git clone https://github.com/frenzieddoll/hanalyze
+```
+
+```cabal
+-- cabal.project
+packages: .
+          ./hanalyze/hanalyze
+          ./hanalyze/hanalyze-core
+          ./hanalyze/hanalyze-frame
+          ./hanalyze/hanalyze-bayes
+          ./hanalyze/hanalyze-models
+          ./hanalyze/hanalyze-design
+          ./hanalyze/hanalyze-viz
+```
+
+`build-depends` は **迷ったら `hanalyze` 一本**でよい (module 名は層を跨いでも
+変わらない)。依存を絞りたいときだけ層を直接指定する。**各 package の README に
+module 地図と単体利用の例**がある。
+
+| package | 役割 |
+|---|---|
+| [`hanalyze`](hanalyze/ARCHITECTURE.ja.md) | 全層を再輸出する umbrella (通常はこれ) |
+| [`-core`](hanalyze-core/README.ja.md) | 記述統計・検定・最適化・数値核 |
+| [`-frame`](hanalyze-frame/README.ja.md) | DataFrame 連携・読込・整形・Fit API |
+| [`-models`](hanalyze-models/README.ja.md) | 回帰・機械学習・時系列・生存・因果 |
+| [`-bayes`](hanalyze-bayes/README.ja.md) | MCMC・HBM |
+| [`-design`](hanalyze-design/README.ja.md) | 実験計画 |
+| [`-viz`](hanalyze-viz/README.ja.md) | Vega-Lite 可視化・HTML レポート |
+| [`-plot`](hanalyze-plot/README.ja.md) | hgg 連携 (`toPlot`)。別 build root |
+| [`-cli`](hanalyze-cli/README.ja.md) | `hanalyze` コマンド |
+| [`-demos`](hanalyze-demos/README.ja.md) | demo / bench の exe 群 |
+
+### opt-in の build root
+
+既定の `cabal.project` は plot 非依存。用途に応じて root を切り替える。
+
+| build root | 含むもの |
+|---|---|
+| `cabal.project` (既定) | library + test (plot 非依存) |
+| `cabal.project.plot` | 上記 + `hanalyze-plot` (sibling の hgg が必要) |
+| `cabal.project.demos` | 上記 + demo / bench の exe 群 |
+
+### CLI だけ使う
+
+```bash
+cabal install hanalyze-cli    # hanalyze コマンドが入る
+```
+
+## クイックスタート
+
+### 30 秒で動かす CLI
+
+```bash
+git clone https://github.com/frenzieddoll/hanalyze
+cd hanalyze
+
+# price と promo で sales を回帰し、HTML レポートを出力。
+cabal run hanalyze -- regress data/readme/sales.csv "price promo" sales --report sales.html
+# β₀=185.05  β(price)=-4.37  β(promo)=+32.29  R²=0.995
+```
+
+`data/readme/sales.csv` は本リポジトリ同梱の 20 行デモ CSV (`price`, `promo`,
+`sales`)。生成される `sales.html` には係数表・診断・対話的予測ウィジェット
+までが入っており、コマンド 1 本でそのまま共有できます。
+
+### 30 秒で動かす Haskell API
+
+```haskell
+import qualified Hanalyze.Stat.Test as ST
+import qualified Numeric.LinearAlgebra as LA
+
+main = do
+  let xs = LA.fromList [12, 14, 13, 15, 17, 11]
+      ys = LA.fromList [18, 22, 20, 19, 25, 17]
+      result = ST.tTestWelch xs ys ST.TwoSided
+  print (ST.trPValue result, ST.trEffect result)
+  -- (1.688e-3, Just ("Cohen's d", -2.527))
+```
+
+詳しい入門は [docs/01-quickstart.ja.md](docs/01-quickstart.ja.md) 参照。
+
+---
+
+## CLI ツール
+
+```
+hanalyze help                     subcommand 一覧
+hanalyze regress <file> <x> <y>   LM/GLM/GP/HBM 等の回帰 + HTML レポート
+hanalyze info <file>              列ごとの型/統計
+hanalyze hist <file> <col>        ヒストグラム + 理論 PDF 重ね描き
+hanalyze ridge <file> ...         正則化回帰 (Ridge/Lasso/EN)
+hanalyze kernel <file> ...        カーネル回帰 (NW/KR/RFF) + 多次元入力
+hanalyze spline <file> ...        スプライン回帰
+hanalyze multireg <file> ...      多出力回帰 + 対話的 HTML
+hanalyze melt <file> ...          long-form 変換
+hanalyze regrid <file> ...        time-axis grid 揃え
+hanalyze doe ortho <NAME> -f ...  直交表生成
+hanalyze taguchi sn / analyze     タグチメソッド
+hanalyze clean <file> --rule ...  汚いデータのクリーニング
+```
+
+各コマンドの詳細フラグは `hanalyze <cmd> --help`、または [docs/01-quickstart.ja.md](docs/01-quickstart.ja.md) 参照。
+
+---
+
+## サンプル / デモ
+
+`hanalyze-demos/demo/` 配下に多数のデモ (このリリース時点で 76)。代表例:
+
+| デモ | 概要 |
+|---|---|
+| `hanalyze-demos/demo/regression/HBMRegressionDemo.hs` | HBM ベイズ線形回帰 + NUTS + HTML |
+| `hanalyze-demos/demo/regression/RFFDemo.hs` | RFF で大規模 GP の高速近似 |
+| `hanalyze-demos/demo/regression/RobustGPDemo.hs` | StudentT 観測尤度の頑健 GP |
+| `hanalyze-demos/demo/doe-optim/NSGADemo.hs` | ZDT 問題で NSGA-II + Pareto |
+| `hanalyze-demos/demo/doe-optim/BayesOptDemo.hs` | Branin/Hartmann6 で BO |
+| `hanalyze-demos/demo/bayesian/HBMComparisonDemo.hs` | WAIC/LOO で HBM 比較 |
+| `hanalyze-demos/demo/bayesian/SimpsonParadoxDemo.hs` | 階層モデルでパラドックスを解明 |
+| `hanalyze-demos/demo/io/DirtyDataDemo.hs` | 19 種の汚い CSV を自動防衛 |
+
+実行: `dist-newstyle/build/x86_64-linux/ghc-9.6.7/hanalyze-demos-0.2.0.1/x/<demo-name>/build/<demo-name>/<demo-name>` で起動。
+
+---
+
+## hanalyze が刺さる場所
+
+Python/R を全面置換するのではなく、Haskell 統合・単一バイナリ CLI・レポート
+統合が効くワークフローを主戦場に据えています。
+
+**刺さる**
+
+- Haskell ネイティブのパイプラインで、Python を呼ばずに統計/ベイズ/最適化を完結させたい
+- 単一バイナリで配布したい (`hanalyze` 1 本で動く、Python venv 不要)
+- 汚い CSV の防衛 + クリーニング + 解析を 1 ワークフローで済ませたい
+- DoE / タグチ / 直交表で製造業・実験系を回したい
+- 解析結果をそのまま HTML レポート化して共有したい
+- 型安全な解析パイプラインで dtype/API のミスを早期に潰したい
+
+**正面勝負を避ける**
+
+- 大規模 DataFrame 処理 (pandas / polars / data.table を使う)
+- GPU 深層学習 (PyTorch / JAX を使う)
+- scikit-learn の成熟したモデル群すべて
+- Stan / PyMC の MCMC 診断エコシステム全体
+- ggplot2 の表現力すべて
+
+---
+
+## Python との比較
+
+> R は機能対応のみ。数値ベンチは Python に対してのみ実施しています。
+
+下の数値は `bench/results/{haskell,python}/*.csv` の最新ラン。
+ベンチ条件 (`OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1`、single-thread、
+固定 seed) は [bench/results/SUMMARY.md](bench/results/SUMMARY.md) 参照。
+
+| 領域 | このベンチでの結果 |
+|---|---|
+| **単目的最適化** (DE/CMAES/L-BFGS/NM) | scipy より速いケースが多い (Rosenbrock_2D/DE 134×、Ackley/CMAES 49×、Griewank/CMAES 54×)。Sphere_30D/L-BFGS の目的関数値はこの実行で 8.1e-40 vs scipy 2.6e-11 |
+| **多目的最適化** (NSGA-II) | ZDT/DTLZ 系で同等〜やや有利 (DTLZ2_3 1.43× 速、ZDT1/2/3 は pymoo と ±5%)。HV/IGD は概ね pymoo と同等以上 |
+| **ベイズ最適化** (BO) | Branin で同等 (1.15×)、Hartmann6 はこの実行で -3.07 vs skopt -2.77 |
+| **シミュレーテッドアニーリング** (Tsallis SA) | 同等。Rastrigin_10D はこの実行で 0.0 (scipy `dual_annealing` 7.8e-14) |
+| **古典回帰** (LM/Ridge/Lasso/GLMM) | ベンチケースでは概ね同等。LME はこの実行で statsmodels 比 30× 速 |
+| **大規模 GLM/Lasso** (n ≥ 10k) | sklearn より現状遅い (3-5×、Cython inner loop に追従不能) |
+| **カーネル/GP** | sklearn より現状遅い (2.5-4.7×) |
+| **ベイズ MCMC** (NUTS/HMC) | 8-schools で ESS 839 (blackjax 810 と同等品質)。PyMC 比 7.4× 速、blackjax 比 2.8× 遅 (JAX-JIT 構造差) |
+| **HBM (確率プログラミング)** | 多相 DSL で一部の PyMC 風モデリング機能 + 一部の分布 (Truncated/Censored/MvNormal/LKJ/...) を提供 |
+| **VI / WAIC / LOO** | ADVI は小規模 logistic で numpyro SVI より 3.0× 速、LOO は arviz より 2.9× 速 (ベンチケースでは) |
+| **仮説検定 / Bootstrap / k-fold** | Welch t-test 39×、KS 11×、k-fold 2.2× 速 vs scipy/sklearn (ベンチケースでは) |
+| **時系列 / Spline / GAM** | ARIMA 128× 速、Spline PCHIP 互角、GAM はベンチケースで pygam の 1.6× 遅 |
+| **生存解析** (KM/Cox PH) | lifelines と概ね同等 |
+| **多出力回帰 / Regrid** | MultiLM 2.3× 速、`regridLong` は pandas+scipy 自前合成版より 20× 速 |
+| **可視化** | hvega 経由の Vega-Lite (grammar-of-graphics 系)。HTML レポート同梱 |
+
+機能対応表は [docs/comparison/python-r.ja.md](docs/comparison/python-r.ja.md)、数値詳細は [bench/results/SUMMARY.md](bench/results/SUMMARY.md) を参照。
+
+---
+
+## ベンチマークハイライト
+
+代表的な結果。各項目は **1 つのベンチ設定** での値で、絶対値は反復数・seed・
+許容誤差に依存します。条件は SUMMARY を参照。
+NUTS はさらに posteriordb reference posteriors に対して検証されています
+([bench/posteriordb/](bench/posteriordb/) 参照)。
+
+- **NUTS 8-schools** (warmup 500, samples 1000): hanalyze 1492 ms / ESS(mu) 839 vs blackjax 530 ms / ESS 810 (この実行)
+- **Holt-Winters seasonal n=500 p=12**: hanalyze 0.19 ms vs statsmodels MLE 96 ms (この実行。なお hanalyze は固定 α=0.3 closed-form、statsmodels は MLE fit)
+- **Sphere_30D/DE**: このベンチでは hanalyze 1.0e-26、scipy 2.8e-5
+- **Sphere_30D/L-BFGS**: このベンチでは hanalyze 8.1e-40、scipy 2.6e-11
+- **Rastrigin_10D/SA**: この実行で hanalyze 0.0、scipy `dual_annealing` 7.8e-14
+- **Hartmann6/BO**: この実行で hanalyze -3.07、skopt -2.77
+- **DTLZ2_3/NSGA-II**: hanalyze 528 ms vs pymoo 758 ms (この実行で 1.43× 速)
+- **DE Rosenbrock_2D**: hanalyze 1.2 ms vs scipy 164 ms (この実行で 134× 速)
+- **Constrained Quad2D (eq)**: hanalyze 0.062 ms vs scipy SLSQP 0.69 ms (この実行)
+- **regridLong (jagged long-form)**: hanalyze 0.99 ms vs pandas+scipy 合成 19.4 ms (この実行)
+
+再現: `OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 cabal run bench-{regression,kernel,optim,mo,bo,mcmc-b7,mcmc-extras,ts-extras,optim-plus,stat-util,multi-output,regrid}`、続いて Python 側を `bench/python/bench_*.py` で実行 (詳細 [bench/README.md](bench/README.md))。
+
+---
+
+## アーキテクチャ
+
+```mermaid
+graph TD
+  IO[DataIO.* CSV/Parquet/JSON]
+  IO --> DF[Hackage dataframe]
+  DF --> Models[Model.* 回帰/ML/ベイズ/時系列/生存]
+  DF --> Stat[Stat.* 検定/CV/効果量/解釈]
+  Models --> Optim[Optim.* 最適化]
+  Models --> MCMC[MCMC.* サンプラー]
+  Models --> Viz[Viz.* HTML/PNG/SVG]
+  Stat --> Viz
+  MCMC --> Viz
+  Optim --> Design[Design.* DOE/タグチ]
+```
+
+**全モジュール Hackage `dataframe` を直接やり取り**。独自 DataFrame.Core は廃止済。
+
+---
+
+## ロードマップと API 安定性
+
+- **Stable** (minor 更新で API 後方互換を維持予定): `Hanalyze.DataIO.*`、`Hanalyze.Stat.{Test, Bootstrap, MultipleTesting, ClassMetrics, CV, Effect, Distribution}`、`Hanalyze.Model.{LM, GLM, Spline, Regularized, RandomForest, DecisionTree, TimeSeries, Survival, GAM}`、`Hanalyze.Optim.{NelderMead, LBFGS, DifferentialEvolution, CMAES, NSGA, BayesOpt, SimulatedAnnealing, ParticleSwarm}`、`Hanalyze.Design.*`、`Hanalyze.Viz.{Scatter, Bar, Histogram}`
+- **Experimental** (API は変動の可能性): `Hanalyze.Model.HBM` DSL、`Hanalyze.MCMC.NUTS` (mass-matrix adaptation は opt-in)、`Hanalyze.Stat.VI` (ADVI)、`Hanalyze.Model.{GP, RFF, GPRobust, GLMM}`、`Hanalyze.Model.{SVM, GradientBoosting, NeuralNetwork}`、`Hanalyze.Model.LiNGAM.*`、`Hanalyze.Design.Custom.*`、`df |-> spec` fit 演算子 (`Hanalyze.Fit`)、hgg 統合 (別パッケージ `hanalyze-plot`)、`Hanalyze.Viz.ReportBuilder`。挙動はベンチで検証済ですが型シグネチャが変わる可能性あり
+- **将来検討**: hmatrix/Massiv/Accelerate を切替えるバックエンド typeclass。実装スケジュールは未定。(以前計画していたトップレベル統一 API と fit 演算子 API は 0.2.0.0 で `module Hanalyze` と `Hanalyze.Fit` として実現済み。)
+
+---
+
+## モジュール構成
+
+module 名は package を跨いでも `Hanalyze.*` で一貫している (どの層に居るかを
+利用者が意識しなくてよい)。どの package に何があるかは
+[インストール](#インストール)の package 表と、各 package の README を参照。
+
+212 module / 2417 テスト例。
+
+---
+
+## ビルド
+
+```bash
+cabal build all                                    # library + test (plot 非依存)
+cabal test all                                     # hspec test suite
+cabal build all --project-file=cabal.project.plot  # + plot 連携 (sibling の hgg が必要)
+cabal build all --project-file=cabal.project.demos # + demo / bench の exe 群
+```
+
+主要依存: `hmatrix` (BLAS/LAPACK)・`hvega` (Vega-Lite)・`statistics`・`mwc-random`・
+`dataframe`・`massiv` (並列配列)・`ad` (自動微分)・`async`。
+
+GHC 9.6.7 + cabal 3.14.2 で確認済。
+
+---
+
+## ベンチマーク実行
+
+```bash
+# 1. 共通テストデータ生成 (固定 seed、deterministic)
+#    bench の exe は demos package にあるので build root を指定する
+cabal run --project-file=cabal.project.demos bench-data-gen
+
+# 2. Haskell 側
+OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 \
+  cabal run --project-file=cabal.project.demos \
+    bench-regression bench-kernel bench-optim bench-mo bench-bo
+
+# 3. Python 側 (venv 必要、bench/requirements.txt)
+OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 \
+  bench/venv/bin/python bench/python/bench_regression.py
+# (similarly for kernel, optim, mo, bo)
+
+# 4. 集約 (Markdown 表)
+bench/venv/bin/python bench/aggregate.py > bench/results/SUMMARY.md
+```
+
+---
+
+## 開発・貢献
+
+- **Issue / PR**: [github.com/frenzieddoll/hanalyze](https://github.com/frenzieddoll/hanalyze)
+- **テスト追加**: `test/Spec.hs` に hspec 形式で
+- **ベンチ追加**: `hanalyze-demos/bench/haskell/Bench*.hs` + 対応 Python 版
+- **コーディング規約**: `CONTRIBUTING.md` に詳細 (hot path で list 経由禁止、unsafe 最小限、等)
+
+---
+
+## ライセンス
+
+BSD-3-Clause License — 詳細は [LICENSE](LICENSE) を参照。
+
+## 著者
+
+Toshiaki Honda <frenzieddoll@gmail.com>
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 > 🌐 **English** | [日本語](README.ja.md)
 
-[![License: BSD-3](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/LICENSE)
+[![License: BSD-3](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/LICENSE)
 [![GHC](https://img.shields.io/badge/GHC-9.6.7-blueviolet.svg)](https://www.haskell.org/ghc/)
 
 **hanalyze** is a Haskell-native statistical engineering toolkit: regression, GLMM, Bayesian inference (HMC/NUTS/Gibbs/ADVI/SMC), Gaussian processes, machine learning (SVM / gradient boosting / neural networks), survival analysis (KM / Cox / AFT / competing risks), time series (ARIMA / GARCH / state space), causal discovery (LiNGAM) and treatment-effect estimation, design of experiments (classical + custom optimal design), multi-objective optimisation, native plotting, and HTML reporting integrated under one API.
@@ -15,7 +15,7 @@
 
 - **Haskell-native**: types catch many dtype/API mismatches; shape checks happen at runtime where needed
 - **Algorithms in Haskell, BLAS for numerics**: hmatrix/BLAS/LAPACK powers linear algebra; no R/Stan/Python bridge
-- **Native plotting**: 90+ documented figure types through the [hgg](https://github.com/frenzieddoll/hgg) grammar-of-graphics integration (`plot-integration` flag) — pure-Haskell SVG output, no browser required (see [Gallery](#gallery))
+- **Native plotting**: 90+ documented figure types through the [hgg](https://github.com/frenzieddoll/hgg) grammar-of-graphics integration (separate `hanalyze-plot` package, build with `cabal build --project-file=cabal.project.plot`) — pure-Haskell SVG output, no browser required (see [Gallery](#gallery))
 - **HTML reporting**: MathJax/Mermaid + Vega-Lite visualisations in one call; PNG/SVG export available for supported plots
 - **Dirty-data defence**: 8 warning codes + auto-sniff (delim/header/encoding) + cleaning DSL
 - **Hackage `dataframe`**: Polars-like DataFrame used directly; CSV native, Parquet/JSON support through `dataframe`
@@ -24,193 +24,114 @@
 
 ## Gallery
 
-Every figure below (and 90+ more across [`docs/`](https://github.com/frenzieddoll/hanalyze/tree/v0.2.0.0/docs/)) is generated straight
+Every figure below (and 90+ more across [`docs/`](https://github.com/frenzieddoll/hanalyze/tree/v0.2.0.1/docs/)) is generated straight
 from analysis results via the hgg integration — pure Haskell, SVG out.
 
 | | |
 |:--:|:--:|
-| ![Linear regression with CI band](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.0/docs/images/lm-scatter-ci.svg)<br>Linear regression — fit + 95% CI ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/01-lm.md)) | ![HBM MCMC dashboard](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.0/docs/images/hbm-dashboard.svg)<br>Bayesian MCMC dashboard — trace / density / R̂ / ESS ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/bayesian/viz-diagnostics.md)) |
-| ![Gaussian process mean and credible band](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.0/docs/images/gp-mean-ci.svg)<br>Gaussian process — mean + credible band ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/04-gp.md)) | ![Kernel SVM decision boundary](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.0/docs/images/svm-rbf-boundary.svg)<br>Kernel SVM (RBF) — decision boundary + support vectors ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/ml/usage-ml-extensions.md)) |
-| ![DOE prediction profiler](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.0/docs/images/doe-profiler.svg)<br>DOE prediction profiler — response vs each factor + CI ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/api-guide/09-doe.md)) | ![RSM 3D response surface](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.0/docs/images/rsm-surface-3d.svg)<br>RSM response surface (3D) ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/doe/01-doe.md)) |
-| ![DirectLiNGAM causal DAG](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.0/docs/images/lingam-dag.svg)<br>DirectLiNGAM causal discovery — estimated DAG ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/api-guide/08-causal.md)) | ![Kaplan-Meier survival curves](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.0/docs/images/km-survival.svg)<br>Kaplan-Meier survival curves ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/10-survival.md)) |
-| ![Time-series forecast](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.0/docs/images/ts-forecast.svg)<br>Time-series forecast ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/09-timeseries.md)) | ![k-means clusters with 95% ellipses](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.0/docs/images/kmeans-ellipse.svg)<br>k-means clusters + 95% ellipses ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/stat/05-cluster.md)) |
+| ![Linear regression with CI band](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.1/docs/images/lm-scatter-ci.svg)<br>Linear regression — fit + 95% CI ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/regression/01-lm.md)) | ![HBM MCMC dashboard](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.1/docs/images/hbm-dashboard.svg)<br>Bayesian MCMC dashboard — trace / density / R̂ / ESS ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/bayesian/viz-diagnostics.md)) |
+| ![Gaussian process mean and credible band](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.1/docs/images/gp-mean-ci.svg)<br>Gaussian process — mean + credible band ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/regression/04-gp.md)) | ![Kernel SVM decision boundary](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.1/docs/images/svm-rbf-boundary.svg)<br>Kernel SVM (RBF) — decision boundary + support vectors ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/ml/usage-ml-extensions.md)) |
+| ![DOE prediction profiler](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.1/docs/images/doe-profiler.svg)<br>DOE prediction profiler — response vs each factor + CI ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/api-guide/09-doe.md)) | ![RSM 3D response surface](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.1/docs/images/rsm-surface-3d.svg)<br>RSM response surface (3D) ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/doe/01-doe.md)) |
+| ![DirectLiNGAM causal DAG](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.1/docs/images/lingam-dag.svg)<br>DirectLiNGAM causal discovery — estimated DAG ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/api-guide/08-causal.md)) | ![Kaplan-Meier survival curves](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.1/docs/images/km-survival.svg)<br>Kaplan-Meier survival curves ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/regression/10-survival.md)) |
+| ![Time-series forecast](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.1/docs/images/ts-forecast.svg)<br>Time-series forecast ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/regression/09-timeseries.md)) | ![k-means clusters with 95% ellipses](https://raw.githubusercontent.com/frenzieddoll/hanalyze/v0.2.0.1/docs/images/kmeans-ellipse.svg)<br>k-means clusters + 95% ellipses ([docs](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/stat/05-cluster.md)) |
 
 ---
 
 ## Capabilities
 
-Features grouped by category. Each capability links to a usage doc and (where relevant) a theory doc.
-The full API reference lives in [`docs/api-guide/`](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/api-guide/README.md) (12 chapters).
-
-### Statistical inference (`Hanalyze.Stat.*`)
-
-| Feature | Module | Usage | Theory |
-|---|---|---|---|
-| 12 hypothesis tests (t/χ²/ANOVA/Wilcoxon/KS/Shapiro/Levene/Bartlett/...) | `Hanalyze.Stat.Test` | [stat/01-test.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/stat/01-test.md) | — |
-| Multiple-testing correction (Bonferroni/Holm/BH/BY) | `Hanalyze.Stat.MultipleTesting` | [stat/06-multipletesting.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/stat/06-multipletesting.md) | — |
-| Bootstrap CI / permutation tests | `Hanalyze.Stat.Bootstrap` | [stat/07-bootstrap.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/stat/07-bootstrap.md) | — |
-| Effect size + power analysis (Cohen's d/η²/Cramér V/n estimation) | `Hanalyze.Stat.Effect` | [stat/09-effect.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/stat/09-effect.md) | — |
-| Cross-validation (k-fold/stratified/LOO) + Grid search | `Hanalyze.Stat.CV` | [stat/04-cv.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/stat/04-cv.md) | — |
-
-### Regression (`Hanalyze.Model.*`)
-
-| Feature | Module | Usage | Theory |
-|---|---|---|---|
-| Formula DSL (declare models as `"y x = b0 + b1*x + bg ! group"` or R `"y ~ x + C(g)"`; `ModelFrame` / `designMatrixF` / `fitLMF` + missing policy / contrast `C(g, Sum)` / WLS `fitWLSF` / nonlinear `fitNLS` / random effects `(1+x|g)` via `fitMixedLME`/`fitMixedGLMM`) | `Hanalyze.Model.Formula` / `.Frame` / `.Design` / `.RFormula` / `.Nonlinear` / `.Mixed` | [regression/11-formula-dsl.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/11-formula-dsl.md) | — |
-| Linear regression (LM) + inference stats (SE/t/p, F, AIC/BIC, leverage, Cook's) | `Hanalyze.Model.LM` / `Hanalyze.Model.LM.Diagnostics` | [regression/01-lm.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/01-lm.md) | [principles/lm.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/principles/lm.md) |
-| GLM (Binomial / Poisson / Gaussian) | `Hanalyze.Model.GLM` | [regression/02-glm.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/02-glm.md) | [principles/glm.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/principles/glm.md) |
-| GLMM / mixed-effects model (LME) | `Hanalyze.Model.GLMM` | [regression/03-glmm.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/03-glmm.md) | [principles/glmm.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/principles/glmm.md) |
-| Spline regression (B-spline / NaturalCubic) | `Hanalyze.Model.Spline` | [regression/04-spline.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/04-spline.md) | [regression/theory-regression-extensions.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/theory-regression-extensions.md) |
-| Kernel regression (NW / Kernel Ridge) + multi-D inputs | `Hanalyze.Model.Kernel` | [regression/04-kernel.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/04-kernel.md) | same |
-| Regularised (Ridge / Lasso / ElasticNet) | `Hanalyze.Model.Regularized` | [regression/04-regularized.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/04-regularized.md) | same |
-| Robust regression (Huber / Tukey biweight M-estimators, IRLS) | `Hanalyze.Model.Robust` | [regression/usage-regularized-advanced.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/usage-regularized-advanced.md) | — |
-| Gaussian process (RBF / Matérn / Periodic + ARD + multi-input) | `Hanalyze.Model.GP` | [regression/04-gp.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/04-gp.md) | [principles/gp.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/principles/gp.md) |
-| Random Fourier Features (large-scale GP approximation) | `Hanalyze.Model.RFF` | [regression/04-rff.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/04-rff.md) | [regression/theory-regression-extensions.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/theory-regression-extensions.md) |
-| Multivariate regression / Multi-output GP | `Hanalyze.Model.{Multivariate,MultiGP,MultiOutput}` | [regression/05-multivariate.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/05-multivariate.md) | [regression/theory-multivariate.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/theory-multivariate.md) |
-| Quantile regression | `Hanalyze.Model.Quantile` | [regression/06-quantile.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/06-quantile.md) | [regression/theory-regression-extensions.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/theory-regression-extensions.md) |
-| Generalized additive model (GAM) | `Hanalyze.Model.GAM` | [regression/06-gam.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/06-gam.md) | same |
-| Random forest (regression) | `Hanalyze.Model.RandomForest` | [regression/06-randomforest.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/06-randomforest.md) | same |
-| Multi-output regression + interactive HTML | `Hanalyze.Model.MultiOutput` | [regression/07-multireg.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/07-multireg.md) | [regression/theory-multivariate.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/theory-multivariate.md) |
-| Partial Least Squares (PLS) regression — NIPALS + VIP + CV component selection | `Hanalyze.Model.PLS` | — | — |
-| Linear / Quadratic Discriminant Analysis (LDA / QDA) | `Hanalyze.Model.Discriminant` | — | — |
-| Gauge R&R (Measurement System Analysis, ANOVA-based crossed / nested) | `Hanalyze.Design.GaugeRR` | — | — |
-
-### Machine learning (`Hanalyze.Model.*` / `Hanalyze.Stat.*`)
+Features are organised by topic, with **the details delegated to the per-topic docs and
+the package READMEs**. The full index is [`docs/README.md`](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/README.md); the
+exhaustive API dictionary is [`docs/api-guide/`](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/api-guide/README.md) (12 chapters).
 
-| Feature | Module | Usage | Theory |
+| Topic | Main items | Guide | API |
 |---|---|---|---|
-| PCA + cumulative variance + standardisation | `Hanalyze.Model.PCA` | [stat/02-pca.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/stat/02-pca.md) | — |
-| Clustering (K-means + k-means++ + silhouette) | `Hanalyze.Model.Cluster` | [stat/05-cluster.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/stat/05-cluster.md) | — |
-| Decision tree (CART classifier) | `Hanalyze.Model.DecisionTree` | [regression/08-decisiontree.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/08-decisiontree.md) | — |
-| Kernel SVM (C-SVC, SMO dual solver) + CV hyperparameter tuning | `Hanalyze.Model.SVM` | [ml/usage-ml-extensions.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/ml/usage-ml-extensions.md) | — |
-| Gradient boosting (regression + binary classification) | `Hanalyze.Model.GradientBoosting` | [ml/usage-ml-extensions.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/ml/usage-ml-extensions.md) | — |
-| k-NN / Naive Bayes (Gaussian + Multinomial) / MLP neural network (mini-batch SGD + Adam) | `Hanalyze.Model.{KNN,NaiveBayes,NeuralNetwork}` | [ml/usage-ml-extensions.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/ml/usage-ml-extensions.md) + [api-guide/05-ml.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/api-guide/05-ml.md) | — |
-| Random forest classifier (+ permutation importance) | `Hanalyze.Model.RandomForestClassifier` | [api-guide/05-ml.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/api-guide/05-ml.md) | — |
-| MDS (classical / Sammon) | `Hanalyze.Model.MDS` | [ml/usage-ml-extensions.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/ml/usage-ml-extensions.md) | — |
-| Hierarchical clustering (agglomerative + dendrogram) | `Hanalyze.Model.HierarchicalCluster` | [stat/05-cluster.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/stat/05-cluster.md) | — |
-| Latent class analysis (EM) + graphical-lasso correlation network | `Hanalyze.Model.LatentClassAnalysis` / `Hanalyze.Stat.CorrelationNetwork` | [stat/usage-misc-stat.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/stat/usage-misc-stat.md) | — |
-| Functional data analysis (basis smoothing + FPCA) | `Hanalyze.Model.FDA` | [fda/usage-fda.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/fda/usage-fda.md) | — |
-| Time series (ARIMA / Holt-Winters / STL / ACF / PACF) | `Hanalyze.Model.TimeSeries` | [regression/09-timeseries.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/09-timeseries.md) | — |
-| GARCH(1,1) volatility / linear-Gaussian state space (Kalman filter + RTS smoother) / VAR(p) | `Hanalyze.Model.{GARCH,StateSpace,VAR}` | [timeseries/usage-ts-surv-advanced.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/timeseries/usage-ts-surv-advanced.md) | — |
-| Survival analysis (Kaplan-Meier / Nelson-Aalen / Log-rank / Cox PH) | `Hanalyze.Model.Survival` | [regression/10-survival.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/regression/10-survival.md) | — |
-| Parametric survival (AFT) + competing risks (CIF) | `Hanalyze.Model.{AFT,CompetingRisks}` | [api-guide/07-survival.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/api-guide/07-survival.md) | — |
-| Classification metrics (Confusion / AUC / F1 / MCC / log-loss / Brier) | `Hanalyze.Stat.ClassMetrics` | [stat/03-classmetrics.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/stat/03-classmetrics.md) | — |
-| Model interpretation (Permutation imp / PDP / ICE) | `Hanalyze.Stat.Interpret` | [stat/13-interpret.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/stat/13-interpret.md) | — |
-| SPC control charts (X̄-R / I-MR / p / np / c / u) + Western Electric / Nelson 8-rule sets | `Hanalyze.Stat.SPC` | — | — |
-| Weibull MLE (censored / uncensored) + B_p life + Wald CI | `Hanalyze.Model.Weibull` | — | — |
-| Accelerated-life models (Arrhenius / Eyring / Inverse Power Law) | `Hanalyze.Model.Reliability` | — | — |
-| NSGA-II all-fronts (rank ≥ 1 alternatives) + per-generation progress callback | `Hanalyze.Optim.NSGA` | — | — |
-| Good vs Bad parallel comparison (Welch t + Cohen's d ranking) | `Hanalyze.Stat.GroupComparison` | — | — |
-| Hotelling T² (1-/2-sample) + one-way MANOVA (Wilks' Λ + Rao F) | `Hanalyze.Stat.Test` | — | — |
-| Lasso/Ridge/ElasticNet λ auto-selection via k-fold CV + 1-SE rule | `Hanalyze.Model.Regularized` | — | — |
-| D-optimal Augment Design (sequential addition with fixed existing rows) | `Hanalyze.Design.Optimal` | — | — |
-| Space-filling designs (LHS / Maximin LHS / Halton) | `Hanalyze.Design.SpaceFilling` | — | — |
-| Definitive Screening Design (k=4 verified, others structural) | `Hanalyze.Design.DSD` | — | — |
-| Mixture design (Simplex Lattice / Simplex Centroid) | `Hanalyze.Design.Mixture` | — | — |
-| Sequential RSM (steepest ascent + next CCD placement) | `Hanalyze.Design.Sequential` | — | — |
-
-### Causal inference (`Hanalyze.Model.LiNGAM.*` / `Hanalyze.Stat.Causal.*`)
+| Statistical inference | 12 hypothesis tests, multiple-comparison correction, bootstrap CI, effect size + power, cross-validation | [stat/](https://github.com/frenzieddoll/hanalyze/tree/v0.2.0.1/docs/stat/) | [10 stat](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/api-guide/10-stat.md) |
+| Regression | LM / GLM / GLMM / robust / quantile / penalized (ridge…SCAD) / spline / GAM / GP / RFF | [regression/](https://github.com/frenzieddoll/hanalyze/tree/v0.2.0.1/docs/regression/) | [02 regression](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/api-guide/02-regression.md) |
+| Machine learning | Random forest / GBM / decision tree / k-NN / naive Bayes / SVM / MLP / MDS / PDP and ICE | [ml/](https://github.com/frenzieddoll/hanalyze/tree/v0.2.0.1/docs/ml/) | [05 ml](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/api-guide/05-ml.md) |
+| Multivariate | PCA / PLS / RRR / CCA / discriminant analysis / clustering / FDA | [fda/](https://github.com/frenzieddoll/hanalyze/tree/v0.2.0.1/docs/fda/) | [04 multivariate](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/api-guide/04-multivariate.md) |
+| Causal | Propensity score / IPW / DR / CATE / all 7 LiNGAM variants | [causal/](https://github.com/frenzieddoll/hanalyze/tree/v0.2.0.1/docs/causal/) | [08 causal](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/api-guide/08-causal.md) |
+| Bayesian | HBM DSL (plates, hierarchy) / MH, HMC, NUTS, Gibbs, ADVI / convergence diagnostics / posterior predictive | [bayesian/](https://github.com/frenzieddoll/hanalyze/tree/v0.2.0.1/docs/bayesian/) | [03 bayesian-hbm](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/api-guide/03-bayesian-hbm.md) |
+| Time series & survival | AR / VAR / GARCH / Kalman / Kaplan-Meier / competing risks / AFT / Cox | [timeseries/](https://github.com/frenzieddoll/hanalyze/tree/v0.2.0.1/docs/timeseries/) | [06](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/api-guide/06-timeseries.md) / [07](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/api-guide/07-survival.md) |
+| Optimization | Nelder-Mead / L-BFGS / DE / CMA-ES / NSGA-II / Bayesian optimization / augmented Lagrangian | [optim/](https://github.com/frenzieddoll/hanalyze/tree/v0.2.0.1/docs/optim/) | — |
+| Design of experiments | Factorial / RSM / D-, A-, I-, G-optimal / orthogonal arrays / Taguchi / custom design / power | [doe/](https://github.com/frenzieddoll/hanalyze/tree/v0.2.0.1/docs/doe/) | [09 doe](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/api-guide/09-doe.md) |
+| Data I/O | CSV / Parquet / JSON loading, cleaning, reshaping (`Data.Transform` / `Data.Wrangle`) | [io/](https://github.com/frenzieddoll/hanalyze/tree/v0.2.0.1/docs/io/) | [11 data](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/api-guide/11-data.md) |
+| Visualization | Vega-Lite based charts, integrated HTML reports, HBM DAG rendering | [visualization/](https://github.com/frenzieddoll/hanalyze/tree/v0.2.0.1/docs/visualization/) | [12 plot](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/api-guide/12-plot.md) |
 
-| Feature | Module | Usage | Theory |
-|---|---|---|---|
-| LiNGAM causal discovery (DirectLiNGAM / ICA-LiNGAM / Pairwise / VAR-LiNGAM / MultiGroup / ParceLiNGAM + bootstrap edge confidence) | `Hanalyze.Model.LiNGAM.*` | [api-guide/08-causal.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/api-guide/08-causal.md) | — |
-| Treatment effects (propensity score / IPW / doubly robust AIPW / CATE S-T-X meta-learners) | `Hanalyze.Stat.Causal.*` | [causal/usage-causal.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/causal/usage-causal.md) | — |
+**One entry point**: every model is fitted with `df |-> spec` and drawn with `toPlot`.
+The plotting integration lives in a separate package, `hanalyze-plot`
+(`cabal build --project-file=cabal.project.plot`).
 
-### Bayesian (`Hanalyze.MCMC.*` / `Hanalyze.Stat.*` / `Hanalyze.Model.HBM`)
+Version compatibility with the plotting ecosystem:
 
-| Feature | Module | Usage | Theory |
-|---|---|---|---|
-| 27 probability distributions (Truncated/Censored/MvNormal/LKJ/Multinomial/...) | `Hanalyze.Stat.Distribution` | [bayesian/01-distributions.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/bayesian/01-distributions.md) | [bayesian/theory-distributions.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/bayesian/theory-distributions.md) |
-| Probabilistic model DSL (HBM polymorphic free monad, incl. `deterministic` / `dataNamed`) | `Hanalyze.Model.HBM` | [bayesian/02-probabilistic-model.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/bayesian/02-probabilistic-model.md) | [principles/hbm.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/principles/hbm.md) |
-| MCMC samplers (MH / HMC / NUTS / Slice / tempered SMC) | `Hanalyze.MCMC.{MH,HMC,NUTS,Slice,SMC}` | [bayesian/03-mcmc-samplers.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/bayesian/03-mcmc-samplers.md) | [bayesian/theory-mcmc.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/bayesian/theory-mcmc.md) / [theory-hmc-nuts.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/bayesian/theory-hmc-nuts.md) |
-| Sampling progress display (aggregate one-liner; IO verb `df \|->! spec`, bit-identical to the pure verb) | `Hanalyze.MCMC.Progress` | [io/04-fit-api.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/io/04-fit-api.md) | — |
-| Gibbs sampling (auto-conjugate detection + hybrid) | `Hanalyze.MCMC.Gibbs` | [bayesian/04-gibbs.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/bayesian/04-gibbs.md) | [bayesian/theory-mcmc.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/bayesian/theory-mcmc.md) |
-| Variational inference (ADVI mean-field Adam) | `Hanalyze.Stat.VI` | [bayesian/05-vi.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/bayesian/05-vi.md) | [bayesian/theory-advanced.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/bayesian/theory-advanced.md) |
-| Model comparison (WAIC / PSIS-LOO / Pseudo-BMA) | `Hanalyze.Stat.ModelSelect` | [bayesian/06-model-comparison.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/bayesian/06-model-comparison.md) | [bayesian/theory-bayesian-basics.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/bayesian/theory-bayesian-basics.md) |
-| Posterior predictive checks; selected PyMC-style modelling features | `Hanalyze.Stat.PosteriorPredictive` | [02-pymc-comparison.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/02-pymc-comparison.md) | — |
-| Marginal likelihood (bridge sampling) / Bayes factors / Bayesian model averaging | `Hanalyze.Stat.{BridgeSampling,BayesFactor,BayesianModelAveraging}` | — | — |
-| Bayesian A/B test (mean difference via NUTS + ROPE/HDI decision) | `Hanalyze.MCMC.BayesianTest` | — | — |
-| Chain diagnostics (R̂, ESS incl. arviz-compatible `essBulk`, HDI, BFMI, rank histogram, KDE, autocorrelation) | `Hanalyze.Stat.MCMC` | [bayesian/viz-diagnostics.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/bayesian/viz-diagnostics.md) | — |
+| hgg | hanalyze | integration packages |
+|---|---|---|
+| 0.2.x | 0.2.0.1+ | `hanalyze-plot` 0.2.0.1 (analyze → plot) / `hgg-analyze-bridge` 0.2 (plot → analyze) |
 
-### Optimisation (`Hanalyze.Optim.*`)
+## Installation
 
-| Feature | Module | Usage | Theory |
-|---|---|---|---|
-| Single-obj (gradient): NM / L-BFGS / Brent | `Hanalyze.Optim.NelderMead`<br>`Hanalyze.Optim.LBFGS`<br>`Hanalyze.Optim.LineSearch` | [optim/01-singleobj.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/optim/01-singleobj.md) | [optim/theory-singleobj.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/optim/theory-singleobj.md) |
-| Single-obj (evolutionary): DE / CMA-ES / SA / PSO | `Hanalyze.Optim.DifferentialEvolution`<br>`Hanalyze.Optim.CMAES`<br>`Hanalyze.Optim.SimulatedAnnealing`<br>`Hanalyze.Optim.ParticleSwarm` | [optim/01-singleobj.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/optim/01-singleobj.md) | [optim/theory-singleobj.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/optim/theory-singleobj.md) |
-| Multi-objective (NSGA-II + Pareto) | `Hanalyze.Optim.{NSGA,Pareto}` | [optim/02-multi-objective.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/optim/02-multi-objective.md) | [optim/theory-pareto-moo.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/optim/theory-pareto-moo.md) |
-| Acquisition functions (EHVI / ParEGO / EI / LCB / PI) | `Hanalyze.Optim.Acquisition` | [optim/02-multi-objective.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/optim/02-multi-objective.md) | [optim/theory-bayesopt.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/optim/theory-bayesopt.md) |
-| Bayesian optimisation (BO + GP-Hedge + analytic gradient) | `Hanalyze.Optim.BayesOpt` | [optim/01-singleobj.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/optim/01-singleobj.md) | [optim/theory-bayesopt.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/optim/theory-bayesopt.md) |
-| Algorithm selection guide | — | [optim/03-algorithm-guide.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/optim/03-algorithm-guide.md) | — |
+### Requirements
 
-### Design of experiments (`Hanalyze.Design.*`)
+| Item | Requirement |
+|---|---|
+| GHC | **9.6.7** (the `tested-with` of every package) |
+| cabal | 3.14.2 or newer (verified with 3.16.1) |
+| BLAS / LAPACK | **Required** by `hmatrix` (Debian/Ubuntu: `libblas-dev liblapack-dev gfortran` / Arch: `blas lapack gcc-fortran`). For OpenBLAS use `--constraint='hmatrix +openblas'` |
+| Graphviz | Optional; only to rasterize the DOT output of `ModelGraphDot` |
 
-| Feature | Module | Usage | Theory |
-|---|---|---|---|
-| DoE (Factorial / Block / Mixed / RSM / Optimal / Power / Quality) | `Hanalyze.Design.{Factorial,Block,Mixed,RSM,Optimal,Power,Quality,MultiRSM,Anova}` | [doe/01-doe.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/doe/01-doe.md) | [doe/theory-doe.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/doe/theory-doe.md) |
-| Orthogonal arrays (L4/L8/L9/L12/L16/L18) + Taguchi (S/N + inner/outer) + process capability (Cp/Cpk) | `Hanalyze.Design.{Orthogonal,Taguchi,Quality}` | [doe/02-orthogonal-taguchi.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/doe/02-orthogonal-taguchi.md) | [doe/theory-doe.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/doe/theory-doe.md) |
-| Custom optimal design (coordinate exchange + modified Fedorov: D/A/G/I criteria, Bayesian D, linear constraints, split-plot, augment menus, design comparison via efficiency/FDS/alias) | `Hanalyze.Design.Custom.*` | [doe/usage-custom-design.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/doe/usage-custom-design.md) + [manual](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/doe/manual-custom-design.md) | — |
-| DOE workflow layer (R-style interactive `Design` object over the low-level design functions) | `Hanalyze.Design.Workflow` | [api-guide/09-doe.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/api-guide/09-doe.md) | — |
+### Using it as a library
 
-### Visualisation (`Hanalyze.Viz.*`)
+This repository is a **10-package multi-package project** and is not published as a
+package yet. Clone it and list the packages in your own `cabal.project`.
 
-| Feature | Module | Usage |
-|---|---|---|
-| Scatter / bar / histograms / MCMC diagnostics / GP plot / Pareto plot | `Hanalyze.Viz.{Scatter,Bar,Histogram,MCMC,GP,Pareto,ModelGraph,Taguchi}` | [visualization/01-visualization.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/visualization/01-visualization.md) |
-| Integrated HTML report (MathJax + Mermaid + interactive) | `Hanalyze.Viz.ReportBuilder` | [visualization/02-report-builder.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/visualization/02-report-builder.md) |
-| Unified fit-and-plot operator `df \|-> spec` (one entry point across LM/GLM/GAM/GP/HBM/... specs) + plot-free coefficient diagnostics | `Hanalyze.Fit` / `Hanalyze.Diagnostics` | [io/04-fit-api.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/io/04-fit-api.md) |
-| **hgg integration** (experimental): `toPlot`/`Plottable` overlays a fitted model (LM line+CI / GP mean+credible band) on the layer grammar; `module Hanalyze` quickstart entry. Flag-gated (`plot-integration`, default off). | `Hanalyze.Plot` + `module Hanalyze` | [visualization/03-plot-integration.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/visualization/03-plot-integration.md) |
-| **HBM ModelGraph (3 routes)**: Mermaid HTML / Graphviz DOT / direct SVG via hgg | `Hanalyze.Viz.{ModelGraph,ModelGraphDot}` + `Hgg.Plot.Bridge.Analyze` | see "ModelGraph — 3 routes" below |
+```bash
+git clone https://github.com/frenzieddoll/hanalyze
+```
 
-#### ModelGraph — 3 routes
+```cabal
+-- cabal.project
+packages: .
+          ./hanalyze/hanalyze
+          ./hanalyze/hanalyze-core
+          ./hanalyze/hanalyze-frame
+          ./hanalyze/hanalyze-bayes
+          ./hanalyze/hanalyze-models
+          ./hanalyze/hanalyze-design
+          ./hanalyze/hanalyze-viz
+```
 
-There are three ways to visualise the DAG of an HBM model; pick by use case:
+For `build-depends`, **`hanalyze` alone is the default answer** (module names do
+not change across layers). Name a layer directly only when you want to narrow the
+dependency. **Each package has a README** with a module map and a standalone example.
 
-| Route | Module | Output / Deps | When to use |
-|---|---|---|---|
-| **Mermaid HTML** | `Hanalyze.Viz.ModelGraph.renderModelGraph` | `.html` + Mermaid CDN script | GitHub / GitLab READMEs, notebook attachments — auto-rendered on GitHub |
-| **Graphviz DOT** | `Hanalyze.Viz.ModelGraphDot.renderModelGraphDot` | `.dot` text + `dot` CLI (install required) | graphviz ecosystem interop (xdot / gephi / `dot -Tpng`), fine-grained directives (`rank=same` / `constraint=false` etc) |
-| **hgg direct** | `Hgg.Plot.Bridge.Analyze.renderModelGraphSVG` ([hgg-analyze-bridge](https://github.com/frenzieddoll/hgg)) | `.svg` (**zero deps**, pure Haskell) | production app embedding, offline batch, fast rendering of large DAGs |
+| Package | Role |
+|---|---|
+| [`hanalyze`](hanalyze/ARCHITECTURE.md) | Umbrella re-exporting every layer (use this unless you have a reason not to) |
+| [`-core`](hanalyze-core/README.md) | Descriptive statistics, tests, optimization, numerical core |
+| [`-frame`](hanalyze-frame/README.md) | DataFrame integration, loading, reshaping, the fit API |
+| [`-models`](hanalyze-models/README.md) | Regression, machine learning, time series, survival, causal |
+| [`-bayes`](hanalyze-bayes/README.md) | MCMC and HBM |
+| [`-design`](hanalyze-design/README.md) | Design of experiments |
+| [`-viz`](hanalyze-viz/README.md) | Vega-Lite visualization and HTML reports |
+| [`-plot`](hanalyze-plot/README.md) | hgg integration (`toPlot`); separate build root |
+| [`-cli`](hanalyze-cli/README.md) | The `hanalyze` command |
+| [`-demos`](hanalyze-demos/README.md) | Demo and benchmark executables |
 
-All three routes take the same `Hanalyze.Model.HBM.ModelGraph` as input. Layout
-quality vs dependency trade-off:
+### Opt-in build roots
 
-- Mermaid: lightweight, but no offline rendering
-- Graphviz DOT: best layout quality, but requires the `dot` CLI
-- hgg: intermediate quality (roughly 70-80% of graphviz dot, pure Haskell); the only option when zero dependencies are required
+The default `cabal.project` is plot-independent; switch roots as needed.
 
-Code example (with `hgg-analyze-bridge` added as a dependency):
+| Build root | Contents |
+|---|---|
+| `cabal.project` (default) | Library + tests (no plot dependency) |
+| `cabal.project.plot` | The above + `hanalyze-plot` (requires the sibling hgg) |
+| `cabal.project.demos` | The above + the demo / benchmark executables |
 
-```haskell
-import qualified Hanalyze.Viz.ModelGraph    as Mermaid
-import qualified Hanalyze.Viz.ModelGraphDot as Dot
-import qualified Data.Text.IO               as TIO
-import           Hgg.Plot.Bridge.Analyze (renderModelGraphSVG)
-import           Hanalyze.Model.HBM          (buildModelGraph)
+### Just the CLI
 
-main = do
-  let mg = buildModelGraph myHBM
-  Mermaid.renderModelGraph "out/dag.html" "My HBM" mg            -- Route 1
-  TIO.writeFile "out/dag.dot" (Dot.renderModelGraphDot mg)       -- Route 2
-  renderModelGraphSVG     "out/dag.svg"  "My HBM" mg             -- Route 3
+```bash
+cabal install hanalyze-cli    # installs the hanalyze command
 ```
 
-Note: for standard plots, hgg also ships native PNG (Rasterific) and PDF
-backends. For the ModelGraph SVG route, convert via `rsvg-convert` / `inkscape`
-when PNG / PDF is needed.
-
-### Data I/O (`Hanalyze.DataIO.*`)
-
-| Feature | Module | Usage |
-|---|---|---|
-| CSV/TSV/SSV (cassava) + Parquet/JSON (Hackage `dataframe`) | `Hanalyze.DataIO.{CSV,External,Convert}` | [io/01-dirty-data.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/io/01-dirty-data.md) |
-| Dirty-data defence (W001-W008 warnings + auto-sniff + clean DSL) | `Hanalyze.DataIO.{Health,Sniff,Clean,Log}` | [io/01-dirty-data.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/io/01-dirty-data.md) |
-| Reshape (pivot_wider / one-hot / lag-lead / rolling window) | `Hanalyze.DataIO.Reshape` | [io/02-reshape.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/io/02-reshape.md) |
-| Preprocessing (impute / groupBy / derived columns / melt) | `Hanalyze.DataIO.Preprocess` | [io/01-dirty-data.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/io/01-dirty-data.md) |
-| Long-form regrid (`regridLong`) | `Hanalyze.DataIO.Preprocess` + `Hanalyze.Stat.Interpolate` | [io/03-regrid.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/io/03-regrid.md) |
-
----
-
 ## Quick start
 
 ### 30 seconds via CLI
@@ -218,10 +139,9 @@
 ```bash
 git clone https://github.com/frenzieddoll/hanalyze
 cd hanalyze
-cabal build all
 
 # Regress sales on price + promo, write an HTML report.
-hanalyze regress data/readme/sales.csv "price promo" sales --report sales.html
+cabal run hanalyze -- regress data/readme/sales.csv "price promo" sales --report sales.html
 # β₀=185.05  β(price)=-4.37  β(promo)=+32.29  R²=0.995
 ```
 
@@ -241,7 +161,7 @@
       ys = LA.fromList [18, 22, 20, 19, 25, 17]
       result = ST.tTestWelch xs ys ST.TwoSided
   print (ST.trPValue result, ST.trEffect result)
-  -- (0.012, Just ("Cohen's d", -1.85))
+  -- (1.688e-3, Just ("Cohen's d", -2.527))
 ```
 
 A single `import Hanalyze` re-exports the core entry points (linear / GLM models,
@@ -249,7 +169,7 @@
 I/O) for quick exploration; reach for the individual `Hanalyze.Model.*` /
 `Hanalyze.Stat.*` modules when you need their full surface.
 
-See [docs/01-quickstart.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/01-quickstart.md) for a fuller introduction.
+See [docs/01-quickstart.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/01-quickstart.md) for a fuller introduction.
 
 ---
 
@@ -271,26 +191,26 @@
 hanalyze clean <file> --rule ...  dirty-data cleaning
 ```
 
-For per-command flags, run `hanalyze <cmd> --help` or see [docs/01-quickstart.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/01-quickstart.md).
+For per-command flags, run `hanalyze <cmd> --help` or see [docs/01-quickstart.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/01-quickstart.md).
 
 ---
 
 ## Examples / demos
 
-`demo/` contains many demos (76 as of this release). Highlights:
+`hanalyze-demos/demo/` contains many demos (76 as of this release). Highlights:
 
 | Demo | Summary |
 |---|---|
-| `demo/regression/HBMRegressionDemo.hs` | HBM Bayesian linear regression with NUTS + HTML |
-| `demo/regression/RFFDemo.hs` | Large-scale GP via Random Fourier Features |
-| `demo/regression/RobustGPDemo.hs` | Robust GP with Student-t observation likelihood |
-| `demo/doe-optim/NSGADemo.hs` | NSGA-II + Pareto on the ZDT suite |
-| `demo/doe-optim/BayesOptDemo.hs` | BO on Branin / Hartmann6 |
-| `demo/bayesian/HBMComparisonDemo.hs` | Compare HBMs with WAIC / LOO |
-| `demo/bayesian/SimpsonParadoxDemo.hs` | Disentangle Simpson's paradox via hierarchical model |
-| `demo/io/DirtyDataDemo.hs` | Auto-defend against 19 dirty CSV variants |
+| `hanalyze-demos/demo/regression/HBMRegressionDemo.hs` | HBM Bayesian linear regression with NUTS + HTML |
+| `hanalyze-demos/demo/regression/RFFDemo.hs` | Large-scale GP via Random Fourier Features |
+| `hanalyze-demos/demo/regression/RobustGPDemo.hs` | Robust GP with Student-t observation likelihood |
+| `hanalyze-demos/demo/doe-optim/NSGADemo.hs` | NSGA-II + Pareto on the ZDT suite |
+| `hanalyze-demos/demo/doe-optim/BayesOptDemo.hs` | BO on Branin / Hartmann6 |
+| `hanalyze-demos/demo/bayesian/HBMComparisonDemo.hs` | Compare HBMs with WAIC / LOO |
+| `hanalyze-demos/demo/bayesian/SimpsonParadoxDemo.hs` | Disentangle Simpson's paradox via hierarchical model |
+| `hanalyze-demos/demo/io/DirtyDataDemo.hs` | Auto-defend against 19 dirty CSV variants |
 
-Run: `dist-newstyle/build/x86_64-linux/ghc-9.6.7/hanalyze-0.2.0.0/x/<demo-name>/build/<demo-name>/<demo-name>`.
+Run: `dist-newstyle/build/x86_64-linux/ghc-9.6.7/hanalyze-demos-0.2.0.1/x/<demo-name>/build/<demo-name>/<demo-name>`.
 
 ---
 
@@ -324,7 +244,7 @@
 > R is included in the feature map only — no numerical bench against R has been run.
 
 Numbers below come from `bench/results/{haskell,python}/*.csv`; see
-[bench/results/SUMMARY.md](bench/results/SUMMARY.md) for the full table and
+[bench/results/SUMMARY.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/bench/results/SUMMARY.md) for the full table and
 benchmark conditions (`OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1`,
 single-thread, deterministic seeds).
 
@@ -346,7 +266,7 @@
 | **Multi-output regression / Regrid** | MultiLM 2.3× faster than sklearn; `regridLong` 20× faster than a hand-written pandas+scipy synthesis. |
 | **Visualisation** | Vega-Lite specs via hvega (grammar-of-graphics-style); HTML reports built-in. |
 
-See [docs/comparison/python-r.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/docs/comparison/python-r.md) for the feature map, and [bench/results/SUMMARY.md](bench/results/SUMMARY.md) for numbers.
+See [docs/comparison/python-r.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/docs/comparison/python-r.md) for the feature map, and [bench/results/SUMMARY.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/bench/results/SUMMARY.md) for numbers.
 
 ---
 
@@ -356,7 +276,7 @@
 benchmark configuration; absolute objective values depend on iteration
 counts, seeds, and tolerances — see the SUMMARY for full conditions.
 NUTS is additionally validated against posteriordb reference posteriors
-(see [bench/posteriordb/](bench/posteriordb/)).
+(see [bench/posteriordb/](https://github.com/frenzieddoll/hanalyze/tree/v0.2.0.1/bench/posteriordb/)).
 
 - **NUTS 8-schools** (warmup 500, samples 1000): hanalyze 1492 ms with ESS(mu) 839 vs blackjax 530 ms / ESS 810 in this run
 - **Holt-Winters seasonal n=500 p=12**: hanalyze 0.19 ms vs statsmodels MLE 96 ms in this run (note: hanalyze uses fixed α=0.3 closed-form; statsmodels does MLE)
@@ -369,7 +289,7 @@
 - **Constrained Quad2D (eq)**: hanalyze 0.062 ms vs scipy SLSQP 0.69 ms in this run
 - **regridLong on jagged long-form**: hanalyze 0.99 ms vs pandas+scipy synthesis 19.4 ms in this run
 
-Reproduce: `OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 cabal run bench-{regression,kernel,optim,mo,bo,mcmc-b7,mcmc-extras,ts-extras,optim-plus,stat-util,multi-output,regrid}`, then `bench/python/bench_*.py` (see [bench/README.md](bench/README.md)).
+Reproduce: `OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 cabal run bench-{regression,kernel,optim,mo,bo,mcmc-b7,mcmc-extras,ts-extras,optim-plus,stat-util,multi-output,regrid}`, then `bench/python/bench_*.py` (see [bench/README.md](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/bench/README.md)).
 
 ---
 
@@ -396,23 +316,30 @@
 ## Roadmap & API stability
 
 - **Stable** (API expected to remain backward-compatible within minor versions): `Hanalyze.DataIO.*`, `Hanalyze.Stat.{Test, Bootstrap, MultipleTesting, ClassMetrics, CV, Effect, Distribution}`, `Hanalyze.Model.{LM, GLM, Spline, Regularized, RandomForest, DecisionTree, TimeSeries, Survival, GAM}`, `Hanalyze.Optim.{NelderMead, LBFGS, DifferentialEvolution, CMAES, NSGA, BayesOpt, SimulatedAnnealing, ParticleSwarm}`, `Hanalyze.Design.*`, `Hanalyze.Viz.{Scatter, Bar, Histogram}`.
-- **Experimental** (API may evolve): `Hanalyze.Model.HBM` DSL, `Hanalyze.MCMC.NUTS` (mass-matrix adaptation is opt-in), `Hanalyze.Stat.VI` (ADVI), `Hanalyze.Model.{GP, RFF, GPRobust, GLMM}`, `Hanalyze.Model.{SVM, GradientBoosting, NeuralNetwork}`, `Hanalyze.Model.LiNGAM.*`, `Hanalyze.Design.Custom.*`, the `df |-> spec` fit operator (`Hanalyze.Fit`), the hgg integration (`plot-integration` flag), `Hanalyze.Viz.ReportBuilder`. Behaviour is benchmarked but type signatures may shift.
+- **Experimental** (API may evolve): `Hanalyze.Model.HBM` DSL, `Hanalyze.MCMC.NUTS` (mass-matrix adaptation is opt-in), `Hanalyze.Stat.VI` (ADVI), `Hanalyze.Model.{GP, RFF, GPRobust, GLMM}`, `Hanalyze.Model.{SVM, GradientBoosting, NeuralNetwork}`, `Hanalyze.Model.LiNGAM.*`, `Hanalyze.Design.Custom.*`, the `df |-> spec` fit operator (`Hanalyze.Fit`), the hgg integration (`cabal.project.plot` build root), `Hanalyze.Viz.ReportBuilder`. Behaviour is benchmarked but type signatures may shift.
 - **Future direction**: a backend-abstraction typeclass for swapping hmatrix/Massiv/Accelerate is under consideration but not on a fixed schedule. (The unified top-level re-export layer and the fit-operator API planned earlier landed in 0.2.0.0 as `module Hanalyze` and `Hanalyze.Fit`.)
 
 ---
 
 ## Module layout
 
+Multi-package since Phase 106 (2026-07-19). The umbrella package `hanalyze`
+re-exports every module under its original name, so downstream imports are unchanged.
+Packages sit flat at the repo root and the root itself is a pure workspace
+(`cabal.project` only, no root package) — the conventional layout for Haskell
+library monorepos (cabal, plutus).
+
 ```
-src/
-  DataIO/      — CSV/JSON/Parquet IO + health checks + sniff + clean DSL + reshape (9 mods)
-  Stat/        — tests/distributions/effect/CV/bootstrap/interpret/causal/MCMC diagnostics (33 mods)
-  Model/       — LM/GLM/GLMM/GP/HBM/SVM/GBM/NN/Cluster/TS/Survival/LiNGAM/FDA etc. (75 mods)
-  Optim/       — single-obj (NM/LBFGS/DE/CMAES/SA/PSO) + multi-obj (NSGA/BO/Pareto) (18 mods)
-  Design/      — Factorial/Block/RSM/Orthogonal/Taguchi + Custom optimal design (30 mods)
-  Viz/         — Vega-Lite-based visualisation + ReportBuilder (19 mods)
-  MCMC/        — MH/HMC/NUTS/Gibbs/Slice/SMC + progress (9 mods)
-  Math/ Data/ Plot/ + Fit/Diagnostics — numeric kernels, data helpers, hgg integration, fit operator
+hanalyze/         — umbrella: Fit/Wrappers/Diagnostics/Analyze + re-exports, test suite
+hanalyze-core/    — Math kernels, low-level Stat, Optim, MCMC.Core, Model.Core (44 mods)
+hanalyze-frame/   — Data/ + DataIO/ (CSV/JSON/Parquet IO, clean DSL, reshape) (14 mods)
+hanalyze-bayes/   — HBM DSL/IR + MCMC samplers (MH/HMC/NUTS/Gibbs/Slice/SMC) + VI (26 mods)
+hanalyze-models/  — LM/GLM/GLMM/GP/SVM/GBM/NN/Cluster/TS/Survival/LiNGAM/FDA etc. (67 mods)
+hanalyze-design/  — Factorial/Block/RSM/Orthogonal/Taguchi + Custom optimal design (30 mods)
+hanalyze-viz/     — Vega-Lite-based visualisation + ReportBuilder (19 mods)
+hanalyze-plot/    — hgg integration (cabal.project.plot root only) (8 mods)
+hanalyze-cli/     — the `hanalyze` CLI executable
+hanalyze-demos/   — hanalyze-demos/demo/posteriordb executables (cabal.project.demos root only)
 ```
 
 As of this release: 212 modules, ~1,390 test examples.
@@ -422,11 +349,15 @@
 ## Build
 
 ```bash
-cabal build all                  # library + all executables (76 demos)
-cabal test                       # hspec test suite
-cabal repl                       # interactive REPL
+cabal build all                  # umbrella library + CLI + test suite
+cabal test all                   # hspec test suite
+cabal repl hanalyze       # interactive REPL (umbrella)
 ```
 
+Build roots: default `cabal.project` (standalone, no plot), `cabal.project.plot`
+(+ hgg integration), `cabal.project.demos` (+ hanalyze-demos/demo/posteriordb executables).
+See CONTRIBUTING for the full table.
+
 Major dependencies: `hmatrix` (BLAS/LAPACK), `hvega` (Vega-Lite), `statistics`, `mwc-random`, `dataframe` (Hackage Polars-like), `massiv` (parallel arrays), `ad` (auto-diff), `async`.
 
 Tested on GHC 9.6.7 + cabal 3.14.2.
@@ -437,11 +368,13 @@
 
 ```bash
 # 1. Generate shared test data (fixed-seed, deterministic)
-cabal run bench-data-gen
+#    The benchmark executables live in the demos package, so pass that build root
+cabal run --project-file=cabal.project.demos bench-data-gen
 
 # 2. Haskell side
 OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 \
-  cabal run bench-regression bench-kernel bench-optim bench-mo bench-bo
+  cabal run --project-file=cabal.project.demos \\
+    bench-regression bench-kernel bench-optim bench-mo bench-bo
 
 # 3. Python side (need bench/venv from bench/requirements.txt)
 OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 \
@@ -458,14 +391,14 @@
 
 - **Issues / PRs**: [github.com/frenzieddoll/hanalyze](https://github.com/frenzieddoll/hanalyze)
 - **Adding tests**: append hspec specs in `test/Spec.hs`
-- **Adding benchmarks**: place `bench/haskell/Bench*.hs` and matching Python script
+- **Adding benchmarks**: place `hanalyze-demos/bench/haskell/Bench*.hs` and matching Python script
 - **Coding rules**: see `CONTRIBUTING.md` (no list-passing on hot paths, minimise `unsafe*`, ...)
 
 ---
 
 ## License
 
-BSD-3-Clause License — see [LICENSE](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.0/LICENSE).
+BSD-3-Clause License — see [LICENSE](https://github.com/frenzieddoll/hanalyze/blob/v0.2.0.1/LICENSE).
 
 ## Author
 
diff --git a/app/Main.hs b/app/Main.hs
deleted file mode 100644
--- a/app/Main.hs
+++ /dev/null
@@ -1,3883 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-module Main where
-
-import Hanalyze.DataIO.CSV        (loadAutoSafeWith, LoadOpts (..), defaultLoadOpts)
-import qualified Hanalyze.DataIO.Log     as Log
-import qualified Hanalyze.DataIO.Clean   as Clean
-import qualified Hanalyze.Stat.Standardize as Std
-import qualified Hanalyze.Stat.NumberFormat as NF
-import Data.Time.Clock (getCurrentTime, diffUTCTime, UTCTime)
-import qualified Hanalyze.DataIO.Preprocess as Pp
-import qualified Hanalyze.Stat.Interpolate  as Interp
-import qualified Hanalyze.Stat.AdaptiveGrid as AG
-import Text.Read (readMaybe)
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified DataFrame.Operations.Core     as DX
-import qualified DataFrame.IO.CSV              as DX
-import qualified DataFrame.Internal.Column    as DXC
-import qualified DataFrame.Internal.DataFrame as DXD
-import Hanalyze.DataIO.Convert     (getDoubleVec, getTextVec, getMaybeTextVec)
-import Hanalyze.Model.Core        (Band (..), FitResult, rSquared1, coeffList, fittedList, residualsV)
-import qualified Hanalyze.Model.Core as Core
-import Hanalyze.Model.GLM         (Family (..), parseFamily, LinkFn (..), parseLink, canonicalLink,
-                          fitGLMWithSmooth, fitGLMFull)
-import Hanalyze.Model.GLMM        (GLMMResult (..), fitLMEDataFrame, fitGLMMDataFrame)
-import Hanalyze.Model.LM          (SmoothFit (..), multiPolyDesignMatrix)
-import Hanalyze.Stat.Distribution (Distribution, parseDistribution)
-import Hanalyze.Viz.Core          (defaultConfig, openInBrowser, OutputFormat (..), parseFormat)
-import Hanalyze.Viz.Scatter       (scatterWithSmoothFile, scatterMultiYFile, scatterPlotFile,
-                          scatterWithGroupsFile, predictedVsActualFile,
-                          predictedVsActual, scatterWithGroups)
-import Hanalyze.Viz.Histogram     (histogramPlotFile, histogramWithDensityFile)
-import Hanalyze.Viz.AnalysisReport (AnalysisReportConfig (..), ModelFit (..), NamedPlot (..),
-                           SmoothData (..), GPKernelFit (..), GPFitSummary (..), FitSummary (..),
-                           GLMMSummary (..), HBMRegSummary (..),
-                           mkFitSummary, mkGLMMSummary,
-                           writeAnalysisReport, writeAnalysisReportPlots)
-import qualified Hanalyze.Design.Orthogonal as OA
-import qualified Hanalyze.Design.Taguchi as TG
-import qualified Hanalyze.Viz.Taguchi as VTG
-import qualified Hanalyze.Viz.ReportBuilder as RB
-import qualified Hanalyze.Viz.ReportInstances as RI
-import qualified Hanalyze.Viz.ModelGraph
-import qualified Graphics.Vega.VegaLite as VL
-import Graphics.Vega.VegaLite (VegaLite, VLProperty, VLSpec)
-import qualified Hanalyze.Model.KernelRegression as Kern
-import qualified Hanalyze.Model.MultiLM as MLM
-import qualified Hanalyze.Model.Regularized as Reg
-import qualified Hanalyze.Model.GAM as GAM
-import qualified Hanalyze.Model.Quantile as QR
-import qualified Hanalyze.Model.RandomForest as RF
-import qualified Hanalyze.Model.RFF as RFF
-import qualified Hanalyze.Model.Spline as Spl
-import Hanalyze.Model.LM (SmoothFit (..))
-import qualified Hanalyze.Model.HBM as HBMod
-import qualified Hanalyze.MCMC.NUTS as HBMnuts
-import qualified Hanalyze.MCMC.Core as MCMCcore
-import qualified Data.Map.Strict as Map
-import Hanalyze.Viz.MCMC (mcmcDiagnostics, autocorrPlot)
-import Hanalyze.Viz.Core (PlotConfig (..))
-import Hanalyze.Model.GP           (Kernel (..), GPModel (..), GPParams, GPPredData,
-                           GPResult, gpMean,
-                           initParamsFromData, optimizeGP, fitGP, logMarginalLikelihood,
-                           gpPredData)
-
-import Hanalyze.Stat.ModelSelect  (lmPosteriorLogLiks, glmPosteriorLogLiks,
-                          lmePosteriorLogLiks, waic, loo,
-                          WAICResult (..), LOOResult (..))
-
-import Control.Monad      (when)
-import Data.Char          (isDigit)
-import Data.List          (intercalate, sort)
-import qualified Data.Set as Set
-import System.FilePath    (dropExtension)
-import qualified Data.Text    as T
-import qualified Data.Text.IO as TIO
-import qualified Data.Vector  as V
-import qualified Numeric.LinearAlgebra as LA
-import System.Environment (getArgs)
-import System.IO          (hPutStrLn, stderr)
-import System.Random.MWC  (createSystemRandom)
-import Text.Printf        (printf)
-
--- ---------------------------------------------------------------------------
--- CLI types
--- ---------------------------------------------------------------------------
-
-data ModelType = LM | GLM | NoReg | GP | HBM deriving (Show, Eq)
-
-data DegreeSpec
-  = AllDegree Int
-  | PerDegree [(Int, Int)]
-  deriving (Show)
-
-data Config = Config
-  { cfgFile     :: FilePath
-  , cfgXCols    :: [T.Text]
-  , cfgYCols    :: [T.Text]   -- one or more y columns
-  , cfgModel    :: ModelType
-  , cfgDist     :: Family
-  , cfgLink     :: LinkFn
-  , cfgDegree   :: DegreeSpec
-  , cfgBand     :: Band
-  , cfgFormat   :: OutputFormat
-  , cfgGroup    :: Maybe T.Text      -- grouping column → LME / GLMM
-  , cfgHistMode :: Bool              -- --hist: draw histogram of x column
-  , cfgFitDist  :: Maybe Distribution  -- --fit DIST PARAMS
-  , cfgReport   :: Maybe FilePath    -- --report [FILE]: generate HTML report
-  , cfgWAIC     :: Bool              -- --waic: compute WAIC/LOO-CV
-  , cfgLoadOpts :: LoadOpts           -- --no-header / --skip / --comment / --strict
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- Argument parsing
--- ---------------------------------------------------------------------------
-
-usageMsg :: String
-usageMsg = unlines
-  [ "Usage: hanalyze <file> <xcols> <ycols> [LM|GLM|NoReg|GP|HBM] [options]"
-  , ""
-  , "  <file>    CSV/TSV/SSV file (auto-detected from extension)"
-  , "  <xcols>   x column name(s); quote multiple: \"x1 x2\""
-  , "  <ycols>   y column name(s); quote multiple: \"y1 y2\" (multi-y → scatter only)"
-  , "  LM|GLM|NoReg|GP|HBM  model type (default: LM)"
-  , "    GP: Gaussian Process regression (single x/y only); compares RBF, Matérn5/2, Periodic"
-  , "    HBM: Bayesian linear regression via NUTS (single x/y only); --report で AnalysisReport 生成"
-  , ""
-  , "Options:"
-  , "  -d, --dist DIST    distribution: gaussian|binomial|poisson  (default: gaussian)"
-  , "  -l, --link LINK    link function: identity|log|logit|sqrt   (default: canonical)"
-  , "  --degree SPEC      degree specification (default: 1)"
-  , "  --ci [LEVEL]       show confidence interval (default level: 0.95)"
-  , "  --pi [LEVEL]       show prediction interval (Gaussian only; default level: 0.95)"
-  , "  --format FORMAT    output format: html|png|svg               (default: html)"
-  , "  --group COL        grouping column → LM+group: LME, GLM+group: GLMM"
-  , "  --report [FILE]    generate HTML analysis report (default: report.html)"
-  , "                     --format png|svg と組み合わせるとプロット部分を画像にも出力"
-  , "  --waic             compute WAIC and LOO-CV and show in report (requires --report)"
-  , ""
-  , "Degree specification:"
-  , "  N                  all columns get degree N"
-  , "  -i1 N1 [-i2 N2…]  column at 1-based position i1 gets degree N1; others: 1"
-  , ""
-  , "Examples:"
-  , "  hanalyze data.csv x y"
-  , "  hanalyze data.tsv \"x1 x2\" y LM --degree -1 2 -2 3 --ci 0.90"
-  , "  hanalyze data.csv x y GLM -d poisson -l log"
-  , "  hanalyze data.csv x y LM --group school"
-  , "  hanalyze data.csv x y GLM -d binomial -l logit --group hospital"
-  , "  hanalyze data.csv x \"y1 y2\" NoReg"
-  ]
-
-parseArgs :: [String] -> Either String Config
-parseArgs args0 =
-  let (lopts, args) = parseLoadOpts args0
-  in case args of
-    (file : xColsStr : yColsStr : rest) -> do
-      let xCols = map T.pack (words xColsStr)
-          yCols = map T.pack (words yColsStr)
-      if null xCols
-        then Left "Error: xcols must not be empty"
-        else if null yCols
-        then Left "Error: ycols must not be empty"
-        else do
-          (model, rest1)                                              <- parseModelType rest
-          (mDist, mLink, degSpec, band, fmt, mGrp, hist, mFit, mRpt, waicF, rest2) <- parseOptions rest1
-          if not (null rest2)
-            then Left ("Unexpected argument(s): " ++ unwords rest2)
-            else do
-              let dist = maybe Gaussian id mDist
-                  lnk  = maybe (canonicalLink dist) id mLink
-              Right Config
-                { cfgFile     = file
-                , cfgXCols    = xCols
-                , cfgYCols    = yCols
-                , cfgModel    = model
-                , cfgDist     = dist
-                , cfgLink     = lnk
-                , cfgDegree   = degSpec
-                , cfgBand     = band
-                , cfgFormat   = fmt
-                , cfgGroup    = mGrp
-                , cfgHistMode = hist
-                , cfgFitDist  = mFit
-                , cfgReport   = mRpt
-                , cfgWAIC     = waicF
-                , cfgLoadOpts = lopts
-                }
-    _ -> Left usageMsg
-
-parseModelType :: [String] -> Either String (ModelType, [String])
-parseModelType ("LM"    : rest) = Right (LM,    rest)
-parseModelType ("GLM"   : rest) = Right (GLM,   rest)
-parseModelType ("NoReg" : rest) = Right (NoReg, rest)
-parseModelType ("GP"    : rest) = Right (GP,    rest)
-parseModelType ("HBM"   : rest) = Right (HBM,   rest)
-parseModelType rest              = Right (LM,    rest)
-
-parseOptions :: [String]
-             -> Either String (Maybe Family, Maybe LinkFn, DegreeSpec, Band, OutputFormat,
-                               Maybe T.Text, Bool, Maybe Distribution, Maybe FilePath, Bool, [String])
-parseOptions = go Nothing Nothing (AllDegree 1) NoBand HTML Nothing False Nothing Nothing False
-  where
-    go mDist mLink deg band fmt mGrp hist mFit mRpt waicF [] =
-      Right (mDist, mLink, deg, band, fmt, mGrp, hist, mFit, mRpt, waicF, [])
-
-    go mDist mLink deg band fmt mGrp hist mFit mRpt waicF (flag : rest)
-      | flag `elem` ["-d", "--dist"] = case rest of
-          (v:rest') -> do fam <- parseFamily v
-                          go (Just fam) mLink deg band fmt mGrp hist mFit mRpt waicF rest'
-          []        -> Left "Error: -d/--dist requires an argument"
-
-      | flag `elem` ["-l", "--link"] = case rest of
-          (v:rest') -> do lnk <- parseLink v
-                          go mDist (Just lnk) deg band fmt mGrp hist mFit mRpt waicF rest'
-          []        -> Left "Error: -l/--link requires an argument"
-
-      | flag == "--degree" = do
-          let (degTokens, remaining) = span isDegreeToken rest
-          if null degTokens
-            then Left "--degree requires a specification (e.g., 2 or -1 2 -2 3)"
-            else do degSpec <- parseDegreeSpec degTokens
-                    go mDist mLink degSpec band fmt mGrp hist mFit mRpt waicF remaining
-
-      | flag == "--ci" =
-          let (level, rest') = consumeLevel 0.95 rest
-          in go mDist mLink deg (CI level) fmt mGrp hist mFit mRpt waicF rest'
-
-      | flag == "--pi" =
-          let (level, rest') = consumeLevel 0.95 rest
-          in go mDist mLink deg (PI level) fmt mGrp hist mFit mRpt waicF rest'
-
-      | flag `elem` ["-f", "--format"] = case rest of
-          (v:rest') -> do f <- parseFormat v
-                          go mDist mLink deg band f mGrp hist mFit mRpt waicF rest'
-          []        -> Left "Error: -f/--format requires an argument"
-
-      | flag == "--group" = case rest of
-          (v:rest') -> go mDist mLink deg band fmt (Just (T.pack v)) hist mFit mRpt waicF rest'
-          []        -> Left "Error: --group requires a column name"
-
-      | flag == "--hist" =
-          go mDist mLink deg band fmt mGrp True mFit mRpt waicF rest
-
-      | flag == "--fit" = case rest of
-          (name:rest') ->
-            let (paramStrs, rest'') = span isNumericToken rest'
-                params = map read paramStrs :: [Double]
-            in case parseDistribution name params of
-                 Left err -> Left ("--fit: " ++ err)
-                 Right d  -> go mDist mLink deg band fmt mGrp hist (Just d) mRpt waicF rest''
-          [] -> Left "--fit requires a distribution name (e.g. --fit normal 0 1)"
-
-      | flag == "--report" = case rest of
-          (v:rest') | not (null v) && head v /= '-' ->
-                        go mDist mLink deg band fmt mGrp hist mFit (Just v) waicF rest'
-          _           -> go mDist mLink deg band fmt mGrp hist mFit (Just "report.html") waicF rest
-
-      | flag == "--waic" =
-          go mDist mLink deg band fmt mGrp hist mFit mRpt True rest
-
-      | otherwise = Right (mDist, mLink, deg, band, fmt, mGrp, hist, mFit, mRpt, waicF, flag : rest)
-
-isNumericToken :: String -> Bool
-isNumericToken s = case (reads s :: [(Double, String)]) of
-  [(_, "")] -> True
-  _         -> False
-
--- Consume an optional level (0 < v < 1) after a band flag.
-consumeLevel :: Double -> [String] -> (Double, [String])
-consumeLevel _   (t:ts) | isLevelToken t = (read t, ts)
-consumeLevel def rest                    = (def, rest)
-
-isLevelToken :: String -> Bool
-isLevelToken s = case (reads s :: [(Double, String)]) of
-  [(v, "")] | v > 0, v < 1 -> True
-  _                          -> False
-
-isDegreeToken :: String -> Bool
-isDegreeToken ('-' : ds) = not (null ds) && all isDigit ds
-isDegreeToken s          = not (null s)  && all isDigit s
-
-parseDegreeSpec :: [String] -> Either String DegreeSpec
-parseDegreeSpec [n] =
-  case (reads n :: [(Int, String)]) of
-    [(v, "")] | v >= 0 -> Right (AllDegree v)
-    _                   -> Left ("Invalid degree: " ++ n)
-parseDegreeSpec tokens = fmap PerDegree (parsePairs tokens)
-  where
-    parsePairs [] = Right []
-    parsePairs (pos : deg : rest) =
-      case (reads pos :: [(Int,String)], reads deg :: [(Int,String)]) of
-        ([(p,"")], [(d,"")]) | p < 0, d >= 0 ->
-          fmap ((abs p, d) :) (parsePairs rest)
-        _ -> Left ("Invalid degree pair near: " ++ pos ++ " " ++ deg)
-    parsePairs [t] = Left ("Odd number of tokens in --degree near: " ++ t)
-
-applyDegreeSpec :: DegreeSpec -> [T.Text] -> [(T.Text, Int)]
-applyDegreeSpec (AllDegree d) cols  = [(c, d) | c <- cols]
-applyDegreeSpec (PerDegree ps) cols =
-  [ (c, maybe 1 id (lookup i ps)) | (i, c) <- zip [1..] cols ]
-
--- | --format PNG/SVG が指定されていれば、AnalysisReport のプロットを
---   個別画像として書き出す (HTML 本体に加えて補助出力)。
-maybeExportReportPlots :: Config -> FilePath -> [NamedPlot] -> IO ()
-maybeExportReportPlots cfg htmlPath plots =
-  case cfgFormat cfg of
-    HTML -> return ()
-    fmt  -> do
-      let prefix = dropExtension htmlPath
-      paths <- writeAnalysisReportPlots prefix fmt plots
-      mapM_ (\p -> putStrLn $ "Plot image:          " ++ p) paths
-
--- ---------------------------------------------------------------------------
--- CLI report builders (Phase 2: regress --report → ReportBuilder 経路)
--- ---------------------------------------------------------------------------
-
--- | NamedPlot を ReportSection に変換 (タイトル付き secVega)。
-namedPlotsToSecs :: [NamedPlot] -> [RB.ReportSection]
-namedPlotsToSecs nps =
-  [ RB.secVega title vega | NamedPlot _ title vega <- nps ]
-
--- | WAIC/LOO 結果 (オプション) を 1 セクションに整形。
-waicSection :: Maybe (WAICResult, LOOResult) -> [RB.ReportSection]
-waicSection Nothing = []
-waicSection (Just (w, l)) =
-  [ RB.secKeyValue "モデル選択 (WAIC / LOO-CV)"
-      [ ("WAIC",     T.pack (printf "%.2f" (waicValue w)))
-      , ("LOO",      T.pack (printf "%.2f" (looValue l)))
-      , ("p_WAIC",   T.pack (printf "%.2f" (waicPwaic w)))
-      , ("k\x0302 > 0.7", T.pack (show (looKHatBad l) ++ " 件"))
-      ]
-  ]
-
--- | 残差の (σ_hat, RMSE, max|r|)。p は推定パラメータ数 (intercept 含む)。
-cliResidStats :: [Double] -> Int -> (Double, Double, Double)
-cliResidStats resid p =
-  let n     = length resid
-      sumSq = sum [ r * r | r <- resid ]
-      sH    = sqrt (sumSq / fromIntegral (max 1 (n - p)))
-      rmse  = sqrt (sumSq / fromIntegral (max 1 n))
-      mAbs  = maximum (0 : map abs resid)
-  in (sH, rmse, mAbs)
-
--- | LM / GLM 用 CLI レポートセクション群。多項式次数と WAIC/LOO に対応。
-cliRegressSections
-  :: Config -> DXD.DataFrame -> Family -> LinkFn
-  -> [(T.Text, Int)] -> FitResult -> Maybe SmoothFit
-  -> Maybe (WAICResult, LOOResult)
-  -> [NamedPlot]
-  -> [RB.ReportSection]
-cliRegressSections cfg df dist lnk colDegs res mSmooth mModelSel pvsaPlots =
-  let xCols   = cfgXCols cfg
-      yCol    = case cfgYCols cfg of (y:_) -> y; _ -> "y"
-      beta    = coeffList res
-      coefLbls = map T.pack (multiCoeffLabels colDegs)
-      coeffs  = zip coefLbls beta
-      fitted  = fittedList res
-      resid   = LA.toList (residualsV res)
-      p       = length beta
-      (sigmaH, rmse, maxAbs) = cliResidStats resid p
-      r2      = rSquared1 res
-      r2Lbl   = T.pack (r2Label dist)
-      isLM    = dist == Gaussian
-      isPoly  = any (\(_, d) -> d > 1) colDegs
-      modelType
-        | isLM      = if isPoly then "LM (polynomial)" else "LM"
-        | otherwise = "GLM(" <> T.pack (show dist) <> ")"
-
-      formulaTex
-        | isLM = "$" <> yCol <> "_i = "
-                 <> T.intercalate " + "
-                     ("\\beta_0" :
-                       [ "\\beta_" <> T.pack (show (i :: Int)) <> " " <> trm
-                       | (i, trm) <- zip [1 ..] (polyTerms colDegs) ])
-                 <> " + \\varepsilon_i$<br>"
-                 <> "$\\varepsilon_i \\sim \\text{Normal}(0, \\sigma^2)$"
-        | otherwise =
-            "$g(\\mu_i) = "
-            <> T.intercalate " + "
-                ("\\beta_0" :
-                  [ "\\beta_" <> T.pack (show (i :: Int)) <> " " <> trm
-                  | (i, trm) <- zip [1 ..] (polyTerms colDegs) ])
-            <> "$<br>"
-            <> "$" <> yCol <> "_i \\sim \\text{" <> T.pack (show dist) <> "}(\\mu_i)$"
-
-      smoothC = case mSmooth of
-        Just sf -> RB.SmoothCurve (sfX sf) (sfFit sf) (sfLower sf) (sfUpper sf)
-        Nothing -> RB.SmoothCurve [] [] [] []
-
-      scatterCard = case (xCols, mSmooth) of
-        ([xc], Just _) -> case (getDoubleVec xc df, getDoubleVec yCol df) of
-          (Just xv, Just yv) ->
-            [ RB.secCard "散布図 + 回帰線"
-                [ RB.secFitScatter xc yCol (V.toList xv) (V.toList yv)
-                    (Just smoothC) ] ]
-          _ -> []
-        _ -> []
-
-      -- 対話的予測: 多項式拡張の場合は係数数と x 列数が合わないので省略。
-      interactiveSecs = case (isPoly, traverse (`getDoubleVec` df) xCols, getDoubleVec yCol df) of
-        (False, Just xVs, Just yV) | not (null xVs) ->
-          let xRows = [ [ xv V.! i | xv <- xVs ]
-                      | i <- [0 .. V.length yV - 1] ]
-              mkSlider xv =
-                let lo = V.minimum xv
-                    hi = V.maximum xv
-                    ext = (hi - lo) * 0.5
-                in (lo - ext, (lo + hi) / 2, hi + ext)
-              im = RB.InteractiveModel
-                     { RB.imXCols     = xCols
-                     , RB.imYCol      = yCol
-                     , RB.imXValues   = xRows
-                     , RB.imYValues   = V.toList yV
-                     , RB.imIntercept = head beta
-                     , RB.imBetas     = drop 1 beta
-                     , RB.imLink      = T.pack (linkLabelLower lnk)
-                     , RB.imSlider    = map mkSlider xVs
-                     , RB.imCISigma   = if isLM then Just sigmaH else Nothing
-                     }
-          in [RB.secInteractiveMulti "対話的予測" im]
-        _ -> []
-
-      statRow =
-        RB.secStatRow
-          [ (r2Lbl,         T.pack (printf "%.4f" r2))
-          , ("方法",        if isLM then "OLS (QR)" else "IRLS")
-          , ("σ_hat",      T.pack (printf "%.4f" sigmaH))
-          , ("RMSE",        T.pack (printf "%.4f" rmse))
-          , ("最大絶対残差", T.pack (printf "%.4f" maxAbs))
-          ]
-
-      resultSec =
-        RB.secCollapsible "<span class=\"sec-icon\">&#128200;</span> 回帰結果" True
-          ([ statRow
-           , RB.secCard "係数"
-               [RB.secCoefficients coeffs (Just (r2Lbl, r2))]
-           ]
-           ++ scatterCard
-           ++ [RB.secCard "残差プロット" [RB.secResiduals fitted resid]])
-
-      modelSec
-        | isLM      = RB.secModelOverview modelType formulaTex Nothing
-        | otherwise = RB.secModelOverviewLink modelType formulaTex
-                        (T.pack (linkLabelLower lnk)) Nothing
-
-      extraPlotSecs = namedPlotsToSecs pvsaPlots
-
-  in [ RB.secDataOverview df xCols yCol
-     , modelSec
-     , resultSec
-     ] ++ interactiveSecs ++ extraPlotSecs ++ waicSection mModelSel
-
--- | colDegs を polynomial 項の文字列に展開: [(x, 2), (z, 1)] → ["x", "x^2", "z"]
-polyTerms :: [(T.Text, Int)] -> [T.Text]
-polyTerms = concatMap (\(c, d) ->
-  [ if k == 1 then c else c <> "^" <> T.pack (show k) | k <- [1 .. d] ])
-
--- | リンク関数を JS 側のリンク名に対応させる (identity / log / logit / sqrt)。
-linkLabelLower :: LinkFn -> String
-linkLabelLower Identity = "identity"
-linkLabelLower Log      = "log"
-linkLabelLower Logit    = "logit"
-linkLabelLower Sqrt     = "sqrt"
-
--- | GLMM (LME) 用 CLI レポートセクション群。
-cliMixedSections
-  :: Config -> DXD.DataFrame -> Family -> LinkFn
-  -> [(T.Text, Int)] -> T.Text -> GLMMResult -> Maybe (WAICResult, LOOResult)
-  -> [NamedPlot]
-  -> [RB.ReportSection]
-cliMixedSections cfg df dist lnk colDegs grpCol gr mModelSel extraPlots =
-  let xCols    = cfgXCols cfg
-      yCol     = case cfgYCols cfg of (y:_) -> y; _ -> "y"
-      base     = RB.toReport (RB.defaultReportConfig "") df xCols yCol
-                   (RI.GLMMReport gr dist lnk grpCol)
-      colDegInfo =
-        [ RB.secKeyValue "Polynomial degrees"
-            [ (c, T.pack (show d)) | (c, d) <- colDegs, d > 1 ]
-        | any (\(_, d) -> d > 1) colDegs
-        ]
-  in base ++ colDegInfo ++ namedPlotsToSecs extraPlots ++ waicSection mModelSel
-
--- | GP 用 CLI レポートセクション群。マルチカーネル比較対応。
--- 呼び出し側で予測グリッド X (`gridX`) を渡す。
-cliGPSections
-  :: T.Text -> T.Text -> DXD.DataFrame -> [Double] -> [Double]
-  -> [Double]              -- ^ 予測グリッド X
-  -> [GPKernelFit]
-  -> [RB.ReportSection]
-cliGPSections xCol yCol df xs ys gridX kfits =
-  let bestK = case kfits of (k:_) -> Just k; _ -> Nothing
-      mainSec = case bestK of
-        Just kf ->
-          RB.toReport (RB.defaultReportConfig "") df [xCol] yCol
-            (RI.GPReport (gkKernel kf) (gkParams kf) (gkResult kf)
-                          gridX xs ys (gkLML kf))
-        Nothing -> []
-      cmpRows = [ [ gkLabel kf
-                  , T.pack (printf "%.2f" (gkLML kf)) ]
-                | kf <- kfits ]
-      cmpSec = case kfits of
-        []  -> []
-        [_] -> []
-        _   -> [RB.secComparisonTable
-                  "カーネル比較 (LML 降順)"
-                  ["カーネル", "log p(y|X,θ)"] cmpRows (Just 0)]
-  in mainSec ++ cmpSec
-
--- | HBM 用 CLI レポートセクション群。
-cliHBMSections
-  :: T.Text -> T.Text -> DXD.DataFrame -> [Double] -> [Double]
-  -> MCMCcore.Chain -> Maybe T.Text -> Maybe (WAICResult, LOOResult)
-  -> [NamedPlot]
-  -> [RB.ReportSection]
-cliHBMSections xCol yCol df xs ys chain mGraph mModelSel extraPlots =
-  let rep = RI.HBMLinearReport
-              { RI.hbmrChain     = chain
-              , RI.hbmrXs        = xs
-              , RI.hbmrYs        = ys
-              , RI.hbmrAlphaName = "alpha"
-              , RI.hbmrBetaName  = "beta"
-              , RI.hbmrSigmaName = "sigma"
-              , RI.hbmrGraph     = mGraph
-              }
-      base = RB.toReport (RB.defaultReportConfig "") df [xCol] yCol rep
-      _    = xCol  -- silence warning
-      _    = yCol
-  in base ++ namedPlotsToSecs extraPlots ++ waicSection mModelSel
-
--- ---------------------------------------------------------------------------
--- Main
--- ---------------------------------------------------------------------------
-
--- ---------------------------------------------------------------------------
--- Subcommand dispatcher (Phase C: hybrid CLI)
---
--- Top-level usage:
---   hanalyze <subcommand> [args...]
---   hanalyze <file> <xcols> <ycols> [LM|GLM|...] [opts]   (legacy = regress)
---
--- Implemented:  regress, info, hist, help
--- Stubs:        ridge, kernel, spline, doe, taguchi
--- ---------------------------------------------------------------------------
-
-helpMsg :: String
-helpMsg = unlines
-  [ "hanalyze \x2014 general-purpose statistical analysis & visualization toolkit"
-  , ""
-  , "Usage: hanalyze <subcommand> [args...]"
-  , "       hanalyze <file> <xcols> <ycols> [LM|GLM|NoReg|GP|HBM] [opts]   (legacy = regress)"
-  , ""
-  , "Subcommands:"
-  , "  regress   Classical/Bayesian regression (LM/GLM/GLMM/GP/HBM)         [implemented]"
-  , "  info      Print per-column type and basic statistics                 [implemented]"
-  , "  hist      Plot a histogram (optionally with theoretical density)     [implemented]"
-  , "  ridge     Regularized regression (Ridge/Lasso/Elastic Net)           [implemented]"
-  , "  kernel    Kernel regression / RFF approximation                      [implemented]"
-  , "  spline    B-spline / natural cubic regression                        [implemented]"
-  , "  quantile  Quantile regression (τ-quantile, MM-IRLS)                  [implemented]"
-  , "  gam       Generalized Additive Model (additive B-splines + Ridge)   [implemented]"
-  , "  rf        Random Forest regression (CART + bagging + feature subset) [implemented]"
-  , "  multireg  Multi-output regression (wide CSV; linear/kernel-rbf)      [implemented]"
-  , "  doe       Orthogonal arrays (L_n) for experimental designs           [implemented]"
-  , "  taguchi   Taguchi method (SN ratio + factor effects + inner/outer)   [implemented]"
-  , ""
-  , "  help      Show this message"
-  , "  --help, -h, help   Same as 'help'"
-  , ""
-  , "Run 'hanalyze regress' (or invoke without a subcommand) to see regression-specific options."
-  ]
-
-futureSubcommands :: [(String, String)]
-futureSubcommands =
-  [
-  ]
-
-isFutureSubcommand :: String -> Bool
-isFutureSubcommand c = c `elem` map fst futureSubcommands
-
-stubMessage :: String -> String
-stubMessage c = case lookup c futureSubcommands of
-  Just msg -> msg
-  Nothing  -> "subcommand '" ++ c ++ "' is not yet implemented"
-
-main :: IO ()
-main = getArgs >>= dispatch
-
-dispatch :: [String] -> IO ()
-dispatch []                           = putStrLn helpMsg
-dispatch ("--help":_)                 = putStrLn helpMsg
-dispatch ("-h":_)                     = putStrLn helpMsg
-dispatch ("help":_)                   = putStrLn helpMsg
-dispatch ("info":rest)                = runInfoCmd rest
-dispatch ("hist":rest)                = runHistCmd rest
-dispatch ("regress":rest)             = runRegressCmd rest
-dispatch ("doe":rest)                 = runDoeCmd rest
-dispatch ("taguchi":rest)             = runTaguchiCmd rest
-dispatch ("ridge":rest)               = runRidgeCmd rest
-dispatch ("kernel":rest)              = runKernelCmd rest
-dispatch ("spline":rest)              = runSplineCmd rest
-dispatch ("quantile":rest)            = runQuantileCmd rest
-dispatch ("gam":rest)                 = runGAMCmd rest
-dispatch ("rf":rest)                  = runRFCmd rest
-dispatch ("clean":rest)               = runCleanCmd rest
-dispatch ("melt":rest)                = runMeltCmd rest
-dispatch ("regrid":rest)              = runRegridCmd rest
-dispatch ("multireg":rest)            = runMultiRegCmd rest
-dispatch (cmd:_) | isFutureSubcommand cmd = do
-  hPutStrLn stderr $ "hanalyze: " ++ stubMessage cmd
-  hPutStrLn stderr "  Run 'hanalyze help' to see implemented subcommands."
-dispatch args                         = runRegressCmd args  -- legacy / bare
-
-runRegressCmd :: [String] -> IO ()
-runRegressCmd args = case parseArgs args of
-  Left err  -> hPutStrLn stderr err
-  Right cfg -> runConfig cfg
-
--- ---------------------------------------------------------------------------
--- info subcommand
--- ---------------------------------------------------------------------------
-
-runInfoCmd :: [String] -> IO ()
-runInfoCmd args = do
-  let (lopts, rest) = parseLoadOpts args
-  case rest of
-    []        -> hPutStrLn stderr
-                   "Usage: hanalyze info <file> [--no-header] [--skip N] [--comment CH] [--strict]"
-    (file:_)  -> do
-      result <- loadAutoSafeWith lopts file
-      case result of
-        Left err          -> hPutStrLn stderr ("Parse error: " ++ err)
-        Right (df, lg)    -> do
-          Log.printLogReport lg
-          printDataFrameInfo file df
-
--- | 共通フラグを切り出す: '--no-header' / '--skip N' / '--comment CH' /
--- '--strict' を 'LoadOpts' に集約し、残った位置引数を返す。
-parseLoadOpts :: [String] -> (LoadOpts, [String])
-parseLoadOpts = go defaultLoadOpts []
-  where
-    go acc rs []                              = (acc, reverse rs)
-    go acc rs ("--no-header":xs)              = go acc { loNoHeader = True } rs xs
-    go acc rs ("--strict":xs)                 = go acc { loStrict   = True } rs xs
-    go acc rs ("--no-sniff":xs)               = go acc { loSniff    = False } rs xs
-    go acc rs ("--skip":n:xs)
-      | Just k <- readMaybeInt n              = go acc { loSkip = k } rs xs
-    go acc rs ("--comment":cs:xs)
-      | (c:_) <- cs                           = go acc { loComment = Just c } rs xs
-    go acc rs (x:xs)                          = go acc (x:rs) xs
-
-readMaybeInt :: String -> Maybe Int
-readMaybeInt s = case reads s of
-  [(n, "")] -> Just n
-  _         -> Nothing
-
--- ---------------------------------------------------------------------------
--- 簡易プロファイリング (Phase 2)
--- ---------------------------------------------------------------------------
-
--- | アクションの実行時間を計測し、結果と合わせて (経過秒, 結果) を返す。
-timed :: IO a -> IO (Double, a)
-timed act = do
-  t0 <- getCurrentTime
-  r  <- act
-  t1 <- getCurrentTime
-  return (realToFrac (diffUTCTime t1 t0), r)
-
--- | 1 段階のタイマー出力。"  [Standardize]  12.3 ms" / "  [Auto-HP]  58.21 s" 形式。
-printPhase :: String -> Double -> IO ()
-printPhase label sec
-  | sec >= 1.0 = printf "  [%-15s] %7.2f s\n" label sec
-  | otherwise  = printf "  [%-15s] %7.0f ms\n" label (sec * 1000)
-
--- 未使用警告抑止
-_dummyTime :: UTCTime -> UTCTime
-_dummyTime = id
-
--- ---------------------------------------------------------------------------
--- clean subcommand (Phase C)
--- ---------------------------------------------------------------------------
-
-runCleanCmd :: [String] -> IO ()
-runCleanCmd args0 = do
-  let (lopts, args1) = parseLoadOpts args0
-      (rules, out, args2) = parseCleanFlags args1
-  case args2 of
-    [] -> hPutStrLn stderr cleanUsage
-    (file:_) -> do
-      result <- loadAutoSafeWith lopts file
-      case result of
-        Left err          -> hPutStrLn stderr ("Parse error: " ++ err)
-        Right (df0, lg0)  -> do
-          Log.printLogReport lg0
-          let (df1, lg1) = Clean.cleanPipeline rules df0
-          Log.printLogReport lg1
-          case out of
-            Nothing -> do
-              -- 出力ファイル指定なし: info を出して終わり
-              putStrLn "Cleaned DataFrame:"
-              putStrLn $ "  Rows / Cols: "
-                          ++ show (fst (DX.dimensions df1)) ++ " × "
-                          ++ show (length (DX.columnNames df1))
-              putStrLn "  Columns:"
-              mapM_ (TIO.putStrLn . ("    - " <>)) (DX.columnNames df1)
-            Just path -> do
-              -- TODO: 簡易 CSV 書出し。今は警告だけ出す。
-              hPutStrLn stderr
-                ("(--output " ++ path ++ " は未実装。ライブラリ API "
-                 ++ "Clean.cleanPipeline + Hackage writeCsv を直接お使いください)")
-
-cleanUsage :: String
-cleanUsage = unlines
-  [ "Usage: hanalyze clean <file> [--rule COL=RULE]... [--output FILE] [load opts]"
-  , ""
-  , "Rules (各列に適用):"
-  , "  StripUnits        \"12.3kg\" → 12.3"
-  , "  ParseCurrency     \"$1,234.56\" → 1234.56"
-  , "  ParseDecimalEU    \"3,14\" → 3.14 (decimal point が ,)"
-  , "  TrimText          前後空白を除去"
-  , "  CoerceNumeric     上記 3 種を順に試す万能変換"
-  , ""
-  , "例:"
-  , "  hanalyze clean data/raw.csv \\"
-  , "      --rule price=ParseCurrency \\"
-  , "      --rule weight=StripUnits \\"
-  , "      --rule note=TrimText"
-  , ""
-  , "Load opts: --no-header / --skip N / --comment CH / --delim CH / --strict / --no-sniff"
-  ]
-
-parseCleanFlags
-  :: [String] -> ([(T.Text, Clean.ColumnRule)], Maybe FilePath, [String])
-parseCleanFlags = go [] Nothing []
-  where
-    go rs out kept []                   = (reverse rs, out, reverse kept)
-    go rs out kept ("--rule":spec:xs)   = case parseRuleSpec spec of
-      Just r  -> go (r:rs) out kept xs
-      Nothing -> go rs out kept xs
-    go rs _   kept ("--output":p:xs)    = go rs (Just p) kept xs
-    go rs _   kept ("-o":p:xs)          = go rs (Just p) kept xs
-    go rs out kept (x:xs)               = go rs out (x:kept) xs
-
--- ---------------------------------------------------------------------------
--- melt subcommand (Phase B/C — wide → long)
--- ---------------------------------------------------------------------------
-
-runMeltCmd :: [String] -> IO ()
-runMeltCmd args0 = do
-  let (lopts, args1)    = parseLoadOpts args0
-      (mopts, args2)    = parseMeltFlags args1
-  case args2 of
-    []       -> hPutStrLn stderr meltUsage
-    (file:_) -> case (moIds mopts, moVars mopts) of
-      ([], _) -> hPutStrLn stderr "melt: --id COL1,COL2,... 必須"
-      (_, []) -> hPutStrLn stderr "melt: --vars COL1,COL2,... 必須"
-      (ids, vars) -> do
-        result <- loadAutoSafeWith lopts file
-        case result of
-          Left err          -> hPutStrLn stderr ("Parse error: " ++ err)
-          Right (df0, lg)   -> do
-            Log.printLogReport lg
-            let df1 = Pp.meltLonger ids vars
-                                    (moVarName mopts) (moValueName mopts)
-                                    (moParseVar mopts) df0
-                (nrows, ncols) = DX.dimensions df1
-            putStrLn "Long-form DataFrame:"
-            putStrLn $ "  Rows / Cols: " ++ show nrows ++ " × " ++ show ncols
-            putStrLn "  Columns:"
-            mapM_ (TIO.putStrLn . ("    - " <>)) (DX.columnNames df1)
-            case moOut mopts of
-              Just path -> do
-                writeMeltedCsv path df1
-                putStrLn $ "Wrote " ++ path
-              Nothing -> return ()
-
--- | melt 結果を簡易 CSV (Hackage の writeCsv 経由) で書き出す。
-writeMeltedCsv :: FilePath -> DXD.DataFrame -> IO ()
-writeMeltedCsv path df = DX.writeCsv path df
-
-data MeltOpts = MeltOpts
-  { moIds       :: [T.Text]
-  , moVars      :: [T.Text]
-  , moVarName   :: T.Text
-  , moValueName :: T.Text
-  , moParseVar  :: Bool
-  , moOut       :: Maybe FilePath
-  } deriving (Show)
-
-defaultMeltOpts :: MeltOpts
-defaultMeltOpts = MeltOpts [] [] "variable" "value" True Nothing
-
-parseMeltFlags :: [String] -> (MeltOpts, [String])
-parseMeltFlags = go defaultMeltOpts []
-  where
-    splitCSV = map T.pack . filter (not . null) . wordsBy (== ',')
-    go acc kept []                    = (acc, reverse kept)
-    go acc kept ("--id":v:xs)         = go acc { moIds = splitCSV v } kept xs
-    go acc kept ("--vars":v:xs)       = go acc { moVars = splitCSV v } kept xs
-    go acc kept ("--var":v:xs)        = go acc { moVarName = T.pack v } kept xs
-    go acc kept ("--value":v:xs)      = go acc { moValueName = T.pack v } kept xs
-    go acc kept ("--no-parse-var":xs) = go acc { moParseVar = False } kept xs
-    go acc kept ("--output":p:xs)     = go acc { moOut = Just p } kept xs
-    go acc kept ("-o":p:xs)           = go acc { moOut = Just p } kept xs
-    go acc kept (x:xs)                = go acc (x:kept) xs
-
-wordsBy :: (Char -> Bool) -> String -> [String]
-wordsBy p s = case dropWhile p s of
-  "" -> []
-  s' -> let (w, rest) = break p s' in w : wordsBy p rest
-
--- ---------------------------------------------------------------------------
--- regrid subcommand (Phase G5)
--- ---------------------------------------------------------------------------
-
-data RegridCliOpts = RegridCliOpts
-  { rcId         :: T.Text
-  , rcZ          :: T.Text
-  , rcY          :: T.Text
-  , rcN          :: Int
-  , rcInterp     :: Interp.InterpKind
-  , rcGrid       :: AG.GridKind
-  , rcZBounds    :: Pp.ZBoundsMode
-  , rcReport     :: Maybe FilePath
-  , rcReportExtra :: Bool
-  , rcOut        :: Maybe FilePath
-  } deriving (Show)
-
-defaultRegridCliOpts :: RegridCliOpts
-defaultRegridCliOpts = RegridCliOpts
-  { rcId          = "id"
-  , rcZ           = "z"
-  , rcY           = "y"
-  , rcN           = 30
-  , rcInterp      = Interp.PCHIP
-  , rcGrid        = AG.Adaptive
-  , rcZBounds     = Pp.ZIntersection
-  , rcReport      = Nothing
-  , rcReportExtra = False
-  , rcOut         = Nothing
-  }
-
-parseRegridFlags :: [String] -> (RegridCliOpts, [String])
-parseRegridFlags = go defaultRegridCliOpts []
-  where
-    go acc kept []                    = (acc, reverse kept)
-    go acc kept ("--id":v:xs)         = go acc { rcId = T.pack v } kept xs
-    go acc kept ("--z":v:xs)          = go acc { rcZ  = T.pack v } kept xs
-    go acc kept ("--y":v:xs)          = go acc { rcY  = T.pack v } kept xs
-    go acc kept ("--n":v:xs)          =
-      go acc { rcN = maybe 30 id (readMaybe v) } kept xs
-    go acc kept ("--interp":v:xs)     =
-      let k = case v of
-                "linear"        -> Interp.Linear
-                "spline"        -> Interp.NaturalSpline
-                "natural"       -> Interp.NaturalSpline
-                "naturalspline" -> Interp.NaturalSpline
-                "pchip"         -> Interp.PCHIP
-                _               -> Interp.PCHIP
-      in go acc { rcInterp = k } kept xs
-    go acc kept ("--grid":v:xs)       =
-      let g = case v of "uniform" -> AG.Uniform
-                        "adaptive" -> AG.Adaptive
-                        _          -> AG.Adaptive
-      in go acc { rcGrid = g } kept xs
-    go acc kept ("--zrange":v:xs)     =
-      let z = case v of "intersect" -> Pp.ZIntersection
-                        "intersection" -> Pp.ZIntersection
-                        "union"     -> Pp.ZUnion
-                        _           -> Pp.ZIntersection
-      in go acc { rcZBounds = z } kept xs
-    go acc kept ("--report":p:xs)     = go acc { rcReport = Just p } kept xs
-    go acc kept ("--report-extra":xs) = go acc { rcReportExtra = True } kept xs
-    go acc kept ("--output":p:xs)     = go acc { rcOut = Just p } kept xs
-    go acc kept ("-o":p:xs)           = go acc { rcOut = Just p } kept xs
-    go acc kept (x:xs)                = go acc (x:kept) xs
-
-runRegridCmd :: [String] -> IO ()
-runRegridCmd args0 = do
-  let (lopts, args1) = parseLoadOpts args0
-      (rcOpts, args2) = parseRegridFlags args1
-  case args2 of
-    []        -> hPutStrLn stderr regridUsage
-    (file:_)  -> do
-      result <- loadAutoSafeWith lopts file
-      case result of
-        Left err          -> hPutStrLn stderr ("Parse error: " ++ err)
-        Right (df0, lg)   -> do
-          Log.printLogReport lg
-          let opts = Pp.RegridOpts
-                       { Pp.roInterp      = rcInterp rcOpts
-                       , Pp.roGridKind    = rcGrid rcOpts
-                       , Pp.roN           = rcN rcOpts
-                       , Pp.roZBoundsMode = rcZBounds rcOpts
-                       , Pp.roCoarseN     = 200
-                       , Pp.roEpsRatio    = 0.05
-                       }
-              rr   = Pp.regridLong (rcId rcOpts) (rcZ rcOpts) (rcY rcOpts)
-                                   opts df0
-              df1  = Pp.rrDataFrame rr
-              (nrows, ncols) = DX.dimensions df1
-          putStrLn "Regridded long-form DataFrame:"
-          putStrLn $ "  Rows / Cols: " ++ show nrows ++ " × " ++ show ncols
-          putStrLn $ "  Z range: ["
-                  ++ show (Pp.rrZMin rr) ++ ", "
-                  ++ show (Pp.rrZMax rr) ++ "]"
-          putStrLn $ "  N grid: " ++ show (length (Pp.rrZGrid rr))
-          putStrLn $ "  IDs: " ++ show (length (Pp.rrIds rr))
-          case rcOut rcOpts of
-            Just path -> do
-              DX.writeCsv path df1
-              putStrLn $ "Wrote " ++ path
-            Nothing   -> return ()
-          case rcReport rcOpts of
-            Just path -> do
-              let kindStr = case rcInterp rcOpts of
-                              Interp.Linear        -> "Linear"
-                              Interp.NaturalSpline -> "NaturalSpline"
-                              Interp.PCHIP         -> "PCHIP"
-                  gridStr = case rcGrid rcOpts of
-                              AG.Uniform  -> "Uniform"
-                              AG.Adaptive -> "Adaptive"
-                  zbStr   = case rcZBounds rcOpts of
-                              Pp.ZIntersection -> "intersect"
-                              Pp.ZUnion        -> "union"
-                  perObs  = [ (i, pts) | (i, pts, _) <- Pp.rrPerIdInterp rr ]
-                  perInterp = [ (i, zip (Pp.rrZGrid rr) (map f (Pp.rrZGrid rr)))
-                              | (i, _, f) <- Pp.rrPerIdInterp rr ]
-                  perSummary = [ ( Pp.piId s, Pp.piNObserved s
-                                 , Pp.piZMin s, Pp.piZMax s
-                                 , Pp.piExtrapBelow s, Pp.piExtrapAbove s
-                                 , Pp.piResidualMax s)
-                               | s <- Pp.rrPerIdStats rr ]
-                  perYRange = [ let ysOrig = map snd pts
-                                    ysGrid = map (\(_,y) -> y) gys
-                                in (i, minimum ysOrig, maximum ysOrig
-                                  , minimum ysGrid, maximum ysGrid)
-                              | ((i, pts, _), gys) <-
-                                  zip (Pp.rrPerIdInterp rr) (map snd perInterp)
-                              ]
-                  ir = RB.InterpReport
-                         { RB.irTitle         = "Regrid summary"
-                         , RB.irInterpKind    = kindStr
-                         , RB.irGridKind      = gridStr
-                         , RB.irN             = rcN rcOpts
-                         , RB.irZBoundsMode   = zbStr
-                         , RB.irZMin          = Pp.rrZMin rr
-                         , RB.irZMax          = Pp.rrZMax rr
-                         , RB.irPerIdObserved = perObs
-                         , RB.irPerIdInterpY  = perInterp
-                         , RB.irGrid          = Pp.rrZGrid rr
-                         , RB.irDensity       = Pp.rrDensity rr
-                         , RB.irPerIdSummary  = perSummary
-                         , RB.irExtraEnabled  = rcReportExtra rcOpts
-                         , RB.irPerIdYRange   = perYRange
-                         }
-              RB.renderReport path
-                              (RB.defaultReportConfig "Regrid report")
-                              [RB.secInterpolation ir]
-              putStrLn $ "Wrote report " ++ path
-            Nothing   -> return ()
-
-regridUsage :: String
-regridUsage = unlines
-  [ "Usage: hanalyze regrid <file> [options] [load opts]"
-  , ""
-  , "歯抜けの long-form データ [id, z, y] を共通 grid に揃える。"
-  , ""
-  , "  --id COL          id 列名 (default: id)"
-  , "  --z  COL          z 列名 (default: z)"
-  , "  --y  COL          y 列名 (default: y)"
-  , "  --n  N            grid 点数 (default: 30)"
-  , "  --interp KIND     linear | spline | pchip (default: pchip)"
-  , "  --grid KIND       uniform | adaptive (default: adaptive)"
-  , "  --zrange MODE     intersect | union (default: intersect)"
-  , "  --output FILE     揃った long-form を CSV で出力"
-  , "  --report FILE     HTML レポート (R1-R7 必須要素)"
-  , "  --report-extra    --report に R8-R10 オプション要素も追加"
-  , ""
-  , "例:"
-  , "  hanalyze regrid data/io/potential_long_jagged.csv \\"
-  , "      --id name --z z --y y --n 30 \\"
-  , "      --interp pchip --grid adaptive --zrange intersect \\"
-  , "      --output regridded.csv --report regrid.html --report-extra"
-  ]
-
-meltUsage :: String
-meltUsage = unlines
-  [ "Usage: hanalyze melt <file> --id COL1,COL2,... --vars COL1,COL2,..."
-  , "                     [--var NAME] [--value NAME]"
-  , "                     [--no-parse-var] [--output FILE] [load opts]"
-  , ""
-  , "wide-form CSV を long-form (tidy) に展開する。"
-  , ""
-  , "  --id    そのまま残す列 (例: name,x1,x2)"
-  , "  --vars  縦方向に展開する wide 列 (例: 1,2,3,4,5,6,7,8,9,10)"
-  , "  --var   新しい variable 列名 (default: 'variable'; 例: --var t)"
-  , "  --value 新しい value 列名    (default: 'value';    例: --value y)"
-  , "  --no-parse-var  variable 列を Double に parse せず Text のまま残す"
-  , "  --output FILE   結果を CSV として書き出す"
-  , ""
-  , "例:"
-  , "  hanalyze melt data/io/wide_sample.csv \\"
-  , "      --id name,x1,x2 \\"
-  , "      --vars 1,2,3,4,5,6,7,8,9,10 \\"
-  , "      --var t --value y \\"
-  , "      --output data/io/melted_sample.csv"
-  ]
-
-parseRuleSpec :: String -> Maybe (T.Text, Clean.ColumnRule)
-parseRuleSpec s = case break (== '=') s of
-  (col, '=':rule) | not (null col), not (null rule) ->
-    case rule of
-      "StripUnits"     -> Just (T.pack col, Clean.StripUnits)
-      "ParseCurrency"  -> Just (T.pack col, Clean.ParseCurrency)
-      "ParseDecimalEU" -> Just (T.pack col, Clean.ParseDecimalEU)
-      "TrimText"       -> Just (T.pack col, Clean.TrimText)
-      "CoerceNumeric"  -> Just (T.pack col, Clean.CoerceNumeric)
-      _                -> Nothing
-  _ -> Nothing
-
-printDataFrameInfo :: FilePath -> DXD.DataFrame -> IO ()
-printDataFrameInfo file df = do
-  let n    = (fst (DX.dimensions df))
-      cols = DX.columnNames df
-  putStrLn $ "File:    " ++ file
-  putStrLn $ "Rows:    " ++ show n
-  putStrLn $ "Columns: " ++ show (length cols)
-  putStrLn ""
-  printf "  %-20s %-7s %5s %10s %10s %10s %10s %10s\n"
-         ("name" :: String) ("type" :: String) ("n" :: String)
-         ("min" :: String) ("max" :: String) ("mean" :: String)
-         ("median" :: String) ("sd" :: String)
-  putStrLn (replicate 92 '-')
-  mapM_ (printColInfo df) cols
-
-printColInfo :: DXD.DataFrame -> T.Text -> IO ()
-printColInfo df name = case getDoubleVec name df of
-  Just v -> do
-    let xs   = V.toList v
-        m    = length xs
-        mn   = if null xs then 0 else minimum xs
-        mx   = if null xs then 0 else maximum xs
-        mean = if null xs then 0 else sum xs / fromIntegral m
-        ss   = sort xs
-        med  = if m == 0 then 0 else ss !! (m `div` 2)
-        var  = if m <= 1 then 0
-               else sum [ (x - mean)^(2 :: Int) | x <- xs ]
-                  / fromIntegral (m - 1)
-        sd_  = sqrt var
-    printf "  %-20s %-7s %5d %10.4f %10.4f %10.4f %10.4f %10.4f\n"
-           (T.unpack name) ("numeric" :: String) m mn mx mean med sd_
-  Nothing -> case getMaybeTextVec name df of
-    Just v -> do
-      let raw    = V.toList v
-          m      = length raw
-          xsOnly = [ x | Just x <- raw ]
-          nMissNull = length [ () | Nothing <- raw ]
-          nMissNA   = length (filter Pp.isNAString xsOnly)
-          nMiss     = nMissNull + nMissNA
-          uniq      = Set.size (Set.fromList xsOnly)
-          topN      = take 3 (countTop xsOnly)
-          topStr    = intercalate ", "
-                        [ T.unpack k ++ "(" ++ show c ++ ")" | (k, c) <- topN ]
-          missStr = if nMiss > 0
-                      then "  NA=" ++ show nMiss
-                      else ""
-      printf "  %-20s %-7s %5d  unique=%-3d top: %s%s\n"
-             (T.unpack name) ("text" :: String) m uniq topStr missStr
-    Nothing -> printf "  %-20s %-7s     ?  (列の取り出しに失敗)\n"
-                      (T.unpack name) ("?" :: String)
-
--- | Count occurrences and return descending list.
-countTop :: Ord a => [a] -> [(a, Int)]
-countTop xs =
-  let counts = foldr (\x -> insertWithInc x) [] xs
-      insertWithInc x []                 = [(x, 1)]
-      insertWithInc x ((y, c) : rest)
-        | x == y    = (y, c + 1) : rest
-        | otherwise = (y, c) : insertWithInc x rest
-      sorted = qSortBy (\(_, a) (_, b) -> compare b a) counts
-  in sorted
-  where
-    qSortBy _ []     = []
-    qSortBy f (p:rs) = qSortBy f [x | x <- rs, f x p == LT || f x p == EQ]
-                    ++ [p]
-                    ++ qSortBy f [x | x <- rs, f x p == GT]
-
--- ---------------------------------------------------------------------------
--- hist subcommand
--- ---------------------------------------------------------------------------
-
-runHistCmd :: [String] -> IO ()
-runHistCmd args0 =
-  let (lopts, args) = parseLoadOpts args0
-  in case parseHistArgs args of
-       Left err  -> hPutStrLn stderr err
-       Right ho  -> runHistOpts (ho { hoLoadOpts = lopts })
-
-data HistOpts = HistOpts
-  { hoFile     :: FilePath
-  , hoCol      :: T.Text
-  , hoFit      :: Maybe Distribution
-  , hoFormat   :: OutputFormat
-  , hoOut      :: FilePath
-  , hoLoadOpts :: LoadOpts
-  } deriving (Show)
-
-parseHistArgs :: [String] -> Either String HistOpts
-parseHistArgs (file : col : rest) = goHistOpts rest
-  HistOpts { hoFile = file, hoCol = T.pack col
-           , hoFit = Nothing, hoFormat = HTML, hoOut = "histogram.html"
-           , hoLoadOpts = defaultLoadOpts }
-parseHistArgs _ = Left $ unlines
-  [ "Usage: hanalyze hist <file> <col> [options]"
-  , ""
-  , "Options:"
-  , "  --fit DIST [PARAMS...]   overlay theoretical density"
-  , "                           (e.g. --fit normal 0 1, --fit poisson 3)"
-  , "  --format html|png|svg    output format (default: html)"
-  , "  --out FILE               output file path (default: histogram.html)"
-  ]
-
-goHistOpts :: [String] -> HistOpts -> Either String HistOpts
-goHistOpts []                ho = Right ho
-goHistOpts ("--fit" : rest)  ho = case rest of
-  (name : rest') ->
-    let (paramStrs, rest'') = span isNumericToken rest'
-        params = map read paramStrs :: [Double]
-    in case parseDistribution name params of
-         Left err -> Left ("--fit: " ++ err)
-         Right d  -> goHistOpts rest'' (ho { hoFit = Just d })
-  [] -> Left "--fit requires a distribution name (e.g. --fit normal 0 1)"
-goHistOpts ("--format" : v : rest) ho =
-  case parseFormat v of
-    Left err -> Left err
-    Right f  -> goHistOpts rest (ho { hoFormat = f })
-goHistOpts ("-f"       : v : rest) ho = goHistOpts ("--format" : v : rest) ho
-goHistOpts ("--out"    : v : rest) ho = goHistOpts rest (ho { hoOut = v })
-goHistOpts (flag       : _)        _  =
-  Left ("hist: unexpected argument '" ++ flag ++ "' (try 'hanalyze hist' for usage)")
-
-runHistOpts :: HistOpts -> IO ()
-runHistOpts ho = do
-  result <- loadAutoSafeWith (hoLoadOpts ho) (hoFile ho)
-  case result of
-    Left err -> hPutStrLn stderr ("Parse error: " ++ err)
-    Right (df, lg) -> do
-      Log.printLogReport lg
-      case getDoubleVec (hoCol ho) df of
-        Nothing -> hPutStrLn stderr $
-          "Error: column '" ++ T.unpack (hoCol ho) ++ "' not found or not numeric"
-        Just xVec -> do
-          let vals    = V.toList xVec
-              histCfg = defaultConfig ("Histogram: " <> hoCol ho)
-              outPath = hoOut ho
-              fmt     = hoFormat ho
-          case hoFit ho of
-            Nothing -> do
-              histogramPlotFile fmt outPath histCfg (hoCol ho) vals Nothing
-              putStrLn $ "Histogram:           " ++ outPath
-            Just dist -> do
-              histogramWithDensityFile fmt outPath histCfg (hoCol ho) vals Nothing dist
-              putStrLn $ "Histogram + density: " ++ outPath
-          openInBrowser outPath
-
-runConfig :: Config -> IO ()
-runConfig cfg = do
-  -- Warn: PI with non-Gaussian falls back to CI
-  case (cfgBand cfg, cfgDist cfg, cfgModel cfg) of
-    (PI _, fam, GLM) | fam /= Gaussian ->
-      hPutStrLn stderr "Warning: PI is only exact for Gaussian. Using CI with same level."
-    _ -> return ()
-  -- Warn: GP only supports single x/y
-  case cfgModel cfg of
-    GP | length (cfgXCols cfg) /= 1 || length (cfgYCols cfg) /= 1 ->
-      hPutStrLn stderr "Warning: GP requires exactly one x column and one y column."
-    _ -> return ()
-
-  result <- loadAutoSafeWith (cfgLoadOpts cfg) (cfgFile cfg)
-  case result of
-    Left err -> putStrLn ("Parse error: " ++ err)
-    Right (df, lg) -> do
-      Log.printLogReport lg
-      putStrLn $ "Loaded " ++ show ((fst (DX.dimensions df))) ++ " rows from " ++ cfgFile cfg
-      putStrLn "Columns:"
-      mapM_ (TIO.putStrLn . ("  - " <>)) (DX.columnNames df)
-
-      let fmt   = cfgFormat cfg
-          xCol1 = head (cfgXCols cfg)
-
-      -- ── Histogram mode ────────────────────────────────────────────────────
-      if cfgHistMode cfg
-        then runHistogram cfg df fmt xCol1
-        else case cfgModel cfg of
-               GP  -> runGP cfg df xCol1
-               HBM -> runHBM cfg df xCol1
-               _   -> runAnalysis cfg df fmt xCol1
-
--- ---------------------------------------------------------------------------
--- Mixed model (LME / GLMM)
--- ---------------------------------------------------------------------------
-
-runMixedModel :: Config -> DXD.DataFrame -> OutputFormat -> T.Text -> T.Text -> T.Text -> IO ()
-runMixedModel cfg df fmt xCol1 yCol grpCol = do
-  let colDegs = applyDegreeSpec (cfgDegree cfg) (cfgXCols cfg)
-      (dist, lnk) = case cfgModel cfg of
-        LM    -> (Gaussian, Identity)
-        GLM   -> (cfgDist cfg, cfgLink cfg)
-        _     -> (Gaussian, Identity)  -- unreachable (NoReg/GP)
-
-  let mResult = case cfgModel cfg of
-        LM    -> fitLMEDataFrame  colDegs grpCol yCol df
-        GLM   -> fitGLMMDataFrame dist lnk colDegs grpCol yCol df
-        _     -> Nothing
-
-  case mResult of
-    Nothing -> putStrLn "\nError: column(s) not found or not numeric/text"
-    Just gr -> do
-      let modelKind = case cfgModel cfg of
-            LM  -> "LME (Gaussian, exact EM)"
-            GLM -> "GLMM (" ++ modelLabel dist lnk ++ ", Laplace)"
-            _   -> ""
-          cs = coeffList (glmmFixed gr)
-
-      putStrLn $ "\nModel: " ++ T.unpack yCol ++ " ~ "
-              ++ modelFormula colDegs
-              ++ "  [" ++ modelKind ++ " | group: " ++ T.unpack grpCol ++ "]"
-
-      putStrLn "Fixed effects:"
-      mapM_ (\(lbl, v) -> printf "  %-30s = %9.4f\n" lbl v)
-            (zip (multiCoeffLabels colDegs) cs)
-
-      putStrLn "Variance components:"
-      printf "  %-30s = %9.4f\n" ("σ²_u (" ++ T.unpack grpCol ++ ")") (glmmRandVar gr)
-      case cfgModel cfg of
-        LM  -> printf "  %-30s = %9.4f\n" ("σ² (residual)" :: String) (glmmResidVar gr)
-        _   -> printf "  %-30s   (fixed by family)\n" ("σ² (residual)" :: String)
-      printf "  %-30s = %9.4f  (%d%% between-group)\n"
-             ("ICC" :: String) (glmmICC gr)
-             (round (glmmICC gr * 100) :: Int)
-
-      putStrLn $ "BLUPs (" ++ T.unpack grpCol ++ "):"
-      mapM_ (\(g, u) -> printf "  %-12s = %+9.4f\n" g u)
-            (zip (map T.unpack (V.toList (glmmGroups gr))) (V.toList (glmmBLUPs gr)))
-
-      let suffix = "  [" <> T.pack modelKind <> " | group: " <> grpCol <> "]"
-
-      -- Scatter with group-level fitted lines (single x only)
-      case (length (cfgXCols cfg), getDoubleVec xCol1 df, getDoubleVec yCol df, getTextVec grpCol df) of
-        (1, Just xVec, Just yVec, Just gVec) -> do
-          let ptData = zip3 (V.toList gVec) (V.toList xVec) (V.toList yVec)
-              lnData = computeGroupLines lnk cs colDegs
-                         (glmmGroups gr) (glmmBLUPs gr) xVec
-              scatterPath = "scatter.html"
-              scatterCfg  = defaultConfig (xCol1 <> " vs " <> yCol <> suffix)
-          scatterWithGroupsFile fmt scatterPath scatterCfg xCol1 yCol ptData lnData
-          putStrLn $ "\nScatter plot:        " ++ scatterPath
-          openInBrowser scatterPath
-        _ ->
-          putStrLn "\n(Scatter plot skipped for multiple x columns)"
-
-      -- Predicted vs Actual
-      case getDoubleVec yCol df of
-        Nothing   -> return ()
-        Just yVec -> do
-          let pvsaPath = "pvsa.html"
-              pvsaCfg  = defaultConfig ("Predicted vs Actual" <> suffix)
-          predictedVsActualFile fmt pvsaPath pvsaCfg (V.toList yVec) (fittedList (glmmFixed gr))
-          putStrLn $ "Predicted vs Actual: " ++ pvsaPath
-          openInBrowser pvsaPath
-
-      -- ── HTML レポート生成 ──────────────────────────────────────────────────
-      case cfgReport cfg of
-        Nothing   -> return ()
-        Just path -> do
-          -- WAIC/LOO 計算 (--waic 指定時、Gaussian/Identity の LME のみ)
-          mModelSel <-
-            if cfgWAIC cfg && dist == Gaussian && lnk == Identity
-            then case (getDoubleVec yCol df, getTextVec grpCol df) of
-                   (Just yVec, Just gVec) -> do
-                     let xVecPairs = [ (xv, deg) | (xc, deg) <- colDegs
-                                     , Just xv <- [getDoubleVec xc df] ]
-                     case xVecPairs of
-                       [] -> return Nothing
-                       _  -> do
-                         let dm = multiPolyDesignMatrix xVecPairs
-                             y  = LA.fromList (V.toList yVec)
-                             groupLabels = V.toList (glmmGroups gr)
-                             blupsList   = V.toList (glmmBLUPs gr)
-                             blupMap     = zip groupLabels blupsList
-                             offsets     = [ maybe 0 id (lookup g blupMap)
-                                           | g <- V.toList gVec ]
-                             nSamples    = 1000
-                         gen <- createSystemRandom
-                         llMat <- lmePosteriorLogLiks
-                                    dm y offsets (glmmFixed gr) nSamples gen
-                         let w = waic llMat
-                             l = loo  llMat
-                         printf "  WAIC=%.2f  LOO=%.2f  p_WAIC=%.2f  k̂>0.7: %d件 (条件付き)\n"
-                                (waicValue w) (looValue l) (waicPwaic w) (looKHatBad l)
-                         return (Just (w, l))
-                   _ -> return Nothing
-            else return Nothing
-
-          let rbCfg = RB.defaultReportConfig
-                        (T.pack modelKind <> ": " <> yCol <> " | " <> grpCol)
-              scatterPlots =
-                case (length (cfgXCols cfg), getDoubleVec xCol1 df, getDoubleVec yCol df, getTextVec grpCol df) of
-                  (1, Just xVec, Just yVec, Just gVec) ->
-                    let ptData  = zip3 (V.toList gVec) (V.toList xVec) (V.toList yVec)
-                        lnData  = computeGroupLines lnk cs colDegs (glmmGroups gr) (glmmBLUPs gr) xVec
-                        scCfg   = defaultConfig (xCol1 <> " vs " <> yCol <> suffix)
-                    in [NamedPlot "vl-scatter" "グループ別散布図"
-                         (scatterWithGroups scCfg xCol1 yCol ptData lnData)]
-                  _ -> []
-              pvsaPlots =
-                case getDoubleVec yCol df of
-                  Just yVec ->
-                    let pvCfg = defaultConfig ("Predicted vs Actual" <> suffix)
-                    in [NamedPlot "vl-pvsa" "Predicted vs Actual"
-                         (predictedVsActual pvCfg (V.toList yVec) (fittedList (glmmFixed gr)))]
-                  Nothing -> []
-              plots = scatterPlots ++ pvsaPlots
-              sections = cliMixedSections cfg df dist lnk colDegs grpCol gr mModelSel plots
-          RB.renderReport path rbCfg sections
-          putStrLn $ "Report:              " ++ path
-          maybeExportReportPlots cfg path plots
-          openInBrowser path
-
--- ---------------------------------------------------------------------------
--- GLM regression (no random effects)
--- ---------------------------------------------------------------------------
-
-runRegression :: Config -> DXD.DataFrame -> OutputFormat -> T.Text -> T.Text -> IO ()
-runRegression cfg df fmt xCol1 yCol = do
-  let colDegs = applyDegreeSpec (cfgDegree cfg) (cfgXCols cfg)
-      (dist, lnk) = case cfgModel cfg of
-        LM  -> (Gaussian, Identity)
-        GLM -> (cfgDist cfg, cfgLink cfg)
-        _   -> (Gaussian, Identity)  -- unreachable (NoReg/GP)
-
-  case fitGLMWithSmooth dist lnk colDegs (cfgBand cfg) 200 df yCol of
-    Nothing -> putStrLn "\nError: column(s) not found or not numeric"
-    Just (res, mSmooth) -> do
-      let cs  = coeffList res
-          eq  = equationLabel dist lnk colDegs cs
-
-      putStrLn $ "\nModel: " ++ T.unpack yCol ++ " ~ "
-              ++ modelFormula colDegs
-              ++ "  [" ++ modelLabel dist lnk ++ "]"
-      mapM_ (\(lbl, v) -> printf "  %-30s = %9.4f\n" lbl v)
-            (zip (multiCoeffLabels colDegs) cs)
-      printf "  %-30s = %9.4f\n" (r2Label dist) (rSquared1 res)
-
-      let bandLabel = case cfgBand cfg of
-            NoBand   -> ""
-            CI level -> ", " ++ show (round (level*100) :: Int) ++ "% CI"
-            PI level -> ", " ++ show (round (level*100) :: Int) ++ "% PI"
-          titleSuffix = "  [" <> T.pack (modelLabel dist lnk) <> T.pack bandLabel <> "]"
-
-      case mSmooth of
-        Just sf -> do
-          let scatterPath = "scatter.html"
-              scatterCfg  = defaultConfig (xCol1 <> " vs " <> yCol <> titleSuffix)
-          scatterWithSmoothFile fmt scatterPath scatterCfg eq df xCol1 yCol sf
-          putStrLn $ "\nScatter plot:        " ++ scatterPath
-          openInBrowser scatterPath
-        Nothing ->
-          putStrLn "\n(Scatter plot skipped for multiple x columns)"
-
-      case getDoubleVec yCol df of
-        Nothing   -> return ()
-        Just yVec -> do
-          let pvsaPath = "pvsa.html"
-              pvsaCfg  = defaultConfig ("Predicted vs Actual  " <> titleSuffix)
-          predictedVsActualFile fmt pvsaPath pvsaCfg (V.toList yVec) (fittedList res)
-          putStrLn $ "Predicted vs Actual: " ++ pvsaPath
-          openInBrowser pvsaPath
-
-      -- ── HTML レポート生成 ──────────────────────────────────────────────────
-      case cfgReport cfg of
-        Nothing   -> return ()
-        Just path -> do
-          let rbCfg = RB.defaultReportConfig
-                        (T.pack (modelLabel dist lnk)
-                          <> ": " <> yCol <> " ~ "
-                          <> T.pack (modelFormula colDegs))
-              pvsaPlots = case getDoubleVec yCol df of
-                Just yVec ->
-                  let pvCfg = defaultConfig ("Predicted vs Actual" <> titleSuffix)
-                  in [NamedPlot "vl-pvsa" "Predicted vs Actual"
-                       (predictedVsActual pvCfg (V.toList yVec) (fittedList res))]
-                Nothing -> []
-
-          -- ── WAIC/LOO-CV 計算 (--waic が指定された場合) ────────────────────
-          mModelSelect <-
-            if not (cfgWAIC cfg)
-            then return Nothing
-            else case getDoubleVec yCol df of
-              Nothing   -> return Nothing
-              Just yVec -> do
-                let xVecPairs = [ (xv, deg)
-                                | (xc, deg) <- colDegs
-                                , Just xv   <- [getDoubleVec xc df] ]
-                case xVecPairs of
-                  [] -> return Nothing
-                  _  -> do
-                    let dm = multiPolyDesignMatrix xVecPairs
-                        y  = LA.fromList (V.toList yVec)
-                        nSamples = 1000 :: Int
-                    gen <- createSystemRandom
-                    llMat <- case dist of
-                      Gaussian -> lmPosteriorLogLiks dm y res nSamples gen
-                      _        -> do
-                        let (_, fisherInv) = fitGLMFull dist lnk dm y
-                        glmPosteriorLogLiks dist lnk dm y fisherInv res nSamples gen
-                    let w = waic llMat
-                        l = loo  llMat
-                    printf "  WAIC=%.2f  LOO=%.2f  p_WAIC=%.2f  k̂>0.7: %d件\n"
-                           (waicValue w) (looValue l) (waicPwaic w) (looKHatBad l)
-                    return (Just (w, l))
-
-          let sections = cliRegressSections cfg df dist lnk colDegs res mSmooth
-                            mModelSelect pvsaPlots
-          RB.renderReport path rbCfg sections
-          putStrLn $ "Report:              " ++ path
-          maybeExportReportPlots cfg path pvsaPlots
-          openInBrowser path
-
--- ---------------------------------------------------------------------------
--- Regression / scatter dispatch (non-histogram path)
--- ---------------------------------------------------------------------------
-
-runAnalysis :: Config -> DXD.DataFrame -> OutputFormat -> T.Text -> IO ()
-runAnalysis cfg df fmt xCol1 = do
-  let yCols    = cfgYCols cfg
-      effModel = if length yCols > 1 then NoReg
-                 else if cfgModel cfg == GP then NoReg  -- GP はここに来ない
-                 else cfgModel cfg
-
-  case effModel of
-    -- ── No regression: scatter plot only ──────────────────────────────────
-    NoReg ->
-      case yCols of
-        [yCol] -> do
-          let scatterPath = "scatter.html"
-              scatterCfg  = defaultConfig (xCol1 <> " vs " <> yCol)
-          scatterPlotFile fmt scatterPath scatterCfg df xCol1 yCol
-          putStrLn $ "\nScatter plot:        " ++ scatterPath
-          openInBrowser scatterPath
-
-        _ -> do
-          let scatterPath = "scatter.html"
-              scatterCfg  = defaultConfig (xCol1 <> " vs " <> T.intercalate ", " yCols)
-          scatterMultiYFile fmt scatterPath scatterCfg df xCol1 yCols
-          putStrLn $ "\nScatter plot (multi-y): " ++ scatterPath
-          openInBrowser scatterPath
-
-    -- ── Regression (LM / GLM) ─────────────────────────────────────────────
-    _ -> case yCols of
-      [yCol] ->
-        case cfgGroup cfg of
-          Just grpCol -> runMixedModel cfg df fmt xCol1 yCol grpCol
-          Nothing     -> runRegression cfg df fmt xCol1 yCol
-      _ -> do
-        putStrLn "\nNote: regression with multiple y columns not supported. Plotting scatter only."
-        let scatterPath = "scatter.html"
-            scatterCfg  = defaultConfig (xCol1 <> " vs " <> T.intercalate ", " yCols)
-        scatterMultiYFile fmt scatterPath scatterCfg df xCol1 yCols
-        putStrLn $ "Scatter plot (multi-y): " ++ scatterPath
-        openInBrowser scatterPath
-
--- ---------------------------------------------------------------------------
--- GP regression
--- ---------------------------------------------------------------------------
-
-runGP :: Config -> DXD.DataFrame -> T.Text -> IO ()
-runGP cfg df xCol1 = do
-  let yCol = head (cfgYCols cfg)
-  case (getDoubleVec xCol1 df, getDoubleVec yCol df) of
-    (Just xVec, Just yVec) -> do
-      let xs = V.toList xVec
-          ys = V.toList yVec
-          p0 = initParamsFromData xs ys
-
-      putStrLn "\nFitting GP kernels (this may take a moment)..."
-
-      let kernelDefs = [(RBF, "RBF"), (Matern52, "Mat\xe9rn5/2"), (Periodic, "Periodic")]
-          xMin = V.minimum xVec
-          xMax = V.maximum xVec
-          span' = max 1e-8 (xMax - xMin)
-          testXs = [ xMin + fromIntegral i * span' / 199 | i <- [0 .. 199 :: Int] ]
-
-      kfits <- mapM (\(ker, lbl) -> do
-        putStrLn $ "  Optimizing " ++ lbl ++ " ..."
-        let params = optimizeGP ker xs ys p0
-            model  = GPModel ker params
-            res    = fitGP model xs ys testXs
-            lml    = logMarginalLikelihood xs ys ker params
-            pd     = gpPredData model xs ys
-        return GPKernelFit
-          { gkLabel    = T.pack lbl
-          , gkKernel   = ker
-          , gkParams   = params
-          , gkResult   = res
-          , gkLML      = lml
-          , gkPredData = pd
-          }
-        ) kernelDefs
-
-      -- LML 降順にソート
-      let sorted = foldr insertByLML [] kfits
-          insertByLML x [] = [x]
-          insertByLML x (y:ys') = if gkLML x >= gkLML y then x:y:ys'
-                                  else y : insertByLML x ys'
-          path   = maybe "report.html" id (cfgReport cfg)
-          rbCfg  = RB.defaultReportConfig
-                     ("GP Regression: " <> xCol1 <> " \x2192 " <> yCol)
-          sections = cliGPSections xCol1 yCol df xs ys testXs sorted
-
-      RB.renderReport path rbCfg sections
-      putStrLn $ "Report: " ++ path
-      maybeExportReportPlots cfg path []
-      openInBrowser path
-
-    _ -> putStrLn "\nError: column(s) not found or not numeric"
-
--- ---------------------------------------------------------------------------
--- HBM (Bayesian linear regression via NUTS)
--- ---------------------------------------------------------------------------
-
-runHBM :: Config -> DXD.DataFrame -> T.Text -> IO ()
-runHBM cfg df xCol = do
-  let yCols = cfgYCols cfg
-      xCols = cfgXCols cfg
-  case (yCols, xCols, getDoubleVec xCol df) of
-    ([yCol], [_], Just xVec) ->
-      case getDoubleVec yCol df of
-        Nothing -> putStrLn $ "Error: y column '" ++ T.unpack yCol ++ "' not numeric"
-        Just yVec -> do
-          let xs = V.toList xVec
-              ys = V.toList yVec
-          putStrLn ""
-          putStrLn "=== HBM Bayesian Linear Regression ==="
-          printf "  y = α + β·x + ε,  α,β ~ Normal(0,10),  ε ~ Normal(0,σ),  σ ~ Exp(1)\n"
-          printf "  サンプリング: NUTS (AD 勾配 + dual averaging)\n"
-          printf "  N = %d 観測, x = %s, y = %s\n\n"
-                 (length xs) (T.unpack xCol) (T.unpack yCol)
-          runHBMRegression xs ys xCol yCol df cfg
-    _ ->
-      putStrLn "Error: HBM requires exactly one x and one y column (numeric)"
-
-runHBMRegression
-  :: [Double] -> [Double] -> T.Text -> T.Text -> DXD.DataFrame -> Config -> IO ()
-runHBMRegression xs ys xCol yCol df cfg = do
-  let nutsCfg = HBMnuts.defaultNUTSConfig
-                  { HBMnuts.nutsIterations = 1500
-                  , HBMnuts.nutsBurnIn     = 500
-                  , HBMnuts.nutsStepSize   = 0.05
-                  }
-      initP   = Map.fromList
-                  [ ("alpha", 0.0), ("beta", 0.0), ("sigma", 1.0) ]
-      hbmModel :: HBMod.ModelP ()
-      hbmModel = do
-        a <- HBMod.sample "alpha" (HBMod.Normal 0 10)
-        b <- HBMod.sample "beta"  (HBMod.Normal 0 10)
-        s <- HBMod.sample "sigma" (HBMod.Exponential 1)
-        mapM_ (\(x, y) ->
-                 let xC = realToFrac x
-                 in HBMod.observe "y" (HBMod.Normal (a + b * xC) s) [y])
-              (zip xs ys)
-
-  gen <- createSystemRandom
-  chain <- HBMnuts.nuts hbmModel nutsCfg initP gen
-  let acc = MCMCcore.acceptanceRate chain
-      n   = length (MCMCcore.chainSamples chain)
-  printf "  受容率: %.1f%%, サンプル数: %d\n" (acc * 100 :: Double) n
-
-  let aMean = maybe 0 id (MCMCcore.posteriorMean "alpha" chain)
-      aSD   = maybe 0 id (MCMCcore.posteriorSD   "alpha" chain)
-      bMean = maybe 0 id (MCMCcore.posteriorMean "beta"  chain)
-      bSD   = maybe 0 id (MCMCcore.posteriorSD   "beta"  chain)
-      sMean = maybe 0 id (MCMCcore.posteriorMean "sigma" chain)
-      sSD   = maybe 0 id (MCMCcore.posteriorSD   "sigma" chain)
-  printf "  α = %+.4f ± %.4f\n" aMean aSD
-  printf "  β = %+.4f ± %.4f\n" bMean bSD
-  printf "  σ = %+.4f ± %.4f\n" sMean sSD
-
-  case cfgReport cfg of
-    Nothing   -> return ()
-    Just path -> do
-      let smooth = makeSmooth xs chain
-          fitted = [aMean + bMean * x | x <- xs]
-          resid  = zipWith (-) ys fitted
-          yBar   = sum ys / fromIntegral (length ys)
-          tss    = sum [(y - yBar) ^ (2::Int) | y <- ys]
-          rss    = sum [r ^ (2::Int) | r <- resid]
-          r2     = if tss < 1e-12 then 0 else 1 - rss / tss
-
-      mWaicLoo <-
-        if cfgWAIC cfg
-          then do
-            let llMat = [ HBMod.perObsLogLiks hbmModel ps
-                        | ps <- MCMCcore.chainSamples chain ]
-                w = waic llMat
-                l = loo  llMat
-            printf "  WAIC=%.2f  LOO=%.2f  p_WAIC=%.2f  k̂>0.7: %d件\n"
-                   (waicValue w) (looValue l) (waicPwaic w) (looKHatBad l)
-            return (Just (w, l))
-          else return Nothing
-
-      let mGraph = Just (Hanalyze.Viz.ModelGraph.buildMermaid (HBMod.buildModelGraph hbmModel))
-          rbCfg  = RB.defaultReportConfig
-                     ("HBM Linear Regression: " <> yCol <> " ~ " <> xCol)
-          sections = cliHBMSections xCol yCol df xs ys chain mGraph mWaicLoo []
-      RB.renderReport path rbCfg sections
-      putStrLn $ "Report:              " ++ path
-      maybeExportReportPlots cfg path []
-      openInBrowser path
-  where
-    -- 信用区間付き予測曲線: 各事後サンプルから μ* = α + β·x* を計算 → 分位点
-    makeSmooth :: [Double] -> MCMCcore.Chain -> SmoothData
-    makeSmooth xs0 ch =
-      let alphas = MCMCcore.chainVals "alpha" ch
-          betas  = MCMCcore.chainVals "beta"  ch
-          xMin   = minimum xs0
-          xMax   = maximum xs0
-          ext    = (xMax - xMin) * 0.5
-          grid   = [xMin - ext + i * (xMax - xMin + 2 * ext) / 99 | i <- [0..99]]
-          atX x  = let ss     = sortListAsc (zipWith (\a b -> a + b * x) alphas betas)
-                       sn     = length ss
-                       qAt p  = ss !! min (sn-1) (max 0 (floor (p * fromIntegral sn) :: Int))
-                   in (qAt 0.5, qAt 0.025, qAt 0.975)
-          (yMid, yLo, yHi) = unzip3 (map atX grid)
-      in SmoothData
-           { sdXs = grid, sdYs = yMid, sdLower = yLo, sdUpper = yHi
-           , sdHasBand = True
-           }
-
-    sortListAsc :: [Double] -> [Double]
-    sortListAsc = qs
-      where
-        qs []     = []
-        qs (p:rs) = qs [x | x <- rs, x <= p] ++ [p] ++ qs [x | x <- rs, x > p]
-
--- ---------------------------------------------------------------------------
--- Histogram mode
--- ---------------------------------------------------------------------------
-
-runHistogram :: Config -> DXD.DataFrame -> OutputFormat -> T.Text -> IO ()
-runHistogram cfg df fmt xCol =
-  case getDoubleVec xCol df of
-    Nothing ->
-      putStrLn $ "Error: column '" ++ T.unpack xCol ++ "' not found or not numeric"
-    Just xVec -> do
-      let vals     = V.toList xVec
-          histPath = "histogram.html"
-          histCfg  = defaultConfig ("Histogram: " <> xCol)
-      case cfgFitDist cfg of
-        Nothing   -> do
-          histogramPlotFile fmt histPath histCfg xCol vals Nothing
-          putStrLn $ "\nHistogram: " ++ histPath
-        Just dist -> do
-          histogramWithDensityFile fmt histPath histCfg xCol vals Nothing dist
-          putStrLn $ "\nHistogram + density: " ++ histPath
-      openInBrowser histPath
-
--- ---------------------------------------------------------------------------
--- Group-level prediction helpers
--- ---------------------------------------------------------------------------
-
-invLink :: LinkFn -> Double -> Double
-invLink Identity eta = eta
-invLink Log      eta = exp eta
-invLink Logit    eta = 1.0 / (1.0 + exp (negate eta))
-invLink Sqrt     eta = eta * eta
-
--- | Generate per-group conditional fitted lines for visualization.
--- Only produces data when there is exactly one x column (scatter plot is 2D).
--- Returns [(group, xGrid, ŷ)] evaluated on a 100-point grid over [min(x), max(x)].
-computeGroupLines
-  :: LinkFn
-  -> [Double]           -- fixed coefficients [β₀, β₁, ..., βd]
-  -> [(T.Text, Int)]    -- x column / degree specs (length 1 → draw lines)
-  -> V.Vector T.Text    -- group labels (sorted)
-  -> V.Vector Double    -- BLUPs (same order as group labels)
-  -> V.Vector Double    -- observed x values (used to determine grid range)
-  -> [(T.Text, Double, Double)]
-computeGroupLines lnk coeffs colDegs groups blups xVec =
-  case colDegs of
-    [(_, deg)] | not (V.null xVec) ->
-      let xMin  = V.minimum xVec
-          xMax  = V.maximum xVec
-          nGrid = 100 :: Int
-          grid  = [ xMin + fromIntegral i * (xMax - xMin) / fromIntegral (nGrid - 1)
-                  | i <- [0 .. nGrid - 1] ]
-          b0    = head coeffs
-          bs    = tail coeffs
-          etaAt x = b0 + sum (zipWith (*) bs [x ^ k | k <- [1 .. deg :: Int]])
-      in [ (grp, x, invLink lnk (etaAt x + u))
-         | (grp, u) <- zip (V.toList groups) (V.toList blups)
-         , x <- grid ]
-    _ -> []
-
--- ---------------------------------------------------------------------------
--- Formatting helpers
--- ---------------------------------------------------------------------------
-
-modelLabel :: Family -> LinkFn -> String
-modelLabel dist lnk = show dist ++ "/" ++ show lnk
-
-r2Label :: Family -> String
-r2Label Gaussian = "R²"
-r2Label _        = "McFadden R²"
-
-modelFormula :: [(T.Text, Int)] -> String
-modelFormula colDegs = intercalate " + " (concatMap terms colDegs)
-  where
-    terms (col, deg) =
-      [ T.unpack col ++ if k == 1 then "" else "^" ++ show k
-      | k <- [1 .. deg]
-      ]
-
-multiCoeffLabels :: [(T.Text, Int)] -> [String]
-multiCoeffLabels colDegs = "β₀ (intercept)" : zipWith fmt [1..] rest
-  where
-    rest          = concatMap expand colDegs
-    expand (col, deg) = [(col, k) | k <- [1 .. deg]]
-    fmt i (col, k) =
-      "β" ++ show (i :: Int) ++ " ("
-      ++ T.unpack col
-      ++ (if k == 1 then "" else "^" ++ show k)
-      ++ ")"
-
--- | Generate a human-readable regression equation for single x-column models.
-equationLabel :: Family -> LinkFn -> [(T.Text, Int)] -> [Double] -> Maybe T.Text
-equationLabel _ _ colDegs _ | length colDegs /= 1 = Nothing
-equationLabel _ _ _ coeffs  | null coeffs          = Nothing
-equationLabel fam lnk [(col, deg)] coeffs = Just (T.pack label)
-  where
-    lhs = case (fam, lnk) of
-      (Gaussian, Identity) -> "y"
-      (_, Identity)        -> "E[y]"
-      _                    -> show lnk ++ "(y)"
-
-    b0    = head coeffs
-    betas = tail coeffs
-
-    termStr b k =
-      let sign = if b >= 0 then " + " else " - "
-          xStr = T.unpack col ++ if k == 1 then "" else "^" ++ show k
-      in sign ++ printf "%.4f" (abs b :: Double) ++ xStr
-
-    label = lhs ++ " = " ++ printf "%.4f" b0
-          ++ concat (zipWith termStr betas [1 .. deg])
-equationLabel _ _ _ _ = Nothing
-
--- ---------------------------------------------------------------------------
--- doe subcommand (Phase E1: orthogonal arrays)
--- ---------------------------------------------------------------------------
-
-doeUsage :: String
-doeUsage = unlines
-  [ "Usage: hanalyze doe <action> [args...]"
-  , ""
-  , "Actions:"
-  , "  list                              List available standard arrays"
-  , "  ortho <NAME> [opts]               Output an orthogonal array (L4/L8/L9/L12/L16/L18)"
-  , ""
-  , "ortho options:"
-  , "  -f, --factor NAME=v1,v2,...       Assign a factor with comma-separated levels"
-  , "                                    (repeat for multiple factors; left-to-right = column 1, 2, ...)"
-  , "  --csv | --tsv | --pretty          Output format (default: pretty)"
-  , "  --out FILE                        Write to file instead of stdout"
-  , ""
-  , "Examples:"
-  , "  hanalyze doe list"
-  , "  hanalyze doe ortho L9 --pretty"
-  , "  hanalyze doe ortho L9 -f temp=150,180,210 -f time=10,20,30 -f catalyst=A,B,C --csv"
-  , "  hanalyze doe ortho L8 -f A=low,high -f B=0,1 --out design.tsv --tsv"
-  ]
-
-runDoeCmd :: [String] -> IO ()
-runDoeCmd []                = putStrLn doeUsage
-runDoeCmd ["help"]           = putStrLn doeUsage
-runDoeCmd ["--help"]         = putStrLn doeUsage
-runDoeCmd ("list":_)         = runDoeList
-runDoeCmd ("ortho":rest)     = runDoeOrtho rest
-runDoeCmd (action:_)         =
-  hPutStrLn stderr ("doe: unknown action '" ++ action ++ "'\n" ++ doeUsage)
-
-runDoeList :: IO ()
-runDoeList = do
-  putStrLn "Available standard orthogonal arrays:"
-  mapM_ (\(name, descr) ->
-    printf "  %-16s %s\n" (T.unpack name) (T.unpack descr))
-    OA.listArrays
-  putStrLn ""
-  putStrLn "Use 'hanalyze doe ortho <NAME>' to output a specific array."
-
-data OrthoOpts = OrthoOpts
-  { ooFactors :: [(T.Text, [T.Text])]   -- name → comma-split levels
-  , ooFormat  :: OrthoOutFormat
-  , ooOut     :: Maybe FilePath
-  } deriving (Show)
-
-data OrthoOutFormat = OrthoCSV | OrthoTSV | OrthoPretty deriving (Show, Eq)
-
-defaultOrthoOpts :: OrthoOpts
-defaultOrthoOpts = OrthoOpts [] OrthoPretty Nothing
-
-runDoeOrtho :: [String] -> IO ()
-runDoeOrtho [] = hPutStrLn stderr ("doe ortho: missing array name\n" ++ doeUsage)
-runDoeOrtho (nameStr : rest) =
-  case OA.lookupOA (T.pack nameStr) of
-    Nothing -> hPutStrLn stderr $
-      "doe ortho: unknown array '" ++ nameStr
-      ++ "' (try 'hanalyze doe list')"
-    Just oa -> case parseOrthoOpts rest defaultOrthoOpts of
-      Left err   -> hPutStrLn stderr ("doe ortho: " ++ err)
-      Right opts -> emitOrtho oa opts
-
-parseOrthoOpts :: [String] -> OrthoOpts -> Either String OrthoOpts
-parseOrthoOpts [] acc = Right acc
-parseOrthoOpts (flag : rest) acc
-  | flag `elem` ["-f", "--factor"] = case rest of
-      (v : rest') -> case parseFactorSpec v of
-        Left err  -> Left err
-        Right fac -> parseOrthoOpts rest' (acc { ooFactors = ooFactors acc ++ [fac] })
-      [] -> Left "-f/--factor requires an argument like NAME=v1,v2,..."
-  | flag == "--csv"    = parseOrthoOpts rest (acc { ooFormat = OrthoCSV })
-  | flag == "--tsv"    = parseOrthoOpts rest (acc { ooFormat = OrthoTSV })
-  | flag == "--pretty" = parseOrthoOpts rest (acc { ooFormat = OrthoPretty })
-  | flag == "--out"    = case rest of
-      (v : rest') -> parseOrthoOpts rest' (acc { ooOut = Just v })
-      []          -> Left "--out requires a file path"
-  | otherwise = Left ("unexpected argument '" ++ flag ++ "'")
-
-parseFactorSpec :: String -> Either String (T.Text, [T.Text])
-parseFactorSpec s =
-  case break (== '=') s of
-    (name, '=' : levelsStr) | not (null name), not (null levelsStr) ->
-      let levels = filter (not . T.null) (T.splitOn "," (T.pack levelsStr))
-      in if null levels
-         then Left ("factor '" ++ name ++ "' has no levels (use NAME=v1,v2,...)")
-         else Right (T.pack name, levels)
-    _ -> Left ("invalid factor spec '" ++ s ++ "' (expected NAME=v1,v2,...)")
-
-emitOrtho :: OA.OA -> OrthoOpts -> IO ()
-emitOrtho oa opts =
-  case ooFactors opts of
-    [] -> emitText (renderRaw (ooFormat opts) oa) (ooOut opts)
-    fs -> do
-      let specs = [ OA.FactorSpec name (map toLevelValue levels)
-                  | (name, levels) <- fs ]
-      case OA.assignFactors oa specs of
-        Left err -> hPutStrLn stderr ("doe ortho: " ++ T.unpack err)
-        Right ad -> emitText (renderAssigned (ooFormat opts) ad) (ooOut opts)
-
-toLevelValue :: T.Text -> OA.LevelValue
-toLevelValue t = case reads (T.unpack t) :: [(Double, String)] of
-  [(d, "")] -> OA.LNumeric d
-  _         -> OA.LText t
-
-renderRaw :: OrthoOutFormat -> OA.OA -> T.Text
-renderRaw OrthoCSV    = OA.renderRawCSV
-renderRaw OrthoTSV    = OA.renderRawTSV
-renderRaw OrthoPretty = OA.renderRawPretty
-
-renderAssigned :: OrthoOutFormat -> OA.AssignedDesign -> T.Text
-renderAssigned OrthoCSV    = OA.renderCSV
-renderAssigned OrthoTSV    = OA.renderTSV
-renderAssigned OrthoPretty = OA.renderPretty
-
-emitText :: T.Text -> Maybe FilePath -> IO ()
-emitText txt Nothing     = TIO.putStrLn txt
-emitText txt (Just path) = do
-  TIO.writeFile path txt
-  putStrLn $ "Written: " ++ path
-
--- ---------------------------------------------------------------------------
--- taguchi subcommand (Phase E2: SN ratio + factor effects + inner/outer)
--- ---------------------------------------------------------------------------
-
-taguchiUsage :: String
-taguchiUsage = unlines
-  [ "Usage: hanalyze taguchi <action> [args...]"
-  , ""
-  , "Actions:"
-  , "  sn <type> <values...>             Compute a single SN ratio (dB)"
-  , "                                    type: smaller | larger | nominal | nominal-target=M"
-  , ""
-  , "  analyze <ARRAY> -f F=v1,v2,... [-f ...] --csv FILE [--sntype TYPE] [--report [FILE]]"
-  , "                                    Analyze observations from a CSV file:"
-  , "                                    rows = inner runs, cols (after factor cols) = repetitions/outer."
-  , "                                    Computes per-row SN ratio, factor effects, and optimum levels."
-  , "                                    --report writes an interactive HTML report (default: taguchi.html)."
-  , ""
-  , "  cross <INNER> <OUTER>"
-  , "    -f Fc=v1,v2,...   [-f ...]      Inner control factors"
-  , "    --noise Fn=v1,v2,...  [...]     Outer noise factors"
-  , "    [--out FILE]                    Output the cross-design CSV template"
-  , ""
-  , "SN types:"
-  , "  smaller          smaller-the-better (e.g. defect rate)"
-  , "  larger           larger-the-better (e.g. strength)"
-  , "  nominal          nominal-the-best (mean^2 / variance)"
-  , "  nominal-target=M nominal with target value M"
-  , ""
-  , "Examples:"
-  , "  hanalyze taguchi sn smaller 1.2 1.5 0.9 1.1"
-  , "  hanalyze taguchi analyze L9 -f temp=150,180,210 -f time=10,20,30 -f cat=A,B,C"
-  , "                              --csv runs.csv --sntype smaller"
-  , "  hanalyze taguchi cross L9 L4 -f temp=150,180,210 -f time=10,20,30 -f cat=A,B,C"
-  , "                                --noise humidity=low,high --noise vibration=on,off --out cross.csv"
-  ]
-
-runTaguchiCmd :: [String] -> IO ()
-runTaguchiCmd []                = putStrLn taguchiUsage
-runTaguchiCmd ["help"]           = putStrLn taguchiUsage
-runTaguchiCmd ["--help"]         = putStrLn taguchiUsage
-runTaguchiCmd ("sn":rest)        = runTaguchiSN rest
-runTaguchiCmd ("analyze":rest)   = runTaguchiAnalyze rest
-runTaguchiCmd ("cross":rest)     = runTaguchiCross rest
-runTaguchiCmd (action:_)         =
-  hPutStrLn stderr ("taguchi: unknown action '" ++ action ++ "'\n" ++ taguchiUsage)
-
--- ── sn ──────────────────────────────────────────────────────────────────
-
-runTaguchiSN :: [String] -> IO ()
-runTaguchiSN [] = hPutStrLn stderr "taguchi sn: missing type and values"
-runTaguchiSN (typeStr : valStrs)
-  | null valStrs = hPutStrLn stderr "taguchi sn: need at least one value"
-  | otherwise = case parseSNType typeStr of
-      Left err -> hPutStrLn stderr ("taguchi sn: " ++ err)
-      Right t  ->
-        let vals = mapM readMaybeD valStrs
-        in case vals of
-             Nothing -> hPutStrLn stderr "taguchi sn: non-numeric value(s)"
-             Just xs -> do
-               let eta = TG.snRatio t xs
-               printf "SN(%s) = %.4f dB  (n=%d)\n"
-                      (T.unpack (TG.snTypeName t)) eta (length xs)
-
-parseSNType :: String -> Either String TG.SNType
-parseSNType s = case s of
-  "smaller"           -> Right TG.SmallerBetter
-  "smaller-better"    -> Right TG.SmallerBetter
-  "larger"            -> Right TG.LargerBetter
-  "larger-better"     -> Right TG.LargerBetter
-  "nominal"           -> Right TG.NominalBest
-  "nominal-best"      -> Right TG.NominalBest
-  _ | "nominal-target=" `isPrefixOfStr` s ->
-      case readMaybeD (drop (length ("nominal-target=" :: String)) s) of
-        Just m  -> Right (TG.NominalBestTarget m)
-        Nothing -> Left ("invalid target value in '" ++ s ++ "'")
-  _ -> Left ("unknown SN type '" ++ s
-          ++ "' (try smaller | larger | nominal | nominal-target=M)")
-
-isPrefixOfStr :: String -> String -> Bool
-isPrefixOfStr p s = take (length p) s == p
-
-readMaybeD :: String -> Maybe Double
-readMaybeD s = case reads s :: [(Double, String)] of
-  [(v, "")] -> Just v
-  _         -> Nothing
-
--- ── analyze ─────────────────────────────────────────────────────────────
-
-data TgAnalyzeOpts = TgAnalyzeOpts
-  { toFactors :: [(T.Text, [T.Text])]
-  , toCSV     :: Maybe FilePath
-  , toSN      :: TG.SNType
-  , toReport  :: Maybe FilePath
-  } deriving (Show)
-
-defaultTgAnalyzeOpts :: TgAnalyzeOpts
-defaultTgAnalyzeOpts = TgAnalyzeOpts [] Nothing TG.SmallerBetter Nothing
-
-runTaguchiAnalyze :: [String] -> IO ()
-runTaguchiAnalyze args0 =
-  let (lopts, args) = parseLoadOpts args0
-  in case args of
-       []                  -> hPutStrLn stderr "taguchi analyze: missing array name"
-       (arrayStr : rest)   ->
-         case OA.lookupOA (T.pack arrayStr) of
-           Nothing -> hPutStrLn stderr $
-             "taguchi analyze: unknown array '" ++ arrayStr ++ "'"
-           Just oa -> case parseTgAnalyzeOpts rest defaultTgAnalyzeOpts of
-             Left err   -> hPutStrLn stderr ("taguchi analyze: " ++ err)
-             Right opts -> case toCSV opts of
-               Nothing   -> hPutStrLn stderr "taguchi analyze: --csv FILE required"
-               Just path -> doTaguchiAnalyze oa opts path lopts
-
-parseTgAnalyzeOpts :: [String] -> TgAnalyzeOpts -> Either String TgAnalyzeOpts
-parseTgAnalyzeOpts [] acc = Right acc
-parseTgAnalyzeOpts (flag : rest) acc
-  | flag `elem` ["-f", "--factor"] = case rest of
-      (v : rs) -> case parseFactorSpec v of
-        Left err  -> Left err
-        Right fac -> parseTgAnalyzeOpts rs
-                       (acc { toFactors = toFactors acc ++ [fac] })
-      [] -> Left "-f/--factor requires NAME=v1,v2,..."
-  | flag == "--csv" = case rest of
-      (v : rs) -> parseTgAnalyzeOpts rs (acc { toCSV = Just v })
-      []       -> Left "--csv requires a file path"
-  | flag == "--sntype" = case rest of
-      (v : rs) -> case parseSNType v of
-        Left err  -> Left err
-        Right t   -> parseTgAnalyzeOpts rs (acc { toSN = t })
-      [] -> Left "--sntype requires an argument"
-  | flag == "--report" = case rest of
-      (v : rs) | not (null v) && head v /= '-' ->
-        parseTgAnalyzeOpts rs (acc { toReport = Just v })
-      _ -> parseTgAnalyzeOpts rest (acc { toReport = Just "taguchi.html" })
-  | otherwise = Left ("unexpected argument '" ++ flag ++ "'")
-
-doTaguchiAnalyze :: OA.OA -> TgAnalyzeOpts -> FilePath -> LoadOpts -> IO ()
-doTaguchiAnalyze oa opts path lopts = do
-  result <- loadAutoSafeWith lopts path
-  case result of
-    Left err          -> hPutStrLn stderr ("Parse error: " ++ err)
-    Right (df, lg)    -> do
-      Log.printLogReport lg
-      let specs = [ OA.FactorSpec name (map toLevelValue lvls)
-                  | (name, lvls) <- toFactors opts ]
-      case OA.assignFactors oa specs of
-        Left err -> hPutStrLn stderr (T.unpack err)
-        Right ad -> runAnalyzeWith ad opts df
-
-runAnalyzeWith :: OA.AssignedDesign -> TgAnalyzeOpts -> DXD.DataFrame -> IO ()
-runAnalyzeWith ad opts df = do
-  let factorNames = map OA.fsName (OA.adFactors ad)
-      yCols = filter (\c -> not (c `elem` factorNames) && c /= "Run")
-                     (DX.columnNames df)
-      n = length (OA.adRows ad)
-  when ((fst (DX.dimensions df)) /= n) $
-    hPutStrLn stderr $
-      "Warning: CSV has " ++ show ((fst (DX.dimensions df)))
-      ++ " rows, expected " ++ show n
-  if null yCols
-    then hPutStrLn stderr
-           "taguchi analyze: no observation columns found in CSV"
-    else do
-      -- Per-inner-run observations (skip non-numeric rows)
-      let yMatrix =
-            [ [ case getDoubleVec c df of
-                  Just v | i < V.length v -> v V.! i
-                  _ -> 0
-              | c <- yCols ]
-            | i <- [0 .. min ((fst (DX.dimensions df))) n - 1] ]
-          sns  = TG.snRatioRows (toSN opts) yMatrix
-          fes  = TG.analyzeSN ad sns
-          opts' = TG.optimalLevels fes
-          predEta = TG.predictSN fes sns
-
-      printf "Array:      %s\n" (T.unpack (OA.oaName (OA.adArray ad)))
-      printf "SN type:    %s\n" (T.unpack (TG.snTypeName (toSN opts)))
-      printf "Inner runs: %d\n" n
-      printf "Repetitions per run: %d (columns %s)\n"
-             (length yCols) (T.unpack (T.intercalate ", " yCols))
-      putStrLn ""
-
-      putStrLn "--- Per-run SN ratios ---"
-      mapM_ (\(i, eta) -> printf "  Run %2d:  SN = %8.3f dB\n" (i :: Int) eta)
-            (zip [1..] sns)
-      putStrLn ""
-
-      putStrLn "--- Factor effects (mean SN per level) ---"
-      mapM_ (printFactorEffect opts') fes
-      putStrLn ""
-
-      putStrLn "--- Optimal levels (max SN per factor) ---"
-      mapM_ (\(f, lvl, eta) ->
-        printf "  %-12s = %-12s  (SN = %8.3f dB)\n"
-               (T.unpack f) (T.unpack (lvText lvl)) eta) opts'
-      putStrLn ""
-      printf "Predicted SN at optimum (additive model): %.3f dB\n" predEta
-
-      -- ── HTML レポート出力 (--report 指定時) ─────────────────────────────
-      case toReport opts of
-        Nothing -> return ()
-        Just path -> do
-          let tr = VTG.TaguchiReport
-                     { VTG.trTitle     = "Taguchi Analysis: "
-                                         <> OA.oaName (OA.adArray ad)
-                                         <> " — "
-                                         <> TG.snTypeName (toSN opts)
-                     , VTG.trArrayName = OA.oaName (OA.adArray ad)
-                     , VTG.trSNType    = toSN opts
-                     , VTG.trPerRunSN  = sns
-                     , VTG.trEffects   = fes
-                     , VTG.trOptimal   = opts'
-                     , VTG.trPredicted = predEta
-                     }
-          VTG.renderTaguchiReport path tr
-          putStrLn ("Report: " ++ path)
-          openInBrowser path
-  where
-    lvText (OA.LText t)    = t
-    lvText (OA.LNumeric d)
-      | d == fromIntegral (round d :: Integer) = T.pack (show (round d :: Integer))
-      | otherwise                              = T.pack (printf "%g" d)
-
-printFactorEffect :: [(T.Text, OA.LevelValue, Double)] -> TG.FactorEffect -> IO ()
-printFactorEffect _opts fe = do
-  printf "  %s:\n" (T.unpack (TG.feFactor fe))
-  let pairs = zip (TG.feLevels fe) (TG.feSNByLevel fe)
-  mapM_ (\(lv, eta) ->
-    printf "    %-12s : %8.3f dB\n"
-      (T.unpack (lvShow lv)) eta) pairs
-  where
-    lvShow (OA.LText t)    = t
-    lvShow (OA.LNumeric d)
-      | d == fromIntegral (round d :: Integer) = T.pack (show (round d :: Integer))
-      | otherwise                              = T.pack (printf "%g" d)
-
--- ── cross ───────────────────────────────────────────────────────────────
-
-data TgCrossOpts = TgCrossOpts
-  { tcInner :: [(T.Text, [T.Text])]
-  , tcOuter :: [(T.Text, [T.Text])]
-  , tcOut   :: Maybe FilePath
-  } deriving (Show)
-
-defaultTgCrossOpts :: TgCrossOpts
-defaultTgCrossOpts = TgCrossOpts [] [] Nothing
-
-runTaguchiCross :: [String] -> IO ()
-runTaguchiCross [] = hPutStrLn stderr "taguchi cross: missing INNER and OUTER array names"
-runTaguchiCross [_] = hPutStrLn stderr "taguchi cross: missing OUTER array name"
-runTaguchiCross (innerStr : outerStr : rest) =
-  case (OA.lookupOA (T.pack innerStr), OA.lookupOA (T.pack outerStr)) of
-    (Nothing, _) -> hPutStrLn stderr $
-      "taguchi cross: unknown inner array '" ++ innerStr ++ "'"
-    (_, Nothing) -> hPutStrLn stderr $
-      "taguchi cross: unknown outer array '" ++ outerStr ++ "'"
-    (Just innerOA, Just outerOA) ->
-      case parseTgCrossOpts rest defaultTgCrossOpts of
-        Left err   -> hPutStrLn stderr ("taguchi cross: " ++ err)
-        Right opts -> doTaguchiCross innerOA outerOA opts
-
-parseTgCrossOpts :: [String] -> TgCrossOpts -> Either String TgCrossOpts
-parseTgCrossOpts [] acc = Right acc
-parseTgCrossOpts (flag : rest) acc
-  | flag `elem` ["-f", "--factor"] = case rest of
-      (v : rs) -> case parseFactorSpec v of
-        Left err  -> Left err
-        Right fac -> parseTgCrossOpts rs (acc { tcInner = tcInner acc ++ [fac] })
-      [] -> Left "-f/--factor requires NAME=v1,v2,..."
-  | flag `elem` ["-fn", "--noise"] = case rest of
-      (v : rs) -> case parseFactorSpec v of
-        Left err  -> Left err
-        Right fac -> parseTgCrossOpts rs (acc { tcOuter = tcOuter acc ++ [fac] })
-      [] -> Left "-fn/--noise requires NAME=v1,v2,..."
-  | flag == "--out" = case rest of
-      (v : rs) -> parseTgCrossOpts rs (acc { tcOut = Just v })
-      []       -> Left "--out requires a file path"
-  | otherwise = Left ("unexpected argument '" ++ flag ++ "'")
-
-doTaguchiCross :: OA.OA -> OA.OA -> TgCrossOpts -> IO ()
-doTaguchiCross innerOA outerOA opts = do
-  let innerSpecs = [ OA.FactorSpec n (map toLevelValue ls)
-                   | (n, ls) <- tcInner opts ]
-      outerSpecs = [ OA.FactorSpec n (map toLevelValue ls)
-                   | (n, ls) <- tcOuter opts ]
-  case (OA.assignFactors innerOA innerSpecs,
-        OA.assignFactors outerOA outerSpecs) of
-    (Left err, _) -> hPutStrLn stderr ("inner: " ++ T.unpack err)
-    (_, Left err) -> hPutStrLn stderr ("outer: " ++ T.unpack err)
-    (Right ai, Right ao) -> do
-      let io = TG.makeInnerOuter ai ao
-          csv = TG.renderInnerOuterCSV io
-      emitText csv (tcOut opts)
-
--- ---------------------------------------------------------------------------
--- ridge / kernel / spline 共通ヘルパ
--- ---------------------------------------------------------------------------
-
--- | CSV を読み、x 列(複数可) と y 列(1) を numeric vector で取り出す。
--- 'LoadOpts' を反映 (--no-header / --skip / --comment / --strict)。
-loadXY :: LoadOpts -> FilePath -> [T.Text] -> T.Text
-       -> IO (Either String (DXD.DataFrame, [V.Vector Double], V.Vector Double))
-loadXY lopts path xCols yCol = do
-  result <- loadAutoSafeWith lopts path
-  case result of
-    Left err          -> return (Left err)
-    Right (df, lg)    -> do
-      Log.printLogReport lg
-      case (mapM (\c -> getDoubleVec c df) xCols, getDoubleVec yCol df) of
-        (Just xs, Just y) -> return (Right (df, xs, y))
-        _ -> return (Left $ "Numeric column(s) not found: x="
-                      ++ T.unpack (T.intercalate "," xCols)
-                      ++ ", y=" ++ T.unpack yCol)
-
--- | RMSE 計算。
-rmseV :: [Double] -> [Double] -> Double
-rmseV ys yhat =
-  let n = length ys
-      sse = sum [ (a - b) ^ (2 :: Int) | (a, b) <- zip ys yhat ]
-  in sqrt (sse / fromIntegral (max 1 n))
-
--- | 散布図 + 滑らか曲線 を出力。
-writeSmoothPlot :: OutputFormat -> FilePath -> T.Text
-                -> DXD.DataFrame -> T.Text -> T.Text -> SmoothFit -> IO ()
-writeSmoothPlot fmt path titleSuffix df xc yc sf =
-  scatterWithSmoothFile fmt path
-    (defaultConfig (xc <> " vs " <> yc <> "  [" <> titleSuffix <> "]"))
-    Nothing df xc yc sf
-
--- | xMin/xMax から評価グリッドを作る。
-makeGrid :: V.Vector Double -> Int -> [Double]
-makeGrid xs n =
-  let lo = V.minimum xs
-      hi = V.maximum xs
-  in [ lo + fromIntegral i * (hi - lo) / fromIntegral (n - 1)
-     | i <- [0 .. n - 1] ]
-
--- ---------------------------------------------------------------------------
--- ridge subcommand (Ridge / Lasso / Elastic Net)
--- ---------------------------------------------------------------------------
-
-ridgeUsage :: String
-ridgeUsage = unlines
-  [ "Usage: hanalyze ridge <file> <xcols> <ycol> [options]"
-  , ""
-  , "  <xcols>   x column name(s); quote multiple: \"x1 x2\""
-  , "  <ycol>    y column name (single)"
-  , ""
-  , "Options:"
-  , "  --penalty TYPE   ridge|lasso|elasticnet (default: ridge)"
-  , "  --lambda L       regularization strength (default: 0.1)"
-  , "  --alpha A        ElasticNet L1 mixing in [0,1] (default: 0.5; only with --penalty elasticnet)"
-  , "  --format FMT     html|png|svg (default: html)"
-  , "  --out FILE       scatter+fit output path (default: ridge.html; single x only)"
-  , "  --report [FILE]  build composite HTML report (default: ridge.html)"
-  , ""
-  , "Examples:"
-  , "  hanalyze ridge data.csv x y --lambda 0.1"
-  , "  hanalyze ridge data.csv \"x1 x2 x3\" y --penalty lasso --lambda 0.05"
-  , "  hanalyze ridge data.csv \"x1 x2\" y --penalty elasticnet --lambda 0.1 --alpha 0.5"
-  ]
-
-data RidgeOpts = RidgeOpts
-  { roPenalty :: T.Text   -- "ridge" / "lasso" / "elasticnet"
-  , roLambda  :: Double
-  , roAlpha   :: Double
-  , roFormat  :: OutputFormat
-  , roOut     :: FilePath
-  , roReport  :: Maybe FilePath
-  }
-
-defaultRidgeOpts :: RidgeOpts
-defaultRidgeOpts = RidgeOpts "ridge" 0.1 0.5 HTML "ridge.html" Nothing
-
-runRidgeCmd :: [String] -> IO ()
-runRidgeCmd args0 =
-  let (lopts, args) = parseLoadOpts args0
-  in case args of
-       (file : xColsStr : yColStr : rest) ->
-         case parseRidgeOpts rest defaultRidgeOpts of
-           Left err   -> hPutStrLn stderr ("ridge: " ++ err)
-           Right opts -> doRidge file xColsStr yColStr opts lopts
-       _ -> putStrLn ridgeUsage
-
-parseRidgeOpts :: [String] -> RidgeOpts -> Either String RidgeOpts
-parseRidgeOpts [] acc = Right acc
-parseRidgeOpts (flag : rest) acc
-  | flag == "--penalty" = case rest of
-      (v : rs) | v `elem` ["ridge","lasso","elasticnet"] ->
-        parseRidgeOpts rs (acc { roPenalty = T.pack v })
-      (v : _) -> Left ("unknown penalty '" ++ v ++ "'")
-      []      -> Left "--penalty requires an argument"
-  | flag == "--lambda" = case rest of
-      (v:rs) -> case reads v :: [(Double, String)] of
-        [(d,"")] -> parseRidgeOpts rs (acc { roLambda = d })
-        _        -> Left ("invalid --lambda value '" ++ v ++ "'")
-      []     -> Left "--lambda requires a value"
-  | flag == "--alpha" = case rest of
-      (v:rs) -> case reads v :: [(Double, String)] of
-        [(d,"")] -> parseRidgeOpts rs (acc { roAlpha = d })
-        _        -> Left ("invalid --alpha value '" ++ v ++ "'")
-      []     -> Left "--alpha requires a value"
-  | flag `elem` ["-f","--format"] = case rest of
-      (v:rs) -> case parseFormat v of
-        Right f -> parseRidgeOpts rs (acc { roFormat = f })
-        Left e  -> Left e
-      []     -> Left "--format requires an argument"
-  | flag == "--out" = case rest of
-      (v:rs) -> parseRidgeOpts rs (acc { roOut = v })
-      []     -> Left "--out requires a file path"
-  | flag == "--report" = case rest of
-      (v:rs) | not (null v) && head v /= '-' ->
-        parseRidgeOpts rs (acc { roReport = Just v })
-      _ -> parseRidgeOpts rest (acc { roReport = Just "ridge.html" })
-  | otherwise = Left ("unexpected argument '" ++ flag ++ "'")
-
-doRidge :: FilePath -> String -> String -> RidgeOpts -> LoadOpts -> IO ()
-doRidge file xColsStr yColStr opts lopts = do
-  let xCols = map T.pack (words xColsStr)
-      yCol  = T.pack yColStr
-  result <- loadXY lopts file xCols yCol
-  case result of
-    Left err -> hPutStrLn stderr err
-    Right (df, xVecs, yVec) -> do
-      let n        = V.length yVec
-          intercept = LA.konst 1 n
-          xMat     = LA.fromColumns
-                       (intercept : map (LA.fromList . V.toList) xVecs)
-          yLA      = LA.fromList (V.toList yVec)
-          pen      = case roPenalty opts of
-            "ridge"      -> Reg.L2 (roLambda opts)
-            "lasso"      -> Reg.L1 (roLambda opts)
-            "elasticnet" -> Reg.ElasticNet
-                             (roLambda opts * roAlpha opts)
-                             (roLambda opts * (1 - roAlpha opts))
-            _            -> Reg.L2 (roLambda opts)
-          fit      = Reg.fitRegularized pen xMat yLA
-          beta     = LA.toList (Reg.rfBeta fit)
-          yhat     = LA.toList (Reg.rfYHat fit)
-          ys       = V.toList yVec
-          rmseVal  = rmseV ys yhat
-      printf "Loaded %d rows from %s\n" n file
-      printf "Penalty: %s, lambda=%g%s\n"
-             (T.unpack (roPenalty opts)) (roLambda opts)
-             (if roPenalty opts == "elasticnet"
-                then ", alpha=" ++ show (roAlpha opts) else "")
-      putStrLn ""
-      putStrLn "Coefficients:"
-      printf "  %-30s = %9.4f\n" ("intercept" :: String) (head beta)
-      mapM_ (\(i, c, b) ->
-        printf "  %-30s = %9.4f\n"
-               ("β_" ++ show (i :: Int) ++ " (" ++ T.unpack c ++ ")") b)
-        (zip3 [1..] xCols (tail beta))
-      printf "R²  = %.4f\n" (Reg.rfR2 fit)
-      printf "|β| > 1e-8: %d / %d (sparsity)\n"
-             (Reg.rfNonZero fit) (length beta)
-      printf "RMSE (in-sample) = %.4f\n" rmseVal
-      -- 単純散布図 + 予測曲線 (1 変数のみ)
-      let coeffPairs = zip ("intercept" : xCols)
-                           (map T.pack (map (printf "%.4f") beta) :: [T.Text])
-          coeffNumPairs = zip ("intercept" : xCols) beta
-          residuals = LA.toList (Reg.rfResid fit)
-      case xCols of
-        [xc1] -> do
-          let xs = V.toList (head xVecs)
-              grid = makeGrid (head xVecs) 100
-              gridMat = LA.fromColumns
-                          [ LA.konst 1 100
-                          , LA.fromList grid ]
-              gridY = LA.toList (Reg.predictRegularized fit gridMat)
-              sf = SmoothFit
-                     { sfX = grid
-                     , sfFit = gridY
-                     , sfLower = []
-                     , sfUpper = []
-                     , sfHasBand = False
-                     }
-              _ = xs
-              _ = coeffPairs
-          writeSmoothPlot (roFormat opts) (roOut opts)
-            (T.pack ("Regularized: " ++ T.unpack (roPenalty opts)))
-            df xc1 yCol sf
-          putStrLn ("Plot: " ++ roOut opts)
-          openInBrowser (roOut opts)
-          -- HTML レポート
-          case roReport opts of
-            Nothing -> return ()
-            Just rpath -> do
-              let smooth = RB.SmoothCurve grid gridY [] []
-                  pathSec = mkRidgePathSection xCols xMat yLA opts
-                  cfg = ridgeReportConfig opts xCols yCol
-                  sections =
-                    [ RB.secDataOverview df xCols yCol
-                    , RB.secModelOverview (ridgeModelLabel opts)
-                        (ridgeFormula opts xCols yCol) Nothing
-                    , RB.secCoefficients coeffNumPairs (Just ("R²", Reg.rfR2 fit))
-                    , RB.secKeyValue "Fit summary"
-                        (ridgeFitKVs opts fit beta rmseVal)
-                    , pathSec
-                    , RB.secFitScatter xc1 yCol xs ys (Just smooth)
-                    , RB.secResiduals yhat residuals
-                    ]
-              RB.renderReport rpath cfg sections
-              putStrLn ("Report: " ++ rpath)
-              openInBrowser rpath
-        _ -> do
-          putStrLn "(scatter plot skipped for multiple x columns)"
-          case roReport opts of
-            Nothing -> return ()
-            Just rpath -> do
-              let pathSec = mkRidgePathSection xCols xMat yLA opts
-                  cfg = ridgeReportConfig opts xCols yCol
-                  sections =
-                    [ RB.secDataOverview df xCols yCol
-                    , RB.secModelOverview (ridgeModelLabel opts)
-                        (ridgeFormula opts xCols yCol) Nothing
-                    , RB.secCoefficients coeffNumPairs (Just ("R²", Reg.rfR2 fit))
-                    , RB.secKeyValue "Fit summary"
-                        (ridgeFitKVs opts fit beta rmseVal)
-                    , pathSec
-                    , RB.secResiduals yhat residuals
-                    ]
-              RB.renderReport rpath cfg sections
-              putStrLn ("Report: " ++ rpath)
-              openInBrowser rpath
-
--- ---------------------------------------------------------------------------
--- kernel subcommand (Nadaraya-Watson / Kernel Ridge / RFF)
--- ---------------------------------------------------------------------------
-
-kernelUsage :: String
-kernelUsage = unlines
-  [ "Usage: hanalyze kernel <file> <xcol> <ycol> [options]"
-  , ""
-  , "Options:"
-  , "  --method M        nw|kr|rff (default: kr)"
-  , "                    nw  = Nadaraya-Watson"
-  , "                    kr  = Kernel Ridge"
-  , "                    rff = Random Fourier Features (RBF)"
-  , "  --kernel KIND     gaussian|epanechnikov|triangular|tricube|uniform"
-  , "                    (default: gaussian; ignored for --method rff)"
-  , "  --bandwidth H     kernel bandwidth h (default: auto via LOO-CV grid)"
-  , "  --lambda L        ridge regularization (default: 0.01; for kr / rff only)"
-  , "  --features D      RFF feature dimension (default: 200; --method rff only)"
-  , "  --format FMT      html|png|svg (default: html)"
-  , "  --out FILE        scatter+fit output path (default: kernel.html)"
-  , "  --report [FILE]   build composite HTML report (default: kernel.html)"
-  , ""
-  , "Multivariate RFF (--method rff with multiple x columns):"
-  , "  --group COL       group column for color-coded scatter+fit (e.g. name)"
-  , "  --xaxis COL       column to use as horizontal axis in the plot (e.g. t)"
-  , "  --interactive     スライダで副軸を変えると JS が予測曲線を再計算"
-  , "                    (--report と併用、--xaxis の列以外がスライダになる)"
-  , "  --standardize     入力 X を z-score 化してから fit (スケール差対策)"
-  , "  --auto-hp         HP 自動決定 (default method=loocv)"
-  , "  --auto-hp-method M  loocv (Ridge LOOCV 解析解、推奨) | mlik (周辺尤度最大化)"
-  , "                    (--bandwidth / --lambda は無視される)"
-  , ""
-  , "Examples:"
-  , "  hanalyze kernel data.csv x y --method kr --bandwidth 0.5"
-  , "  hanalyze kernel data.csv x y --method nw   # auto-bandwidth via LOO-CV"
-  , "  hanalyze kernel data.csv x y --method rff --features 200"
-  , "  # 多変量 RFF (melted データに対して):"
-  , "  hanalyze kernel data/io/melted_sample.csv \"x1 t\" y --method rff \\"
-  , "      --features 200 --bandwidth 1.0 --lambda 0.001 \\"
-  , "      --group name --xaxis t --out plot.html"
-  ]
-
-data KernelOpts = KernelOpts
-  { koMethod    :: T.Text       -- "nw" / "kr" / "rff"
-  , koKernel    :: Kern.Kernel  -- Gaussian / Epanechnikov / ...
-  , koBandwidth :: Maybe Double
-  , koLambda    :: Double
-  , koFeatures  :: Int
-  , koFormat    :: OutputFormat
-  , koOut       :: FilePath
-  , koReport    :: Maybe FilePath
-  , koGroup     :: Maybe T.Text  -- 多変量 RFF プロット用 group 列
-  , koXAxis     :: Maybe T.Text  -- 多変量 RFF プロット用 横軸列名
-  , koInteractive :: Bool        -- インタラクティブ予測 (--report と併用)
-  , koStandardize :: Bool        -- 入力標準化 (Phase 4)
-  , koAutoHP      :: Bool        -- HP 自動決定
-  , koAutoHPMethod :: T.Text     -- "loocv" / "mlik" (default loocv = 速い)
-  }
-
-defaultKernelOpts :: KernelOpts
-defaultKernelOpts = KernelOpts
-  { koMethod    = "kr"
-  , koKernel    = Kern.Gaussian
-  , koBandwidth = Nothing
-  , koLambda    = 0.01
-  , koFeatures  = 200
-  , koFormat    = HTML
-  , koOut       = "kernel.html"
-  , koReport    = Nothing
-  , koGroup     = Nothing
-  , koXAxis     = Nothing
-  , koInteractive = False
-  , koStandardize = False
-  , koAutoHP      = False
-  , koAutoHPMethod = "loocv"
-  }
-
-parseKernelKind :: String -> Either String Kern.Kernel
-parseKernelKind s = case s of
-  "gaussian"     -> Right Kern.Gaussian
-  "epanechnikov" -> Right Kern.Epanechnikov
-  "triangular"   -> Right Kern.Triangular
-  "tricube"      -> Right Kern.TriCube
-  "uniform"      -> Right Kern.Uniform
-  _              -> Left ("unknown kernel '" ++ s ++ "'")
-
-runKernelCmd :: [String] -> IO ()
-runKernelCmd args0 =
-  let (lopts, args) = parseLoadOpts args0
-  in case args of
-       (file : xColStr : yColStr : rest) ->
-         case parseKernelOpts rest defaultKernelOpts of
-           Left err   -> hPutStrLn stderr ("kernel: " ++ err)
-           Right opts -> doKernel file xColStr yColStr opts lopts
-       _ -> putStrLn kernelUsage
-
-parseKernelOpts :: [String] -> KernelOpts -> Either String KernelOpts
-parseKernelOpts [] acc = Right acc
-parseKernelOpts (flag : rest) acc
-  | flag == "--method" = case rest of
-      (v:rs) | v `elem` ["nw","kr","rff"] ->
-        parseKernelOpts rs (acc { koMethod = T.pack v })
-      (v:_) -> Left ("unknown method '" ++ v ++ "'")
-      []    -> Left "--method requires an argument"
-  | flag == "--kernel" = case rest of
-      (v:rs) -> case parseKernelKind v of
-        Right k -> parseKernelOpts rs (acc { koKernel = k })
-        Left e  -> Left e
-      []     -> Left "--kernel requires an argument"
-  | flag == "--bandwidth" = case rest of
-      (v:rs) -> case reads v :: [(Double, String)] of
-        [(d,"")] -> parseKernelOpts rs (acc { koBandwidth = Just d })
-        _        -> Left ("invalid --bandwidth '" ++ v ++ "'")
-      []     -> Left "--bandwidth requires a value"
-  | flag == "--lambda" = case rest of
-      (v:rs) -> case reads v :: [(Double, String)] of
-        [(d,"")] -> parseKernelOpts rs (acc { koLambda = d })
-        _        -> Left ("invalid --lambda '" ++ v ++ "'")
-      []     -> Left "--lambda requires a value"
-  | flag == "--features" = case rest of
-      (v:rs) -> case reads v :: [(Int, String)] of
-        [(d,"")] -> parseKernelOpts rs (acc { koFeatures = d })
-        _        -> Left ("invalid --features '" ++ v ++ "'")
-      []     -> Left "--features requires a value"
-  | flag `elem` ["-f","--format"] = case rest of
-      (v:rs) -> case parseFormat v of
-        Right f -> parseKernelOpts rs (acc { koFormat = f })
-        Left e  -> Left e
-      []     -> Left "--format requires an argument"
-  | flag == "--out" = case rest of
-      (v:rs) -> parseKernelOpts rs (acc { koOut = v })
-      []     -> Left "--out requires a file path"
-  | flag == "--report" = case rest of
-      (v:rs) | not (null v) && head v /= '-' ->
-        parseKernelOpts rs (acc { koReport = Just v })
-      _ -> parseKernelOpts rest (acc { koReport = Just "kernel.html" })
-  | flag == "--group" = case rest of
-      (v:rs) -> parseKernelOpts rs (acc { koGroup = Just (T.pack v) })
-      []     -> Left "--group requires a column name"
-  | flag == "--xaxis" = case rest of
-      (v:rs) -> parseKernelOpts rs (acc { koXAxis = Just (T.pack v) })
-      []     -> Left "--xaxis requires a column name"
-  | flag == "--interactive" =
-      parseKernelOpts rest (acc { koInteractive = True })
-  | flag == "--standardize" =
-      parseKernelOpts rest (acc { koStandardize = True })
-  | flag == "--auto-hp" =
-      parseKernelOpts rest (acc { koAutoHP = True })
-  | flag == "--auto-hp-method" = case rest of
-      (v:rs) | v `elem` ["loocv", "mlik"] ->
-        parseKernelOpts rs (acc { koAutoHP = True
-                                , koAutoHPMethod = T.pack v })
-      (v:_) -> Left ("unknown --auto-hp-method '" ++ v ++ "' (choose loocv|mlik)")
-      []    -> Left "--auto-hp-method requires loocv|mlik"
-  | otherwise = Left ("unexpected argument '" ++ flag ++ "'")
-
-doKernel :: FilePath -> String -> String -> KernelOpts -> LoadOpts -> IO ()
-doKernel file xColStr yColStr opts lopts = do
-  let xCols = map T.pack (words xColStr)
-      yCol  = T.pack yColStr
-  case xCols of
-    []       -> hPutStrLn stderr "kernel: x 列が指定されていません"
-    [xCol]   -> do
-      result <- loadXY lopts file [xCol] yCol
-      case result of
-        Left err -> hPutStrLn stderr err
-        Right (df, [xVec], yVec) ->
-          runKernelOn df xCol yCol xVec yVec opts
-        Right _ -> hPutStrLn stderr "kernel: expected single x column"
-    _multiple -> case koMethod opts of
-      "rff" -> do
-        result <- loadXY lopts file xCols yCol
-        case result of
-          Left err -> hPutStrLn stderr err
-          Right (df, xVecs, yVec) ->
-            runKernelMV df xCols yCol xVecs yVec opts
-      "kr"  -> do
-        result <- loadXY lopts file xCols yCol
-        case result of
-          Left err -> hPutStrLn stderr err
-          Right (_, xVecs, yVec) ->
-            runKernelMVKR xCols yCol xVecs yVec opts
-      "nw"  -> do
-        result <- loadXY lopts file xCols yCol
-        case result of
-          Left err -> hPutStrLn stderr err
-          Right (_, xVecs, yVec) ->
-            runKernelMVNW xCols yCol xVecs yVec opts
-      m -> hPutStrLn stderr $
-        "kernel --method " ++ T.unpack m
-          ++ " (unknown method)"
-
--- | Multi-input Kernel Ridge (Phase K5) — fit and report training metrics.
--- 多次元 X (n×p) を取り、Hanalyze.Model.KernelRegression.kernelRidgeMV で fit。
--- 予測図は生成しない (多次元のため)、R² と RMSE をログ出力。
-runKernelMVKR
-  :: [T.Text] -> T.Text -> [V.Vector Double] -> V.Vector Double
-  -> KernelOpts -> IO ()
-runKernelMVKR xCols _yCol xVecs yVec opts = do
-  let n     = V.length yVec
-      p     = length xCols
-      xMat  = LA.fromColumns (map (LA.fromList . V.toList) xVecs)
-      yMat  = LA.asColumn (LA.fromList (V.toList yVec))
-      ker   = koKernel opts
-      h     = case koBandwidth opts of
-                Just b  -> b
-                Nothing -> 1.0
-      lam   = koLambda opts
-      fit   = Kern.kernelRidgeMV ker h lam xMat yMat
-      yhat  = Kern.fittedKernelRidgeMV fit
-      ss    = LA.sumElements ((yMat - yhat) ** 2)
-      muY   = LA.sumElements yMat / fromIntegral n
-      stTot = LA.sumElements ((yMat - LA.konst muY (n, 1)) ** 2)
-      r2    = 1 - ss / stTot
-      rmse  = sqrt (ss / fromIntegral n)
-  printf "Loaded %d rows × %d features (%s); method=kr (multivariate)\n"
-         n p (T.unpack (T.intercalate "," xCols))
-  printf "  bandwidth h = %.4g, lambda = %.4g, kernel = %s\n"
-         h lam (show ker)
-  printf "  R² (train) = %.4f\n" r2
-  printf "  RMSE (train) = %.4f\n" rmse
-
--- | Multi-input Nadaraya-Watson (Phase K5) — fit and report training metrics.
-runKernelMVNW
-  :: [T.Text] -> T.Text -> [V.Vector Double] -> V.Vector Double
-  -> KernelOpts -> IO ()
-runKernelMVNW xCols _yCol xVecs yVec opts = do
-  let n     = V.length yVec
-      p     = length xCols
-      xMat  = LA.fromColumns (map (LA.fromList . V.toList) xVecs)
-      yMat  = LA.asColumn (LA.fromList (V.toList yVec))
-      ker   = koKernel opts
-      h     = case koBandwidth opts of
-                Just b  -> b
-                Nothing -> 1.0
-      yhat  = Kern.nwRegressionMV ker h xMat yMat xMat
-      ss    = LA.sumElements ((yMat - yhat) ** 2)
-      muY   = LA.sumElements yMat / fromIntegral n
-      stTot = LA.sumElements ((yMat - LA.konst muY (n, 1)) ** 2)
-      r2    = 1 - ss / stTot
-      rmse  = sqrt (ss / fromIntegral n)
-  printf "Loaded %d rows × %d features (%s); method=nw (multivariate)\n"
-         n p (T.unpack (T.intercalate "," xCols))
-  printf "  bandwidth h = %.4g, kernel = %s\n" h (show ker)
-  printf "  R² (train) = %.4f\n" r2
-  printf "  RMSE (train) = %.4f\n" rmse
-
--- | 多変量 RFF Ridge を走らせる (Phase B-RFF)。
--- '--group' / '--xaxis' が指定されていれば、グループ別観測点 + 予測曲線の
--- 散布図を出力する。
--- '--standardize' / '--auto-hp' で前処理 / HP 自動決定。
-runKernelMV
-  :: DXD.DataFrame -> [T.Text] -> T.Text
-  -> [V.Vector Double] -> V.Vector Double
-  -> KernelOpts -> IO ()
-runKernelMV df xCols yCol xVecs yVec opts = do
-  let n = V.length yVec
-      p = length xCols
-      cols   = map V.toList xVecs
-      xMatRaw = LA.fromColumns (map LA.fromList cols)
-      ys     = V.toList yVec
-      yV     = LA.fromList ys
-  printf "Loaded %d rows × %d features (%s); method=rff (multivariate)\n"
-         n p (T.unpack (T.intercalate "," xCols))
-
-  -- ステップ 1: 標準化 (タイマー付き)
-  (tStd, (stdr, xMat)) <- timed $ do
-    let s = if koStandardize opts
-              then Std.fitStandardizer xMatRaw
-              else Std.identityStandardizer p
-        xm = if koStandardize opts
-               then Std.applyStandardizer s xMatRaw
-               else xMatRaw
-    return (s, xm)
-  if koStandardize opts
-    then do
-      putStrLn "  Standardize: ON"
-      printf "    μ = [%s]\n" (T.unpack (T.intercalate ", " (map NF.fmtNumT (Std.stMu stdr))))
-      printf "    σ = [%s]\n" (T.unpack (T.intercalate ", " (map NF.fmtNumT (Std.stSd stdr))))
-    else putStrLn "  Standardize: OFF"
-
-  -- ステップ 2: HP の決定 (タイマー付き)
-  hpGen <- createSystemRandom
-  (tHP, (ell, lam, sigF)) <- timed $
-    if koAutoHP opts
-      then case koAutoHPMethod opts of
-        "loocv" -> do
-          putStrLn "  Auto-HP (LOOCV): RFF Ridge の解析的 LOO を最小化中..."
-          res <- RFF.gridSearchLOOCVRBFMV p (koFeatures opts) xMat yV Nothing hpGen
-          let ellOpt = RFF.lcEll res
-              sfOpt  = RFF.lcSigmaF res
-              lamOpt = RFF.lcLambda res
-          ellOpt `seq` sfOpt `seq` lamOpt `seq` return ()
-          printf "    ℓ      = %s\n" (NF.fmtNum ellOpt)
-          printf "    σ_f    = %s\n" (NF.fmtNum sfOpt)
-          printf "    λ      = %s\n" (NF.fmtNum lamOpt)
-          printf "    LOOCV  = %s  (グリッド %d 点評価)\n"
-                 (NF.fmtNum (RFF.lcLOOCV res)) (RFF.lcGridPts res)
-          return (ellOpt, lamOpt, sfOpt)
-        _ -> do  -- "mlik"
-          putStrLn "  Auto-HP (周辺尤度): Cholesky で marg-lik を最大化中..."
-          let res = RFF.maximizeMarginalLikRBFMV xMat yV Nothing
-              ellOpt = RFF.mlEll res
-              sfOpt  = RFF.mlSigmaF res
-              snOpt  = RFF.mlSigmaN res
-              lamOpt = snOpt * snOpt
-          ellOpt `seq` sfOpt `seq` snOpt `seq` return ()
-          printf "    ℓ      = %s\n" (NF.fmtNum ellOpt)
-          printf "    σ_f    = %s\n" (NF.fmtNum sfOpt)
-          printf "    σ_n    = %s  (λ = σ_n² = %s)\n"
-                 (NF.fmtNum snOpt) (NF.fmtNum lamOpt)
-          printf "    log_mlik = %s  (グリッド %d 点評価)\n"
-                 (NF.fmtNum (RFF.mlLogMlik res)) (RFF.mlGridPts res)
-          return (ellOpt, lamOpt, sfOpt)
-      else do
-        let ell0 = case koBandwidth opts of
-              Just h  -> h
-              Nothing -> defaultLengthScale (map LA.toList (LA.toColumns xMat))
-        printf "  ell=%s  lambda=%s\n"
-               (NF.fmtNum ell0) (NF.fmtNum (koLambda opts))
-        return (ell0, koLambda opts, 1.0)
-
-  let d = koFeatures opts
-  printf "  D=%d\n" d
-
-  -- ステップ 3: RFF サンプリング + Ridge fit + 評価 (各タイマー付き)
-  gen   <- createSystemRandom
-  (tSample, feats) <- timed (RFF.sampleRFFRBFMV p d ell sigF gen)
-  (tFit, fit) <- timed $ do
-    let f = RFF.rffRidgeMV feats xMat ys lam
-    LA.size (RFF.rffrmvWeights f) `seq` return f
-  let yhat = RFF.predictRFFRidgeMV fit xMat
-      sse  = sum (zipWith (\a b -> (a - b)^(2::Int)) ys yhat)
-      sst  = let m = sum ys / fromIntegral (max 1 (length ys))
-             in sum [(y - m)^(2::Int) | y <- ys]
-      r2   = if sst < 1e-12 then 0 else 1 - sse / sst
-  printf "RFF (multivariate) Ridge fit:\n"
-  printf "  R^2 = %s\n" (NF.fmtNum r2)
-  printf "  RMSE = %s\n" (NF.fmtNum (sqrt (sse / fromIntegral n)))
-
-  putStrLn ""
-  putStrLn "Profiling (cumulative wall time):"
-  printPhase "Standardize" tStd
-  printPhase "Auto-HP" tHP
-  printPhase "Sample RFF" tSample
-  printPhase "Fit Ridge" tFit
-
-  -- --group + --xaxis が両方指定されていればプロット
-  case (koGroup opts, koXAxis opts) of
-    (Just gCol, Just xCol) -> do
-      let outPath = koOut opts
-          fmt     = koFormat opts
-      (tPlot, _) <- timed (writeMVPlot fmt outPath df gCol xCol xCols yCol fit stdr cols ys)
-      putStrLn $ "Plot: " ++ outPath
-      printPhase "Plot" tPlot
-      -- --report 指定時は ReportBuilder で統合 HTML を出力
-      case koReport opts of
-        Just rpath -> do
-          (tRep, _) <- timed $ do
-            let rep    = RI.RFFMVReport
-                          { RI.rfmvFit         = fit
-                          , RI.rfmvGroup       = gCol
-                          , RI.rfmvXAxis       = xCol
-                          , RI.rfmvInteractive = koInteractive opts
-                          , RI.rfmvStandardizer =
-                              if koStandardize opts then Just stdr else Nothing
-                          }
-                cfg    = RB.defaultReportConfig
-                          (yCol <> " — Multivariate RFF Ridge"
-                              <> if koInteractive opts then " (interactive)" else "")
-                secs   = RB.toReport cfg df xCols yCol rep
-            RB.renderReport rpath cfg secs
-          putStrLn $ "Report: " ++ rpath
-          printPhase "Render report" tRep
-        Nothing -> return ()
-    _ -> putStrLn
-      "Plot skipped (use --group COL --xaxis COL to draw scatter+fit by group)"
-
--- | name (group) ごとに観測点と予測曲線をプロット。
--- 標準化 ON のときは予測グリッドを raw → 標準化空間に変換してから predict。
--- 横軸 / 観測点は raw 単位で表示する。
-writeMVPlot
-  :: OutputFormat -> FilePath
-  -> DXD.DataFrame
-  -> T.Text -> T.Text -> [T.Text] -> T.Text
-  -> RFF.RFFRidgeFitMV
-  -> Std.Standardizer
-  -> [[Double]]             -- ^ raw cols
-  -> [Double]
-  -> IO ()
-writeMVPlot fmt path df gCol xCol xCols yCol fit stdr cols ys = do
-  case getMaybeTextVec gCol df of
-    Nothing -> hPutStrLn stderr $
-      "plot: group column '" ++ T.unpack gCol ++ "' not found"
-    Just gv ->
-      let groups = [ maybe "" id g | g <- V.toList gv ]
-          xColIdx = case [ i | (i, c) <- zip [0..] xCols, c == xCol ] of
-                      (i:_) -> i
-                      []    -> 0
-          xValuesAll = cols !! xColIdx
-          xMin = minimum xValuesAll
-          xMax = maximum xValuesAll
-          ngrid = 100
-          xGrid = [ xMin + fromIntegral i * (xMax - xMin) / fromIntegral (ngrid - 1)
-                  | i <- [0 .. ngrid - 1] ]
-          ptData = zip3 groups xValuesAll ys
-          uniqGroups = uniq groups
-          rowsForGroup g = [ i | (i, gg) <- zip [0..] groups, gg == g ]
-          repValues g = [ (cols !! j) !! head (rowsForGroup g)
-                        | j <- [0 .. length xCols - 1] ]
-          mkLineData g =
-            let rep = repValues g
-                -- raw 値で row を組む
-                makeRowRaw t =
-                  [ if j == xColIdx then t else rep !! j
-                  | j <- [0 .. length xCols - 1] ]
-                xMatRaw = LA.fromLists [ makeRowRaw t | t <- xGrid ]
-                -- 標準化空間に変換してから predict
-                xMatStd = Std.applyStandardizer stdr xMatRaw
-                ys'     = RFF.predictRFFRidgeMV fit xMatStd
-            in [ (g, t, y') | (t, y') <- zip xGrid ys' ]
-          lnData = concatMap mkLineData uniqGroups
-          plotCfg = (defaultConfig (yCol <> " by " <> gCol))
-                      { plotWidth = 720, plotHeight = 480 }
-      in scatterWithGroupsFile fmt path plotCfg xCol yCol ptData lnData
-
-uniq :: Ord a => [a] -> [a]
-uniq []     = []
-uniq (x:xs) = x : uniq (filter (/= x) xs)
-
--- | 各列の標準偏差の幾何平均で長さスケールを推定 (median heuristic 簡易版)。
-defaultLengthScale :: [[Double]] -> Double
-defaultLengthScale cols =
-  let stds = [ std c | c <- cols, length c > 1 ]
-      std xs = let n  = fromIntegral (length xs)
-                   m  = sum xs / n
-                   v  = sum [ (x - m)^(2::Int) | x <- xs ] / max 1 (n - 1)
-               in sqrt v
-      g  = product stds ** (1.0 / fromIntegral (max 1 (length stds)))
-  in if g <= 0 then 1.0 else g
-
-runKernelOn :: DXD.DataFrame -> T.Text -> T.Text -> V.Vector Double -> V.Vector Double
-            -> KernelOpts -> IO ()
-runKernelOn df xCol yCol xVec yVec opts = do
-  let n = V.length xVec
-      method = koMethod opts
-      ker    = koKernel opts
-      grid   = makeGrid xVec 100
-      gridV  = V.fromList grid
-  printf "Loaded %d rows; method=%s, kernel=%s\n"
-         n (T.unpack method) (show ker)
-
-  -- Bandwidth selection
-  h <- case koBandwidth opts of
-    Just hVal -> do
-      printf "Bandwidth (specified): h = %.4f\n" hVal
-      return hVal
-    Nothing -> do
-      let xMin = V.minimum xVec
-          xMax = V.maximum xVec
-          range = xMax - xMin
-          hCands = [range/40, range/20, range/10, range/5, range/2.5]
-          (bestH, bestRMSE) = Kern.gridSearchBandwidth ker xVec yVec hCands
-      printf "Bandwidth (LOO-CV best): h = %.4f  (CV-RMSE = %.4f)\n"
-             bestH bestRMSE
-      return bestH
-
-  -- Fit + predict on grid
-  (gridY, sumStr) <- case method of
-    "nw" -> do
-      let ys = Kern.nwRegression ker h xVec yVec gridV
-      return (V.toList ys, "Nadaraya-Watson, h=" ++ show h)
-    "kr" -> do
-      let lam = koLambda opts
-          fit = Kern.kernelRidge ker h lam xVec yVec
-          ys  = Kern.predictKernelRidge fit gridV
-      return (V.toList ys
-             , "Kernel Ridge, h=" ++ show h ++ ", lambda=" ++ show lam)
-    "rff" -> do
-      gen   <- createSystemRandom
-      feats <- RFF.sampleRFFRBF (koFeatures opts) h 1.0 gen
-      let lam = koLambda opts
-          fit = RFF.rffRidge feats (V.toList xVec) (V.toList yVec) lam
-          ys  = RFF.predictRFFRidge fit grid
-      return (ys, "RFF, D=" ++ show (koFeatures opts)
-                  ++ ", h=" ++ show h ++ ", lambda=" ++ show lam)
-    _ -> error "unreachable"
-
-  -- In-sample RMSE
-  let predictX :: V.Vector Double -> [Double]
-      predictX xs = case method of
-        "nw" -> V.toList (Kern.nwRegression ker h xVec yVec xs)
-        "kr" -> V.toList (Kern.predictKernelRidge
-                          (Kern.kernelRidge ker h (koLambda opts) xVec yVec) xs)
-        _    -> []        -- rff requires gen; skip in-sample for now
-      ys = V.toList yVec
-  case method of
-    "rff" -> printf "Predictions on %d test points; in-sample RMSE skipped (RFF re-samples)\n"
-                    (length grid)
-    _     -> printf "RMSE (in-sample) = %.4f\n" (rmseV ys (predictX xVec))
-  putStrLn $ "(" ++ sumStr ++ ")"
-
-  -- Plot
-  let sf = SmoothFit
-             { sfX = grid
-             , sfFit = gridY
-             , sfLower = []
-             , sfUpper = []
-             , sfHasBand = False
-             }
-  writeSmoothPlot (koFormat opts) (koOut opts)
-    (T.pack ("Kernel: " ++ T.unpack method)) df xCol yCol sf
-  putStrLn ("Plot: " ++ koOut opts)
-  openInBrowser (koOut opts)
-
-  -- HTML レポート (--report)
-  case koReport opts of
-    Nothing -> return ()
-    Just rpath -> do
-      let xs = V.toList xVec
-          ys = V.toList yVec
-          smooth = RB.SmoothCurve grid gridY [] []
-          modelLbl = "Kernel regression (" <> method <> ")"
-          formula = T.pack (T.unpack yCol ++ " ~ f(" ++ T.unpack xCol ++ ")")
-          cfg = RB.defaultReportConfig
-                  ("Kernel regression — " <> yCol <> " ~ " <> xCol)
-          baseKVs =
-            [ ("Method",    method)
-            , ("Kernel",    T.pack (show ker))
-            , ("Bandwidth", T.pack (printf "%.4f" h))
-            ]
-          extraKVs = case method of
-            "kr"  -> [("Lambda", T.pack (printf "%g" (koLambda opts)))]
-            "rff" -> [("Features", T.pack (show (koFeatures opts)))
-                     ,("Lambda",   T.pack (printf "%g" (koLambda opts)))]
-            _     -> []
-          sections =
-            [ RB.secDataOverview df [xCol] yCol
-            , RB.secModelOverview modelLbl formula Nothing
-            , RB.secKeyValue "Fit summary" (baseKVs ++ extraKVs)
-            , RB.secFitScatter xCol yCol xs ys (Just smooth)
-            ]
-      RB.renderReport rpath cfg sections
-      putStrLn ("Report: " ++ rpath)
-      openInBrowser rpath
-
--- ---------------------------------------------------------------------------
--- spline subcommand
--- ---------------------------------------------------------------------------
-
-splineUsage :: String
-splineUsage = unlines
-  [ "Usage: hanalyze spline <file> <xcol> <ycol> [options]"
-  , ""
-  , "Options:"
-  , "  --type T          bspline|natural (default: bspline)"
-  , "  --knots N         number of internal knots (default: 5)"
-  , "  --degree D        B-spline degree (default: 3 = cubic)"
-  , "  --format FMT      html|png|svg (default: html)"
-  , "  --out FILE        scatter+fit output path (default: spline.html)"
-  , "  --report [FILE]   build composite HTML report (default: spline.html)"
-  , ""
-  , "Examples:"
-  , "  hanalyze spline data.csv x y --knots 8"
-  , "  hanalyze spline data.csv x y --type natural"
-  , "  hanalyze spline data.csv x y --type bspline --degree 3 --knots 10"
-  ]
-
-data SplineOpts = SplineOpts
-  { soType   :: T.Text
-  , soKnots  :: Int
-  , soDegree :: Int
-  , soFormat :: OutputFormat
-  , soOut    :: FilePath
-  , soReport :: Maybe FilePath
-  }
-
-defaultSplineOpts :: SplineOpts
-defaultSplineOpts = SplineOpts "bspline" 5 3 HTML "spline.html" Nothing
-
-runSplineCmd :: [String] -> IO ()
-runSplineCmd args0 =
-  let (lopts, args) = parseLoadOpts args0
-  in case args of
-       (file : xColStr : yColStr : rest) ->
-         case parseSplineOpts rest defaultSplineOpts of
-           Left err   -> hPutStrLn stderr ("spline: " ++ err)
-           Right opts -> doSpline file xColStr yColStr opts lopts
-       _ -> putStrLn splineUsage
-
-parseSplineOpts :: [String] -> SplineOpts -> Either String SplineOpts
-parseSplineOpts [] acc = Right acc
-parseSplineOpts (flag : rest) acc
-  | flag == "--type" = case rest of
-      (v:rs) | v `elem` ["bspline","natural"] ->
-        parseSplineOpts rs (acc { soType = T.pack v })
-      (v:_) -> Left ("unknown spline type '" ++ v ++ "'")
-      []    -> Left "--type requires an argument"
-  | flag == "--knots" = case rest of
-      (v:rs) -> case reads v :: [(Int, String)] of
-        [(d,"")] -> parseSplineOpts rs (acc { soKnots = d })
-        _        -> Left ("invalid --knots '" ++ v ++ "'")
-      []     -> Left "--knots requires a value"
-  | flag == "--degree" = case rest of
-      (v:rs) -> case reads v :: [(Int, String)] of
-        [(d,"")] -> parseSplineOpts rs (acc { soDegree = d })
-        _        -> Left ("invalid --degree '" ++ v ++ "'")
-      []     -> Left "--degree requires a value"
-  | flag `elem` ["-f","--format"] = case rest of
-      (v:rs) -> case parseFormat v of
-        Right f -> parseSplineOpts rs (acc { soFormat = f })
-        Left e  -> Left e
-      []     -> Left "--format requires an argument"
-  | flag == "--out" = case rest of
-      (v:rs) -> parseSplineOpts rs (acc { soOut = v })
-      []     -> Left "--out requires a file path"
-  | flag == "--report" = case rest of
-      (v:rs) | not (null v) && head v /= '-' ->
-        parseSplineOpts rs (acc { soReport = Just v })
-      _ -> parseSplineOpts rest (acc { soReport = Just "spline.html" })
-  | otherwise = Left ("unexpected argument '" ++ flag ++ "'")
-
-doSpline :: FilePath -> String -> String -> SplineOpts -> LoadOpts -> IO ()
-doSpline file xColStr yColStr opts lopts = do
-  let xCol = T.pack xColStr
-      yCol = T.pack yColStr
-  result <- loadXY lopts file [xCol] yCol
-  case result of
-    Left err -> hPutStrLn stderr err
-    Right (df, [xVec], yVec) -> do
-      let kind = case soType opts of
-            "natural" -> Spl.NaturalCubic
-            _         -> Spl.BSpline (soDegree opts)
-          k    = soKnots opts
-          xMin = V.minimum xVec
-          xMax = V.maximum xVec
-          knots = [ xMin + fromIntegral i * (xMax - xMin) / fromIntegral (k + 1)
-                  | i <- [1 .. k] ]
-          fit   = Spl.fitSpline kind knots xVec yVec
-          grid  = makeGrid xVec 100
-          gridV = V.fromList grid
-          gridY = V.toList (Spl.predictSpline fit gridV)
-          n     = V.length xVec
-          ys    = V.toList yVec
-          yhatIn = V.toList (Spl.predictSpline fit xVec)
-          rmseVal = rmseV ys yhatIn
-      printf "Loaded %d rows; type=%s, knots=%d%s\n"
-             n (T.unpack (soType opts)) k
-             (if soType opts == "bspline"
-                then ", degree=" ++ show (soDegree opts) else "")
-      printf "RMSE (in-sample) = %.4f\n" rmseVal
-      let sf = SmoothFit
-                 { sfX = grid
-                 , sfFit = gridY
-                 , sfLower = []
-                 , sfUpper = []
-                 , sfHasBand = False
-                 }
-      writeSmoothPlot (soFormat opts) (soOut opts)
-        (T.pack ("Spline: " ++ T.unpack (soType opts))) df xCol yCol sf
-      putStrLn ("Plot: " ++ soOut opts)
-      openInBrowser (soOut opts)
-      -- HTML レポート (--report)
-      case soReport opts of
-        Nothing -> return ()
-        Just rpath -> do
-          let smooth = RB.SmoothCurve grid gridY [] []
-              modelLbl = "Spline regression (" <> soType opts <> ")"
-              formula = T.pack (T.unpack yCol ++ " ~ s("
-                                ++ T.unpack xCol ++ "; knots="
-                                ++ show k ++ ")")
-              cfg = RB.defaultReportConfig
-                      ("Spline regression — " <> yCol <> " ~ " <> xCol)
-              sections =
-                [ RB.secDataOverview df [xCol] yCol
-                , RB.secModelOverview modelLbl formula Nothing
-                , RB.secKeyValue "Fit summary"
-                    [ ("Type",      soType opts)
-                    , ("Knots",     T.pack (show k))
-                    , ("Degree",    T.pack (show (soDegree opts)))
-                    , ("RMSE (in-sample)", T.pack (printf "%.4f" rmseVal))
-                    ]
-                , RB.secFitScatter xCol yCol (V.toList xVec) ys
-                    (Just smooth)
-                , RB.secResiduals yhatIn (zipWith (-) ys yhatIn)
-                ]
-          RB.renderReport rpath cfg sections
-          putStrLn ("Report: " ++ rpath)
-          openInBrowser rpath
-    Right _ -> hPutStrLn stderr "spline: expected single x column"
-
--- ---------------------------------------------------------------------------
--- ridge report ヘルパ
--- ---------------------------------------------------------------------------
-
-ridgeModelLabel :: RidgeOpts -> T.Text
-ridgeModelLabel opts =
-  "Regularized regression (" <> roPenalty opts <> ")"
-
-ridgeFormula :: RidgeOpts -> [T.Text] -> T.Text -> T.Text
-ridgeFormula opts xCols yCol =
-  T.pack (T.unpack yCol ++ " ~ "
-          ++ intercalate " + " (map T.unpack xCols)
-          ++ "  (lambda=" ++ show (roLambda opts) ++ ")")
-
-ridgeReportConfig :: RidgeOpts -> [T.Text] -> T.Text -> RB.ReportConfig
-ridgeReportConfig _opts xCols yCol = RB.defaultReportConfig
-  ("Regularized regression — "
-   <> yCol <> " ~ " <> T.intercalate " + " xCols)
-
-ridgeFitKVs :: RidgeOpts -> Reg.RegFit -> [Double] -> Double -> [(T.Text, T.Text)]
-ridgeFitKVs opts fit beta rmseVal =
-  [ ("RMSE (in-sample)", T.pack (printf "%.4f" rmseVal))
-  , ("|β| > 1e-8", T.pack (show (Reg.rfNonZero fit) <> " / "
-                            <> show (length beta)))
-  , ("Penalty", roPenalty opts)
-  , ("Lambda", T.pack (printf "%g" (roLambda opts)))
-  ]
-
--- | Regularization path: λ を 1e-4 .. 1e2 で対数スケール掃引、
--- 各 λ で fit して係数を集める。intercept は除外して可視化。
-mkRidgePathSection :: [T.Text] -> LA.Matrix Double -> LA.Vector Double
-                   -> RidgeOpts -> RB.ReportSection
-mkRidgePathSection xCols xMat yLA opts =
-  let lambdas = [10 ** (-4 + 0.1 * fromIntegral i) | i <- [0 .. 60 :: Int]]
-      mkPen lam = case roPenalty opts of
-        "ridge"       -> Reg.L2 lam
-        "lasso"       -> Reg.L1 lam
-        "elasticnet"  -> Reg.ElasticNet (lam * roAlpha opts)
-                                         (lam * (1 - roAlpha opts))
-        _             -> Reg.L2 lam
-      path = Reg.regularizationPath mkPen lambdas xMat yLA
-      -- intercept (係数 0) を除外
-      pathNoInt = [ (lam, drop 1 coefs) | (lam, coefs) <- path ]
-      title = "Regularization path (" <> roPenalty opts <> ")"
-      spec  = RB.regPathSpec xCols pathNoInt
-  in RB.secVega title spec
-
--- ---------------------------------------------------------------------------
--- quantile subcommand
--- ---------------------------------------------------------------------------
-
-quantileUsage :: String
-quantileUsage = unlines
-  [ "Usage: hanalyze quantile <file> <xcols> <ycol> [options]"
-  , ""
-  , "  <xcols>   x column name(s); quote multiple: \"x1 x2\""
-  , "  <ycol>    y column name (single)"
-  , ""
-  , "Options:"
-  , "  --tau T          quantile in (0, 1) (default: 0.5 = median)"
-  , "  --taus T1,T2,... overlay multiple quantiles in the report (e.g. 0.1,0.5,0.9)"
-  , "  --format FMT     html|png|svg (default: html)"
-  , "  --out FILE       scatter+fit output path (default: quantile.html)"
-  , "  --report [FILE]  build composite HTML report (default: quantile.html)"
-  , ""
-  , "Examples:"
-  , "  hanalyze quantile data.csv x y --tau 0.5"
-  , "  hanalyze quantile data.csv x y --taus 0.1,0.5,0.9 --report"
-  ]
-
-data QuantileOpts = QuantileOpts
-  { qoTau    :: Double
-  , qoTaus   :: [Double]    -- when not empty, overlay multiple quantiles
-  , qoFormat :: OutputFormat
-  , qoOut    :: FilePath
-  , qoReport :: Maybe FilePath
-  }
-
-defaultQuantileOpts :: QuantileOpts
-defaultQuantileOpts = QuantileOpts 0.5 [] HTML "quantile.html" Nothing
-
-runQuantileCmd :: [String] -> IO ()
-runQuantileCmd args0 =
-  let (lopts, args) = parseLoadOpts args0
-  in case args of
-       (file : xColsStr : yColStr : rest) ->
-         case parseQuantileOpts rest defaultQuantileOpts of
-           Left err   -> hPutStrLn stderr ("quantile: " ++ err)
-           Right opts -> doQuantile file xColsStr yColStr opts lopts
-       _ -> putStrLn quantileUsage
-
-parseQuantileOpts :: [String] -> QuantileOpts -> Either String QuantileOpts
-parseQuantileOpts [] acc = Right acc
-parseQuantileOpts (flag : rest) acc
-  | flag == "--tau" = case rest of
-      (v:rs) -> case reads v :: [(Double, String)] of
-        [(d,"")] | d > 0, d < 1 -> parseQuantileOpts rs (acc { qoTau = d })
-        _        -> Left ("invalid --tau '" ++ v ++ "' (must be in (0,1))")
-      []     -> Left "--tau requires a value"
-  | flag == "--taus" = case rest of
-      (v:rs) ->
-        let parts = filter (not . null) (splitOnComma v)
-        in case mapM (\s -> case reads s :: [(Double, String)] of
-                              [(d,"")] | d > 0, d < 1 -> Just d
-                              _ -> Nothing) parts of
-             Just ds -> parseQuantileOpts rs (acc { qoTaus = ds })
-             Nothing -> Left ("invalid --taus '" ++ v
-                              ++ "' (comma-separated values in (0,1))")
-      [] -> Left "--taus requires a value"
-  | flag `elem` ["-f","--format"] = case rest of
-      (v:rs) -> case parseFormat v of
-        Right f -> parseQuantileOpts rs (acc { qoFormat = f })
-        Left e  -> Left e
-      []     -> Left "--format requires an argument"
-  | flag == "--out" = case rest of
-      (v:rs) -> parseQuantileOpts rs (acc { qoOut = v })
-      []     -> Left "--out requires a file path"
-  | flag == "--report" = case rest of
-      (v:rs) | not (null v) && head v /= '-' ->
-        parseQuantileOpts rs (acc { qoReport = Just v })
-      _ -> parseQuantileOpts rest (acc { qoReport = Just "quantile.html" })
-  | otherwise = Left ("unexpected argument '" ++ flag ++ "'")
-
-splitOnComma :: String -> [String]
-splitOnComma s = case break (== ',') s of
-  (a, ',' : rest) -> a : splitOnComma rest
-  (a, _)          -> [a]
-
-doQuantile :: FilePath -> String -> String -> QuantileOpts -> LoadOpts -> IO ()
-doQuantile file xColsStr yColStr opts lopts = do
-  let xCols = map T.pack (words xColsStr)
-      yCol  = T.pack yColStr
-  result <- loadXY lopts file xCols yCol
-  case result of
-    Left err -> hPutStrLn stderr err
-    Right (df, xVecs, yVec) -> do
-      let n = V.length yVec
-          intercept = LA.konst 1 n
-          xMat = LA.fromColumns
-                   (intercept : map (LA.fromList . V.toList) xVecs)
-          yLA  = LA.fromList (V.toList yVec)
-          tau  = qoTau opts
-          fit  = QR.fitQuantile tau xMat yLA
-          beta = LA.toList (QR.qfBeta fit)
-      printf "Loaded %d rows from %s\n" n file
-      printf "Quantile: tau = %.3f  (median: %s)\n" tau
-             (if abs (tau - 0.5) < 1e-9 then ("yes" :: String) else "no")
-      printf "MM-IRLS converged in %d iterations\n" (QR.qfIters fit)
-      putStrLn ""
-      putStrLn "Coefficients:"
-      printf "  %-30s = %9.4f\n" ("intercept" :: String) (head beta)
-      mapM_ (\(i, c, b) ->
-        printf "  %-30s = %9.4f\n"
-               ("β_" ++ show (i :: Int) ++ " (" ++ T.unpack c ++ ")") b)
-        (zip3 [1..] xCols (tail beta))
-      printf "Pinball loss V̂_τ: %.4f\n" (QR.qfPinball fit)
-      printf "Pseudo R¹_τ:      %.4f\n" (QR.qfR1 fit)
-
-      -- 単変数なら scatter + fit (+ overlay multiple quantiles)
-      case (xCols, xVecs) of
-        ([xc1], [xVec]) -> do
-          let xs = V.toList xVec
-              ys = V.toList yVec
-              grid = makeGrid xVec 100
-              gridMat = LA.fromColumns
-                          [ LA.konst 1 100, LA.fromList grid ]
-              gridY = LA.toList (QR.predictQuantile fit gridMat)
-              sf = SmoothFit
-                     { sfX = grid
-                     , sfFit = gridY
-                     , sfLower = []
-                     , sfUpper = []
-                     , sfHasBand = False
-                     }
-              _ = xs
-          writeSmoothPlot (qoFormat opts) (qoOut opts)
-            (T.pack ("Quantile τ=" ++ show tau)) df xc1 yCol sf
-          putStrLn ("Plot: " ++ qoOut opts)
-          openInBrowser (qoOut opts)
-
-          -- HTML レポート
-          case qoReport opts of
-            Nothing -> return ()
-            Just rpath -> do
-              let coeffPairs = zip ("intercept" : xCols) beta
-                  modelLbl = "Quantile regression (τ=" <> T.pack (show tau) <> ")"
-                  formula = T.pack ("Q_τ(" ++ T.unpack yCol ++ "|x) = "
-                                    ++ "β₀ + " ++ T.unpack (T.intercalate " + "
-                                                              [ "β" <> T.pack (show i)
-                                                                <> "·" <> c
-                                                              | (i, c) <- zip [(1::Int)..] xCols ]))
-                  cfg = RB.defaultReportConfig
-                          ("Quantile regression — τ=" <> T.pack (show tau)
-                           <> ",  " <> yCol <> " ~ " <> T.intercalate " + " xCols)
-                  baseSections =
-                    [ RB.secDataOverview df xCols yCol
-                    , RB.secModelOverview modelLbl formula Nothing
-                    , RB.secCoefficients coeffPairs (Just ("Pseudo R¹_τ", QR.qfR1 fit))
-                    , RB.secKeyValue "Fit summary"
-                        [ ("τ",                T.pack (printf "%.3f" tau))
-                        , ("Pinball loss V̂_τ", T.pack (printf "%.4f" (QR.qfPinball fit)))
-                        , ("Iterations",       T.pack (show (QR.qfIters fit)))
-                        ]
-                    , RB.secFitScatter xc1 yCol xs ys (Just (RB.SmoothCurve grid gridY [] []))
-                    , RB.secResiduals (LA.toList (QR.qfYHat fit))
-                                      (LA.toList (QR.qfResid fit))
-                    ]
-                  -- overlay multi quantile chart
-                  multiSec = case qoTaus opts of
-                    [] -> []
-                    taus ->
-                      let curves = [ ( T.pack ("τ=" ++ show t)
-                                     , LA.toList (QR.predictQuantile
-                                                   (QR.fitQuantile t xMat yLA)
-                                                   gridMat))
-                                   | t <- taus ]
-                          spec = multiQuantileSpec xc1 yCol xs ys grid curves
-                      in [RB.secVega "Multiple quantile fits" spec]
-              RB.renderReport rpath cfg (baseSections ++ multiSec)
-              putStrLn ("Report: " ++ rpath)
-              openInBrowser rpath
-        _ -> putStrLn "(scatter plot skipped for multiple x columns)"
-
--- 複数分位線を 1 枚の Vega-Lite spec で描く
-multiQuantileSpec :: T.Text -> T.Text -> [Double] -> [Double] -> [Double]
-                  -> [(T.Text, [Double])] -> VegaLite
-multiQuantileSpec xc yc xs ys grid curves =
-  VL.toVegaLite
-    [ VL.layer
-        [ VL.asSpec
-            [ VL.dataFromColumns []
-                . VL.dataColumn xc (VL.Numbers xs)
-                . VL.dataColumn yc (VL.Numbers ys)
-                $ []
-            , VL.mark VL.Point
-                [VL.MOpacity 0.5, VL.MSize 40, VL.MColor "#888888"]
-            , VL.encoding
-                . VL.position VL.X
-                    [VL.PName xc, VL.PmType VL.Quantitative,
-                     VL.PAxis [VL.AxTitle xc]]
-                . VL.position VL.Y
-                    [VL.PName yc, VL.PmType VL.Quantitative,
-                     VL.PAxis [VL.AxTitle yc]]
-                $ []
-            ]
-        , VL.asSpec (multiLineLayer xc yc grid curves)
-        ]
-    , VL.width 640
-    , VL.height 320
-    ]
-
-multiLineLayer :: T.Text -> T.Text -> [Double] -> [(T.Text, [Double])]
-               -> [(VLProperty, VLSpec)]
-multiLineLayer xc yc grid curves =
-  let rowsX  = concat [ replicate (length grid) lbl | (lbl, _) <- curves ]
-      rowsXs = concat [ grid                         | _       <- curves ]
-      rowsYs = concat [ ys'                          | (_, ys') <- curves ]
-  in [ VL.dataFromColumns []
-         . VL.dataColumn "tau" (VL.Strings rowsX)
-         . VL.dataColumn xc    (VL.Numbers rowsXs)
-         . VL.dataColumn yc    (VL.Numbers rowsYs)
-         $ []
-     , VL.mark VL.Line [VL.MStrokeWidth 2.2]
-     , VL.encoding
-         . VL.position VL.X [VL.PName xc, VL.PmType VL.Quantitative]
-         . VL.position VL.Y [VL.PName yc, VL.PmType VL.Quantitative]
-         . VL.color [VL.MName "tau", VL.MmType VL.Nominal,
-                     VL.MScale [VL.SScheme "tableau10" []]]
-         $ []
-     ]
-
--- ---------------------------------------------------------------------------
--- gam subcommand
--- ---------------------------------------------------------------------------
-
-gamUsage :: String
-gamUsage = unlines
-  [ "Usage: hanalyze gam <file> <xcols> <ycol> [options]"
-  , ""
-  , "  <xcols>  x column names; quote multiple: \"x1 x2 x3\""
-  , "  <ycol>   y column name"
-  , ""
-  , "Options:"
-  , "  --knots N        per-feature internal knot count (default: 5)"
-  , "  --degree D       B-spline degree (default: 3 = cubic)"
-  , "  --lambda L       Ridge regularization on spline coefficients (default: 0.01)"
-  , "  --report [FILE]  build composite HTML report with per-feature partials"
-  , ""
-  , "Example:"
-  , "  hanalyze gam data.csv \"x1 x2 x3\" y --knots 8 --lambda 0.05 --report"
-  ]
-
-data GAMOpts = GAMOpts
-  { goKnots  :: Int
-  , goDegree :: Int
-  , goLambda :: Double
-  , goReport :: Maybe FilePath
-  }
-
-defaultGAMOpts :: GAMOpts
-defaultGAMOpts = GAMOpts 5 3 0.01 Nothing
-
-runGAMCmd :: [String] -> IO ()
-runGAMCmd args0 =
-  let (lopts, args) = parseLoadOpts args0
-  in case args of
-       (file : xColsStr : yColStr : rest) ->
-         case parseGAMOpts rest defaultGAMOpts of
-           Left err   -> hPutStrLn stderr ("gam: " ++ err)
-           Right opts -> doGAM file xColsStr yColStr opts lopts
-       _ -> putStrLn gamUsage
-
-parseGAMOpts :: [String] -> GAMOpts -> Either String GAMOpts
-parseGAMOpts [] acc = Right acc
-parseGAMOpts (flag:rest) acc
-  | flag == "--knots" = case rest of
-      (v:rs) -> case reads v :: [(Int,String)] of
-        [(d,"")] -> parseGAMOpts rs (acc { goKnots = d })
-        _ -> Left ("invalid --knots '" ++ v ++ "'")
-      [] -> Left "--knots requires a value"
-  | flag == "--degree" = case rest of
-      (v:rs) -> case reads v :: [(Int,String)] of
-        [(d,"")] -> parseGAMOpts rs (acc { goDegree = d })
-        _ -> Left ("invalid --degree '" ++ v ++ "'")
-      [] -> Left "--degree requires a value"
-  | flag == "--lambda" = case rest of
-      (v:rs) -> case reads v :: [(Double,String)] of
-        [(d,"")] -> parseGAMOpts rs (acc { goLambda = d })
-        _ -> Left ("invalid --lambda '" ++ v ++ "'")
-      [] -> Left "--lambda requires a value"
-  | flag == "--report" = case rest of
-      (v:rs) | not (null v) && head v /= '-' ->
-        parseGAMOpts rs (acc { goReport = Just v })
-      _ -> parseGAMOpts rest (acc { goReport = Just "gam.html" })
-  | otherwise = Left ("unexpected argument '" ++ flag ++ "'")
-
-doGAM :: FilePath -> String -> String -> GAMOpts -> LoadOpts -> IO ()
-doGAM file xColsStr yColStr opts lopts = do
-  let xCols = map T.pack (words xColsStr)
-      yCol  = T.pack yColStr
-  result <- loadXY lopts file xCols yCol
-  case result of
-    Left err -> hPutStrLn stderr err
-    Right (df, xVecs, yVec) -> do
-      let fit = GAM.fitGAM (goDegree opts) (goKnots opts) (goLambda opts)
-                            xVecs yVec
-          n = V.length yVec
-          ys = V.toList yVec
-          yhat = LA.toList (GAM.gamYHat fit)
-          resid = LA.toList (GAM.gamResid fit)
-      printf "Loaded %d rows from %s\n" n file
-      printf "GAM: degree=%d, knots=%d/feature, lambda=%g\n"
-             (goDegree opts) (goKnots opts) (goLambda opts)
-      printf "Features: %d (%s)\n" (length xCols)
-             (T.unpack (T.intercalate ", " xCols))
-      printf "Intercept: %.4f\n" (GAM.gamIntercept fit)
-      printf "R²:        %.4f\n" (GAM.gamR2 fit)
-      let rmseVal = sqrt (sum [ r ^ (2 :: Int) | r <- resid ]
-                          / fromIntegral n)
-      printf "RMSE (in-sample): %.4f\n" rmseVal
-
-      case goReport opts of
-        Nothing -> return ()
-        Just rpath -> do
-          let modelLbl = "Generalized Additive Model"
-              formula = yCol <> " = β₀ + " <> T.intercalate " + "
-                          [ "s(" <> c <> ")" | c <- xCols ]
-              cfg = RB.defaultReportConfig
-                      ("GAM — " <> yCol <> " ~ s("
-                       <> T.intercalate ") + s(" xCols <> ")")
-              partialSecs =
-                [ RB.secVega ("Partial effect: s(" <> c <> ")")
-                    (gamPartialSpec c xVec fit j)
-                | (j, c, xVec) <- zip3 [0..] xCols xVecs ]
-              sections =
-                [ RB.secDataOverview df xCols yCol
-                , RB.secModelOverview modelLbl formula Nothing
-                , RB.secKeyValue "Fit summary"
-                    [ ("Degree",   T.pack (show (goDegree opts)))
-                    , ("Knots",    T.pack (show (goKnots opts)))
-                    , ("Lambda",   T.pack (printf "%g" (goLambda opts)))
-                    , ("Intercept",T.pack (printf "%.4f"
-                                             (GAM.gamIntercept fit)))
-                    , ("R²",       T.pack (printf "%.4f" (GAM.gamR2 fit)))
-                    , ("RMSE",     T.pack (printf "%.4f" rmseVal))
-                    ]
-                ] ++ partialSecs ++
-                [ RB.secResiduals yhat resid ]
-              _ = ys
-          RB.renderReport rpath cfg sections
-          putStrLn ("Report: " ++ rpath)
-          openInBrowser rpath
-
--- ---------------------------------------------------------------------------
--- rf subcommand
--- ---------------------------------------------------------------------------
-
-rfUsage :: String
-rfUsage = unlines
-  [ "Usage: hanalyze rf <file> <xcols> <ycol> [options]"
-  , ""
-  , "Options:"
-  , "  --trees N        number of trees (default: 100)"
-  , "  --max-depth D    maximum tree depth (default: 12)"
-  , "  --min-samples N  minimum samples per leaf (default: 3)"
-  , "  --mtry M         features per split (default: max(1, d/3))"
-  , "  --report [FILE]  build composite HTML report (with feature importance)"
-  , ""
-  , "Example:"
-  , "  hanalyze rf data.csv \"x1 x2 x3\" y --trees 200 --report"
-  ]
-
-data RFOpts = RFOpts
-  { roTrees      :: Int
-  , roMaxDepth   :: Int
-  , roMinSamples :: Int
-  , roMtry       :: Maybe Int
-  , roReport_    :: Maybe FilePath
-  }
-
-defaultRFOpts :: RFOpts
-defaultRFOpts = RFOpts 100 12 3 Nothing Nothing
-
-runRFCmd :: [String] -> IO ()
-runRFCmd args0 =
-  let (lopts, args) = parseLoadOpts args0
-  in case args of
-       (file : xColsStr : yColStr : rest) ->
-         case parseRFOpts rest defaultRFOpts of
-           Left err   -> hPutStrLn stderr ("rf: " ++ err)
-           Right opts -> doRF file xColsStr yColStr opts lopts
-       _ -> putStrLn rfUsage
-
-parseRFOpts :: [String] -> RFOpts -> Either String RFOpts
-parseRFOpts [] acc = Right acc
-parseRFOpts (flag:rest) acc
-  | flag == "--trees" = case rest of
-      (v:rs) -> case reads v :: [(Int,String)] of
-        [(d,"")] -> parseRFOpts rs (acc { roTrees = d })
-        _ -> Left ("invalid --trees '" ++ v ++ "'")
-      [] -> Left "--trees requires a value"
-  | flag == "--max-depth" = case rest of
-      (v:rs) -> case reads v :: [(Int,String)] of
-        [(d,"")] -> parseRFOpts rs (acc { roMaxDepth = d })
-        _ -> Left ("invalid --max-depth '" ++ v ++ "'")
-      [] -> Left "--max-depth requires a value"
-  | flag == "--min-samples" = case rest of
-      (v:rs) -> case reads v :: [(Int,String)] of
-        [(d,"")] -> parseRFOpts rs (acc { roMinSamples = d })
-        _ -> Left ("invalid --min-samples '" ++ v ++ "'")
-      [] -> Left "--min-samples requires a value"
-  | flag == "--mtry" = case rest of
-      (v:rs) -> case reads v :: [(Int,String)] of
-        [(d,"")] -> parseRFOpts rs (acc { roMtry = Just d })
-        _ -> Left ("invalid --mtry '" ++ v ++ "'")
-      [] -> Left "--mtry requires a value"
-  | flag == "--report" = case rest of
-      (v:rs) | not (null v) && head v /= '-' ->
-        parseRFOpts rs (acc { roReport_ = Just v })
-      _ -> parseRFOpts rest (acc { roReport_ = Just "rf.html" })
-  | otherwise = Left ("unexpected argument '" ++ flag ++ "'")
-
-doRF :: FilePath -> String -> String -> RFOpts -> LoadOpts -> IO ()
-doRF file xColsStr yColStr opts lopts = do
-  let xCols = map T.pack (words xColsStr)
-      yCol  = T.pack yColStr
-  result <- loadXY lopts file xCols yCol
-  case result of
-    Left err -> hPutStrLn stderr err
-    Right (df, xVecs, yVec) -> do
-      let n     = V.length yVec
-          rows  = [ [ xv V.! i | xv <- xVecs ] | i <- [0 .. n - 1] ]
-          ys    = V.toList yVec
-          cfg   = RF.defaultRandomForest
-                    { RF.rfTrees      = roTrees opts
-                    , RF.rfMaxDepth   = roMaxDepth opts
-                    , RF.rfMinSamples = roMinSamples opts
-                    , RF.rfMtry       = roMtry opts
-                    }
-      gen <- createSystemRandom
-      forest <- RF.fitRF cfg rows ys gen
-      let yhat = map (RF.predictRF forest) rows
-          resid = zipWith (-) ys yhat
-          yMean = sum ys / fromIntegral n
-          tss   = sum [ (y - yMean) ^ (2 :: Int) | y <- ys ]
-          rss   = sum [ r ^ (2 :: Int) | r <- resid ]
-          r2    = if tss < 1e-12 then 0 else 1 - rss / tss
-          rmseVal = sqrt (rss / fromIntegral n)
-          imp   = V.toList (RF.featureImportance forest)
-          impPairs = zip xCols imp
-      printf "Loaded %d rows from %s\n" n file
-      printf "RandomForest: trees=%d, max-depth=%d, min-samples=%d\n"
-             (roTrees opts) (roMaxDepth opts) (roMinSamples opts)
-      printf "R²:               %.4f\n" r2
-      printf "RMSE (in-sample): %.4f\n" rmseVal
-      putStrLn ""
-      putStrLn "Feature importance (split-count fraction):"
-      mapM_ (\(c, v) -> printf "  %-20s = %.4f\n" (T.unpack c) v) impPairs
-
-      case roReport_ opts of
-        Nothing -> return ()
-        Just rpath -> do
-          let modelLbl = "Random Forest regression"
-              formula = yCol <> " ~ ensemble of " <> T.pack (show (roTrees opts))
-                        <> " CART trees over (" <> T.intercalate ", " xCols <> ")"
-              cfg' = RB.defaultReportConfig
-                       ("Random Forest — " <> yCol <> " ~ "
-                        <> T.intercalate " + " xCols)
-              sections =
-                [ RB.secDataOverview df xCols yCol
-                , RB.secModelOverview modelLbl formula Nothing
-                , RB.secKeyValue "Fit summary"
-                    [ ("Trees",       T.pack (show (roTrees opts)))
-                    , ("Max depth",   T.pack (show (roMaxDepth opts)))
-                    , ("Min samples", T.pack (show (roMinSamples opts)))
-                    , ("R²",          T.pack (printf "%.4f" r2))
-                    , ("RMSE",        T.pack (printf "%.4f" rmseVal))
-                    ]
-                , RB.secBarChart "Feature importance"
-                    [ (c, v) | (c, v) <- impPairs ]
-                , RB.secResiduals yhat resid
-                ]
-          RB.renderReport rpath cfg' sections
-          putStrLn ("Report: " ++ rpath)
-          openInBrowser rpath
-
--- 1 特徴の partial effect s_j(x_j) を Vega-Lite 散布+曲線で
-gamPartialSpec :: T.Text -> V.Vector Double -> GAM.GAMFit -> Int -> VegaLite
-gamPartialSpec col xVec fit j =
-  let xs = V.toList xVec
-      lo = V.minimum xVec
-      hi = V.maximum xVec
-      grid = [ lo + fromIntegral i * (hi - lo) / 99 | i <- [0..99::Int]]
-      gridV = V.fromList grid
-      sj = V.toList (GAM.predictGAMComponent fit j gridV)
-      -- partial residuals: resid + s_j(x_i) (説明用にプロット)
-      partialAtData = V.toList (GAM.predictGAMComponent fit j xVec)
-      residList = LA.toList (GAM.gamResid fit)
-      partials = zipWith (+) residList partialAtData
-  in VL.toVegaLite
-       [ VL.layer
-           [ VL.asSpec
-               [ VL.dataFromColumns []
-                   . VL.dataColumn col (VL.Numbers xs)
-                   . VL.dataColumn "partial" (VL.Numbers partials)
-                   $ []
-               , VL.mark VL.Point
-                   [VL.MOpacity 0.5, VL.MSize 40, VL.MColor "#888888"]
-               , VL.encoding
-                   . VL.position VL.X
-                       [VL.PName col, VL.PmType VL.Quantitative,
-                        VL.PAxis [VL.AxTitle col]]
-                   . VL.position VL.Y
-                       [VL.PName "partial", VL.PmType VL.Quantitative,
-                        VL.PAxis [VL.AxTitle "Partial residual"]]
-                   $ []
-               ]
-           , VL.asSpec
-               [ VL.dataFromColumns []
-                   . VL.dataColumn col (VL.Numbers grid)
-                   . VL.dataColumn "s_j" (VL.Numbers sj)
-                   $ []
-               , VL.mark VL.Line
-                   [VL.MStrokeWidth 2.5, VL.MColor "#DD5566"]
-               , VL.encoding
-                   . VL.position VL.X
-                       [VL.PName col, VL.PmType VL.Quantitative]
-                   . VL.position VL.Y
-                       [VL.PName "s_j", VL.PmType VL.Quantitative]
-                   $ []
-               ]
-           ]
-       , VL.width 500
-       , VL.height 240
-       ]
-
--- ---------------------------------------------------------------------------
--- multireg subcommand (多出力回帰: wide CSV → 対話的予測曲線)
--- ---------------------------------------------------------------------------
-
-multiRegUsage :: String
-multiRegUsage = unlines
-  [ "Usage: hanalyze multireg <file> <xcol> <yspec> [options]"
-  , ""
-  , "wide-form CSV (1 行 = 入力 1 値、複数列 = q 個の出力) を読み込み、"
-  , "1 入力 → q 出力の多出力回帰を実行。dose スライダで対話的に予測曲線を更新。"
-  , ""
-  , "<yspec>: カンマ区切り列名 (例 'y_z001,y_z002,...') または prefix*"
-  , "         (例 'y_z*' で y_z で始まる全列)"
-  , ""
-  , "Options:"
-  , "  --method M       linear | kernel-rbf  (default: linear)"
-  , "  --bandwidth H    kernel-rbf の bandwidth (default: auto via LOOCV)"
-  , "  --lambda L       kernel-rbf の Ridge λ  (default: auto via LOOCV)"
-  , "  --auto-hp        kernel-rbf で h, λ を LOOCV 解析解で自動決定 (default: ON)"
-  , "  --report FILE    対話的 HTML レポート出力先 (default: multireg.html)"
-  , "  --xaxis LABEL    出力グリッドの x 軸ラベル (default: 'index')"
-  , ""
-  , "前提: y 列が共通の z grid を表す場合、列名末尾の数値で z 座標を内挿。"
-  , "      例: y_z001..y_z100 のとき z = 0..99 を等間隔展開 (--xaxis-min/max で上書き)."
-  , ""
-  , "Options (出力 grid):"
-  , "  --xaxis-min V    出力 grid の最小値 (default: 1)"
-  , "  --xaxis-max V    出力 grid の最大値 (default: q)"
-  , ""
-  , "Examples:"
-  , "  hanalyze multireg data/io/potential_wide.csv dose 'y_z*' \\"
-  , "      --method kernel-rbf --report trash/pot.html \\"
-  , "      --xaxis 'z [nm]' --xaxis-min 0 --xaxis-max 200"
-  ]
-
-data MROpts = MROpts
-  { mroMethod   :: String         -- "linear" | "kernel-rbf"
-  , mroH        :: Maybe Double
-  , mroLambda   :: Maybe Double
-  , mroAutoHP   :: Bool
-  , mroReport   :: FilePath
-  , mroXAxis    :: String
-  , mroXAxisMin :: Maybe Double
-  , mroXAxisMax :: Maybe Double
-  } deriving Show
-
-defaultMROpts :: MROpts
-defaultMROpts = MROpts "linear" Nothing Nothing True "multireg.html" "index" Nothing Nothing
-
-parseMROpts :: [String] -> (MROpts, [String])
-parseMROpts = go defaultMROpts []
-  where
-    go o acc [] = (o, reverse acc)
-    go o acc ("--method":m:rest)     = go o { mroMethod = m } acc rest
-    go o acc ("--bandwidth":v:rest)  = go o { mroH      = Just (read v) } acc rest
-    go o acc ("--lambda":v:rest)     = go o { mroLambda = Just (read v) } acc rest
-    go o acc ("--auto-hp":rest)      = go o { mroAutoHP = True } acc rest
-    go o acc ("--no-auto-hp":rest)   = go o { mroAutoHP = False } acc rest
-    go o acc ("--report":p:rest)     = go o { mroReport = p } acc rest
-    go o acc ("--xaxis":s:rest)      = go o { mroXAxis = s } acc rest
-    go o acc ("--xaxis-min":v:rest)  = go o { mroXAxisMin = Just (read v) } acc rest
-    go o acc ("--xaxis-max":v:rest)  = go o { mroXAxisMax = Just (read v) } acc rest
-    go o acc (x:rest)                = go o (x:acc) rest
-
-runMultiRegCmd :: [String] -> IO ()
-runMultiRegCmd args0 = do
-  let (lopts, args1) = parseLoadOpts args0
-      (opts,  args2) = parseMROpts args1
-  case args2 of
-    (file:xCol:ySpec:_) -> do
-      result <- loadAutoSafeWith lopts file
-      case result of
-        Left err          -> hPutStrLn stderr ("Parse error: " ++ err)
-        Right (df, lg)    -> do
-          Log.printLogReport lg
-          let allCols = map T.unpack (DX.columnNames df)
-              yCols   = resolveYSpec ySpec allCols
-              xColT   = T.pack xCol
-              yColTs  = map T.pack yCols
-          if null yCols
-            then hPutStrLn stderr ("multireg: yspec '" ++ ySpec
-                                    ++ "' に該当する列がありません")
-            else case getDoubleVec xColT df of
-              Nothing -> hPutStrLn stderr ("multireg: 入力列 '" ++ xCol
-                                            ++ "' が見つかりません")
-              Just xV -> do
-                let n   = V.length xV
-                    yMs = [ getDoubleVec c df | c <- yColTs ]
-                if any null (map mtoMaybe yMs)
-                  then hPutStrLn stderr "multireg: y 列の取得失敗"
-                  else do
-                    let yVecs   = [v | Just v <- yMs]
-                        q       = length yVecs
-                        xMat1   = LA.fromLists [[1.0, xV V.! i]
-                                               | i <- [0 .. n - 1]]
-                        ys      = LA.fromLists
-                                    [ [ (yVecs !! j) V.! i
-                                      | j <- [0 .. q - 1] ]
-                                    | i <- [0 .. n - 1] ]
-                        xObsL   = V.toList xV
-                        yObsL   = [ [ (yVecs !! j) V.! i
-                                    | j <- [0 .. q - 1] ]
-                                  | i <- [0 .. n - 1] ]
-                        outGrid =
-                          let lo = maybe 1.0 id (mroXAxisMin opts)
-                              hi = maybe (fromIntegral q) id (mroXAxisMax opts)
-                              step = if q < 2 then 0 else (hi - lo) / fromIntegral (q - 1)
-                          in [ lo + step * fromIntegral i | i <- [0 .. q - 1] ]
-                        xMin    = minimum xObsL - (maximum xObsL - minimum xObsL) * 0.2
-                        xMax    = maximum xObsL + (maximum xObsL - minimum xObsL) * 0.2
-                        xMid    = 0.5 * (xMin + xMax)
-                    putStrLn $ "Loaded " ++ show n ++ " rows × " ++ show q
-                                ++ " outputs; method=" ++ mroMethod opts
-                    sections <- case mroMethod opts of
-                      "linear" -> do
-                        let mf       = MLM.fitMultiLM xMat1 ys
-                            betaB    = Core.coefficients (MLM.mfFit mf)
-                            ints     = LA.toList (betaB LA.! 0)
-                            slps     = LA.toList (betaB LA.! 1)
-                            res      = Core.residuals (MLM.mfFit mf)
-                            rmse     = sqrt (LA.sumElements (res*res)
-                                              / fromIntegral (n * q))
-                            r2v      = Core.rSquared (MLM.mfFit mf)
-                            r2mean   = LA.sumElements r2v / fromIntegral q
-                            imo      = RB.mkInteractiveMOLinear
-                                         (T.pack xCol)
-                                         "y" (T.pack (mroXAxis opts))
-                                         outGrid xObsL yObsL
-                                         ints slps (xMin, xMid, xMax)
-                        printf "  RMSE = %.4f, R^2 mean = %.4f\n" rmse r2mean
-                        return
-                          [ RB.secModelOverview "Multi-output Linear (B = (X'X)^-1 X'Y)"
-                              "$\\hat{Y} = X B$" Nothing
-                          , RB.secStatRow
-                              [ ("N", T.pack (show n))
-                              , ("q", T.pack (show q))
-                              , ("RMSE", T.pack (printf "%.4f" rmse))
-                              , ("R^2 mean", T.pack (printf "%.4f" r2mean))
-                              ]
-                          , RB.secInteractiveMultiOut "予測曲線 (スライダ)" imo
-                          ]
-                      "kernel-rbf" -> do
-                        let hs   = case mroH opts of
-                                     Just h | not (mroAutoHP opts) -> [h]
-                                     _ -> Kern.defaultHGrid xV
-                            lams = case mroLambda opts of
-                                     Just l | not (mroAutoHP opts) -> [l]
-                                     _ -> Kern.defaultLamGrid
-                            (fit, bestH, bestL, looMSE) =
-                              Kern.autoTuneKernelRidgeMulti
-                                Kern.Gaussian xV ys hs lams
-                            yhat = Kern.fittedKernelRidgeMulti fit
-                            r2v  = Kern.r2Multi ys yhat
-                            res  = ys - yhat
-                            rmse = sqrt (LA.sumElements (res*res)
-                                          / fromIntegral (n * q))
-                            r2mean = V.sum r2v / fromIntegral q
-                            alpha2 = [ LA.toList (LA.flatten (Kern.krmAlpha fit LA.? [i]))
-                                     | i <- [0 .. n - 1] ]
-                            imo    = RB.mkInteractiveMOKernelRBF
-                                       (T.pack xCol) "y"
-                                       (T.pack (mroXAxis opts))
-                                       outGrid xObsL yObsL
-                                       xObsL alpha2 bestH
-                                       (xMin, xMid, xMax)
-                        printf "  best h=%.3g  λ=%.3g  LOO MSE=%.3g  RMSE=%.4f  R^2 mean=%.4f\n"
-                          bestH bestL looMSE rmse r2mean
-                        return
-                          [ RB.secModelOverview "Multi-output Kernel Ridge (RBF)"
-                              "$\\hat{y}_j(x)=\\sum_i K_h(x,x_i)\\,\\alpha_{ij}$" Nothing
-                          , RB.secStatRow
-                              [ ("N", T.pack (show n))
-                              , ("q", T.pack (show q))
-                              , ("h",      T.pack (printf "%.3g" bestH))
-                              , ("λ",      T.pack (printf "%.3g" bestL))
-                              , ("LOO MSE", T.pack (printf "%.3g" looMSE))
-                              , ("RMSE",   T.pack (printf "%.4f" rmse))
-                              , ("R^2 mean", T.pack (printf "%.4f" r2mean))
-                              ]
-                          , RB.secInteractiveMultiOut "予測曲線 (スライダ)" imo
-                          ]
-                      m -> do
-                        hPutStrLn stderr ("multireg: unknown method '" ++ m
-                                            ++ "' (use linear|kernel-rbf)")
-                        return []
-                    if null sections
-                      then return ()
-                      else do
-                        let cfg = RB.defaultReportConfig
-                                    (T.pack ("multireg: " ++ file))
-                        RB.renderReport (mroReport opts) cfg sections
-                        putStrLn ("Wrote " ++ mroReport opts)
-    _ -> hPutStrLn stderr multiRegUsage
-  where
-    mtoMaybe Nothing  = []
-    mtoMaybe (Just _) = ["x"]
-    -- "y_z*" → all columns starting with "y_z"
-    -- "a,b,c" → ["a","b","c"]
-    resolveYSpec spec allCols
-      | last' spec == Just '*' =
-          let pre = init spec
-          in [ c | c <- allCols, take (length pre) c == pre, c /= xCol0 spec ]
-      | otherwise = wordsBy (== ',') spec
-    last' []     = Nothing
-    last' s      = Just (last s)
-    -- xCol0 is irrelevant for filtering but ensure no accidental match
-    xCol0 _      = ""
-
diff --git a/bench/haskell/BenchBO.hs b/bench/haskell/BenchBO.hs
deleted file mode 100644
--- a/bench/haskell/BenchBO.hs
+++ /dev/null
@@ -1,115 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Bayesian Optimization benchmarks (B5).
---
--- Branin (2D) and Hartmann6 (6D) for 5 seeds, budget = 30 evaluations.
--- Reports median wall time and median final f(x*).
-
-module Main where
-
-import qualified Hanalyze.Optim.BayesOpt          as BO
-import qualified System.Random.MWC       as MWC
-import qualified Data.Vector             as V
-import           Data.Word               (Word32)
-import           Data.List               (sort)
-import           Control.Monad           (forM)
-
-import           BenchUtil
-
--- ---------------------------------------------------------------------------
--- Test functions
--- ---------------------------------------------------------------------------
-
--- | Branin global minimum is f* = 0.397887 at three points.
-branin :: [Double] -> IO Double
-branin [x1, x2] =
-  let a = 1
-      b = 5.1 / (4 * pi * pi)
-      c = 5 / pi
-      r = 6
-      s = 10
-      t = 1 / (8 * pi)
-  in return $ a * (x2 - b * x1 * x1 + c * x1 - r) ** 2
-            + s * (1 - t) * cos x1 + s
-branin _ = return 1e30
-
-braninBounds :: [(Double, Double)]
-braninBounds = [(-5, 10), (0, 15)]
-
-braninStar :: Double
-braninStar = 0.397887
-
--- | Hartmann 6D, global min f* = -3.32237 at known x*.
-hartmann6 :: [Double] -> IO Double
-hartmann6 xs =
-  let alpha = [1.0, 1.2, 3.0, 3.2]
-      a = [ [10, 3, 17, 3.5, 1.7, 8]
-          , [0.05, 10, 17, 0.1, 8, 14]
-          , [3, 3.5, 1.7, 10, 17, 8]
-          , [17, 8, 0.05, 10, 0.1, 14] ]
-      p = [ [0.1312, 0.1696, 0.5569, 0.0124, 0.8283, 0.5886]
-          , [0.2329, 0.4135, 0.8307, 0.3736, 0.1004, 0.9991]
-          , [0.2348, 0.1451, 0.3522, 0.2883, 0.3047, 0.6650]
-          , [0.4047, 0.8828, 0.8732, 0.5743, 0.1091, 0.0381] ]
-      term i =
-        let aRow = a !! i
-            pRow = p !! i
-            inner = sum [ aRow !! j * (xs !! j - pRow !! j) ** 2 | j <- [0..5] ]
-        in alpha !! i * exp (- inner)
-  in return $ negate $ sum [term i | i <- [0..3]]
-
-hartmann6Bounds :: [(Double, Double)]
-hartmann6Bounds = replicate 6 (0, 1)
-
-hartmann6Star :: Double
-hartmann6Star = -3.32237
-
--- ---------------------------------------------------------------------------
--- Driver
--- ---------------------------------------------------------------------------
-
-nSeeds :: Int
-nSeeds = 5
-
-mainBranin :: IO BenchRow
-mainBranin = do
-  let cfg = BO.defaultBayesOptConfig
-              { BO.boIterations = 30, BO.boInitPoints = 5 }
-  rs <- mapM (\s -> runND cfg branin braninBounds s) [1 .. nSeeds]
-  let (ts, ys) = unzip rs
-      medT = median ts
-      medY = median ys
-  return $ BenchRow "haskell" "bo" "Branin/BO" medT medY
-             braninStar
-             ("median over " ++ show nSeeds ++ " seeds; star=" ++ show braninStar)
-
-mainHartmann6 :: IO BenchRow
-mainHartmann6 = do
-  let cfg = BO.defaultBayesOptConfig
-              { BO.boIterations = 30, BO.boInitPoints = 10 }
-  rs <- mapM (\s -> runND cfg hartmann6 hartmann6Bounds s) [1 .. nSeeds]
-  let (ts, ys) = unzip rs
-      medT = median ts
-      medY = median ys
-  return $ BenchRow "haskell" "bo" "Hartmann6/BO" medT medY
-             hartmann6Star
-             ("median over " ++ show nSeeds ++ " seeds; star=" ++ show hartmann6Star)
-
-{-# NOINLINE runND #-}
-runND :: BO.BayesOptConfig -> ([Double] -> IO Double) -> [(Double, Double)]
-      -> Int -> IO (Double, Double)
-runND cfg f bs seed = do
-  gen <- MWC.initialize (V.singleton (fromIntegral seed) :: V.Vector Word32)
-  (ms, (_hist, (_xstar, ystar))) <- timeitIO 1 (\(_,(_,y)) -> y)
-                                      (\_ -> BO.bayesOptND cfg 20 f bs gen)
-  return (ms, ystar)
-
-main :: IO ()
-main = do
-  rs <- sequence [mainBranin, mainHartmann6]
-  writeRows "bench/results/haskell/bo.csv" rs
-  putStrLn $ "wrote " ++ show (length rs)
-          ++ " rows → bench/results/haskell/bo.csv"
-
-median :: Ord a => [a] -> a
-median xs = sort xs !! (length xs `div` 2)
diff --git a/bench/haskell/BenchBetaIsolate.hs b/bench/haskell/BenchBetaIsolate.hs
deleted file mode 100644
--- a/bench/haskell/BenchBetaIsolate.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module Main where
-import qualified System.Random.MWC as MWC
-import qualified System.Random.MWC.Distributions as MWCD
-import Data.Time.Clock (getCurrentTime, diffUTCTime)
-import qualified Data.Vector.Storable as VS
-import Hanalyze.MCMC.Gibbs (sampleBetaBB)
-
-sampleBetaGamma :: Double -> Double -> MWC.GenIO -> IO Double
-sampleBetaGamma a b gen = do
-  x <- MWCD.gamma a 1 gen
-  y <- MWCD.gamma b 1 gen
-  return (x / (x + y))
-
-timeit :: String -> IO a -> IO a
-timeit name act = do
-  t0 <- getCurrentTime
-  r  <- act
-  t1 <- getCurrentTime
-  putStrLn $ name ++ ": " ++ show (1000.0 * realToFrac (diffUTCTime t1 t0) :: Double) ++ " ms"
-  return r
-
-main :: IO ()
-main = do
-  gen <- MWC.create
-  let n = 10000 :: Int
-  _ <- VS.replicateM n (sampleBetaGamma 14 10 gen)  -- warmup
-  _ <- timeit "10000 sampleBetaGamma (2 gamma + div)" (VS.replicateM n (sampleBetaGamma 14 10 gen))
-  _ <- timeit "10000 sampleBetaBB    (Cheng BB)    " (VS.replicateM n (sampleBetaBB    14 10 gen))
-  _ <- timeit "10000 gamma 14                       " (VS.replicateM n (MWCD.gamma 14 1 gen))
-  _ <- timeit "10000 uniform                        " (VS.replicateM n (MWC.uniform gen :: IO Double))
-  return ()
diff --git a/bench/haskell/BenchBootstrapIsolate.hs b/bench/haskell/BenchBootstrapIsolate.hs
deleted file mode 100644
--- a/bench/haskell/BenchBootstrapIsolate.hs
+++ /dev/null
@@ -1,91 +0,0 @@
-module Main where
-import qualified System.Random.MWC as MWC
-import qualified System.Random.MWC.Distributions as MWCD
-import qualified Numeric.LinearAlgebra as LA
-import qualified Data.Vector.Storable as VS
-import qualified Data.Vector.Storable.Mutable as MVS
-import Data.Time.Clock (getCurrentTime, diffUTCTime)
-import Data.Word (Word64)
-import qualified Data.Bits as Bits
-
-timeit :: String -> IO a -> IO a
-timeit name act = do
-  t0 <- getCurrentTime
-  r  <- act
-  t1 <- getCurrentTime
-  putStrLn $ name ++ ": " ++ show (1000.0 * realToFrac (diffUTCTime t1 t0) :: Double) ++ " ms"
-  return r
-
-main :: IO ()
-main = do
-  gen <- MWC.create
-  let n = 1000 :: Int
-      total = n * n      -- 1M ops, B=1000 × n=1000
-      xs = LA.fromList [fromIntegral i | i <- [0..n-1]] :: LA.Vector Double
-  -- Warmup
-  buf0 <- MVS.unsafeNew total
-  let warm i | i >= total = pure () | otherwise = do
-                j <- MWC.uniformR (0, n - 1) gen
-                MVS.unsafeWrite buf0 i (xs `LA.atIndex` j)
-                warm (i+1)
-  warm 0
-
-  -- 1) uniformR Int × 1M
-  buf1 <- MVS.unsafeNew total
-  _ <- timeit "uniformR (0,n-1) × 1M, write Int as Double" $ do
-    let go i | i >= total = pure () | otherwise = do
-                j <- MWC.uniformR (0, n - 1) gen
-                MVS.unsafeWrite buf1 i (fromIntegral j :: Double)
-                go (i+1)
-    go 0
-
-  -- 2) gather only (deterministic indices)
-  buf2 <- MVS.unsafeNew total
-  _ <- timeit "gather only (det. idx) × 1M" $ do
-    let go i | i >= total = pure () | otherwise = do
-                let j = i `mod` n
-                MVS.unsafeWrite buf2 i (xs `LA.atIndex` j)
-                go (i+1)
-    go 0
-
-  -- 3) full bootstrap fill (uniformR + gather)
-  buf3 <- MVS.unsafeNew total
-  _ <- timeit "uniformR + gather × 1M (full bootstrap fill)" $ do
-    let go i | i >= total = pure () | otherwise = do
-                j <- MWC.uniformR (0, n - 1) gen
-                MVS.unsafeWrite buf3 i (xs `LA.atIndex` j)
-                go (i+1)
-    go 0
-
-  -- 4) raw uniform Word64 × 1M (cheaper than uniformR)
-  buf4 <- MVS.unsafeNew total :: IO (MVS.IOVector Double)
-  _ <- timeit "raw uniform Word64 × 1M (no range, no gather)" $ do
-    let go i | i >= total = pure () | otherwise = do
-                _ <- MWC.uniform gen :: IO Word64
-                MVS.unsafeWrite buf4 i 0
-                go (i+1)
-    go 0
-
-  -- 5) uniform Word64 + bitmask gather (assuming n is power-of-2-ish)
-  buf5 <- MVS.unsafeNew total
-  let mask = fromIntegral (n - 1) :: Word64   -- only valid if n is 2^k; here 1000 isn't, so this is a Lower-bound timing
-  _ <- timeit "uniform Word64 + bitmask + gather × 1M (LB)" $ do
-    let go i | i >= total = pure () | otherwise = do
-                w <- MWC.uniform gen :: IO Word64
-                let j = fromIntegral (w Bits..&. mask) `mod` n  -- still % to be safe
-                MVS.unsafeWrite buf5 i (xs `LA.atIndex` j)
-                go (i+1)
-    go 0
-
-  -- 6) sumElements × B=1000 (per-row mean dispatch overhead)
-  let mat0 = LA.reshape n (LA.fromList [fromIntegral (i `mod` 7) | i <- [0..total-1]])
-  _ <- timeit "B=1000 × LA.sumElements (per-row stat dispatch)" $ do
-    let !sums = sum [LA.sumElements (mat0 LA.! r) | r <- [0..n-1]]
-    print sums
-
-  -- 7) BLAS row-sum via GEMV (mat #> ones)
-  let ones = LA.konst 1 n :: LA.Vector Double
-  _ <- timeit "B=1000 × n=1000 GEMV row sums (mat #> ones)" $ do
-    let !s = LA.sumElements (mat0 LA.#> ones)
-    print s
-  return ()
diff --git a/bench/haskell/BenchCustomDesign.hs b/bench/haskell/BenchCustomDesign.hs
deleted file mode 100644
--- a/bench/haskell/BenchCustomDesign.hs
+++ /dev/null
@@ -1,574 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -Wno-unused-imports #-}
--- | Phase 27: Custom Design (JMP 同等性) 検証ベンチ。
---
--- 文献例題と JMP 公式 example の **参照値 (criterion / D-eff)** を golden CSV
--- として保持し、 hanalyze 実装の出力との比較を deterministic seed 固定で
--- 実行する。
---
--- 入力 (golden):  @bench/custom-design/golden/<example>.csv@
---   - reference 設計行列 (whole_plot, factor1, factor2, ... header)
---   - 値は論文記載値そのまま (Jones-Goos (2012) Table 2/4 等)
---
--- 出力 (results): @bench/custom-design/results/golden-comparison.csv@
---   - schema: @example,metric,hanalyze_value,reference_value,ratio,tolerance,pass@
---
--- ## 実装メモ: split-plot D-criterion の重複実装
---
--- Phase 25 で SplitPlot.evalCritSP は internal (非 public)。 bench から
--- 直接呼べないので、 同じ M⁻¹ 構築ロジックをここに重複実装している。
--- 仕様変更時は src/Hanalyze/Design/Custom/SplitPlot.hs と本ファイル両方を
--- 更新すること (簡易 REML criterion: critValueM(DOpt, chol(X' M⁻¹ X)) =
--- -det(X' M⁻¹ X))。
-module Main where
-
-import qualified Data.ByteString            as BS
-import qualified Data.ByteString.Lazy.Char8 as BL
-import           Data.Csv                   (HasHeader (..), decode)
-import qualified Data.Text                  as T
-import qualified Data.Vector                as V
-import qualified Data.Vector.Storable       as VS
-import qualified Numeric.LinearAlgebra      as LA
-import           System.Directory           (createDirectoryIfMissing,
-                                             doesFileExist)
-import           System.IO                  (BufferMode (..), IOMode (..),
-                                             hPutStrLn, hSetBuffering, withFile)
-import           Text.Printf                (printf)
-
-import qualified Hanalyze.Design.Custom.Bayesian     as CB
-import qualified Hanalyze.Design.Custom.Constraint   as CC
-import qualified Hanalyze.Design.Custom.Coordinate   as CX
-import qualified Hanalyze.Design.Custom.Factor       as CF
-import qualified Hanalyze.Design.Custom.Model        as CM
-import qualified Hanalyze.Design.Custom.RegionMoment as RM
-import qualified Hanalyze.Design.Custom.SplitPlot    as SP
-import qualified Hanalyze.Design.Optimal             as OPT
-
--- ===========================================================================
--- 比較結果 row (results/golden-comparison.csv の schema)
--- ===========================================================================
-
--- | 1 (example, metric) ペアの比較結果。
-data GoldenRow = GoldenRow
-  { grExample   :: String
-  , grMetric    :: String
-  , grhanalyze   :: Double
-  , grReference :: Double
-  , grTolerance :: Double
-  } deriving Show
-
-grRatio :: GoldenRow -> Double
-grRatio r
-  | grReference r == 0 = 0 / 0
-  | otherwise          = grhanalyze r / grReference r
-
-grPass :: GoldenRow -> Bool
-grPass r =
-  let ratio = grRatio r
-  in  not (isNaN ratio) && abs (ratio - 1) <= grTolerance r
-
-writeGoldenRows :: FilePath -> [GoldenRow] -> IO ()
-writeGoldenRows path rows = withFile path WriteMode $ \h -> do
-  hSetBuffering h LineBuffering
-  hPutStrLn h "example,metric,hanalyze_value,reference_value,ratio,tolerance,pass"
-  mapM_ (\r -> hPutStrLn h
-          (printf "%s,%s,%.10g,%.10g,%.10g,%.6g,%s"
-            (grExample r) (grMetric r)
-            (grhanalyze r) (grReference r)
-            (grRatio r) (grTolerance r)
-            (if grPass r then "true" else "false" :: String))) rows
-
--- ===========================================================================
--- 文献参照値 CSV の読み込み
--- ===========================================================================
-
--- | 設計行列 CSV (header: whole_plot,x1,x2,...) を読み、
--- (raw matrix, whole-plot indicator) を返す。
-readDesignCSV
-  :: FilePath
-  -> IO (Either String (LA.Matrix Double, VS.Vector Int, Int))
-readDesignCSV path = do
-  exists <- doesFileExist path
-  if not exists
-    then pure (Left ("design file not found: " ++ path))
-    else do
-      bytes <- BL.fromStrict <$> BS.readFile path
-      case decode HasHeader bytes :: Either String (V.Vector (V.Vector Double)) of
-        Left  err -> pure (Left ("decode " ++ path ++ ": " ++ err))
-        Right rs
-          | V.null rs -> pure (Left ("empty CSV: " ++ path))
-          | otherwise ->
-              let nCols = V.length (rs V.! 0)
-                  nRows = V.length rs
-                  -- col 0 = whole_plot id (1-based in CSV → 0-based internal)
-                  wpIds = VS.fromList
-                            [ round (rs V.! i V.! 0) - 1
-                            | i <- [0 .. nRows - 1] ]
-                  rawMat = LA.fromLists
-                             [ [ rs V.! i V.! j | j <- [1 .. nCols - 1] ]
-                             | i <- [0 .. nRows - 1] ]
-                  nWP   = maximum (VS.toList wpIds) + 1
-              in  pure (Right (rawMat, wpIds, nWP))
-
--- ===========================================================================
--- Split-Plot D-criterion 評価 (SplitPlot.evalCritSP の重複実装)
--- ===========================================================================
-
--- | M⁻¹ を block-diagonal で構築。 各 WP block の diag = 1 - η/(1 + η n_w)、
--- off-diag = -η/(1 + η n_w)。 SplitPlot.buildMInv と同じロジック。
-buildMInv :: Int -> Double -> VS.Vector Int -> Int -> LA.Matrix Double
-buildMInv n eta wpId nWP =
-  let wpSizes = [ length [ i | i <- [0 .. n - 1], wpId VS.! i == w ]
-                | w <- [0 .. nWP - 1] ]
-      entry i j
-        | wpId VS.! i /= wpId VS.! j = 0
-        | otherwise =
-            let w   = wpId VS.! i
-                nwD = fromIntegral (wpSizes !! w) :: Double
-                off = - eta / (1 + eta * nwD)
-            in if i == j then 1 + off else off
-  in (n LA.>< n) [ entry i j | i <- [0 .. n - 1], j <- [0 .. n - 1] ]
-
--- | det(X' M⁻¹ X) を返す。 模型展開 (intercept + main + interaction + quadratic)
--- は expandDesignMatrix に委譲。
---
--- 返り値 = SplitPlot criterion 値の絶対値 (positive)。
--- hanalyze の `spdGEFFEst` は -det(X' M⁻¹ X)、 ここでは +det(X' M⁻¹ X)
--- を返すので、 比較時に sign 注意。
-splitPlotDDet
-  :: [CF.Factor]
-  -> CM.Model
-  -> Double                 -- ^ η
-  -> VS.Vector Int          -- ^ WP id per row
-  -> Int                    -- ^ nWP
-  -> LA.Matrix Double       -- ^ raw design matrix
-  -> Either String Double
-splitPlotDDet factors model eta wpId nWP raw =
-  case CM.expandDesignMatrix factors model raw of
-    Left e  -> Left (T.unpack e)
-    Right x ->
-      let n    = LA.rows x
-          mInv = buildMInv n eta wpId nWP
-          xtmx = LA.tr x LA.<> (mInv LA.<> x)
-      in  Right (LA.det xtmx)
-
--- ===========================================================================
--- Jones-Goos (2012) Table 2 比較: 20-run Split-Plot
--- ===========================================================================
-
--- 共通仕様: 1 WP 因子 w + 1 SP 因子 s、 連続 [-1, 1]、 full quadratic、
--- 4 WP × 5 SP runs、 η = 1。
-jonesGoosTable2Spec :: CX.CustomDesignSpec
-jonesGoosTable2Spec = CX.CustomDesignSpec
-  { CX.cdsFactors =
-      [ CF.Factor "w" (CF.Continuous (-1) 1) CF.HardToChange
-      , CF.Factor "s" (CF.Continuous (-1) 1) CF.Controllable
-      ]
-  , CX.cdsModel =
-      CM.Model
-        [ CM.TIntercept
-        , CM.TMain "w", CM.TMain "s"
-        , CM.TInter ["w","s"]
-        , CM.TPower "w" 2, CM.TPower "s" 2
-        ]
-        CM.NCoded
-  , CX.cdsConstraints = []
-  , CX.cdsNRuns       = 20
-  , CX.cdsCriterion   = OPT.DOpt
-  , CX.cdsBudget      = CX.defaultBudget
-  , CX.cdsSeed        = Just 42
-  , CX.cdsInitial     = Nothing
-
-  , CX.cdsDJConvention = False
-  }
-
-benchJonesGoosTable2 :: IO [GoldenRow]
-benchJonesGoosTable2 = do
-  let factors = CX.cdsFactors jonesGoosTable2Spec
-      model   = CX.cdsModel   jonesGoosTable2Spec
-      eta     = 1.0           -- ^ Jones-Goos (2012) Table 3 η=1 列に対応
-      example = "jones-goos-2012-table2-splitplot-20run"
-      pathD   = "bench/custom-design/golden/jones-goos-2012-table2-dopt-design.csv"
-
-  -- 参照値 (Jones-Goos D-opt design) を読み込み、 D-criterion を計算
-  ref <- readDesignCSV pathD
-  case ref of
-    Left err -> do
-      putStrLn ("[skip] " ++ example ++ ": " ++ err)
-      pure []
-    Right (refRaw, refWpId, refNWP) ->
-      case splitPlotDDet factors model eta refWpId refNWP refRaw of
-        Left err -> do
-          putStrLn ("[skip] " ++ example ++ ": ref det failed: " ++ err)
-          pure []
-        Right refDet -> do
-          -- hanalyze で同じ仕様の D-opt を生成
-          let cfg = SP.SplitPlotConfig
-                { SP.spcNWhole = 4, SP.spcVarRatio = eta, SP.spcNStrip = Nothing }
-          oursE <- SP.generateSplitPlot jonesGoosTable2Spec cfg
-          case oursE of
-            Left err -> do
-              putStrLn ("[skip] " ++ example ++ ": hanalyze gen failed: "
-                        ++ T.unpack err)
-              pure []
-            Right ours -> do
-              -- hanalyze 側の D-criterion = -spdGEFFEst (sign 反転)
-              -- (spdGEFFEst = -det(X' M⁻¹ X))
-              let oursDet = - SP.spdGEFFEst ours
-                  -- D-efficiency = (det_ours / det_ref)^(1/p)、
-                  -- p = 模型項数 = 6 (Intercept, w, s, ws, w², s²)
-                  pTerms  = 6 :: Int
-                  invP    = 1.0 / fromIntegral pTerms
-                  dEffRaw = oursDet / refDet
-                  dEffPth = if dEffRaw <= 0 then 0
-                              else dEffRaw ** invP
-              putStrLn $ printf "  refDet=%.6g oursDet=%.6g  D-eff (raw)=%.4f  D-eff (pth root)=%.4f"
-                refDet oursDet dEffRaw dEffPth
-              pure
-                [ GoldenRow
-                    { grExample   = example
-                    , grMetric    = "D-criterion-ratio-raw"
-                    , grhanalyze   = oursDet
-                    , grReference = refDet
-                    , grTolerance = 0.02
-                    }
-                , GoldenRow
-                    { grExample   = example
-                    , grMetric    = "D-efficiency-pth-root"
-                    , grhanalyze   = dEffPth
-                    , grReference = 1.0
-                    , grTolerance = 0.02
-                    }
-                ]
-
--- ===========================================================================
--- DuMouchel-Jones (1994) Example 3 "Both" 比較
--- ===========================================================================
---
--- 一次根拠: DuMouchel & Jones (1994) "A Simple Bayesian Modification of
--- D-Optimal Designs", Technometrics 36(1):37-47、 §3.3 Example 3、 Table 1
--- (page 41) "Both" 列。
---
--- 仕様: 4 連続因子 A/B/C/D ∈ {-1, 0, 1}、 n=9、 primary p=5 (intercept + 4 main)、
--- potential q=10 (4 squares + 6 2-factor interactions)、 τ=1。
--- 「Both」 設計 = 8-run resolution IV 2^(4-1) FF (I = ABCD) + 1 centerpoint。
-
-dumouchelJonesEx3Factors :: [CF.Factor]
-dumouchelJonesEx3Factors =
-  [ CF.Factor n (CF.Continuous (-1) 1) CF.Controllable
-  | n <- ["A", "B", "C", "D"]
-  ]
-
--- | primary + potential を一括 expand する model。
--- DuMouchel-Jones 1994 §3.3 では「Both」 列の potential は q=10 (4 squares + 6 2fi)。
--- primary は p=5 (intercept + 4 main effects)。
-dumouchelJonesEx3Model :: CM.Model
-dumouchelJonesEx3Model = CM.Model
-  ( [CM.TIntercept]
-    ++ [CM.TMain n          | n <- ["A","B","C","D"]]
-    ++ [CM.TPower n 2       | n <- ["A","B","C","D"]]
-    ++ [CM.TInter [a, b]    | (a, b) <- [("A","B"),("A","C"),("A","D")
-                                        ,("B","C"),("B","D"),("C","D")]]
-  ) CM.NCoded
-
--- | DJ Example 3 候補集合: {-1, 0, 1}^4 = 81 点 (paper §3.3、 dbCxStepGrid=3)。
-dumouchelJonesEx3Candidate :: LA.Matrix Double
-dumouchelJonesEx3Candidate = LA.fromLists
-  [ [a, b, c, d] | a <- vs, b <- vs, c <- vs, d <- vs ]
-  where vs = [-1, 0, 1] :: [Double]
-
-benchDuMouchelJonesEx3Both :: IO [GoldenRow]
-benchDuMouchelJonesEx3Both = do
-  let factors = dumouchelJonesEx3Factors
-      model   = dumouchelJonesEx3Model
-      tau2    = 1.0
-      kPrior  = CB.priorPrecisionDefault factors model tau2
-      example = "dumouchel-jones-1994-example3-both"
-      pathRef = "bench/custom-design/golden/dumouchel-jones-1994-example3-both.csv"
-      cand    = dumouchelJonesEx3Candidate
-
-  -- 文献設計を読み込み (CSV: A,B,C,D の 9 行)
-  refE <- readPlainDesignCSV pathRef
-  case refE of
-    Left err -> do
-      putStrLn ("[skip] " ++ example ++ ": " ++ err)
-      pure []
-    Right refRaw -> case CB.djFitTransform factors model cand of
-      Left e -> do
-        putStrLn ("[skip] " ++ example ++ ": DJ transform fit failed: "
-                  ++ T.unpack e)
-        pure []
-      Right djT -> do
-        -- 参照: expand → DJ transform → det(X_t' X_t + K)
-        let refDet =
-              case CM.expandDesignMatrix factors model refRaw of
-                Left e  -> error ("ref expand failed: " ++ T.unpack e)
-                Right x -> CB.bayesianDValueM kPrior (CB.djApplyTransform djT x)
-
-        -- hanalyze で同じ仕様 + BayesianD K で 9-run 設計を生成。
-        -- 注意: coordinateExchange は DJ 規約適用前の生 X で BayesianD を
-        -- 最適化する (28-12 では coordinateExchange への自動適用は未対応)。
-        -- 生成後の設計に対し DJ 変換を適用して det 比較する。
-        let spec = CX.CustomDesignSpec
-              { CX.cdsFactors     = factors
-              , CX.cdsModel       = model
-              , CX.cdsConstraints = []
-              , CX.cdsNRuns       = 9
-              , CX.cdsCriterion   = OPT.BayesianD (CB.precisionToMatrix kPrior)
-              , CX.cdsBudget      = CX.defaultBudget
-                  { CX.dbCxStepGrid = 3   -- ^ {-1, 0, 1} で論文と同じ候補集合
-                  , CX.dbRestarts   = 10  -- ^ 4 因子で multi-start を確保
-                  }
-              , CX.cdsSeed        = Just 42
-              , CX.cdsInitial     = Nothing
-
-              , CX.cdsDJConvention = True   -- ^ Phase 28-12 auto DJ 規約適用
-              }
-        oursE <- CX.coordinateExchange spec
-        case oursE of
-          Left err -> do
-            putStrLn ("[skip] " ++ example ++ ": hanalyze gen failed: "
-                      ++ T.unpack err)
-            pure []
-          Right cd -> do
-            let oursDet =
-                  case CM.expandDesignMatrix factors model (CX.cdMatrix cd) of
-                    Left e  -> error ("ours expand failed: " ++ T.unpack e)
-                    Right x -> CB.bayesianDValueM kPrior (CB.djApplyTransform djT x)
-                pTerms  = 1 + 4 + 4 + 6 :: Int  -- intercept + main + sq + 2fi = 15
-                invP    = 1.0 / fromIntegral pTerms
-                dEffRaw = if refDet <= 0 then 0 else oursDet / refDet
-                dEffPth = if dEffRaw <= 0 then 0 else dEffRaw ** invP
-            putStrLn $ printf "  [DJ §2.2 規約適用後] refDet=%.6g oursDet=%.6g  D-eff (raw)=%.4f  D-eff (pth root)=%.4f"
-              refDet oursDet dEffRaw dEffPth
-            pure
-              [ GoldenRow
-                  -- Phase 28-12 auto DJ 適用後: hanalyze coordinateExchange は
-                  -- DJ 変換後の det を直接最適化、 raw ratio が 1.0 近傍で収束する
-                  -- ことを確認 (tolerance 0.02)
-                  { grExample   = example
-                  , grMetric    = "BayesianD-criterion-ratio-raw-DJ"
-                  , grhanalyze   = oursDet
-                  , grReference = refDet
-                  , grTolerance = 0.02
-                  }
-              , GoldenRow
-                  { grExample   = example
-                  , grMetric    = "BayesianD-efficiency-pth-root-DJ"
-                  , grhanalyze   = dEffPth
-                  , grReference = 1.0
-                  , grTolerance = 0.02
-                  }
-              ]
-
--- | 設計行列 CSV (header: x1,x2,...) を 1 つの Matrix Double として読む
--- (WP indicator を含まない平 raw 形式)。
-readPlainDesignCSV :: FilePath -> IO (Either String (LA.Matrix Double))
-readPlainDesignCSV path = do
-  exists <- doesFileExist path
-  if not exists
-    then pure (Left ("design file not found: " ++ path))
-    else do
-      bytes <- BL.fromStrict <$> BS.readFile path
-      case decode HasHeader bytes :: Either String (V.Vector (V.Vector Double)) of
-        Left  err -> pure (Left ("decode " ++ path ++ ": " ++ err))
-        Right rs
-          | V.null rs -> pure (Left ("empty CSV: " ++ path))
-          | otherwise ->
-              let nRows = V.length rs
-                  nCols = V.length (rs V.! 0)
-                  mat = LA.fromLists
-                          [ [ rs V.! i V.! j | j <- [0 .. nCols - 1] ]
-                          | i <- [0 .. nRows - 1] ]
-              in  pure (Right mat)
-
--- ===========================================================================
--- Phase 28-4d: JMP RSM Constraints + Categorical (18-run I-opt) 比較
--- ===========================================================================
---
--- 一次根拠: JMP 12 「Design of Experiments Example: A Response Surface Design
--- with Constraints and a Categorical Factor」 PDF (JMP community sample-data
--- attachment、 公開資料)。
---
--- 仕様:
---   * 因子: Time ∈ [500, 560] (coded [-1, 1])、 Temperature ∈ [350, 750]
---     (coded [-1, 1])、 Catalyst ∈ {A, B, C}
---   * 模型: RSM (intercept + main + 2fi + 連続因子の x²)
---     - p = 1 + 3 (main: Time/Temp/Cat = 1+1+2) + 5 (2fi: T·Temp/T·Cat/Temp·Cat
---       = 1+2+2) + 2 (T²/Temp²) = 12
---   * 制約: Conditional (Catalyst = B → Temp_coded ≥ -0.75)
---           Conditional (Catalyst = C → Temp_coded ≤ +0.5)
---           (元: B→Temp≥400 / C→Temp≤650、 coded 換算)
---   * JMP setup: I-opt criterion、 seed=654321、 starts=1000、 18-run
---
--- 比較 metric: IOptRegion criterion (= trace((X'X)⁻¹ · M_R))
---   * Phase 28-4c 後: 制約 region 込みの **MC 版 M_R** (Halton N=10000) を使用、
---     厳密な constrained-region I-criterion で比較
---   * hanalyze 側: 同一 spec + 同一制約で coordinateExchange を回し、
---     I-opt criterion 値を比較。 coordinateExchange も内部で同じ MC M_R に
---     基づいて IOpt を最適化する (resolveIOptRegion の自動 MC fallback)。
---     ratio ≤ 1 = hanalyze が JMP 参照より同じ M_R 評価で劣らない
-
-jmpRsmFactors :: [CF.Factor]
-jmpRsmFactors =
-  [ CF.Factor "Time"        (CF.Continuous (-1) 1) CF.Controllable
-  , CF.Factor "Temperature" (CF.Continuous (-1) 1) CF.Controllable
-  , CF.Factor "Catalyst"    (CF.Categorical ["A","B","C"]) CF.Controllable
-  ]
-
-jmpRsmModel :: CM.Model
-jmpRsmModel = CM.Model
-  [ CM.TIntercept
-  , CM.TMain "Time", CM.TMain "Temperature", CM.TMain "Catalyst"
-  , CM.TInter ["Time","Temperature"]
-  , CM.TInter ["Time","Catalyst"]
-  , CM.TInter ["Temperature","Catalyst"]
-  , CM.TPower "Time" 2
-  , CM.TPower "Temperature" 2
-  ]
-  CM.NCoded
-
-jmpRsmConstraints :: [CC.Constraint]
-jmpRsmConstraints =
-  [ CC.Conditional (CC.GuardEq "Catalyst" (CC.FVText "B"))
-      [ CC.LinearIneq [("Temperature", 1)] CC.CGeq (-0.75) ]
-  , CC.Conditional (CC.GuardEq "Catalyst" (CC.FVText "C"))
-      [ CC.LinearIneq [("Temperature", 1)] CC.CLeq 0.5 ]
-  ]
-
--- | Time/Temperature/Catalyst の raw 値を coded matrix に変換。
--- Time: (t - 530)/30、 Temperature: (T - 550)/200、 Catalyst: A/B/C → 0/1/2。
-codeJmpRsmRow :: [String] -> Either String [Double]
-codeJmpRsmRow xs = case xs of
-  [_runIdx, tStr, tempStr, catStr] -> do
-    t    <- readD tStr
-    temp <- readD tempStr
-    cat  <- case dropWhile (== ' ') catStr of
-              ('A':_) -> Right 0
-              ('B':_) -> Right 1
-              ('C':_) -> Right 2
-              _       -> Left ("unknown Catalyst level: " ++ catStr)
-    pure [(t - 530) / 30, (temp - 550) / 200, fromIntegral (cat :: Int)]
-  _ -> Left ("expected 4 columns, got " ++ show (length xs))
-  where
-    readD s = case reads (filter (/= ' ') s) :: [(Double, String)] of
-      [(d, "")] -> Right d
-      _         -> Left ("cannot parse Double: " ++ s)
-
-readJmpRsmCSV :: FilePath -> IO (Either String (LA.Matrix Double))
-readJmpRsmCSV path = do
-  exists <- doesFileExist path
-  if not exists
-    then pure (Left ("design file not found: " ++ path))
-    else do
-      txt <- readFile path
-      let allLines = lines txt
-      case allLines of
-        [] -> pure (Left ("empty CSV: " ++ path))
-        (_hdr : rows) -> do
-          let parsed = traverse (codeJmpRsmRow . splitComma) rows
-          case parsed of
-            Left  e -> pure (Left e)
-            Right rs -> pure (Right (LA.fromLists rs))
-  where
-    splitComma = foldr step [[]]
-    step ',' acc      = [] : acc
-    step c   (h : t)  = (c : h) : t
-    step _   []       = []  -- unreachable (acc starts with [[]])
-
-benchJmpRsmConstraints :: IO [GoldenRow]
-benchJmpRsmConstraints = do
-  let factors = jmpRsmFactors
-      model   = jmpRsmModel
-      cons    = jmpRsmConstraints
-      example = "jmp-rsm-constraints-categorical-18run"
-      pathRef = "bench/custom-design/golden/jmp-rsm-constraints-categorical-design.csv"
-
-  refE <- readJmpRsmCSV pathRef
-  case refE of
-    Left err -> do
-      putStrLn ("[skip] " ++ example ++ ": " ++ err)
-      pure []
-    Right refRaw -> case RM.regionMomentMatrixMC 10000 factors model cons of
-      Left e -> do
-        putStrLn ("[skip] " ++ example ++ ": M_R (MC) failed: " ++ T.unpack e)
-        pure []
-      Right mR -> case CM.expandDesignMatrix factors model refRaw of
-        Left e -> do
-          putStrLn ("[skip] " ++ example ++ ": ref expand failed: " ++ T.unpack e)
-          pure []
-        Right refX -> do
-          let refI = RM.iValueRegionM mR refX
-          putStrLn $ printf "  ref design IOptRegion = %.6g" refI
-
-          -- hanalyze 側: 同 spec + 同制約 で coordinateExchange
-          let spec = CX.CustomDesignSpec
-                { CX.cdsFactors     = factors
-                , CX.cdsModel       = model
-                , CX.cdsConstraints = cons
-                , CX.cdsNRuns       = 18
-                , CX.cdsCriterion   = OPT.IOpt
-                , CX.cdsBudget      = CX.defaultBudget
-                , CX.cdsSeed        = Just 654321
-                , CX.cdsInitial     = Nothing
-
-                , CX.cdsDJConvention = False
-                }
-          oursE <- CX.coordinateExchange spec
-          case oursE of
-            Left e -> do
-              putStrLn ("[skip] " ++ example ++ ": hanalyze gen failed: "
-                        ++ T.unpack e)
-              pure []
-            Right ours -> case CM.expandDesignMatrix factors model (CX.cdMatrix ours) of
-              Left e -> do
-                putStrLn ("[skip] " ++ example ++ ": hanalyze expand failed: "
-                          ++ T.unpack e)
-                pure []
-              Right oursX -> do
-                let oursI = RM.iValueRegionM mR oursX
-                    ratio = if refI <= 0 || isInfinite oursI then 0/0
-                              else oursI / refI
-                putStrLn $ printf "  hanalyze IOptRegion = %.6g  ratio=%.4f"
-                  oursI ratio
-                pure
-                  [ GoldenRow
-                      { grExample   = example
-                      , grMetric    = "IOptRegion-criterion-ratio"
-                      , grhanalyze   = oursI
-                      , grReference = refI
-                        -- hanalyze が JMP より大きく劣らない (≤ 5% 増) を pass 基準。
-                        -- 制約条件が analytic M_R で無視されるため、 厳密同等は
-                        -- 期待できない (Phase 28-4c MC fallback で改善)
-                      , grTolerance = 0.05
-                      }
-                  ]
-
--- ===========================================================================
--- Main
--- ===========================================================================
-
-main :: IO ()
-main = do
-  createDirectoryIfMissing True "bench/custom-design/golden"
-  createDirectoryIfMissing True "bench/custom-design/results"
-
-  putStrLn "=== Phase 27-2: Jones-Goos (2012) Table 2 (20-run Split-Plot) ==="
-  rowsT2 <- benchJonesGoosTable2
-
-  putStrLn ""
-  putStrLn "=== Phase 27-3: DuMouchel-Jones (1994) Example 3 \"Both\" (9-run Bayesian-D) ==="
-  rowsDJ <- benchDuMouchelJonesEx3Both
-
-  putStrLn ""
-  putStrLn "=== Phase 28-4d: JMP RSM Constraints + Categorical (18-run I-opt) ==="
-  rowsRSM <- benchJmpRsmConstraints
-
-  let allRows = rowsT2 ++ rowsDJ ++ rowsRSM
-  writeGoldenRows "bench/custom-design/results/golden-comparison.csv" allRows
-
-  let nPass = length (filter grPass allRows)
-      nTot  = length allRows
-  putStrLn ""
-  putStrLn (printf "✓ %d/%d metrics pass、 結果: bench/custom-design/results/golden-comparison.csv"
-             nPass nTot)
diff --git a/bench/haskell/BenchDataGen.hs b/bench/haskell/BenchDataGen.hs
deleted file mode 100644
--- a/bench/haskell/BenchDataGen.hs
+++ /dev/null
@@ -1,196 +0,0 @@
--- | 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) []
diff --git a/bench/haskell/BenchFormulaRef.hs b/bench/haskell/BenchFormulaRef.hs
deleted file mode 100644
--- a/bench/haskell/BenchFormulaRef.hs
+++ /dev/null
@@ -1,137 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
--- | Phase 47 A5: Formula DSL の Haskell 参照値生成器。
---   bench/python/bench_formula.py が statsmodels / scipy と突合するための ŷ/R²/係数を
---   実際の Hanalyze 実装で計算し formula_haskell_ref.json に書き出す (再現可能化)。
---
---   実行: cabal run formula-ref-gen   (cwd は repo root を想定、 bench/python/ に書く)
-module Main (main) where
-
-import           Data.List              (intercalate)
-import           Data.Text              (Text)
-import qualified Data.Text              as T
-import qualified DataFrame.Internal.Column    as DX
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified Numeric.LinearAlgebra  as LA
-
-import qualified Hanalyze.Model.Core              as Core
-import           Hanalyze.Model.Formula.RFormula  (parseModel)
-import           Hanalyze.Model.Formula.Design    (fitLMF, fitWLSF, defaultWLS,
-                                                   WLSConfig (..))
-import           Hanalyze.Model.Formula.Nonlinear (fitNLS, nlsParams)
-import           Hanalyze.Model.Formula.Mixed     (fitMixedLME)
-import           Hanalyze.Model.GLMM              (GLMMResultRE (..))
-
--- ============================================================================
--- データ (Python 側と完全一致させる)
--- ============================================================================
-
-dfOLS :: DX.DataFrame
-dfOLS = DX.fromNamedColumns
-  [ ("y", DX.fromList ([10,20,30,40,50,60,12,22,34,44,52,62] :: [Double]))
-  , ("g", DX.fromList (["A","A","B","B","C","C","A","A","B","B","C","C"] :: [Text]))
-  , ("t", DX.fromList (["P","Q","P","Q","P","Q","P","Q","P","Q","P","Q"] :: [Text]))
-  , ("x", DX.fromList ([1,2,3,4,5,6,1,2,3,4,5,6] :: [Double]))
-  ]
-
-olsFormulas :: [Text]
-olsFormulas =
-  [ "y ~ x"
-  , "y ~ C(g) * C(t)"
-  , "y ~ C(g) + C(g):x"
-  , "y ~ x + I(x**2)"
-  , "y ~ C(g, Sum)"                  -- A2 contrast (ŷ は treatment と不変 → statsmodels 一致)
-  , "y ~ C(g, Sum) + C(g, Sum):x"
-  ]
-
-dfWLS :: DX.DataFrame
-dfWLS = DX.fromNamedColumns
-  [ ("y", DX.fromList ([2.1,3.9,6.2,7.8,10.1,12.2,13.8,16.1] :: [Double]))
-  , ("x", DX.fromList ([1,2,3,4,5,6,7,8] :: [Double]))
-  , ("w", DX.fromList ([1,1,2,2,3,3,4,4] :: [Double]))
-  ]
-
--- Phase 48: mixed-effects (random intercept + slope) 突合用データ。
--- 4 群 × 5 obs、 群ごとに切片・傾きが異なる線形 (固定平均 β≈[2,3])。
-dfMixed :: DX.DataFrame
-dfMixed = DX.fromNamedColumns
-  [ ("y", DX.fromList ([ 3.01,6.49,10.01,13.49,17.00
-                       , 1.01,3.49, 6.01, 8.49,11.00
-                       , 2.51,5.19, 7.91,10.59,13.30
-                       , 1.51,4.79, 8.11,11.39,14.70 ] :: [Double]))
-  , ("x", DX.fromList (concat (replicate 4 [0,1,2,3,4]) :: [Double]))
-  , ("g", DX.fromList (concatMap (replicate 5) (["A","B","C","D"] :: [Text])))
-  ]
-
-nlsXs :: [Double]
-nlsXs = [0,0.5,1,1.5,2,2.5,3,3.5,4,4.5,5]
-
-dfNLS :: DX.DataFrame
-dfNLS = DX.fromNamedColumns
-  [ ("y", DX.fromList (map (\x -> 3.0 * exp (negate 0.5 * x)) nlsXs))   -- a=3 b=0.5
-  , ("x", DX.fromList nlsXs)
-  ]
-
--- ============================================================================
--- JSON (手書き・依存最小)
--- ============================================================================
-
-jstr :: String -> String
-jstr s = "\"" ++ s ++ "\""
-
-jarr :: [Double] -> String
-jarr xs = "[" ++ intercalate ", " (map show xs) ++ "]"
-
-main :: IO ()
-main = do
-  olsEntries <- mapM olsEntry olsFormulas
-  wlsJson    <- pure wlsEntry
-  nlsJson    <- pure nlsEntry
-  mixedJson  <- pure mixedEntry
-  let body = intercalate ",\n  " (olsEntries ++ [wlsJson, nlsJson, mixedJson])
-      json = "{\n  " ++ body ++ "\n}\n"
-  writeFile "bench/python/formula_haskell_ref.json" json
-  putStrLn ("formula_haskell_ref.json 生成: OLS " ++ show (length olsEntries)
-            ++ " + __wls__ + __nls__ + __mixed__")
-
-olsEntry :: Text -> IO String
-olsEntry f =
-  case parseModel f >>= \fm -> fitLMF fm dfOLS of
-    Left e        -> error ("OLS " ++ T.unpack f ++ ": " ++ e)
-    Right (fr, _) ->
-      pure $ jstr (T.unpack f) ++ ": {\"r2\": " ++ show (Core.rSquared1 fr)
-             ++ ", \"yhat\": " ++ jarr (Core.fittedList fr) ++ "}"
-
--- WLS 係数 (parameterization 同一ゆえ係数も突合可)。
-wlsEntry :: String
-wlsEntry =
-  case parseModel "y ~ x" >>= \fm -> fitWLSF defaultWLS { wcWeights = Just "w" } fm dfWLS of
-    Left e        -> error ("WLS: " ++ e)
-    Right (fr, _) ->
-      jstr "__wls__" ++ ": {\"coef\": " ++ jarr (LA.toList (Core.coefficientsV fr)) ++ "}"
-
--- NLS パラメータ。
-nlsEntry :: String
-nlsEntry =
-  case parseModel "y x = a * exp(-b * x)" of
-    Left e   -> error ("NLS parse: " ++ e)
-    Right fm -> case fitNLS fm dfNLS [("a",1),("b",1)] of
-      Left e  -> error ("NLS: " ++ e)
-      Right r -> let pm = nlsParams r
-                     val k = maybe (0/0) id (lookup k pm)
-                 in jstr "__nls__" ++ ": {\"a\": " ++ show (val "a")
-                    ++ ", \"b\": " ++ show (val "b") ++ "}"
-
--- 混合効果 (random intercept + slope) の β / G (2×2) / σ²。
--- 突合先 = statsmodels smf.mixedlm(..., re_formula="~x").fit(reml=False) (ML)。
-mixedEntry :: String
-mixedEntry =
-  case fitMixedLME "y ~ x + (1+x|g)" dfMixed of
-    Left e         -> error ("mixed: " ++ e)
-    Right (res, _) ->
-      let beta = LA.toList (Core.coefficientsV (reFixed res))
-          g    = LA.toLists (reRandCov res)        -- [[g00,g01],[g10,g11]]
-          flatG = concat g
-      in jstr "__mixed__" ++ ": {\"beta\": " ++ jarr beta
-         ++ ", \"cov_re\": " ++ jarr flatG
-         ++ ", \"sigma2\": " ++ show (reResidVar res) ++ "}"
diff --git a/bench/haskell/BenchHBM54a.hs b/bench/haskell/BenchHBM54a.hs
deleted file mode 100644
--- a/bench/haskell/BenchHBM54a.hs
+++ /dev/null
@@ -1,231 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
-
--- | Phase 54.4a per-call 勾配ベンチ (推測するな計測せよ)。
---
--- 54.4a で gradADU をハイブリッド化した (Gaussian-恒等リンク ObserveLM ブロックの
--- 観測尤度勾配を自作 vector-op tape で計算・他は ad)。 その「実速度」 を、 同一の
--- 階層 Gaussian モデル (M2: random intercept) を 2 通りにエンコードして比較する:
---
---   scalar = glmmRandomIntercept (per-obs scalar observe) → gradADU は全体 ad
---   vecLM  = 同型を observeLM で表現 (群効果を設計行列の指示列に畳む)
---            → gradADU はハイブリッド (ObserveLM 部を vec-tape)
---
--- NUTS は 1 draw あたり leapfrog ごとに gradADU を多数回呼ぶので、 per-call の
--- gradADU 単価がそのまま per-draw コストの支配項。 ここでは per-call を直接測る
--- (NUTS の分散・固定費を排した最もクリーンな比較)。 各サイズで中心差分一致も確認。
-module Main where
-
-import           Control.Monad   (forM, forM_)
-import qualified Data.Map.Strict as Map
-import qualified Data.Text       as T
-import qualified Data.Vector     as V
-import qualified System.Random.MWC               as MWC
-import           System.Random.MWC.Distributions (standard)
-import           Text.Printf     (printf)
-
-import           Hanalyze.Model.HBM
-                   ( Distribution (..), ModelP, LMFamily (..), REff (..)
-                   , sample, observe, observeLMR
-                   , sampleNames, getTransforms, gradADU, compileGradU
-                   , logJointUnconstrained )
-import           Hanalyze.Stat.Distribution (Transform)
-import           Hanalyze.MCMC.NUTS         (NUTSConfig (..), defaultNUTSConfig, nuts)
-import           Hanalyze.MCMC.Core         (Chain, chainTotal)
-
-import           BenchUtil (timeitIO)
-
--- ---------------------------------------------------------------------------
--- 決定的データ (BenchHBMScaling.genM2 と同型)
--- ---------------------------------------------------------------------------
-
-normals :: Int -> Int -> IO [Double]
-normals seed k = do
-  g <- MWC.initialize (V.singleton (fromIntegral seed))
-  mapM (const (standard g)) [1 .. k]
-
--- | nG 群 × perG。 xRows=[[1,x]]、 gids、 ys を返す。
-genM2 :: Int -> Int -> IO ([[Double]], [Int], [Double])
-genM2 nG perG = do
-  let n = nG * perG
-      (b0, b1, tauU, s) = (1.0, 0.8, 1.5, 1.0) :: (Double, Double, Double, Double)
-  xz <- normals 21 n
-  ez <- normals 22 n
-  uz <- normals 23 nG
-  let us   = map (* tauU) uz
-      gids = [ i `div` perG | i <- [0 .. n - 1] ]
-      xs   = map (* 2.0) xz
-      ys   = [ b0 + b1 * x + (us !! g) + s * e | (x, g, e) <- zip3 xs gids ez ]
-      xRows = [ [1.0, x] | x <- xs ]
-  pure (xRows, gids, ys)
-
--- ---------------------------------------------------------------------------
--- 2 通りのエンコード
--- ---------------------------------------------------------------------------
-
--- | prior 部のみ (observe 無し)。 gradADU は ObserveLM 無しゆえ全体 `ad`、
---   = vec 経路の priorGrad 部 (prior+jacobian) の単体コスト計測用 (54.4c 内訳)。
-m2PriorOnly :: [[Double]] -> [Int] -> [Double] -> ModelP ()
-m2PriorOnly xRows gids _ys = do
-  let p  = if null xRows then 0 else length (head xRows)
-      nG = if null gids then 0 else maximum gids + 1
-  _   <- mapM (\k -> sample (T.pack ("beta_" ++ show k)) (Normal 0 5)) [0 .. p - 1]
-  tau <- sample "tau_u" (HalfNormal 5)
-  _   <- mapM (\j -> sample (T.pack ("u_" ++ show j)) (Normal 0 tau)) [0 .. nG - 1]
-  _   <- sample "sigma" (Exponential 1)
-  pure ()
-
--- | scalar 経路 (per-obs observe を手書き)。 全体が `ad` で微分される基準。
---   latent 宣言・順序は m2VecLM と完全一致 (beta_0,beta_1,tau_u,u_*,sigma)。
-m2Scalar :: [[Double]] -> [Int] -> [Double] -> ModelP ()
-m2Scalar xRows gids ys = do
-  let p  = if null xRows then 0 else length (head xRows)
-      nG = if null gids then 0 else maximum gids + 1
-  betas <- mapM (\k -> sample (T.pack ("beta_" ++ show k)) (Normal 0 5)) [0 .. p - 1]
-  tau   <- sample "tau_u" (HalfNormal 5)
-  us    <- mapM (\j -> sample (T.pack ("u_" ++ show j)) (Normal 0 tau)) [0 .. nG - 1]
-  s     <- sample "sigma" (Exponential 1)
-  forM_ (zip3 [0 :: Int ..] (zip3 xRows gids ys) (repeat ())) $ \(i, (xr, g, y), _) ->
-    let eta = sum (zipWith (\b x -> b * realToFrac x) betas xr) + us !! g
-    in observe (T.pack ("y_" ++ show i)) (Normal eta s) [y]
-
--- | vec 経路 (observeLMR)。 固定効果 β は密設計行列、 群効果 u_j は gather
---   (REff) で疎に表現する。 prior 宣言は scalar 版と完全に同一 (同じ分布・同じ
---   順序) ゆえ logJoint/gradADU は一致する。
-m2VecLM :: [[Double]] -> [Int] -> [Double] -> ModelP ()
-m2VecLM xRows gids ys = do
-  let p  = if null xRows then 0 else length (head xRows)
-      nG = if null gids then 0 else maximum gids + 1
-      betaNames = [ T.pack ("beta_" ++ show k) | k <- [0 .. p - 1] ]
-      uNames    = [ T.pack ("u_" ++ show j)    | j <- [0 .. nG - 1] ]
-  _   <- forM [0 .. p - 1] $ \k -> sample (betaNames !! k) (Normal 0 5)
-  tau <- sample "tau_u" (HalfNormal 5)
-  _   <- forM [0 .. nG - 1] $ \j -> sample (uNames !! j) (Normal 0 tau)
-  _   <- sample "sigma" (Exponential 1)
-  observeLMR "y" betaNames xRows [REff uNames gids Nothing] (LMGaussian "sigma") ys
-
--- | 54.4c 経路: m2VecLM と latent 宣言・観測は完全同一で、 REff に prior スケール
---   名 @Just "tau_u"@ を載せた版。 これにより compileGradU が u-prior 勾配を解析的
---   (O(nG) の素な Double) に計算し、 u_j Sample を ad walk から除外する。
---   m2VecLM (prior を ad) と数値は一致 (test で担保)・per-call で prior の O(nG) ad
---   が消えるぶん速くなるはず (計測で確認)。
-m2VecLMAna :: [[Double]] -> [Int] -> [Double] -> ModelP ()
-m2VecLMAna xRows gids ys = do
-  let p  = if null xRows then 0 else length (head xRows)
-      nG = if null gids then 0 else maximum gids + 1
-      betaNames = [ T.pack ("beta_" ++ show k) | k <- [0 .. p - 1] ]
-      uNames    = [ T.pack ("u_" ++ show j)    | j <- [0 .. nG - 1] ]
-  _   <- forM [0 .. p - 1] $ \k -> sample (betaNames !! k) (Normal 0 5)
-  tau <- sample "tau_u" (HalfNormal 5)
-  _   <- forM [0 .. nG - 1] $ \j -> sample (uNames !! j) (Normal 0 tau)
-  _   <- sample "sigma" (Exponential 1)
-  observeLMR "y" betaNames xRows [REff uNames gids (Just "tau_u")] (LMGaussian "sigma") ys
-
--- ---------------------------------------------------------------------------
--- 計測補助
--- ---------------------------------------------------------------------------
-
--- | 真値近傍の unconstrained 初期点 (β/u は identity、 tau_u/sigma は log)。
-initU :: [T.Text] -> [Double]
-initU names =
-  [ case n of
-      "tau_u" -> log 1.5
-      "sigma" -> log 1.0
-      _       -> 0.1
-  | n <- names ]
-
-centralDiff :: ([Double] -> Double) -> [Double] -> [Double]
-centralDiff f ps =
-  [ let h = 1e-6 * (abs (ps !! j) + 1e-3)
-    in (f (bump j h) - f (bump j (-h))) / (2 * h)
-  | j <- [0 .. length ps - 1] ]
-  where bump j d = [ if k == j then p + d else p | (k, p) <- zip [0 ..] ps ]
-
-relErr :: [Double] -> [Double] -> Double
-relErr a b = maximum [ abs (x - y) / (abs y + 1e-6) | (x, y) <- zip a b ]
-
--- | gradADU の per-call median 時間 (ms)。 静的部分を毎回再構築 (54.4a 経路)。
---   index で入力を微小摂動し CSE を防ぐ。
-timeGrad :: ModelP () -> [T.Text] -> [Transform] -> [Double] -> IO Double
-timeGrad m names trans us = do
-  (ms, _) <- timeitIO 50 (sum . map abs)
-               (\i -> let us' = [ u + fromIntegral i * 1e-12 | u <- us ]
-                      in pure (gradADU m names trans us'))
-  pure ms
-
--- | compileGradU で静的部分を **1 度だけ**前処理しクロージャを 50 回再利用した
---   per-call median 時間 (ms) (54.4b 経路・NUTS と同じ使い方)。
-timeGradCompiled :: ModelP () -> [T.Text] -> [Transform] -> [Double] -> IO Double
-timeGradCompiled m names trans us = do
-  let cl = compileGradU m names trans     -- 静的前処理は 1 度だけ
-  (ms, _) <- timeitIO 50 (sum . map abs)
-               (\i -> let us' = [ u + fromIntegral i * 1e-12 | u <- us ]
-                      in pure (cl us'))
-  pure ms
-
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  putStrLn "=== Phase 54.4a per-call 勾配ベンチ (scalar=全ad vs vecLM=ハイブリッド) ===\n"
-  putStrLn "対象: 階層 Gaussian (M2 random intercept)。 obs/群=12。"
-  putStrLn "gradADU 1 回の median 時間 (ms・50 reps)。 sc=scalar(全ad)・vl=vecLM(vec-tape)。\n"
-  putStrLn "vlc=vecLM compiled(54.4b・prior ad)・vla=同 compiled(54.4c・prior 解析)・vlc/vla=54.4c 短縮率。"
-  printf "%4s %4s %5s | %9s %9s | %8s\n"
-    ("nG"::String) ("p"::String) ("n"::String)
-    ("vlc(ms)"::String) ("vla(ms)"::String) ("vlc/vla"::String)
-  forM_ [2, 4, 8, 16, 32] $ \nG -> do
-    (xRows, gids, ys) <- genM2 nG 12
-    -- ModelP は rank-N 多相エイリアスゆえ let 束縛せず各 rank-N 消費箇所へ直接渡す。
-    let names = sampleNames (m2VecLM xRows gids ys)
-        tmap  = getTransforms (m2VecLM xRows gids ys)
-        trans = [ tmap Map.! n | n <- names ]
-        us    = initU names
-        p     = length (head xRows)
-        n     = length ys
-        -- 正しさ: 解析 prior 経路 (54.4c) が ad 経路・中心差分と一致 (relErr)。
-        gSc = gradADU (m2Scalar    xRows gids ys) names trans us
-        gVa = gradADU (m2VecLMAna  xRows gids ys) names trans us
-        cd  = centralDiff (\vs -> logJointUnconstrained (m2VecLM xRows gids ys) names trans
-                                    (Map.fromList (zip names vs))) us
-        e   = max (relErr gVa cd) (relErr gVa gSc)
-    printf "  (relErr 54.4c vs ad/中心差分 nG=%d: %.2e)\n" nG e
-    tVlc <- timeGradCompiled (m2VecLM    xRows gids ys) names trans us
-    tVla <- timeGradCompiled (m2VecLMAna xRows gids ys) names trans us
-    printf "%4d %4d %5d | %9.4f %9.4f | %8s\n"
-      nG p n tVlc tVla
-      (printf "x%.2f" (tVlc / tVla) :: String)
-
-  -- per-draw NUTS wall-time (per-call とは別。 NUTS 統合後の実速度)。
-  putStrLn "\n=== per-draw NUTS wall-time (warmup 300 + 300 draws・3 reps median) ==="
-  putStrLn "sc=scalar(全ad)・vl=vecLM(54.4b prior ad)・vla=vecLM(54.4c prior 解析)。"
-  printf "%4s %5s | %11s %11s %11s | %8s %8s\n"
-    ("nG"::String) ("n"::String)
-    ("sc(ms/dr)"::String) ("vl(ms/dr)"::String) ("vla(ms/dr)"::String)
-    ("sc/vla"::String) ("vl/vla"::String)
-  forM_ [8, 32] $ \nG -> do
-    (xRows, gids, ys) <- genM2 nG 12
-    let n     = length ys
-        nGn   = nG
-        initP = Map.fromList $
-          [ ("beta_0", 1.0), ("beta_1", 0.8), ("tau_u", 1.5), ("sigma", 1.0) ]
-          ++ [ (T.pack ("u_" ++ show j), 0.0) | j <- [0 .. nGn - 1] ]
-        cfg = defaultNUTSConfig
-          { nutsIterations = 300, nutsBurnIn = 300, nutsStepSize = 0.1
-          , nutsMaxDepth = 10, nutsAdaptStepSize = True
-          , nutsTargetAccept = 0.8, nutsAdaptMass = True }
-        runWith :: ModelP () -> Int -> IO Chain
-        runWith mdl i = do
-          g <- MWC.initialize (V.singleton (fromIntegral (42 + i)))
-          nuts mdl cfg initP g
-        probe ch = fromIntegral (chainTotal ch)
-    (msSc, _)  <- timeitIO 3 probe (runWith (m2Scalar    xRows gids ys))
-    (msVl, _)  <- timeitIO 3 probe (runWith (m2VecLM     xRows gids ys))
-    (msVla, _) <- timeitIO 3 probe (runWith (m2VecLMAna  xRows gids ys))
-    -- 総 wall-time を draw 数 (300) で割って per-draw に正規化。
-    let perDraw t = t / 300.0
-    printf "%4d %5d | %11.4f %11.4f %11.4f | %8s %8s\n"
-      nG n (perDraw msSc) (perDraw msVl) (perDraw msVla)
-      (printf "x%.1f" (msSc / msVla) :: String)
-      (printf "x%.2f" (msVl / msVla) :: String)
diff --git a/bench/haskell/BenchHBMADModes.hs b/bench/haskell/BenchHBMADModes.hs
deleted file mode 100644
--- a/bench/haskell/BenchHBMADModes.hs
+++ /dev/null
@@ -1,127 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | HBM 勾配の AD モード比較 (Phase 53 追加調査)。
---
--- forward が低次元で速く高次元で O(p) 悪化、 generic reverse が逆 (tape
--- オーバヘッドで低次元が遅い) と判明したため、 ad の 4 モードを直接突合し
--- 「低次元も高次元も両立する単一モードが無いか」 を計測する:
---
---   * Numeric.AD.Mode.Forward        (前進・O(p))
---   * Numeric.AD.Mode.Reverse        (逆・generic・tape boxing 有)
---   * Numeric.AD.Mode.Reverse.Double (逆・Double 特化・boxing 回避)
---   * Numeric.AD.Mode.Kahn           (逆・reflection-free)
---
--- 各モードで `logJointUnconstrained` の勾配 (= gradADU と同一計算) を計時。
-module Main where
-
-import           Control.Monad                    (forM_)
-import qualified Data.Map.Strict                  as Map
-import qualified Data.Text                        as T
-import qualified Data.Vector                      as V
-import qualified System.Random.MWC                as MWC
-import           System.Random.MWC.Distributions  (standard)
-import           Text.Printf                      (printf)
-
-import qualified Numeric.AD.Mode.Forward          as Fwd
-import qualified Numeric.AD.Mode.Reverse          as Rev
-import qualified Numeric.AD.Mode.Reverse.Double   as RevD
-import qualified Numeric.AD.Mode.Kahn             as Kahn
-
-import           Hanalyze.Model.HBM
-  ( Distribution (..), ModelP, sample, observe
-  , glmmRandomIntercept, GlmmFamily (..)
-  , sampleNames, getTransforms, logJointUnconstrained )
-import           Hanalyze.Stat.Distribution       (Transform)
-
-import           BenchUtil
-
--- ---------------------------------------------------------------------------
--- モデル
--- ---------------------------------------------------------------------------
-
-m1Model :: [Double] -> [Double] -> ModelP ()
-m1Model xs ys = do
-  a <- sample "a"     (Normal 0 10)
-  b <- sample "b"     (Normal 0 10)
-  s <- sample "sigma" (Exponential 1)
-  forM_ (zip3 [0 :: Int ..] xs ys) $ \(i, x, y) ->
-    observe (T.pack ("y_" ++ show i)) (Normal (a + b * realToFrac x) s) [y]
-
-m2Model :: [[Double]] -> [Int] -> [Double] -> ModelP ()
-m2Model xRows gids ys = glmmRandomIntercept GlmmGaussian xRows gids ys
-
-normals :: Int -> Int -> IO [Double]
-normals seed k = do
-  g <- MWC.initialize (V.singleton (fromIntegral seed))
-  mapM (const (standard g)) [1 .. k]
-
-genM1Data :: Int -> IO ([Double], [Double])
-genM1Data n = do
-  xz <- normals 11 n
-  ez <- normals 12 n
-  let xs = map (* 2.0) xz
-      ys = zipWith (\x e -> 2.0 + 1.5 * x + e) xs ez
-  return (xs, ys)
-
-genM2Data :: Int -> Int -> IO ([[Double]], [Int], [Double])
-genM2Data nG perG = do
-  let n = nG * perG
-  xz <- normals 21 n
-  ez <- normals 22 n
-  uz <- normals 23 nG
-  let us'  = map (* 1.5) uz
-      gids = [ i `div` perG | i <- [0 .. n - 1] ]
-      xs   = map (* 2.0) xz
-      ys   = [ 1.0 + 0.8 * x + (us' !! g) + e | (x, g, e) <- zip3 xs gids ez ]
-      xRows = [ [1.0, x] | x <- xs ]
-  return (xRows, gids, ys)
-
--- ---------------------------------------------------------------------------
--- 各モードの勾配 (logJointUnconstrained の grad = gradADU と同一計算)
--- ---------------------------------------------------------------------------
-
--- f :: 多相 numeric で us → Map に詰め直し logJointUnconstrained を評価。
-mkF :: (Floating a, Ord a)
-    => ModelP () -> [T.Text] -> [Transform] -> [a] -> a
-mkF m names trans us =
-  logJointUnconstrained m names trans (Map.fromList (zip names us))
-
-gradFwd, gradRev, gradRevD, gradKahn
-  :: ModelP () -> [T.Text] -> [Transform] -> [Double] -> [Double]
-gradFwd  m names trans = Fwd.grad  (mkF m names trans)
-gradRev  m names trans = Rev.grad  (mkF m names trans)
-gradRevD m names trans = RevD.grad (mkF m names trans)
-gradKahn m names trans = Kahn.grad (mkF m names trans)
-
--- ---------------------------------------------------------------------------
--- 計時: 1 モデルについて 4 モードの per-grad ナノ秒を出す
--- ---------------------------------------------------------------------------
-
-profileModel :: String -> ModelP () -> IO ()
-profileModel tag m = do
-  let names = sampleNames m
-      p     = length names
-      tmap  = getTransforms m
-      trans = [ Map.findWithDefault err n tmap | n <- names ]
-      err   = error "transform missing"
-      us0   = take p (cycle [0.1, -0.2, 0.15, 0.05, -0.1, 0.2, 0.3, 0.0])
-  (fMs,  _) <- timeitIO 30 (sum . map abs) (\_ -> pure (gradFwd  m names trans us0))
-  (rMs,  _) <- timeitIO 30 (sum . map abs) (\_ -> pure (gradRev  m names trans us0))
-  (rdMs, _) <- timeitIO 30 (sum . map abs) (\_ -> pure (gradRevD m names trans us0))
-  (kMs,  _) <- timeitIO 30 (sum . map abs) (\_ -> pure (gradKahn m names trans us0))
-  printf "%-16s p=%-3d | fwd=%8.4f | rev=%8.4f | revDouble=%8.4f | kahn=%8.4f ms\n"
-    tag p fMs rMs rdMs kMs
-
-main :: IO ()
-main = do
-  putStrLn "=== Phase 53: AD モード別 1 勾配時間 (ms) ==="
-  putStrLn "forward=O(p) / reverse=generic / revDouble=Double特化 / kahn=reflection-free\n"
-
-  (x1, y1) <- genM1Data 100
-  profileModel "M1_pooled(100)" (m1Model x1 y1)
-
-  putStrLn "\n--- M2 群数↑ で p↑ (obs/群=12) ---"
-  forM_ [2, 4, 8, 16, 32] $ \nG -> do
-    (xr, g, y) <- genM2Data nG 12
-    profileModel (printf "M2_g%d" nG) (m2Model xr g y)
diff --git a/bench/haskell/BenchHBMDist.hs b/bench/haskell/BenchHBMDist.hs
deleted file mode 100644
--- a/bench/haskell/BenchHBMDist.hs
+++ /dev/null
@@ -1,277 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Phase 56.6 bench: 観測分布ごとの per-call 勾配 A/B (bench-hbm-het の一般化)。
---
--- 56.3-56.5 で IR 吸収された各分布の canonical 回帰形 (n=100・test/Spec.hs の
--- synthVecIR test と同形) について、 同一 unconstrained 全勾配を
---
---   (a) HBM.gradADU      — 実経路 (IR 吸収・NUTS が払う値)
---   (b) RevD.grad (walk) — 旧 fallback 相当 (モデル全体を ad で毎回 walk)
---
--- で計測し 1 表にする。 各モデルは計測前に synthVecIR = Just (吸収) と
--- 相対誤差 (IR vs walk) を確認してから走らせる。
---
--- ⚠ ここで出るのは **per-call の改善倍率のみ** (per-draw への波及は M 系
--- bench でしか測れない・M9_negbin 以外は未計測)。 「PyMC 同等」 等の比較は
--- この表からは言えない。
---
--- 引数: 無し=全 family / family 名の列挙 (例: @negbin gamma@) = その family のみ。
--- 結果: bench/results/haskell/hbm_dist_grad_ab.csv
-module Main where
-
-import           Control.Monad                  (forM, unless)
-import qualified Data.Map.Strict                as Map
-import qualified Data.Text                      as T
-import           System.Environment             (getArgs)
-import           Text.Printf                    (printf)
-
-import qualified Numeric.AD.Mode.Reverse.Double as RevD
-
-import           Hanalyze.Model.HBM             (Distribution (..), Model,
-                                                 ModelP,
-                                                 sample, observe, gradADU,
-                                                 sampleNames, getTransforms,
-                                                 logJoint, invTransformF,
-                                                 logJacF, synthVecIR)
-
-import           BenchUtil                      (BenchRow (..), timeitIO,
-                                                 writeRows)
-
--- ===========================================================================
--- 決定的データ (乱数不要の固定系列・n=100)
--- ===========================================================================
-
-nObs :: Int
-nObs = 100
-
--- 説明変数: [-1, 1) 等間隔グリッド。
-xsD :: [Double]
-xsD = [ 2 * fromIntegral i / fromIntegral nObs - 1 | i <- [0 .. nObs - 1] ]
-
--- 決定的 pseudo-noise (sin 系列・[-1,1])。
-wiggle :: [Double]
-wiggle = [ sin (fromIntegral (7 * i :: Int)) | i <- [0 .. nObs - 1] ]
-
--- 実数値 (非有界): 位置-尺度系 (Gauss/StudentT/Cauchy/Logistic/Gumbel/LogN の log 前)。
-ysReal :: [Double]
-ysReal = [ 1.2 + 0.5 * x + 0.4 * w | (x, w) <- zip xsD wiggle ]
-
--- 正値: Expo/Weibull/LogNormal/Gamma。
-ysPos :: [Double]
-ysPos = [ exp (0.3 * x + 0.5 * w) | (x, w) <- zip xsD wiggle ]
-
--- (0,1): Beta。
-ysUnit :: [Double]
-ysUnit = [ 0.5 + 0.35 * w | w <- wiggle ]
-
--- 非負整数 count: Poisson/NegBin。
-ysCount :: [Double]
-ysCount = [ fromIntegral (max 0 (round (2 + 1.5 * x + 1.2 * w) :: Int))
-          | (x, w) <- zip xsD wiggle ]
-
--- 0/1: Bernoulli。
-ysBin01 :: [Double]
-ysBin01 = [ if w > 0 then 1 else 0 | w <- wiggle ]
-
--- 0..10: Binomial(10)。
-ysBinom :: [Double]
-ysBinom = [ fromIntegral (min 10 (max 0 (round (5 + 3 * x + 2 * w) :: Int)))
-          | (x, w) <- zip xsD wiggle ]
-
--- 1..: Geometric (試行回数パラメタ化)。
-ysGeom :: [Double]
-ysGeom = [ fromIntegral (max 1 (round (2 + 1.5 * w) :: Int)) | w <- wiggle ]
-
--- ===========================================================================
--- canonical 回帰形 (test/Spec.hs の synthVecIR test と同形・n=100 化)
--- ===========================================================================
-
--- | per-obs 手書きの共通骨格: 観測分布だけ差し替える (x は AD 型に持ち上げて渡す)。
-perObs :: (Floating a, Ord a)
-       => (a -> Distribution a) -> [Double] -> Model a ()
-perObs mk ys =
-  mapM_ (\(i, (x, y)) ->
-           observe (T.pack ("y_" ++ show (i :: Int)))
-             (mk (realToFrac x)) [y])
-        (zip [0 ..] (zip xsD ys))
-
-invLogit :: Floating a => a -> a
-invLogit e = 1 / (1 + exp (negate e))
-
--- | family 名 + モデル + unconstrained 初期点 (sampleNames 順)。
---   ModelP は rank-2 ゆえタプル格納不可 → data でラップ (Phase 51 の既知罠)。
-data Fam = Fam String (ModelP ()) [Double]
-
-mGauss, mPois, mBern, mStudentT, mCauchy, mLogistic, mGumbel,
-  mExpo, mWeibull, mLogNormal, mGamma, mBeta, mBinomial, mGeometric,
-  mNegBin :: ModelP ()
-
-mGauss = do
-  a <- sample "a" (Normal 0 10)
-  b <- sample "b" (Normal 0 10)
-  s <- sample "sigma" (Exponential 1)
-  perObs (\x -> Normal (a + b * x) s) ysReal
-
-mPois = do
-  a <- sample "a" (Normal 0 5)
-  b <- sample "b" (Normal 0 5)
-  perObs (\x -> Poisson (exp (a + b * x))) ysCount
-
-mBern = do
-  a <- sample "a" (Normal 0 5)
-  b <- sample "b" (Normal 0 5)
-  perObs (\x -> Bernoulli (invLogit (a + b * x))) ysBin01
-
--- 56.3 (ν=SC 定数)
-mStudentT = do
-  a <- sample "a" (Normal 0 10)
-  b <- sample "b" (Normal 0 10)
-  s <- sample "sigma" (Exponential 1)
-  perObs (\x -> StudentT 4 (a + b * x) s) ysReal
-
-mCauchy = do
-  a <- sample "a" (Normal 0 10)
-  b <- sample "b" (Normal 0 10)
-  g <- sample "gamma" (Exponential 1)
-  perObs (\x -> Cauchy (a + b * x) g) ysReal
-
-mLogistic = do
-  a <- sample "a" (Normal 0 10)
-  b <- sample "b" (Normal 0 10)
-  s <- sample "s" (Exponential 1)
-  perObs (\x -> Logistic (a + b * x) s) ysReal
-
-mGumbel = do
-  a <- sample "a" (Normal 0 10)
-  b <- sample "b" (Normal 0 10)
-  be <- sample "beta" (Exponential 1)
-  perObs (\x -> Gumbel (a + b * x) be) ysReal
-
--- 56.4 (rate=exp(η))
-mExpo = do
-  a <- sample "a" (Normal 0 5)
-  b <- sample "b" (Normal 0 5)
-  perObs (\x -> Exponential (exp (a + b * x))) ysPos
-
--- 56.4 (k latent, λ=exp(η))
-mWeibull = do
-  k <- sample "k" (Exponential 1)
-  a <- sample "a" (Normal 0 5)
-  b <- sample "b" (Normal 0 5)
-  perObs (\x -> Weibull k (exp (a + b * x))) ysPos
-
--- 56.4 (Gaussian ノード再利用)
-mLogNormal = do
-  a <- sample "a" (Normal 0 5)
-  b <- sample "b" (Normal 0 5)
-  s <- sample "sigma" (Exponential 1)
-  perObs (\x -> LogNormal (a + b * x) s) ysPos
-
--- 56.4 (α latent, rate=exp(η))
-mGamma = do
-  al <- sample "alpha" (Exponential 1)
-  a  <- sample "a" (Normal 0 5)
-  b  <- sample "b" (Normal 0 5)
-  perObs (\x -> Gamma al (exp (a + b * x))) ysPos
-
--- 56.4 (α=μφ, β=(1-μ)φ・φ は整数回避 = lgamma FD 罠の慣例)
-mBeta = do
-  a  <- sample "a" (Normal 0 5)
-  b  <- sample "b" (Normal 0 5)
-  ph <- sample "phi" (Exponential 0.5)
-  perObs (\x -> let mu = invLogit (a + b * x)
-                  in Beta (mu * ph) ((1 - mu) * ph)) ysUnit
-
--- 56.5 (n=10 定数)
-mBinomial = do
-  a <- sample "a" (Normal 0 5)
-  b <- sample "b" (Normal 0 5)
-  perObs (\x -> Binomial 10 (invLogit (a + b * x))) ysBinom
-
--- 56.5 (p=invLogit(η))
-mGeometric = do
-  a <- sample "a" (Normal 0 5)
-  b <- sample "b" (Normal 0 5)
-  perObs (\x -> Geometric (invLogit (a + b * x))) ysGeom
-
--- 56.5 (μ=exp(η), α latent)
-mNegBin = do
-  a  <- sample "a" (Normal 0 5)
-  b  <- sample "b" (Normal 0 5)
-  al <- sample "alpha" (Exponential 0.5)
-  perObs (\x -> NegativeBinomial (exp (a + b * x)) al) ysCount
-
-families :: [Fam]
-families =
-  [ Fam "gauss"     mGauss     [1.0, 0.4, log 0.8]
-  , Fam "pois"      mPois      [0.3, 0.4]
-  , Fam "bern"      mBern      [0.2, 0.5]
-  , Fam "studentt"  mStudentT  [1.2, 0.4, log 0.6]
-  , Fam "cauchy"    mCauchy    [1.2, 0.4, log 0.5]
-  , Fam "logistic"  mLogistic  [1.2, 0.4, log 0.5]
-  , Fam "gumbel"    mGumbel    [1.2, 0.4, log 0.6]
-  , Fam "expo"      mExpo      [0.2, -0.3]
-  , Fam "weibull"   mWeibull   [log 1.3, 0.2, -0.3]
-  , Fam "lognormal" mLogNormal [0.1, 0.3, log 0.7]
-  , Fam "gamma"     mGamma     [log 1.6, 0.2, -0.3]
-  , Fam "beta"      mBeta      [0.3, -0.2, log 3.3]
-  , Fam "binomial"  mBinomial  [0.2, 0.6]
-  , Fam "geometric" mGeometric [-0.2, 0.5]
-  , Fam "negbin"    mNegBin    [0.4, 0.3, log 1.7]
-  ]
-
--- ===========================================================================
--- 計測 (bench-hbm-het と同一手順)
--- ===========================================================================
-
--- | 1 family の per-call A/B。 計測前に (1) IR 吸収を確認 (吸収されなければ
---   FALLBACK 行として速度比 1 を返す) (2) 相対誤差を検証する。
-runFamily :: Fam -> IO BenchRow
-runFamily (Fam tag mdl uvs) = do
-  let names = sampleNames mdl
-      tmap  = getTransforms mdl
-      trans = [ tmap Map.! nm | nm <- names ]
-      absorbed = case synthVecIR mdl of
-                   Just (_, fams, _) -> null fams   -- 残余 family 無し = 全吸収
-                   Nothing           -> False
-      gIR = gradADU mdl names trans
-      gAD uv = RevD.grad
-                 (\uv' -> logJoint mdl
-                            (Map.fromList
-                               (zip names (zipWith invTransformF trans uv')))
-                          + sum (zipWith logJacF trans uv'))
-                 uv
-      relErr = maximum [ abs (x - y) / (1 + abs y)
-                       | (x, y) <- zip (gIR uvs) (gAD uvs) ]
-  unless (relErr < 1e-8) $
-    fail (tag ++ ": relErr IR vs ad-full = " ++ show relErr ++ " (>1e-8)")
-  -- 1 計測 = batch 回の勾配呼出 (µs 級なので)。 入力を毎回微小摂動して
-  -- CSE/共有を防ぐ (1e-12 は数値に実質影響しない)。
-  let batch = 1000 :: Int
-      runBatch g i = pure $! sum
-        [ sum (g (map (+ (1e-12 * fromIntegral (i * batch + j))) uvs))
-        | j <- [1 .. batch] ]
-  (msIR, _) <- timeitIO 7 id (runBatch gIR)
-  (msAD, _) <- timeitIO 7 id (runBatch gAD)
-  let pcIR = msIR / fromIntegral batch
-      pcAD = msAD / fromIntegral batch
-      sp   = pcAD / pcIR
-  printf "%-10s %s  IR %.5f ms/call  walk %.5f ms/call  x%.1f  (relErr %.1e)\n"
-    tag (if absorbed then "[IR]      " else "[FALLBACK]" :: String)
-    pcIR pcAD sp relErr
-  return (BenchRow "haskell" "hbm_dist" tag pcIR pcAD sp
-            (printf ("absorbed=%s relErr=%.2e n=%d batch=%d "
-                     ++ "per-call only (per-draw 波及は未計測)")
-               (show absorbed) relErr nObs batch))
-
-main :: IO ()
-main = do
-  args <- getArgs
-  let sel = if null args then families
-            else [ f | f@(Fam tag _ _) <- families, tag `elem` args ]
-  putStrLn "family     path        per-call gradient A/B (IR 吸収 vs 全体 ad walk)"
-  rows <- forM sel runFamily
-  writeRows "bench/results/haskell/hbm_dist_grad_ab.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/hbm_dist_grad_ab.csv"
diff --git a/bench/haskell/BenchHBMFuseSpike.hs b/bench/haskell/BenchHBMFuseSpike.hs
deleted file mode 100644
--- a/bench/haskell/BenchHBMFuseSpike.hs
+++ /dev/null
@@ -1,370 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Phase 85.3a: vecIR 融合方式の feasibility spike (計測先行・推測するな計測せよ)。
---
--- 85.1 で radon per-eval ~103µs の ~86% が forward+backward の解釈実行と確定した。
--- 本実装 (IR 融合) に入る前に、 synthetic な elementwise 連鎖 (n=919・二項 7 op =
--- radon の VIBin 48 本×平均 390 セルと同じ per-cell 構造) で 4 方式を実測し、
--- 融合の利得上限と採用方式を決める:
---
---   [A] 現行 forwardArena 相当: 命令ごと全ベクトル走査 + @sBinF op@ の
---       unknown-call ディスパッチ (IR.hs:1367 の実形)
---   [B] A + op 特化ループ: 命令ごと走査は同じだが case op で直接演算
---       (backward VIBin と同形) — unknown-call/boxing の寄与を分離
---   [C] 完全融合 1-pass (手書き): 全連鎖を 1 ループで registers 評価 =
---       compile-time codegen の理論天井 (解釈では到達不能)
---   [D] 要素ごと解釈融合: 要素 j ごとに opcode 列を内側ループで dispatch =
---       「fused interpreter」 方式の現実値 (arena traffic ゼロだが
---       per-node dispatch が要素ごとに走る)
---
--- backward (勾配) 側は [A'] 現行 gradVecIRGo 相当 (forward arena + adj arena +
--- op 特化・現行 backward は既に特化済) と [C'] 完全融合 1-pass (forward 再計算 +
--- registers 逆伝播) の 2 点で床と天井を測る。
---
--- ★この spike は HBM 本体を一切いじらない独立実験。 勾配は全方式で突合し
---   max|Δ| を表示する (正しさの担保)。
-module Main where
-
-import           Control.Monad                    (forM_, when)
-import           Control.Monad.ST                 (ST, runST)
-import qualified Data.Vector                      as BV
-import qualified Data.Vector.Storable             as VS
-import qualified Data.Vector.Storable.Mutable     as VSM
-import qualified Data.Vector.Unboxed              as VU
-import           Text.Printf                      (printf)
-
-import           Hanalyze.Model.HBM.IR            (SBin (..), sBinF)
-
-import           BenchUtil                        (timeitTastyIO)
-
--- ---------------------------------------------------------------------------
--- 設定: acc_0 = x0、 acc_t = acc_{t-1} ⊕_t x_t (t=1..7)、 obj = Σ_j acc_7[j]
--- ---------------------------------------------------------------------------
-
-nObs :: Int
-nObs = 919
-
-nOps :: Int
-nOps = 7
-
-ops :: [SBin]
-ops = [SAddO, SMulO, SSubO, SDivO, SAddO, SMulO, SSubO]
-
--- | 入力 8 本 (値域 [0.5, 1.5] = div 破綻なし・決定的)。
-mkInputs :: Int -> BV.Vector (VS.Vector Double)
-mkInputs salt = BV.fromList
-  [ VS.generate nObs $ \j ->
-      0.5 + fromIntegral ((j * 31 + t * 17 + salt) `mod` 1000) / 1000.0
-  | t <- [0 .. nOps] ]
-
--- ---------------------------------------------------------------------------
--- forward 4 方式
--- ---------------------------------------------------------------------------
-
--- | [A] 現行 forwardArena 相当: slot ごと全走査 + unknown-call。
-fwA :: BV.Vector (VS.Vector Double) -> Double
-fwA xs = runST $ do
-  ar <- VSM.unsafeNew ((nOps + 1) * nObs)
-  let x0 = xs BV.! 0
-      copy0 !j | j >= nObs = pure ()
-               | otherwise = do
-                   VSM.unsafeWrite ar j (x0 `VS.unsafeIndex` j)
-                   copy0 (j + 1)
-  copy0 0
-  forM_ [1 .. nOps] $ \t -> do
-    let f  = sBinF (ops !! (t - 1))
-        xt = xs BV.! t
-        oPrev = (t - 1) * nObs
-        oCur  = t * nObs
-        go !j | j >= nObs = pure ()
-              | otherwise = do
-                  a <- VSM.unsafeRead ar (oPrev + j)
-                  VSM.unsafeWrite ar (oCur + j) (f a (xt `VS.unsafeIndex` j))
-                  go (j + 1)
-    go 0
-  let oL = nOps * nObs
-      sumGo !acc !j | j >= nObs = pure acc
-                    | otherwise = do
-                        v <- VSM.unsafeRead ar (oL + j)
-                        sumGo (acc + v) (j + 1)
-  sumGo 0 0
-
--- | [B] A + op 特化ループ (case op で直接演算・backward VIBin と同形)。
-fwB :: BV.Vector (VS.Vector Double) -> Double
-fwB xs = runST $ do
-  ar <- VSM.unsafeNew ((nOps + 1) * nObs)
-  let x0 = xs BV.! 0
-      copy0 !j | j >= nObs = pure ()
-               | otherwise = do
-                   VSM.unsafeWrite ar j (x0 `VS.unsafeIndex` j)
-                   copy0 (j + 1)
-  copy0 0
-  forM_ [1 .. nOps] $ \t -> do
-    let xt = xs BV.! t
-        oPrev = (t - 1) * nObs
-        oCur  = t * nObs
-        loopWith f =
-          let go !j | j >= nObs = pure ()
-                    | otherwise = do
-                        a <- VSM.unsafeRead ar (oPrev + j)
-                        VSM.unsafeWrite ar (oCur + j)
-                          (f a (xt `VS.unsafeIndex` j))
-                        go (j + 1)
-          in go 0
-    case ops !! (t - 1) of
-      SAddO -> loopWith (+)
-      SSubO -> loopWith (-)
-      SMulO -> loopWith (*)
-      SDivO -> loopWith (/)
-  let oL = nOps * nObs
-      sumGo !acc !j | j >= nObs = pure acc
-                    | otherwise = do
-                        v <- VSM.unsafeRead ar (oL + j)
-                        sumGo (acc + v) (j + 1)
-  sumGo 0 0
-
--- | [C] 完全融合 1-pass (手書き・registers のみ) = codegen の理論天井。
-fwC :: BV.Vector (VS.Vector Double) -> Double
-fwC xs =
-  let x0 = xs BV.! 0
-      x1 = xs BV.! 1
-      x2 = xs BV.! 2
-      x3 = xs BV.! 3
-      x4 = xs BV.! 4
-      x5 = xs BV.! 5
-      x6 = xs BV.! 6
-      x7 = xs BV.! 7
-      go !acc !j
-        | j >= nObs = acc
-        | otherwise =
-            let v1 = x0 `VS.unsafeIndex` j + x1 `VS.unsafeIndex` j
-                v2 = v1 * x2 `VS.unsafeIndex` j
-                v3 = v2 - x3 `VS.unsafeIndex` j
-                v4 = v3 / x4 `VS.unsafeIndex` j
-                v5 = v4 + x5 `VS.unsafeIndex` j
-                v6 = v5 * x6 `VS.unsafeIndex` j
-                v7 = v6 - x7 `VS.unsafeIndex` j
-            in go (acc + v7) (j + 1)
-  in go 0 0
-
--- | [D] 要素ごと解釈融合: opcode 列を要素ごとに内側 dispatch (arena なし)。
-fwD :: VU.Vector Int -> BV.Vector (VS.Vector Double) -> Double
-fwD opCodes xs =
-  let go !acc !j
-        | j >= nObs = acc
-        | otherwise =
-            let inner !v !t
-                  | t > nOps = v
-                  | otherwise =
-                      let xtj = (xs BV.! t) `VS.unsafeIndex` j
-                          v'  = case opCodes `VU.unsafeIndex` (t - 1) of
-                                  0 -> v + xtj
-                                  1 -> v - xtj
-                                  2 -> v * xtj
-                                  _ -> v / xtj
-                      in inner v' (t + 1)
-                v0 = (xs BV.! 0) `VS.unsafeIndex` j
-            in go (acc + inner v0 1) (j + 1)
-  in go 0 0
-
-opCode :: SBin -> Int
-opCode SAddO = 0
-opCode SSubO = 1
-opCode SMulO = 2
-opCode SDivO = 3
-
--- ---------------------------------------------------------------------------
--- backward 2 方式 (勾配 = ∂obj/∂x_t 全要素・fw+bw 込みの per-eval)
--- ---------------------------------------------------------------------------
-
--- | [A'] 現行 gradVecIRGo 相当: forward arena + adj arena (replicate 0) +
---   op 特化 backward。 勾配は gxs ((nOps+1)*nObs) へ加算。
-gradA :: BV.Vector (VS.Vector Double) -> VS.Vector Double
-gradA xs = runST $ do
-  -- forward (B と同じ特化形: 現行 backward は特化済なので forward も B 形で公平に)
-  ar <- VSM.unsafeNew ((nOps + 1) * nObs)
-  let x0 = xs BV.! 0
-      copy0 !j | j >= nObs = pure ()
-               | otherwise = do
-                   VSM.unsafeWrite ar j (x0 `VS.unsafeIndex` j)
-                   copy0 (j + 1)
-  copy0 0
-  forM_ [1 .. nOps] $ \t -> do
-    let xt = xs BV.! t
-        oPrev = (t - 1) * nObs
-        oCur  = t * nObs
-        loopWith f =
-          let go !j | j >= nObs = pure ()
-                    | otherwise = do
-                        a <- VSM.unsafeRead ar (oPrev + j)
-                        VSM.unsafeWrite ar (oCur + j)
-                          (f a (xt `VS.unsafeIndex` j))
-                        go (j + 1)
-          in go 0
-    case ops !! (t - 1) of
-      SAddO -> loopWith (+)
-      SSubO -> loopWith (-)
-      SMulO -> loopWith (*)
-      SDivO -> loopWith (/)
-  -- backward
-  adj <- VSM.replicate ((nOps + 1) * nObs) 0
-  gxs <- VSM.replicate ((nOps + 1) * nObs) 0
-  -- obj = Σ acc_7 → adj_7 = 1
-  let oL = nOps * nObs
-      init1 !j | j >= nObs = pure ()
-               | otherwise = VSM.unsafeWrite adj (oL + j) 1 >> init1 (j + 1)
-  init1 0
-  forM_ [nOps, nOps - 1 .. 1] $ \t -> do
-    let xt = xs BV.! t
-        oPrev = (t - 1) * nObs
-        oCur  = t * nObs
-        gOff  = t * nObs
-    case ops !! (t - 1) of
-      SAddO ->
-        let go !j | j >= nObs = pure ()
-                  | otherwise = do
-                      a <- VSM.unsafeRead adj (oCur + j)
-                      VSM.unsafeModify adj (+ a) (oPrev + j)
-                      VSM.unsafeModify gxs (+ a) (gOff + j)
-                      go (j + 1)
-        in go 0
-      SSubO ->
-        let go !j | j >= nObs = pure ()
-                  | otherwise = do
-                      a <- VSM.unsafeRead adj (oCur + j)
-                      VSM.unsafeModify adj (+ a) (oPrev + j)
-                      VSM.unsafeModify gxs (subtract a) (gOff + j)
-                      go (j + 1)
-        in go 0
-      SMulO ->
-        let go !j | j >= nObs = pure ()
-                  | otherwise = do
-                      a  <- VSM.unsafeRead adj (oCur + j)
-                      vp <- VSM.unsafeRead ar (oPrev + j)
-                      VSM.unsafeModify adj (+ (a * xt `VS.unsafeIndex` j)) (oPrev + j)
-                      VSM.unsafeModify gxs (+ (a * vp)) (gOff + j)
-                      go (j + 1)
-        in go 0
-      SDivO ->
-        let go !j | j >= nObs = pure ()
-                  | otherwise = do
-                      a  <- VSM.unsafeRead adj (oCur + j)
-                      vp <- VSM.unsafeRead ar (oPrev + j)
-                      let x = xt `VS.unsafeIndex` j
-                      VSM.unsafeModify adj (+ (a / x)) (oPrev + j)
-                      VSM.unsafeModify gxs (+ (negate (a * vp / (x * x)))) (gOff + j)
-                      go (j + 1)
-        in go 0
-  -- acc_0 = x0 → gx_0 = adj_0
-  let fin !j | j >= nObs = pure ()
-             | otherwise = do
-                 a <- VSM.unsafeRead adj j
-                 VSM.unsafeModify gxs (+ a) j
-                 fin (j + 1)
-  fin 0
-  VS.unsafeFreeze gxs
-
--- | [C'] 完全融合 1-pass: 要素ごとに forward re-計算 + registers 逆伝播。
-gradC :: BV.Vector (VS.Vector Double) -> VS.Vector Double
-gradC xs = runST $ do
-  gxs <- VSM.replicate ((nOps + 1) * nObs) 0
-  let x0 = xs BV.! 0
-      x1 = xs BV.! 1
-      x2 = xs BV.! 2
-      x3 = xs BV.! 3
-      x4 = xs BV.! 4
-      x5 = xs BV.! 5
-      x6 = xs BV.! 6
-      x7 = xs BV.! 7
-      go !j
-        | j >= nObs = pure ()
-        | otherwise = do
-            let i0 = x0 `VS.unsafeIndex` j
-                i1 = x1 `VS.unsafeIndex` j
-                i2 = x2 `VS.unsafeIndex` j
-                i3 = x3 `VS.unsafeIndex` j
-                i4 = x4 `VS.unsafeIndex` j
-                i5 = x5 `VS.unsafeIndex` j
-                i6 = x6 `VS.unsafeIndex` j
-                i7 = x7 `VS.unsafeIndex` j
-                v1 = i0 + i1
-                v2 = v1 * i2
-                v3 = v2 - i3
-                v4 = v3 / i4
-                v5 = v4 + i5
-                v6 = v5 * i6
-                -- v7 = v6 - i7 (obj 側は Σ ゆえ adj_7 = 1)
-                a7 = 1 :: Double
-                a6 = a7
-                a5 = a6 * i6
-                a4 = a5
-                a3 = a4 / i4
-                a2 = a3
-                a1 = a2 * i2
-                a0 = a1
-            VSM.unsafeWrite gxs (7 * nObs + j) (negate a7)
-            VSM.unsafeWrite gxs (6 * nObs + j) (a6 * v5)
-            VSM.unsafeWrite gxs (5 * nObs + j) a5
-            VSM.unsafeWrite gxs (4 * nObs + j) (negate (a4 * v3 / (i4 * i4)))
-            VSM.unsafeWrite gxs (3 * nObs + j) (negate a3)
-            VSM.unsafeWrite gxs (2 * nObs + j) (a2 * v1)
-            VSM.unsafeWrite gxs (1 * nObs + j) a1
-            VSM.unsafeWrite gxs (0 * nObs + j) a0
-            go (j + 1)
-  go 0
-  VS.unsafeFreeze gxs
-
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  putStrLn "=== Phase 85.3a: vecIR 融合方式 spike (n=919・二項 7 op 連鎖) ==="
-  printf "セル数 (bin cells) = %d (radon VIBin 18744 セルの縮小相似形)\n\n"
-    (nOps * nObs)
-  let opCodes = VU.fromList (map opCode ops)
-
-  -- 正しさ突合 (A' vs C')
-  let xs0 = mkInputs 0
-      gA  = gradA xs0
-      gC  = gradC xs0
-      dMax = VS.maximum (VS.zipWith (\a b -> abs (a - b)) gA gC)
-      vA = fwA xs0
-      vC = fwC xs0
-      vD = fwD opCodes xs0
-  printf "突合: fwA=%.6f fwC=%.6f fwD=%.6f  grad max|Δ|=%.3e\n\n" vA vC vD dMax
-  when (dMax > 1e-12) $ error "gradA と gradC が不一致"
-
-  -- forward 4 方式
-  putStrLn "--- forward per-eval (µs・ns/セル) ---"
-  let cells = fromIntegral (nOps * nObs) :: Double
-      report tag tMs = printf "  %-34s %8.2f µs  %6.2f ns/セル\n"
-        (tag :: String) (tMs * 1000) (tMs * 1e6 / cells)
-  (tA, _) <- timeitTastyIO id (\i -> pure $! fwA (mkInputsCached i))
-  report "[A] 現行 (arena+unknown-call)" tA
-  (tB, _) <- timeitTastyIO id (\i -> pure $! fwB (mkInputsCached i))
-  report "[B] arena+op特化ループ" tB
-  (tC, _) <- timeitTastyIO id (\i -> pure $! fwC (mkInputsCached i))
-  report "[C] 完全融合1-pass (天井)" tC
-  (tD, _) <- timeitTastyIO id (\i -> pure $! fwD opCodes (mkInputsCached i))
-  report "[D] 要素ごと解釈融合" tD
-
-  -- backward (fw+bw)
-  putStrLn "\n--- gradient (fw+bw) per-eval (µs・ns/セル) ---"
-  (tGA, _) <- timeitTastyIO id
-    (\i -> pure $! VS.unsafeIndex (gradA (mkInputsCached i)) (i `mod` nObs))
-  report "[A'] 現行 (2 arena+op特化bw)" tGA
-  (tGC, _) <- timeitTastyIO id
-    (\i -> pure $! VS.unsafeIndex (gradC (mkInputsCached i)) (i `mod` nObs))
-  report "[C'] 完全融合1-pass (天井)" tGC
-
-  printf "\n倍率: fw A/C=%.1f×  A/B=%.2f×  A/D=%.1f×  grad A'/C'=%.1f×\n"
-    (tA / tC) (tA / tB) (tA / tD) (tGA / tGC)
-
--- | 入力 16 変種を事前生成 (CSE 回避・生成コストを計測に混ぜない)。
-inputsPool :: BV.Vector (BV.Vector (VS.Vector Double))
-inputsPool = BV.fromList [ mkInputs s | s <- [0 .. 15] ]
-{-# NOINLINE inputsPool #-}
-
-mkInputsCached :: Int -> BV.Vector (VS.Vector Double)
-mkInputsCached i = inputsPool BV.! (i `mod` 16)
diff --git a/bench/haskell/BenchHBMHet.hs b/bench/haskell/BenchHBMHet.hs
deleted file mode 100644
--- a/bench/haskell/BenchHBMHet.hs
+++ /dev/null
@@ -1,81 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Phase 55.3 小 bench: heteroscedastic モデルの per-call 勾配 A/B。
---
---   mHet: y_i ~ N(a, exp(g0 + g1·z_i))   (n=100, θ=3・σ が行依存の式)
---
--- 55.3 で σ 位置が「単一 latent」 → 任意 SExp に拡張され、 このモデルは
--- ベクトル式 IR に吸収されるようになった (旧 = σ 検出不能で全体 ad fallback)。
--- 比較 2 通り (同一 unconstrained 全勾配・相対誤差検証後に計測):
---
---   (a) HBM.gradADU      — 実経路 (55.3 後 = IR 吸収・NUTS が払う値)
---   (b) RevD.grad (walk) — 旧 fallback 相当 (モデル全体を ad で毎回 walk)
---
--- per-draw への波及は M 系 bench (55.5) で測る。 ここは勾配カーネル単体。
-module Main where
-
-import           Control.Monad                  (forM_)
-import qualified Data.Map.Strict                as Map
-import qualified Data.Text                      as T
-import           Text.Printf                    (printf)
-
-import qualified Numeric.AD.Mode.Reverse.Double as RevD
-
-import           Hanalyze.Model.HBM             (Distribution (..), ModelP,
-                                                 sample, observe, gradADU,
-                                                 sampleNames, getTransforms,
-                                                 logJoint, invTransformF,
-                                                 logJacF)
-
-import           BenchUtil                      (timeitIO)
-
-nHet :: Int
-nHet = 100
-
--- 決定的データ (DGP は等間隔 z + 線形 y・乱数不要の固定系列)。
-zsHet, ysHet :: [Double]
-zsHet = [ fromIntegral i / fromIntegral nHet * 2 - 1 | i <- [0 .. nHet - 1] ]
-ysHet = [ 1.2 + 0.1 * z | z <- zsHet ]
-
-mHet :: ModelP ()
-mHet = do
-  a  <- sample "a"  (Normal 0 10)
-  g0 <- sample "g0" (Normal 0 2)
-  g1 <- sample "g1" (Normal 0 2)
-  forM_ (zip3 [0 :: Int ..] zsHet ysHet) $ \(i, z, y) ->
-    observe (T.pack ("y_" ++ show i))
-      (Normal a (exp (g0 + g1 * realToFrac z))) [y]
-
-main :: IO ()
-main = do
-  let names = sampleNames mHet
-      tmap  = getTransforms mHet
-      trans = [ tmap Map.! nm | nm <- names ]
-      uvs   = [0.9, -0.3, 0.4]
-      -- (a) 実経路 (55.3 後 = IR 吸収)
-      gIR = gradADU mHet names trans
-      -- (b) 旧 fallback 相当: モデル全体を ad で walk (compileGradUV の
-      --     synthVecIR Nothing 分岐 gradFull と同形)
-      gAD uv = RevD.grad
-                 (\uv' -> logJoint mHet
-                            (Map.fromList
-                               (zip names (zipWith invTransformF trans uv')))
-                          + sum (zipWith logJacF trans uv'))
-                 uv
-      relErr = maximum [ abs (x - y) / (1 + abs y)
-                       | (x, y) <- zip (gIR uvs) (gAD uvs) ]
-  printf "relErr IR vs ad-full = %.2e\n" relErr
-  -- 1 計測 = batch 回の勾配呼出 (µs 級なので)。 入力を毎回微小摂動して
-  -- CSE/共有を防ぐ (1e-12 は数値に実質影響しない)。
-  let batch = 1000 :: Int
-      runBatch g i = pure $! sum
-        [ sum (g (map (+ (1e-12 * fromIntegral (i * batch + j))) uvs))
-        | j <- [1 .. batch] ]
-  (msIR, _) <- timeitIO 7 id (runBatch gIR)
-  (msAD, _) <- timeitIO 7 id (runBatch gAD)
-  let pcIR = msIR / fromIntegral batch
-      pcAD = msAD / fromIntegral batch
-  printf "gradADU (IR 吸収・実経路): %.5f ms/call\n" pcIR
-  printf "RevD walk (旧 fallback) : %.5f ms/call\n" pcAD
-  printf "speedup x%.1f\n" (pcAD / pcIR)
diff --git a/bench/haskell/BenchHBMProfile.hs b/bench/haskell/BenchHBMProfile.hs
deleted file mode 100644
--- a/bench/haskell/BenchHBMProfile.hs
+++ /dev/null
@@ -1,132 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | HBM 勾配評価ボトルネック診断 (Phase 53)。
---
--- 仮説 (一次根拠: @HBM.hs:146@ が @Numeric.AD.Mode.Forward.grad@ を使用・
--- ad-4.5.6 ソースが「reverse mode より O(n) 遅い」と明記) =
--- **前進モード AD ゆえ勾配 1 本が latent 数 p 回の関数評価を要する**。
---
--- 本ベンチで決定的に確認する:
---   (1) 1 勾配 (gradADU) / 1 log-joint (logJoint) の時間比 ≈ p か
---   (2) p (群数で latent を増やす) を振ったとき gradADU 時間が p に線形か
---   (3) per-eval が観測数 N_obs に対してどう伸びるか
---
--- これが真なら最適化方針 = reverse-mode AD への切替で勾配を O(1) sweep 化。
-module Main where
-
-import           Control.Monad                    (forM_)
-import qualified Data.Map.Strict                  as Map
-import qualified Data.Text                        as T
-import qualified Data.Vector                      as V
-import qualified System.Random.MWC                as MWC
-import           System.Random.MWC.Distributions  (standard)
-import           Text.Printf                      (printf)
-
-import           Hanalyze.Model.HBM
-  ( Distribution (..), ModelP, sample, observe
-  , glmmRandomIntercept, GlmmFamily (..)
-  , sampleNames, getTransforms, gradADU, logJoint )
-import           Hanalyze.Stat.Distribution       (fromUnconstrained)
-
-import           BenchUtil
-
--- ---------------------------------------------------------------------------
--- モデル
--- ---------------------------------------------------------------------------
-
-m1Model :: [Double] -> [Double] -> ModelP ()
-m1Model xs ys = do
-  a <- sample "a"     (Normal 0 10)
-  b <- sample "b"     (Normal 0 10)
-  s <- sample "sigma" (Exponential 1)
-  forM_ (zip3 [0 :: Int ..] xs ys) $ \(i, x, y) ->
-    observe (T.pack ("y_" ++ show i)) (Normal (a + b * realToFrac x) s) [y]
-
-m2Model :: [[Double]] -> [Int] -> [Double] -> ModelP ()
-m2Model xRows gids ys = glmmRandomIntercept GlmmGaussian xRows gids ys
-
--- ---------------------------------------------------------------------------
--- データ生成 (決定的・CSV 不要・内部診断用)
--- ---------------------------------------------------------------------------
-
-normals :: Int -> Int -> IO [Double]
-normals seed k = do
-  g <- MWC.initialize (V.singleton (fromIntegral seed))
-  mapM (const (standard g)) [1 .. k]
-
-genM1Data :: Int -> IO ([Double], [Double])
-genM1Data n = do
-  xz <- normals 11 n
-  ez <- normals 12 n
-  let xs = map (* 2.0) xz
-      ys = zipWith (\x e -> 2.0 + 1.5 * x + e) xs ez
-  return (xs, ys)
-
--- | nG 群 × perG 観測の random-intercept データ。
-genM2Data :: Int -> Int -> IO ([[Double]], [Int], [Double])
-genM2Data nG perG = do
-  let n = nG * perG
-  xz <- normals 21 n
-  ez <- normals 22 n
-  uz <- normals 23 nG
-  let us   = map (* 1.5) uz
-      gids = [ i `div` perG | i <- [0 .. n - 1] ]
-      xs   = map (* 2.0) xz
-      ys   = [ 1.0 + 0.8 * x + (us !! g) + e | (x, g, e) <- zip3 xs gids ez ]
-      xRows = [ [1.0, x] | x <- xs ]
-  return (xRows, gids, ys)
-
--- ---------------------------------------------------------------------------
--- 計時ヘルパ: 1 モデルについて logJoint と gradADU の per-eval を測る
--- ---------------------------------------------------------------------------
-
--- | unconstrained 初期点 (latent 全 0 = 制約空間でも有限) を作り、
---   logJoint(Double) 1 回と gradADU 1 回の per-eval ナノ秒を返す。
-profileModel :: String -> ModelP () -> IO ()
-profileModel tag m = do
-  let names = sampleNames m
-      p     = length names
-      tmap  = getTransforms m
-      trans = [ Map.findWithDefault err n tmap | n <- names ]
-      err   = error "transform missing"
-      us0   = replicate p (0.0 :: Double)
-      -- logJoint を constrained 空間で評価するための params
-      paramsC = Map.fromList
-        [ (n, fromUnconstrained t u) | (n, t, u) <- zip3 names trans us0 ]
-
-  -- logJoint 1 回 (Double・前進walk 1 本)
-  (ljMs, _) <- timeitIO 50 id (\_ -> pure (logJoint m paramsC))
-  -- gradADU 1 回 (前進モード grad = p sweep のはず)
-  (gMs, _)  <- timeitIO 50 (sum . map abs)
-                 (\_ -> pure (gradADU m names trans us0))
-
-  let ratio = gMs / max 1e-12 ljMs
-  printf "%-18s p=%-3d | logJoint=%8.4f ms | gradADU=%9.4f ms | ratio=%6.2f (≈p?)\n"
-    tag p ljMs gMs ratio
-
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  putStrLn "=== Phase 53: HBM 勾配ボトルネック診断 ==="
-  putStrLn "仮説: gradADU/logJoint 比 ≈ p なら前進モード AD が O(p) ボトルネック\n"
-
-  -- (1) M1 / M2 の比較
-  (x1, y1) <- genM1Data 100
-  (xr, g2, y2) <- genM2Data 8 12
-  putStrLn "--- (1) M1 (pooled) vs M2 (random intercept) ---"
-  profileModel "M1_pooled(100obs)" (m1Model x1 y1)
-  profileModel "M2_ranint(8grp)"   (m2Model xr g2 y2)
-
-  -- (2) 群数を振って p を増やす → gradADU が p に線形か
-  putStrLn "\n--- (2) 群数↑ で latent p↑: gradADU が p に線形か (obs/群=12 固定) ---"
-  forM_ [2, 4, 8, 16, 32] $ \nG -> do
-    (xr', g', y') <- genM2Data nG 12
-    profileModel (printf "M2_g%d" nG) (m2Model xr' g' y')
-
-  -- (3) 観測数を振る (p=3 固定の M1) → per-eval が N_obs に線形か
-  putStrLn "\n--- (3) 観測数↑ (M1 p=3 固定): logJoint/gradADU が N_obs に線形か ---"
-  forM_ [50, 100, 200, 400, 800] $ \nObs -> do
-    (xs, ys) <- genM1Data nObs
-    profileModel (printf "M1_n%d" nObs) (m1Model xs ys)
diff --git a/bench/haskell/BenchHBMScaling.hs b/bench/haskell/BenchHBMScaling.hs
deleted file mode 100644
--- a/bench/haskell/BenchHBMScaling.hs
+++ /dev/null
@@ -1,709 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | HBM サンプラ性能スケーリングベンチ (hanalyze NUTS vs PyMC)。
---
--- 目的: HBM (NUTS) の wall-time が post-warmup サンプル数 @iter@ に対して
--- 線形 (O(iter)) に伸びるか、 また per-sample 単価が PyMC と比べてどうかを
--- 計測する。 warmup を固定し本サンプル数だけ掃くことで
---   total = (warmup 固定費) + (1 サンプル単価) * iter
--- の線形フィットで切片 (固定費) と傾き (単価) を分離する。
---
--- モデル階層 (簡単→複雑):
---   M1 pooled 単回帰        y_i ~ N(a + b·x_i, σ)
---   M2 階層 random intercept y_ij ~ N(β0 + β1·x_ij + u_{g(i)}, σ),
---                            u_j ~ N(0, τ_u)
---   M3 階層 random intercept+slope (Phase 54.8 後拡張)
---                            y_ij ~ N(β0 + β1·x + u_g + v_g·x, σ)
---   M4 多変量 X pooled       y_i ~ N(β0 + Σ_{k=1..10} β_k·x_ik, σ)
---   M5 パラメタ非線形        y_i ~ N(a·exp(-b·x_i) + c, σ)
---   M6 組合せ (階層×非線形)   y_ij ~ N(a_g·exp(-b·x_ij), σ), a_g ~ N(μ_a, τ_a)
---   M7 Poisson 回帰 (Phase 55) y_i ~ Poisson(exp(a + b·x_i))
---   M8 logistic 回帰 (Phase 55) y_i ~ Bernoulli(invLogit(a + b·x_i))
---   M9 NegBin 回帰 (Phase 56.6) y_i ~ NegBin(exp(a + b·x_i), α)
---
--- M3-M9 は **per-obs scalar observe の手書き** (汎用 authoring) で組む:
--- M3/M4 は affine ゆえ Phase 54.8 の自動 ObserveLM 合成が乗り、 M5/M6 は
--- パラメタ非線形ゆえ合成不可 = walk+ad fallback。 M7/M8 は非 Gaussian 観測
--- ゆえ高速経路対象外 (Phase 55 改善前 baseline)。 M9 は 56.5 の IR 吸収
--- (lgamma 項含む) の per-draw 効果測定用。 「汎用に書いてどこまで
--- 速いか」 を正直に測る構成。
---
--- データは決定的 DGP で生成し @bench/data/hbm_m{1..9}.csv@ に書き出す
--- (Python 側 @bench_hbm_scaling.py@ が同じ CSV を読む = 公平比較)。
--- 結果は unified BenchRow CSV @bench/results/haskell/hbm_scaling.csv@ へ。
--- 引数: 無し=M1-M8 全部 / @glm@=M7-M9 のみ (hbm_scaling_glm.csv) /
--- @m5-long@/@m7-long@/@m8-long@/@m9-long@=延長 grid (hbm_scaling_<m>_long.csv)。
-module Main where
-
-import           Control.Monad                    (forM_)
-import           System.Environment               (getArgs)
-import qualified Data.Map.Strict                  as Map
-import qualified Data.Text                        as T
-import qualified Data.Vector                      as V
-import qualified System.Random.MWC                as MWC
-import           System.Random.MWC.Distributions  (standard)
-import           Text.Printf                      (printf)
-import           System.IO                        (withFile, IOMode (..),
-                                                   hPutStrLn, hSetBuffering,
-                                                   BufferMode (..))
-
-import           Hanalyze.Model.HBM               (Distribution (..), ModelP,
-                                                   sample, observe, sampleDist,
-                                                   glmmRandomIntercept,
-                                                   GlmmFamily (..))
--- Distribution(..) は HalfCauchy 等 全コンストラクタを含む (eight schools で使用)。
-import           Hanalyze.MCMC.Core               (Chain, chainAccepted,
-                                                   chainTreeDepths,
-                                                   chainTotal, chainVals,
-                                                   posteriorMean)
-import           Hanalyze.MCMC.NUTS               (NUTSConfig (..),
-                                                   defaultNUTSConfig, nuts)
-import           Hanalyze.Stat.MCMC               (ess)
-import           Hanalyze.Fit                     (designHBMProgram)
-
-import           BenchUtil
-
--- ---------------------------------------------------------------------------
--- 共通設定
--- ---------------------------------------------------------------------------
-
-iterGrid :: [Int]
-iterGrid = [50, 100, 200, 400, 800, 1600]
-
-warmupFixed :: Int
-warmupFixed = 500
-
-timingReps :: Int
-timingReps = 5
-
--- 真値 (DGP)
-m1True :: (Double, Double, Double)   -- (a, b, sigma)
-m1True = (2.0, 1.5, 1.0)
-
-m2True :: (Double, Double, Double, Double)  -- (beta0, beta1, tau_u, sigma)
-m2True = (1.0, 0.8, 1.5, 1.0)
-
-nM1 :: Int
-nM1 = 100
-
-nGroupsM2, perGroupM2 :: Int
-nGroupsM2  = 8
-perGroupM2 = 12     -- 計 96 観測
-
--- M3: (beta0, beta1, tau_u, tau_v, sigma)。 群構成は M2 と同じ 8×12。
-m3True :: (Double, Double, Double, Double, Double)
-m3True = (1.0, 0.8, 1.0, 0.5, 1.0)
-
--- M4: 多変量 X (p=10 + intercept)。 (intercept:betas, sigma)。
-m4BetaTrue :: [Double]
-m4BetaTrue = [1.0, 1.2, -0.8, 0.5, 0.3, -0.2, 0.7, -0.4, 0.6, -0.5, 0.1]
-
-m4SigmaTrue :: Double
-m4SigmaTrue = 1.0
-
-nM4, pM4 :: Int
-nM4 = 200
-pM4 = 10
-
--- M5: y = a·exp(-b·x) + c。 (a, b, c, sigma)。
-m5True :: (Double, Double, Double, Double)
-m5True = (2.5, 1.2, 0.5, 0.3)
-
-nM5 :: Int
-nM5 = 100
-
--- M6: y_ij = a_g·exp(-b·x) (a_g ~ N(mu_a, tau_a))。 (mu_a, tau_a, b, sigma)。
-m6True :: (Double, Double, Double, Double)
-m6True = (2.0, 0.5, 1.0, 0.3)
-
--- M7: y ~ Poisson(exp(a + b·x))。 (a, b)。 x ~ N(0,1) で λ ∈ おおよそ
--- [0.1, 20] (Knuth サンプラ・logFactorial とも十分な範囲)。
-m7True :: (Double, Double)
-m7True = (0.5, 0.8)
-
-nM7 :: Int
-nM7 = 100
-
--- M8: y ~ Bernoulli(invLogit(a + b·x))。 (a, b)。
-m8True :: (Double, Double)
-m8True = (0.3, 1.2)
-
-nM8 :: Int
-nM8 = 100
-
--- M9: y ~ NegBin(exp(a + b·x), α)。 (a, b, alpha)。 x ~ N(0,1) で
--- μ ∈ おおよそ [0.15, 18] (M7 と同レンジ)。 α は過分散が見える 1.5
--- (整数回避は lgammaApprox 境界 FD 罠の慣例に合わせる・56.1 記録)。
-m9True :: (Double, Double, Double)
-m9True = (0.5, 0.8, 1.5)
-
-nM9 :: Int
-nM9 = 100
-
--- ---------------------------------------------------------------------------
--- 決定的データ生成
--- ---------------------------------------------------------------------------
-
--- | seed 固定の N(0,1) 列。
-normals :: Int -> Int -> IO [Double]
-normals seed k = do
-  g <- MWC.initialize (V.singleton (fromIntegral seed))
-  mapM (const (standard g)) [1 .. k]
-
--- | M1: x ~ N(0,2), y = a + b·x + N(0,σ)。 (x, y) を返し CSV も書く。
-genM1 :: IO ([Double], [Double])
-genM1 = do
-  let (a, b, s) = m1True
-  xz <- normals 11 nM1
-  ez <- normals 12 nM1
-  let xs = map (* 2.0) xz
-      ys = zipWith (\x e -> a + b * x + s * e) xs ez
-  writeCsv "bench/data/hbm_m1.csv" "x0,y"
-    [ printf "%.6f,%.6f" x y | (x, y) <- zip xs ys ]
-  return (xs, ys)
-
--- | M2: 8 群 × 12。 x ~ N(0,2), u_g ~ N(0,τ_u), y = β0+β1·x+u_g+N(0,σ)。
---   (xRows [[1,x]], gids, ys) を返し CSV (x0,group,y) も書く。
-genM2 :: IO ([[Double]], [Int], [Double])
-genM2 = do
-  let (b0, b1, tauU, s) = m2True
-      n = nGroupsM2 * perGroupM2
-  xz <- normals 21 n
-  ez <- normals 22 n
-  uz <- normals 23 nGroupsM2
-  let us   = map (* tauU) uz
-      gids = [ i `div` perGroupM2 | i <- [0 .. n - 1] ]
-      xs   = map (* 2.0) xz
-      ys   = [ b0 + b1 * x + (us !! g) + s * e
-             | (x, g, e) <- zip3 xs gids ez ]
-      xRows = [ [1.0, x] | x <- xs ]
-  writeCsv "bench/data/hbm_m2.csv" "x0,group,y"
-    [ printf "%.6f,%d,%.6f" x g y | (x, g, y) <- zip3 xs gids ys ]
-  return (xRows, gids, ys)
-
--- | M3: 8 群 × 12。 y = β0 + β1·x + u_g + v_g·x + N(0,σ)。
---   (xs, gids, ys) を返し CSV (x0,group,y) も書く。
-genM3 :: IO ([Double], [Int], [Double])
-genM3 = do
-  let (b0, b1, tauU, tauV, s) = m3True
-      n = nGroupsM2 * perGroupM2
-  xz <- normals 31 n
-  ez <- normals 32 n
-  uz <- normals 33 nGroupsM2
-  vz <- normals 34 nGroupsM2
-  let us   = map (* tauU) uz
-      vs   = map (* tauV) vz
-      gids = [ i `div` perGroupM2 | i <- [0 .. n - 1] ]
-      xs   = map (* 2.0) xz
-      ys   = [ b0 + b1 * x + (us !! g) + (vs !! g) * x + s * e
-             | (x, g, e) <- zip3 xs gids ez ]
-  writeCsv "bench/data/hbm_m3.csv" "x0,group,y"
-    [ printf "%.6f,%d,%.6f" x g y | (x, g, y) <- zip3 xs gids ys ]
-  return (xs, gids, ys)
-
--- | M4: n=200, p=10。 y = β0 + Σ β_k·x_k + N(0,σ)。
---   (xRows (p 列・intercept 含まず), ys) を返し CSV (x0..x9,y) も書く。
-genM4 :: IO ([[Double]], [Double])
-genM4 = do
-  xz <- normals 41 (nM4 * pM4)
-  ez <- normals 42 nM4
-  let xRows = [ take pM4 (drop (i * pM4) xz) | i <- [0 .. nM4 - 1] ]
-      (b0 : bks) = m4BetaTrue
-      ys = [ b0 + sum (zipWith (*) bks xr) + m4SigmaTrue * e
-           | (xr, e) <- zip xRows ez ]
-      hdr = concat [ "x" ++ show k ++ "," | k <- [0 .. pM4 - 1] ] ++ "y"
-  writeCsv "bench/data/hbm_m4.csv" hdr
-    [ concat [ printf "%.6f," x | x <- xr ] ++ printf "%.6f" y
-    | (xr, y) <- zip xRows ys ]
-  return (xRows, ys)
-
--- | M5: x = [0,3) 等間隔グリッド。 y = a·exp(-b·x) + c + N(0,σ)。
-genM5 :: IO ([Double], [Double])
-genM5 = do
-  let (a, b, c, s) = m5True
-  ez <- normals 51 nM5
-  let xs = [ 3.0 * (fromIntegral i + 0.5) / fromIntegral nM5
-           | i <- [0 .. nM5 - 1] ]
-      ys = [ a * exp (negate b * x) + c + s * e | (x, e) <- zip xs ez ]
-  writeCsv "bench/data/hbm_m5.csv" "x0,y"
-    [ printf "%.6f,%.6f" x y | (x, y) <- zip xs ys ]
-  return (xs, ys)
-
--- | M6: 8 群 × 12・x は群内 [0,3) グリッド。 y = a_g·exp(-b·x) + N(0,σ)。
-genM6 :: IO ([Double], [Int], [Double])
-genM6 = do
-  let (muA, tauA, b, s) = m6True
-      n = nGroupsM2 * perGroupM2
-  ez <- normals 61 n
-  az <- normals 62 nGroupsM2
-  let as   = [ muA + tauA * z | z <- az ]
-      gids = [ i `div` perGroupM2 | i <- [0 .. n - 1] ]
-      xs   = [ 3.0 * (fromIntegral (i `mod` perGroupM2) + 0.5)
-                   / fromIntegral perGroupM2
-             | i <- [0 .. n - 1] ]
-      ys   = [ (as !! g) * exp (negate b * x) + s * e
-             | (x, g, e) <- zip3 xs gids ez ]
-  writeCsv "bench/data/hbm_m6.csv" "x0,group,y"
-    [ printf "%.6f,%d,%.6f" x g y | (x, g, y) <- zip3 xs gids ys ]
-  return (xs, gids, ys)
-
--- | M7: x ~ N(0,1)。 y ~ Poisson(exp(a + b·x)) (sampleDist で決定的生成)。
-genM7 :: IO ([Double], [Double])
-genM7 = do
-  let (a, b) = m7True
-  xs <- normals 71 nM7
-  g  <- MWC.initialize (V.singleton 72)
-  ys <- mapM (\x -> sampleDist (Poisson (exp (a + b * x))) g) xs
-  writeCsv "bench/data/hbm_m7.csv" "x0,y"
-    [ printf "%.6f,%.0f" x y | (x, y) <- zip xs ys ]
-  return (xs, ys)
-
--- | M8: x ~ N(0,1)。 y ~ Bernoulli(invLogit(a + b·x))。
-genM8 :: IO ([Double], [Double])
-genM8 = do
-  let (a, b) = m8True
-  xs <- normals 81 nM8
-  g  <- MWC.initialize (V.singleton 82)
-  ys <- mapM (\x -> sampleDist
-                      (Bernoulli (1 / (1 + exp (negate (a + b * x))))) g) xs
-  writeCsv "bench/data/hbm_m8.csv" "x0,y"
-    [ printf "%.6f,%.0f" x y | (x, y) <- zip xs ys ]
-  return (xs, ys)
-
--- | M9: x ~ N(0,1)。 y ~ NegBin(exp(a + b·x), α) (sampleDist で決定的生成)。
-genM9 :: IO ([Double], [Double])
-genM9 = do
-  let (a, b, al) = m9True
-  xs <- normals 91 nM9
-  g  <- MWC.initialize (V.singleton 92)
-  ys <- mapM (\x -> sampleDist
-                      (NegativeBinomial (exp (a + b * x)) al) g) xs
-  writeCsv "bench/data/hbm_m9.csv" "x0,y"
-    [ printf "%.6f,%.0f" x y | (x, y) <- zip xs ys ]
-  return (xs, ys)
-
-writeCsv :: FilePath -> String -> [String] -> IO ()
-writeCsv path hdr rows = withFile path WriteMode $ \h -> do
-  hSetBuffering h LineBuffering
-  hPutStrLn h hdr
-  mapM_ (hPutStrLn h) rows
-
--- | Radon 生 CSV を読む (Python 側と同一ファイル)。 列 =
---   county(str), county_idx(int), floor(0/1), log_radon, log_uranium。
---   返り値 = (designX=[[1,floor,uranium]], county_idx, floor 列, log_radon)。
-readRadon :: IO ([[Double]], [Int], [Double], [Double])
-readRadon = do
-  txt <- readFile "bench/data/radon.csv"
-  let recs = map parseRow (drop 1 (lines txt))
-      parseRow ln = case splitComma ln of
-        (_c : ci : fl : lr : lu : _) ->
-          (read ci :: Int, read fl :: Double, read lr :: Double, read lu :: Double)
-        _ -> error ("readRadon: 列不足 " ++ ln)
-      cidx    = [ c | (c, _, _, _) <- recs ]
-      floors  = [ f | (_, f, _, _) <- recs ]
-      ys      = [ y | (_, _, y, _) <- recs ]
-      designX = [ [1.0, f, u] | (_, f, _, u) <- recs ]
-  return (designX, cidx, floors, ys)
-
--- | 単純な comma split (Data.List.Split 非依存)。
-splitComma :: String -> [String]
-splitComma s = case break (== ',') s of
-  (a, ',' : rest) -> a : splitComma rest
-  (a, _)          -> [a]
-
--- ---------------------------------------------------------------------------
--- モデル定義
--- ---------------------------------------------------------------------------
-
--- | M1 pooled 単回帰。 Distribution はスカラ専用ゆえ観測は 1 点ずつ展開。
-m1Model :: [Double] -> [Double] -> ModelP ()
-m1Model xs ys = do
-  a <- sample "a"     (Normal 0 10)
-  b <- sample "b"     (Normal 0 10)
-  s <- sample "sigma" (Exponential 1)
-  forM_ (zip3 [0 :: Int ..] xs ys) $ \(i, x, y) ->
-    observe (T.pack ("y_" ++ show i)) (Normal (a + b * realToFrac x) s) [y]
-
--- | M2 階層 random intercept。 既存 helper をそのまま使う
---   (latent: beta_0, beta_1, tau_u, u_0..u_{nG-1}, sigma)。
-m2Model :: [[Double]] -> [Int] -> [Double] -> ModelP ()
-m2Model xRows gids ys = glmmRandomIntercept GlmmGaussian xRows gids ys
-
--- | M3 階層 random intercept+slope (per-obs 手書き)。 u_g は係数 1 ゆえ
---   54.8 合成で REff gather、 v_g は係数 x ゆえ dense 列 + prior は
---   residual ad walk に残る (中間ケース)。
-m3Model :: [Double] -> [Int] -> [Double] -> ModelP ()
-m3Model xs gids ys = do
-  let nG = if null gids then 0 else maximum gids + 1
-  b0 <- sample "beta_0" (Normal 0 5)
-  b1 <- sample "beta_1" (Normal 0 5)
-  tu <- sample "tau_u"  (HalfNormal 5)
-  tv <- sample "tau_v"  (HalfNormal 5)
-  us <- mapM (\j -> sample (T.pack ("u_" ++ show j)) (Normal 0 tu)) [0 .. nG - 1]
-  vs <- mapM (\j -> sample (T.pack ("v_" ++ show j)) (Normal 0 tv)) [0 .. nG - 1]
-  s  <- sample "sigma" (Exponential 1)
-  forM_ (zip3 [0 :: Int ..] (zip xs gids) ys) $ \(i, (x, g), y) ->
-    observe (T.pack ("y_" ++ show i))
-      (Normal (b0 + b1 * realToFrac x + us !! g + (vs !! g) * realToFrac x) s) [y]
-
--- | M4 多変量 X pooled (per-obs 手書き)。 全 affine ゆえ 54.8 合成で
---   全 latent が dense β 列 + 定数 prior = 完全解析経路。
-m4Model :: [[Double]] -> [Double] -> ModelP ()
-m4Model xRows ys = do
-  bs <- mapM (\k -> sample (T.pack ("beta_" ++ show k)) (Normal 0 5))
-             [0 .. pM4]
-  s  <- sample "sigma" (Exponential 1)
-  let (b0 : bks) = bs
-  forM_ (zip3 [0 :: Int ..] xRows ys) $ \(i, xr, y) ->
-    observe (T.pack ("y_" ++ show i))
-      (Normal (b0 + sum (zipWith (\b x -> b * realToFrac x) bks xr)) s) [y]
-
--- | M5 パラメタ非線形 (per-obs 手書き)。 μ = a·exp(-b·x) + c は非 affine ゆえ
---   54.8 合成不可 → walk + ad fallback (汎用経路の弱点を正直に測る)。
-m5Model :: [Double] -> [Double] -> ModelP ()
-m5Model xs ys = do
-  a <- sample "a" (Normal 0 10)
-  b <- sample "b" (HalfNormal 2)
-  c <- sample "c" (Normal 0 10)
-  s <- sample "sigma" (Exponential 1)
-  forM_ (zip3 [0 :: Int ..] xs ys) $ \(i, x, y) ->
-    observe (T.pack ("y_" ++ show i))
-      (Normal (a * exp (negate b * realToFrac x) + c) s) [y]
-
--- | M6 階層 × 非線形 (per-obs 手書き)。 a_g·exp(-b·x) も非 affine → fallback。
-m6Model :: [Double] -> [Int] -> [Double] -> ModelP ()
-m6Model xs gids ys = do
-  let nG = if null gids then 0 else maximum gids + 1
-  muA  <- sample "mu_a"  (Normal 0 10)
-  tauA <- sample "tau_a" (HalfNormal 2)
-  as   <- mapM (\j -> sample (T.pack ("a_" ++ show j)) (Normal muA tauA))
-               [0 .. nG - 1]
-  b    <- sample "b" (HalfNormal 2)
-  s    <- sample "sigma" (Exponential 1)
-  forM_ (zip3 [0 :: Int ..] (zip xs gids) ys) $ \(i, (x, g), y) ->
-    observe (T.pack ("y_" ++ show i))
-      (Normal ((as !! g) * exp (negate b * realToFrac x)) s) [y]
-
--- | M7 Poisson 回帰 (per-obs 手書き・log link)。
---   ★旧コメント「非Gaussian観測ゆえ高速経路対象外」は古い情報 (Phase 89 で
---   訂正): `VGPois` 族対応が後に追加され、現在は vecIR (a) 経路に乗る
---   (`synthVecIR` = Just・`bench/posteriordb/glm-poisson` で実測確認)。
-m7Model :: [Double] -> [Double] -> ModelP ()
-m7Model xs ys = do
-  a <- sample "a" (Normal 0 5)
-  b <- sample "b" (Normal 0 5)
-  forM_ (zip3 [0 :: Int ..] xs ys) $ \(i, x, y) ->
-    observe (T.pack ("y_" ++ show i))
-      (Poisson (exp (a + b * realToFrac x))) [y]
-
--- | M8 logistic 回帰 (per-obs 手書き・logit link)。 M7 と同じく fallback。
-m8Model :: [Double] -> [Double] -> ModelP ()
-m8Model xs ys = do
-  a <- sample "a" (Normal 0 5)
-  b <- sample "b" (Normal 0 5)
-  forM_ (zip3 [0 :: Int ..] xs ys) $ \(i, x, y) ->
-    observe (T.pack ("y_" ++ show i))
-      (Bernoulli (1 / (1 + exp (negate (a + b * realToFrac x))))) [y]
-
--- | M9 NegBin 回帰 (per-obs 手書き・log link・α latent)。 Phase 56.5 で
---   IR 吸収 (lgammaΓ(k+α) は SLgammaO elementwise・Γ(k+1) は compile 時定数)。
-m9Model :: [Double] -> [Double] -> ModelP ()
-m9Model xs ys = do
-  a  <- sample "a"     (Normal 0 5)
-  b  <- sample "b"     (Normal 0 5)
-  al <- sample "alpha" (Exponential 0.5)
-  forM_ (zip3 [0 :: Int ..] xs ys) $ \(i, x, y) ->
-    observe (T.pack ("y_" ++ show i))
-      (NegativeBinomial (exp (a + b * realToFrac x)) al) [y]
-
--- | Radon 相関 varying intercept+slope (flagship・Phase 84)。 固定効果 =
---   (Intercept)+floor+uranium、 ランダム効果 = county 群の相関 (切片+floor 傾き)。
---   designHBMProgram の相関 RE branch (非中心化・LKJ・Phase 80.2b) に載る。
---   Python 側 bench_radon と同一 prior・同一データ (radon.csv)。
-radonModel :: [[Double]] -> [Int] -> [Double] -> [Double] -> ModelP ()
-radonModel designX cidx floorCol ys =
-  designHBMProgram designX ["(Intercept)", "floor", "uranium"]
-                   [(cidx, nCounties, [floorCol])] ys
-  where nCounties = if null cidx then 0 else maximum cidx + 1
-
--- | Eight Schools (精度エッジ・Phase 84)。 古典的階層正規・funnel の定番。
---   非中心化パラメタ化: θ_j = μ + τ·θ̃_j (θ̃_j ~ N(0,1))・観測 SE σ_j は既知。
---   μ~N(0,5)・τ~HalfCauchy(5)。 Python 側 bench_eightschools と同一 prior・データ。
---   主役 = τ (funnel の首・サンプラ品質が出る)。
-eightSchoolsModel :: ModelP ()
-eightSchoolsModel = do
-  let ys     = [28, 8, -3, 7, -1, 1, 18, 12] :: [Double]
-      sigmas = [15, 10, 16, 11, 9, 11, 10, 18] :: [Double]
-  mu  <- sample "mu"  (Normal 0 5)
-  tau <- sample "tau" (HalfCauchy 5)
-  tts <- mapM (\j -> sample (T.pack ("theta_t_" ++ show j)) (Normal 0 1)) [0 .. 7]
-  forM_ (zip3 [0 :: Int ..] (zip ys sigmas) tts) $ \(i, (y, s), tt) ->
-    observe (T.pack ("y_" ++ show i)) (Normal (mu + tau * tt) (realToFrac s)) [y]
-
--- ---------------------------------------------------------------------------
--- NUTS 実行 (warmup 固定・iter 可変)
--- ---------------------------------------------------------------------------
-
-mkConfig :: Int -> NUTSConfig
-mkConfig iters = defaultNUTSConfig
-  { nutsIterations    = iters
-  , nutsBurnIn        = warmupFixed
-  , nutsStepSize      = 0.1
-  , nutsMaxDepth      = 10
-  , nutsAdaptStepSize = True
-  , nutsTargetAccept  = 0.8
-  , nutsAdaptMass     = True
-  }
-
-acceptRate :: Chain -> Double
-acceptRate ch =
-  fromIntegral (chainAccepted ch) / max 1 (fromIntegral (chainTotal ch))
-
-probeChain :: [T.Text] -> Chain -> Double
-probeChain names ch =
-  sum [ maybe 0 id (posteriorMean p ch) | p <- names ]
-
--- | 1 (モデル, iter) について timingReps 回計測 (index-seed で CSE 回避)、
---   返り値の chain (seed 42) から ESS/posterior mean を取る。
-runBench
-  :: String              -- ^ モデル名タグ (M1_pooled / M2_ranint)
-  -> ModelP ()           -- ^ モデル (init は別途・seed は gen 側)
-  -> Map.Map T.Text Double  -- ^ init params
-  -> [T.Text]               -- ^ probe 用全パラメタ名
-  -> T.Text                 -- ^ 主役パラメタ (ESS 報告対象: slope)
-  -> Int                    -- ^ iter
-  -> IO BenchRow
-runBench = runBenchReps timingReps
-
--- | 'runBench' の reps 可変版。 重いモデル (radon 等) は少ない reps で回す。
-runBenchReps
-  :: Int -> String -> ModelP () -> Map.Map T.Text Double
-  -> [T.Text] -> T.Text -> Int -> IO BenchRow
-runBenchReps reps tag mdl initP allNames keyParam iters = do
-  let cfg = mkConfig iters
-      run :: Int -> IO Chain
-      run i = do
-        g <- MWC.initialize (V.singleton (fromIntegral (42 + i)))
-        nuts mdl cfg initP g
-  (ms, ch) <- timeitIO reps (probeChain allNames) run
-  let keyEss   = ess (chainVals keyParam ch)
-      keyMean  = maybe 0 id (posteriorMean keyParam ch)
-      acc      = acceptRate ch
-      essPerSec = keyEss / max 1e-9 (ms / 1000.0)
-      name     = tag ++ "_iter" ++ show iters
-      -- Phase 85.3: per-draw tree depth の平均 (PyMC の tree_depth と直接比較)
-      depths   = chainTreeDepths ch
-      meanDepth = if null depths then 0
-                  else fromIntegral (sum depths)
-                       / fromIntegral (length depths) :: Double
-      extra    = printf ("iter=%d warmup=%d key=%s ess=%.1f ess_per_sec=%.2f "
-                         ++ "accept=%.3f tree_depth=%.2f time_ms=%.1f")
-                   iters warmupFixed (T.unpack keyParam)
-                   keyEss essPerSec acc meanDepth ms
-  return (BenchRow "haskell" "hbm_scaling" name ms keyMean keyEss extra)
-
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  args <- getArgs
-  case args of
-    ["m5-long"] -> do
-      (x5, y5) <- genM5
-      mainLong "M5_nonlin" (m5Model x5 y5)
-        (Map.fromList [("a", 2.5), ("b", 1.2), ("c", 0.5), ("sigma", 0.3)])
-        ["a", "b", "c", "sigma"] "m5"
-    ["m7-long"] -> do
-      (x7, y7) <- genM7
-      mainLong "M7_pois" (m7Model x7 y7)
-        (Map.fromList [("a", 0.5), ("b", 0.8)]) ["a", "b"] "m7"
-    ["m8-long"] -> do
-      (x8, y8) <- genM8
-      mainLong "M8_logit" (m8Model x8 y8)
-        (Map.fromList [("a", 0.3), ("b", 1.2)]) ["a", "b"] "m8"
-    ["m9-long"] -> do
-      (x9, y9) <- genM9
-      mainLong "M9_negbin" (m9Model x9 y9)
-        (Map.fromList [("a", 0.5), ("b", 0.8), ("alpha", 1.5)])
-        ["a", "b", "alpha"] "m9"
-    ["glm"]          -> mainGlm
-    ["radon"]        -> mainRadon
-    ["radon1600"]    -> mainRadon1600
-    ["eightschools"] -> mainEight
-    _                -> mainAll
-
--- | Eight Schools を通常 grid で掃く (引数 @eightschools@)。 小モデルゆえ軽い。
-mainEight :: IO ()
-mainEight = do
-  let eightInit = Map.fromList [("mu", 4.0), ("tau", 3.0)]
-      allNames  = ["mu", "tau"]
-  rows <- mapM
-    (runBench "eightschools" eightSchoolsModel eightInit allNames "tau")
-    iterGrid
-  writeRows "bench/results/haskell/hbm_scaling_eightschools.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/hbm_scaling_eightschools.csv"
-
--- | Radon (flagship) を通常 grid で掃く (引数 @radon@)。 別 CSV
---   @hbm_scaling_radon.csv@ へ (M 系 CSV を上書きしない)。 相関 RE の
---   正値制約 latent (sigma/tau/LKJ の Beta) だけ init を与え、 z/beta は 0 既定。
--- radon は 919 obs・相関 RE で 1 サンプルあたり deep tree (~max depth 10) ゆえ
--- 重い。 reps=2・短め grid で回す (Python 側 radonGrid と一致させる)。
-radonGrid :: [Int]
-radonGrid = [50, 100, 200, 400]
-
-radonReps :: Int
-radonReps = 2
-
-mainRadon :: IO ()
-mainRadon = do
-  (designX, cidx, floorCol, ys) <- readRadon
-  let radonInit = Map.fromList
-        [ ("(Intercept)", 1.3), ("floor", -0.6), ("uranium", 0.7)
-        , ("sigma", 0.7)
-        , ("tau_g0_0", 0.5), ("tau_g0_1", 0.3), ("Lcorr_g0_u1_0", 0.5) ]
-      allNames = ["(Intercept)", "floor", "uranium", "sigma"]
-  rows <- mapM
-    (\it -> do
-        r <- runBenchReps radonReps "radon"
-               (radonModel designX cidx floorCol ys)
-               radonInit allNames "floor" it
-        putStrLn $ "  radon iter=" ++ show it ++ ": "
-                 ++ show (round (brTimeMs r) :: Int) ++ " ms"
-        pure r)
-    radonGrid
-  writeRows "bench/results/haskell/hbm_scaling_radon.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/hbm_scaling_radon.csv"
-
--- | Phase 88 追補: radon を実運用規模 iter=1600 単点で計測する (引数
---   @radon1600@)。 iter400 は短めの grid で「有利なベンチ」になりうる
---   (Phase 87 で iter400→1600 だけで hanalyze 対 PyMC-C 比が 0.80×→
---   0.49-0.68× に動いた前例あり)。 別 CSV @hbm_scaling_radon1600.csv@ へ。
-mainRadon1600 :: IO ()
-mainRadon1600 = do
-  (designX, cidx, floorCol, ys) <- readRadon
-  let radonInit = Map.fromList
-        [ ("(Intercept)", 1.3), ("floor", -0.6), ("uranium", 0.7)
-        , ("sigma", 0.7)
-        , ("tau_g0_0", 0.5), ("tau_g0_1", 0.3), ("Lcorr_g0_u1_0", 0.5) ]
-      allNames = ["(Intercept)", "floor", "uranium", "sigma"]
-  r <- runBenchReps radonReps "radon"
-         (radonModel designX cidx floorCol ys)
-         radonInit allNames "floor" 1600
-  putStrLn $ "  radon iter=1600: " ++ show (round (brTimeMs r) :: Int) ++ " ms"
-  writeRows "bench/results/haskell/hbm_scaling_radon1600.csv" [r]
-  putStrLn "wrote 1 rows → bench/results/haskell/hbm_scaling_radon1600.csv"
-
--- | 1 モデルだけを延長 grid で掃く (引数 @m5-long@/@m7-long@/@m8-long@)。
---
--- 通常 grid (50-1600) では PyMC 側 total が固定費 (compile+tune ~2s) に支配され
--- per-draw 線形フィットが R² ~0.13 と不定のままだった (M5・54.11)。 iter を
--- 25600 まで延ばし draw 部分を固定費より大きくして傾きを確定する (Python 側 =
--- @bench_hbm_scaling.py <m>-long@・同 grid)。 結果は別 CSV
--- (@hbm_scaling_<m>_long.csv@) へ (通常 bench の CSV は上書きしない)。
-iterGridLong :: [Int]
-iterGridLong = [400, 800, 1600, 3200, 6400, 12800, 25600]
-
-mainLong
-  :: String -> ModelP () -> Map.Map T.Text Double -> [T.Text] -> String -> IO ()
-mainLong tag mdl initP names short = do
-  rows <- mapM (runBench tag mdl initP names "b") iterGridLong
-  let out = "bench/results/haskell/hbm_scaling_" ++ short ++ "_long.csv"
-  writeRows out rows
-  putStrLn $ "wrote " ++ show (length rows) ++ " rows → " ++ out
-
--- | M7-M9 (GLM 系) だけ通常 grid で掃く (引数 @glm@・Phase 55.1 baseline 用、
---   M9 は Phase 56.6 追加)。 M1-M6 の既存 CSV を上書きしないよう別 CSV へ。
-mainGlm :: IO ()
-mainGlm = do
-  (x7, y7) <- genM7
-  (x8, y8) <- genM8
-  (x9, y9) <- genM9
-  m7Rows <- mapM
-    (runBench "M7_pois" (m7Model x7 y7)
-       (Map.fromList [("a", 0.5), ("b", 0.8)]) ["a", "b"] "b")
-    iterGrid
-  m8Rows <- mapM
-    (runBench "M8_logit" (m8Model x8 y8)
-       (Map.fromList [("a", 0.3), ("b", 1.2)]) ["a", "b"] "b")
-    iterGrid
-  m9Rows <- mapM
-    (runBench "M9_negbin" (m9Model x9 y9)
-       (Map.fromList [("a", 0.5), ("b", 0.8), ("alpha", 1.5)])
-       ["a", "b", "alpha"] "b")
-    iterGrid
-  let rows = m7Rows ++ m8Rows ++ m9Rows
-  writeRows "bench/results/haskell/hbm_scaling_glm.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/hbm_scaling_glm.csv"
-
-mainAll :: IO ()
-mainAll = do
-  (x1, y1)          <- genM1
-  (xRows, gids, y2) <- genM2
-  (x3, g3, y3)      <- genM3
-  (xR4, y4)         <- genM4
-  (x5, y5)          <- genM5
-  (x6, g6, y6)      <- genM6
-  (x7, y7)          <- genM7
-  (x8, y8)          <- genM8
-
-  let groupNames pre = [ T.pack (pre ++ show j) | j <- [0 .. nGroupsM2 - 1] ]
-      m1Init = Map.fromList [("a", 2.0), ("b", 1.5), ("sigma", 1.0)]
-      m2Init = Map.fromList $
-        [ ("beta_0", 1.0), ("beta_1", 0.8), ("tau_u", 1.5), ("sigma", 1.0) ]
-        ++ [ (u, 0.0) | u <- groupNames "u_" ]
-      m3Init = Map.fromList $
-        [ ("beta_0", 1.0), ("beta_1", 0.8), ("tau_u", 1.0), ("tau_v", 0.5)
-        , ("sigma", 1.0) ]
-        ++ [ (u, 0.0) | u <- groupNames "u_" ++ groupNames "v_" ]
-      m4Init = Map.fromList $
-        [ (T.pack ("beta_" ++ show k), 0.0) | k <- [0 .. pM4] ]
-        ++ [("sigma", 1.0)]
-      m5Init = Map.fromList
-        [("a", 2.5), ("b", 1.2), ("c", 0.5), ("sigma", 0.3)]
-      m6Init = Map.fromList $
-        [ ("mu_a", 2.0), ("tau_a", 0.5), ("b", 1.0), ("sigma", 0.3) ]
-        ++ [ (a, 2.0) | a <- groupNames "a_" ]
-      m1AllNames = ["a", "b", "sigma"]
-      m2AllNames = ["beta_0", "beta_1", "tau_u", "sigma"] ++ groupNames "u_"
-      m3AllNames = ["beta_0", "beta_1", "tau_u", "tau_v", "sigma"]
-                   ++ groupNames "u_" ++ groupNames "v_"
-      m4AllNames = [ T.pack ("beta_" ++ show k) | k <- [0 .. pM4] ] ++ ["sigma"]
-      m5AllNames = ["a", "b", "c", "sigma"]
-      m6AllNames = ["mu_a", "tau_a", "b", "sigma"] ++ groupNames "a_"
-
-  m1Rows <- mapM
-    (runBench "M1_pooled" (m1Model x1 y1) m1Init m1AllNames "b")
-    iterGrid
-  m2Rows <- mapM
-    (runBench "M2_ranint" (m2Model xRows gids y2) m2Init m2AllNames "beta_1")
-    iterGrid
-  m3Rows <- mapM
-    (runBench "M3_ranslope" (m3Model x3 g3 y3) m3Init m3AllNames "beta_1")
-    iterGrid
-  m4Rows <- mapM
-    (runBench "M4_multix" (m4Model xR4 y4) m4Init m4AllNames "beta_1")
-    iterGrid
-  m5Rows <- mapM
-    (runBench "M5_nonlin" (m5Model x5 y5) m5Init m5AllNames "b")
-    iterGrid
-  m6Rows <- mapM
-    (runBench "M6_hier_nonlin" (m6Model x6 g6 y6) m6Init m6AllNames "b")
-    iterGrid
-  m7Rows <- mapM
-    (runBench "M7_pois" (m7Model x7 y7)
-       (Map.fromList [("a", 0.5), ("b", 0.8)]) ["a", "b"] "b")
-    iterGrid
-  m8Rows <- mapM
-    (runBench "M8_logit" (m8Model x8 y8)
-       (Map.fromList [("a", 0.3), ("b", 1.2)]) ["a", "b"] "b")
-    iterGrid
-
-  let rows = m1Rows ++ m2Rows ++ m3Rows ++ m4Rows ++ m5Rows ++ m6Rows
-          ++ m7Rows ++ m8Rows
-  writeRows "bench/results/haskell/hbm_scaling.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/hbm_scaling.csv"
diff --git a/bench/haskell/BenchHBMVecADSpike.hs b/bench/haskell/BenchHBMVecADSpike.hs
deleted file mode 100644
--- a/bench/haskell/BenchHBMVecADSpike.hs
+++ /dev/null
@@ -1,560 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- GHC 9.6.7 の exitification パスが本モジュールで simplifier panic (completeCall)
--- を起こすため無効化。 4 手法とも同一フラグでコンパイルされるので比較は不変。
-{-# OPTIONS_GHC -fno-exitification #-}
--- | Phase 54 専用ベクトル化 AD feasibility spike (計測先行・推測するな計測せよ)。
---
--- 「numpyro に階層モデルで追いつくには `ad` のボックススカラ tape を、 Storable
--- 配列上の tape-free なベクトル化 reverse-mode に置換する必要がある」 という
--- 仮説を、 **本実装の前に**実測で検証する小実験。
---
--- 対象 = 階層 Gaussian (random intercept GLMM、 BenchHBMADModes の M2 と同型):
---   η_i = Σ_k β_k X_ik + u_{g(i)},  y_i ~ Normal(η_i, σ)
---   prior: β_k~N(0,5)、 τ_u~HalfNormal(5)、 u_j~N(0,τ_u)、 σ~Exp(1)
---   unconstrained: β/u は identity、 τ_u/σ は log 変換 (PositiveT、 jacobian +u)。
---
--- 勾配を 2 通りで計算し per-grad 時間を比較:
---   (a) `Numeric.AD.Mode.Reverse.Double.grad` — 現状の方式 (スカラ tape)。
---   (b) 手書きベクトル化解析勾配 — Storable/Unboxed 配列上の reduction のみ。
---       これは tape を一切作らない = ベクトル化 reverse-mode AD の **時間の下限**
---       (汎用エンジンはこれより遅いが、 ここが (a) を桁で上回らなければ専用 AD を
---        作っても勝てない、 という feasibility の天井判定に使う)。
---
--- (b) の正しさは中心差分 (同じ logp) で検証してから時間を測る。
-module Main where
-
-import           Control.Monad                  (forM_, when)
-import           Control.Monad.ST               (ST, runST)
-import           Data.Array.ST                  (STArray, newArray, readArray,
-                                                 writeArray)
-import           Data.List                      (foldl')
-import           Data.STRef                     (STRef, modifySTRef', newSTRef,
-                                                 readSTRef, writeSTRef)
-import qualified Data.Vector.Storable           as VS
-import qualified Data.Vector.Unboxed            as VU
-import           Text.Printf                    (printf)
-
-import qualified Numeric.AD.Mode.Reverse.Double as RevD
-
-import           Numeric.Backprop               (BVar, Reifies, W, auto, gradBP,
-                                                 liftOp1, liftOp2, op1, op2)
-
-import           BenchUtil                      (timeitIO)
-
--- ---------------------------------------------------------------------------
--- 問題サイズと合成データ
--- ---------------------------------------------------------------------------
-
-data Prob = Prob
-  { pP      :: !Int          -- 固定効果数
-  , pNG     :: !Int          -- 群数
-  , pXRows  :: ![[Double]]   -- design X (n × p)
-  , pGids   :: ![Int]        -- group id (length n)
-  , pYs     :: ![Double]     -- 観測 (length n)
-  }
-
--- BenchHBMADModes.genM2Data と同型の決定的データ。
-genProb :: Int -> Int -> Prob
-genProb nG perG =
-  let n    = nG * perG
-      xz   = [ sin (0.7 * fromIntegral i) | i <- [0 .. n - 1] ]
-      ez   = [ 0.3 * cos (1.3 * fromIntegral i) | i <- [0 .. n - 1] ]
-      uz   = [ 0.9 * sin (2.1 * fromIntegral j) | j <- [0 .. nG - 1] ]
-      gids = [ i `div` perG | i <- [0 .. n - 1] ]
-      xs   = map (* 2.0) xz
-      ys   = [ 1.0 + 0.8 * x + (uz !! g) + e
-             | (x, g, e) <- zip3 xs gids ez ]
-      xRows = [ [1.0, x] | x <- xs ]
-  in Prob { pP = 2, pNG = nG, pXRows = xRows, pGids = gids, pYs = ys }
-
--- パラメタ θ のレイアウト: [β_0..β_{p-1}, logτ, u_0..u_{nG-1}, logσ]
-paramLen :: Prob -> Int
-paramLen pr = pP pr + 1 + pNG pr + 1
-
--- 真値近傍の初期 θ。
-theta0 :: Prob -> [Double]
-theta0 pr =
-  let p  = pP pr; nG = pNG pr
-  in replicate p 0.5 ++ [log 1.2] ++ replicate nG 0.1 ++ [log 0.35]
-
--- ---------------------------------------------------------------------------
--- (a) 多相 logp (ad で grad する対象)
--- ---------------------------------------------------------------------------
-
-logN :: Floating a => a -> a -> a -> a
-logN x m s = -0.5 * log (2 * pi) - log s - 0.5 * ((x - m) / s) ^ (2 :: Int)
-
-logHalfNormal :: Floating a => a -> a -> a
-logHalfNormal x s = 0.5 * log (2 / pi) - log s - 0.5 * (x / s) ^ (2 :: Int)
-
-logp :: forall a. Floating a => Prob -> [a] -> a
-logp pr theta =
-  let p   = pP pr; nG = pNG pr
-      b   = take p theta
-      logTau = theta !! p
-      us  = take nG (drop (p + 1) theta)
-      logSig = theta !! (p + 1 + nG)
-      tau = exp logTau
-      sig = exp logSig
-      priorB   = sum [ logN bk 0 5 | bk <- b ]
-      priorTau = logHalfNormal tau 5 + logTau          -- + jacobian (log 変換)
-      priorU   = sum [ logN uj 0 tau | uj <- us ]
-      priorSig = negate sig + logSig                   -- logExp(σ;1)=-σ + jacobian
-      etas = [ sum (zipWith (\bk x -> bk * realToFrac x) b xr) + (us !! g)
-             | (xr, g) <- zip (pXRows pr) (pGids pr) ]
-      loglik = sum [ logN (realToFrac y) eta sig | (eta, y) <- zip etas (pYs pr) ]
-  in priorB + priorTau + priorU + priorSig + loglik
-
-gradAD :: Prob -> [Double] -> [Double]
-gradAD pr = RevD.grad (logp pr)
-
--- ---------------------------------------------------------------------------
--- (b) 手書きベクトル化解析勾配 (Storable/Unboxed 配列・tape なし)
--- ---------------------------------------------------------------------------
-
--- 事前計算: 列ごとの X (length n の VS が p 本)、 group id (VU)。
-data Compiled = Compiled
-  { cP     :: !Int
-  , cNG    :: !Int
-  , cN     :: !Int
-  , cXCols :: ![VS.Vector Double]  -- p 本、 各 length n
-  , cGids  :: !(VU.Vector Int)
-  , cYs    :: !(VS.Vector Double)
-  }
-
-compile :: Prob -> Compiled
-compile pr =
-  let p = pP pr; nG = pNG pr; n = length (pYs pr)
-      xcols = [ VS.fromList [ row !! k | row <- pXRows pr ] | k <- [0 .. p - 1] ]
-  in Compiled p nG n xcols (VU.fromList (pGids pr)) (VS.fromList (pYs pr))
-
-gradVec :: Compiled -> [Double] -> [Double]
-gradVec c theta =
-  let p = cP c; nG = cNG c; n = cN c
-      b   = take p theta
-      logTau = theta !! p
-      us  = take nG (drop (p + 1) theta)
-      logSig = theta !! (p + 1 + nG)
-      tau = exp logTau
-      sig = exp logSig
-      uv  = VS.fromList us
-      -- η = Xβ + u[g]  (length n、 VS)
-      xb  = foldl' (\acc (col, bk) -> VS.zipWith (+) acc (VS.map (* bk) col))
-                   (VS.replicate n 0) (zip (cXCols c) b)
-      ug  = VS.generate n (\i -> uv VS.! (cGids c VU.! i))
-      eta = VS.zipWith (+) xb ug
-      r   = VS.zipWith (-) (cYs c) eta            -- 残差 y - η
-      sig2 = sig * sig
-      -- ∂/∂β_k = -β_k/25 + (1/σ²) Σ_i r_i X_ik
-      gB  = [ negate bk / 25 + VS.sum (VS.zipWith (*) col r) / sig2
-            | (col, bk) <- zip (cXCols c) b ]
-      -- ∂/∂u_j = -u_j/τ² + (1/σ²) Σ_{i:g_i=j} r_i  (scatter-add で O(n))
-      rGroup = VU.accumulate (+) (VU.replicate nG 0)
-                 (VU.zip (cGids c) (VU.convert r :: VU.Vector Double))
-      gU  = [ negate (us !! j) / (tau * tau) + (rGroup VU.! j) / sig2
-            | j <- [0 .. nG - 1] ]
-      sumU2 = VS.sum (VS.map (\x -> x * x) uv)
-      sumR2 = VS.sum (VS.map (\x -> x * x) r)
-      -- logHalfNormal(τ;5) の scale は定数 5 ゆえ τ 由来は -τ²/25 のみ。
-      -- + log 変換 jacobian (+1) + Σ_j logN(u_j;0,τ) の -nG + Σu²/τ²。
-      gLogTau = 1 - fromIntegral nG - (tau * tau) / 25 + sumU2 / (tau * tau)
-      gLogSig = negate sig + 1 - fromIntegral n + sumR2 / sig2
-  in gB ++ [gLogTau] ++ gU ++ [gLogSig]
-
--- ---------------------------------------------------------------------------
--- (案A) backprop ライブラリによる汎用ベクトル化 reverse-mode AD
---
--- theta を 1 本の Storable Vector とみなし backprop で grad する。 ベクトル演算
--- (scale/add/sub/gather/dot/sum) は liftOp で随伴を手書きするので tape は
--- 「ベクトル演算 1 個 = 1 ノード」 になる (= 狙い)。 chain rule と tape の所有は
--- backprop が担う。 スカラ演算 (exp/log/+/*) は BVar の Num/Floating で書ける。
--- ※随伴は (案B) と共有 → 案A/案B の差は「tape をライブラリが持つか自前か」に純化。
--- ---------------------------------------------------------------------------
-
--- 全長 L の theta から要素 i を取り出す。 随伴は e_i*dy (長さ L)。
-idxV :: Reifies s W => Int -> Int -> BVar s (VS.Vector Double) -> BVar s Double
-idxV l i = liftOp1 $ op1 $ \v ->
-  (v VS.! i, \dy -> VS.generate l (\j -> if j == i then dy else 0))
-
--- 全長 L の theta から [off, off+len) を切り出す。 随伴は zeros L に散布。
-sliceV :: Reifies s W
-       => Int -> Int -> Int -> BVar s (VS.Vector Double) -> BVar s (VS.Vector Double)
-sliceV l off len = liftOp1 $ op1 $ \v ->
-  ( VS.slice off len v
-  , \dy -> VS.generate l (\j -> if j >= off && j < off + len then dy VS.! (j - off) else 0) )
-
--- scalar * vector。 ∂scalar = dy·v、 ∂v = scalar*dy。
-scaleV :: Reifies s W => BVar s Double -> BVar s (VS.Vector Double) -> BVar s (VS.Vector Double)
-scaleV = liftOp2 $ op2 $ \k v ->
-  (VS.map (* k) v, \dy -> (VS.sum (VS.zipWith (*) dy v), VS.map (* k) dy))
-
-vaddV :: Reifies s W => BVar s (VS.Vector Double) -> BVar s (VS.Vector Double) -> BVar s (VS.Vector Double)
-vaddV = liftOp2 $ op2 $ \a b -> (VS.zipWith (+) a b, \dy -> (dy, dy))
-
-vsubV :: Reifies s W => BVar s (VS.Vector Double) -> BVar s (VS.Vector Double) -> BVar s (VS.Vector Double)
-vsubV = liftOp2 $ op2 $ \a b -> (VS.zipWith (-) a b, \dy -> (dy, VS.map negate dy))
-
--- 内積。 ∂a = dy*b、 ∂b = dy*a (a·a なら勾配は 2a を backprop の和算で得る)。
-dotV :: Reifies s W => BVar s (VS.Vector Double) -> BVar s (VS.Vector Double) -> BVar s Double
-dotV = liftOp2 $ op2 $ \a b ->
-  (VS.sum (VS.zipWith (*) a b), \dy -> (VS.map (* dy) b, VS.map (* dy) a))
-
--- u[gids] gather (gids/nG は定数)。 随伴は scatter-add。
-gatherV :: Reifies s W => VU.Vector Int -> Int -> BVar s (VS.Vector Double) -> BVar s (VS.Vector Double)
-gatherV gids nG = liftOp1 $ op1 $ \u ->
-  ( VS.generate (VU.length gids) (\i -> u VS.! (gids VU.! i))
-  , \dy -> VS.convert $
-      VU.accumulate (+) (VU.replicate nG 0)
-        (VU.zip gids (VU.convert dy :: VU.Vector Double)) )
-
-logpBP :: forall s. Reifies s W => Compiled -> BVar s (VS.Vector Double) -> BVar s Double
-logpBP c theta =
-  let p = cP c; nG = cNG c; n = cN c
-      l = p + 1 + nG + 1
-      bVec   = sliceV l 0 p theta
-      logTau = idxV l p theta
-      uVec   = sliceV l (p + 1) nG theta
-      logSig = idxV l (p + 1 + nG) theta
-      tau = exp logTau
-      sig = exp logSig
-      -- Xβ = Σ_k β_k * col_k  (β_k は bVec の第 k 要素)
-      colC k = auto (cXCols c !! k)
-      xb = foldl' (\acc k -> vaddV acc (scaleV (idxV p k bVec) (colC k)))
-                  (auto (VS.replicate n 0)) [0 .. p - 1]
-      ug  = gatherV (cGids c) nG uVec
-      eta = vaddV xb ug
-      r   = vsubV (auto (cYs c)) eta
-      nD  = fromIntegral n
-      pD  = fromIntegral p
-      ngD = fromIntegral nG
-      sumB2 = dotV bVec bVec
-      sumU2 = dotV uVec uVec
-      sumR2 = dotV r r
-      priorB   = negate (0.5 * pD * log (2 * pi)) - pD * log 5 - sumB2 / (2 * 25)
-      priorTau = 0.5 * log (2 / pi) - log 5 - tau * tau / (2 * 25) + logTau
-      priorU   = negate (0.5 * ngD * log (2 * pi)) - ngD * log tau - sumU2 / (2 * tau * tau)
-      priorSig = negate sig + logSig
-      loglik   = negate (0.5 * nD * log (2 * pi)) - nD * log sig - sumR2 / (2 * sig * sig)
-  in priorB + priorTau + priorU + priorSig + loglik
-
-gradBackprop :: Compiled -> [Double] -> [Double]
-gradBackprop c theta = VS.toList $ gradBP (logpBP c) (VS.fromList theta)
-
--- ---------------------------------------------------------------------------
--- (案B) 自作・最小 reverse-mode AD (vector-op tape)
---
--- forward で「ベクトル演算ごとにノードを発番」 し、 各ノードの随伴更新クロージャを
--- 逆順リストに積む (= 自前 Wengert tape)。 backward で出力に 1 を seed し、 逆位相順
--- (= 発番の逆順 = prepend したリストの先頭から) にクロージャを replay して入力 (theta
--- leaf) の随伴を得る。 随伴の式は案A の liftOp と同一 → 差は「tape 所有が自前か否か」。
--- スカラは長さ 1 の VS で随伴を持ち、 ノード随伴は単一の mutable 配列に統一格納する。
--- ---------------------------------------------------------------------------
-
--- reverse-mode の値ハンドル: ノード id + primal (scalar / vector)。
-data Rval = RScal !Int !Double | RVec !Int !(VS.Vector Double)
-
-ridOf :: Rval -> Int
-ridOf (RScal i _) = i
-ridOf (RVec  i _) = i
-
-type Adj s = STArray s Int (VS.Vector Double)
-
--- 発番カウンタ + backward クロージャ列 (prepend = 発番の逆順)。
-data Ctx s = Ctx !(STRef s Int) !(STRef s [Adj s -> ST s ()])
-
-fresh :: Ctx s -> ST s Int
-fresh (Ctx cnt _) = do
-  n <- readSTRef cnt
-  writeSTRef cnt (n + 1)
-  pure n
-
-record :: Ctx s -> (Adj s -> ST s ()) -> ST s ()
-record (Ctx _ bw) f = modifySTRef' bw (f :)
-
--- 随伴の加算 (空 = ゼロ扱い)。
-bumpA :: Adj s -> Int -> VS.Vector Double -> ST s ()
-bumpA adj i contrib = do
-  cur <- readArray adj i
-  writeArray adj i (if VS.null cur then contrib else VS.zipWith (+) cur contrib)
-
-readAdjS :: Adj s -> Int -> ST s Double
-readAdjS adj i = do
-  v <- readArray adj i
-  pure (if VS.null v then 0 else v VS.! 0)
-
--- leaf (theta)。 backward 無し・勾配は最終的にこの随伴を読む。
-inputVec :: Ctx s -> VS.Vector Double -> ST s Rval
-inputVec ctx v = do
-  i <- fresh ctx
-  pure (RVec i v)
-
--- 全長 l の vec から要素 i を取り出す (scalar 化)。
-idxHR :: Ctx s -> Int -> Int -> Rval -> ST s Rval
-idxHR ctx l i (RVec vid v) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    g <- readAdjS adj o
-    when (g /= 0) $ bumpA adj vid (VS.generate l (\j -> if j == i then g else 0))
-  pure (RScal o (v VS.! i))
-idxHR _ _ _ _ = error "idxHR: scalar input"
-
--- 全長 l の vec から [off, off+len) を切り出す。
-sliceHR :: Ctx s -> Int -> Int -> Int -> Rval -> ST s Rval
-sliceHR ctx l off len (RVec vid v) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    dy <- readArray adj o
-    when (not (VS.null dy)) $
-      bumpA adj vid (VS.generate l (\j -> if j >= off && j < off + len then dy VS.! (j - off) else 0))
-  pure (RVec o (VS.slice off len v))
-sliceHR _ _ _ _ _ = error "sliceHR: scalar input"
-
--- scalar * vector。
-scaleHR :: Ctx s -> Rval -> Rval -> ST s Rval
-scaleHR ctx (RScal kid k) (RVec vid v) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    dy <- readArray adj o
-    when (not (VS.null dy)) $ do
-      bumpA adj kid (VS.singleton (VS.sum (VS.zipWith (*) dy v)))
-      bumpA adj vid (VS.map (* k) dy)
-  pure (RVec o (VS.map (* k) v))
-scaleHR _ _ _ = error "scaleHR: shape"
-
-vaddHR :: Ctx s -> Rval -> Rval -> ST s Rval
-vaddHR ctx (RVec aid a) (RVec bid b) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    dy <- readArray adj o
-    when (not (VS.null dy)) $ do
-      bumpA adj aid dy
-      bumpA adj bid dy
-  pure (RVec o (VS.zipWith (+) a b))
-vaddHR _ _ _ = error "vaddHR: shape"
-
-vsubHR :: Ctx s -> Rval -> Rval -> ST s Rval
-vsubHR ctx (RVec aid a) (RVec bid b) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    dy <- readArray adj o
-    when (not (VS.null dy)) $ do
-      bumpA adj aid dy
-      bumpA adj bid (VS.map negate dy)
-  pure (RVec o (VS.zipWith (-) a b))
-vsubHR _ _ _ = error "vsubHR: shape"
-
-dotHR :: Ctx s -> Rval -> Rval -> ST s Rval
-dotHR ctx (RVec aid a) (RVec bid b) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    g <- readAdjS adj o
-    when (g /= 0) $ do
-      bumpA adj aid (VS.map (* g) b)
-      bumpA adj bid (VS.map (* g) a)
-  pure (RScal o (VS.sum (VS.zipWith (*) a b)))
-dotHR _ _ _ = error "dotHR: shape"
-
--- u[gids] gather (gids/nG 定数)。
-gatherHR :: Ctx s -> VU.Vector Int -> Int -> Rval -> ST s Rval
-gatherHR ctx gids nG (RVec uid u) = do
-  let n = VU.length gids
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    dy <- readArray adj o
-    when (not (VS.null dy)) $
-      bumpA adj uid (VS.convert $
-        VU.accumulate (+) (VU.replicate nG 0) (VU.zip gids (VU.convert dy :: VU.Vector Double)))
-  pure (RVec o (VS.generate n (\i -> u VS.! (gids VU.! i))))
-gatherHR _ _ _ _ = error "gatherHR: shape"
-
--- scalar 演算群。
-cstS :: Ctx s -> Double -> ST s Rval
-cstS ctx x = do { i <- fresh ctx; pure (RScal i x) }
-
-binS :: Ctx s -> (Double -> Double -> Double) -> (Double -> Double -> (Double, Double))
-     -> Rval -> Rval -> ST s Rval
-binS ctx f df (RScal aid a) (RScal bid b) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    g <- readAdjS adj o
-    when (g /= 0) $ do
-      let (da, db) = df a b
-      bumpA adj aid (VS.singleton (g * da))
-      bumpA adj bid (VS.singleton (g * db))
-  pure (RScal o (f a b))
-binS _ _ _ _ _ = error "binS: scalar expected"
-
-addS, mulS, subS :: Ctx s -> Rval -> Rval -> ST s Rval
-addS ctx = binS ctx (+) (\_ _ -> (1, 1))
-subS ctx = binS ctx (-) (\_ _ -> (1, -1))
-mulS ctx = binS ctx (*) (\a b -> (b, a))
-
-unS :: Ctx s -> (Double -> Double) -> (Double -> Double) -> Rval -> ST s Rval
-unS ctx f df (RScal aid a) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    g <- readAdjS adj o
-    when (g /= 0) $ bumpA adj aid (VS.singleton (g * df a))
-  pure (RScal o (f a))
-unS _ _ _ _ = error "unS: scalar expected"
-
-expS, logS :: Ctx s -> Rval -> ST s Rval
-expS ctx = unS ctx exp exp
-logS ctx = unS ctx log (\a -> 1 / a)
-
-mulConstS, addConstS :: Ctx s -> Double -> Rval -> ST s Rval
-mulConstS ctx c = unS ctx (* c) (const c)
-addConstS ctx c = unS ctx (+ c) (const 1)
-
--- 案B logp (logpBP と同じ式を B エンジンで構築)。 戻りは出力ノード。
-logpHR :: Ctx s -> Compiled -> Rval -> ST s Rval
-logpHR ctx c theta = do
-  let p = cP c; nG = cNG c; n = cN c
-      l = p + 1 + nG + 1
-  bVec   <- sliceHR ctx l 0 p theta
-  logTau <- idxHR ctx l p theta
-  uVec   <- sliceHR ctx l (p + 1) nG theta
-  logSig <- idxHR ctx l (p + 1 + nG) theta
-  tau <- expS ctx logTau
-  sig <- expS ctx logSig
-  -- Xβ = Σ_k β_k * col_k
-  xb <- do
-    cols <- mapM (\k -> do
-                    bk  <- idxHR ctx p k bVec
-                    col <- constVecM ctx (cXCols c !! k)
-                    scaleHR ctx bk col) [0 .. p - 1]
-    foldM1 (vaddHR ctx) cols
-  ug  <- gatherHR ctx (cGids c) nG uVec
-  eta <- vaddHR ctx xb ug
-  yC  <- constVecM ctx (cYs c)
-  r   <- vsubHR ctx yC eta
-  sumB2 <- dotHR ctx bVec bVec
-  sumU2 <- dotHR ctx uVec uVec
-  sumR2 <- dotHR ctx r r
-  let nD = fromIntegral n; pD = fromIntegral p; ngD = fromIntegral nG
-  -- priorB = constB - sumB2/50
-  priorB <- do { t <- mulConstS ctx (-1 / (2 * 25)) sumB2
-               ; addConstS ctx (negate (0.5 * pD * log (2 * pi)) - pD * log 5) t }
-  -- priorTau = constT - tau²/50 + logTau
-  priorTau <- do
-    tau2  <- mulS ctx tau tau
-    t1    <- mulConstS ctx (-1 / (2 * 25)) tau2
-    t2    <- addS ctx t1 logTau
-    addConstS ctx (0.5 * log (2 / pi) - log 5) t2
-  -- priorU = constU - nG*log tau - sumU2/(2τ²)
-  priorU <- do
-    lt    <- logS ctx tau
-    a1    <- mulConstS ctx (negate ngD) lt
-    tau2  <- mulS ctx tau tau
-    inv   <- mulConstS ctx (-0.5) =<< divByS ctx sumU2 tau2
-    s     <- addS ctx a1 inv
-    addConstS ctx (negate (0.5 * ngD * log (2 * pi))) s
-  -- priorSig = -sig + logSig
-  priorSig <- do { ns <- mulConstS ctx (-1) sig; addS ctx ns logSig }
-  -- loglik = constL - n*log sig - sumR2/(2σ²)
-  loglik <- do
-    ls    <- logS ctx sig
-    a1    <- mulConstS ctx (negate nD) ls
-    sig2  <- mulS ctx sig sig
-    inv   <- mulConstS ctx (-0.5) =<< divByS ctx sumR2 sig2
-    s     <- addS ctx a1 inv
-    addConstS ctx (negate (0.5 * nD * log (2 * pi))) s
-  -- 総和
-  s1 <- addS ctx priorB priorTau
-  s2 <- addS ctx s1 priorU
-  s3 <- addS ctx s2 priorSig
-  addS ctx s3 loglik
-  where
-    foldM1 _ []       = error "foldM1: empty"
-    foldM1 _ [x]      = pure x
-    foldM1 g (x:y:xs) = g x y >>= \z -> foldM1 g (z : xs)
-
--- 定数ベクトルノード (backward 無し)。
-constVecM :: Ctx s -> VS.Vector Double -> ST s Rval
-constVecM ctx v = do { i <- fresh ctx; pure (RVec i v) }
-
--- scalar 除算 (a/b)。
-divByS :: Ctx s -> Rval -> Rval -> ST s Rval
-divByS ctx = binS ctx (/) (\a b -> (1 / b, negate a / (b * b)))
-
-gradHandroll :: Compiled -> [Double] -> [Double]
-gradHandroll c theta = runST $ do
-  cnt <- newSTRef 0
-  bw  <- newSTRef []
-  let ctx = Ctx cnt bw
-  th  <- inputVec ctx (VS.fromList theta)
-  out <- logpHR ctx c th
-  n   <- readSTRef cnt
-  adj <- newArray (0, n - 1) VS.empty
-  writeArray adj (ridOf out) (VS.singleton 1)
-  closures <- readSTRef bw
-  mapM_ ($ adj) closures
-  g <- readArray adj (ridOf th)
-  pure (if VS.null g then replicate (length theta) 0 else VS.toList g)
-
--- ---------------------------------------------------------------------------
--- 中心差分 (検証用)
--- ---------------------------------------------------------------------------
-
-centralDiff :: ([Double] -> Double) -> [Double] -> [Double]
-centralDiff f ps =
-  [ let h = 1e-6 * (abs (ps !! j) + 1e-3)
-    in (f (bump j h) - f (bump j (-h))) / (2 * h)
-  | j <- [0 .. length ps - 1] ]
-  where bump j d = [ if k == j then p + d else p | (k, p) <- zip [0 ..] ps ]
-
-relErr :: [Double] -> [Double] -> Double
-relErr a b = maximum [ abs (x - y) / (abs y + 1e-6) | (x, y) <- zip a b ]
-
--- ---------------------------------------------------------------------------
--- main
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  putStrLn "=== Phase 54 専用ベクトル化 AD feasibility spike ===\n"
-  putStrLn "対象: 階層 Gaussian (random intercept、 M2 同型)。 obs/群=12。"
-  putStrLn "(a) ad Reverse.Double.grad  vs  (b) 手書きベクトル化解析勾配 (tape なし)\n"
-
-  putStrLn "--- (検証) 各勾配 vs 中心差分 (rel err) ---"
-  forM_ [2, 8, 32] $ \nG -> do
-    let pr  = genProb nG 12
-        c   = compile pr
-        t0  = theta0 pr
-        cd  = centralDiff (logp pr) t0
-        eV  = relErr (gradVec c t0) cd
-        eBP = relErr (gradBackprop c t0) cd
-        eHR = relErr (gradHandroll c t0) cd
-    printf "nG=%-3d p=%-3d | vec=%.3e | backprop=%.3e | handroll=%.3e\n"
-      nG (paramLen pr) eV eBP eHR
-
-  putStrLn "\n--- (デバッグ) nG=2 の成分比較 [央差 / ad / vec] ---"
-  let prD = genProb 2 12
-      cD  = compile prD
-      t0D = theta0 prD
-      cd  = centralDiff (logp prD) t0D
-      ga  = gradAD prD t0D
-      gv  = gradVec cD t0D
-  forM_ (zip3 [0 :: Int ..] (zip3 cd ga gv) (theta0 prD)) $ \(j, (a, b, v), _) ->
-    printf "  θ%-2d | cd=%10.4f | ad=%10.4f | vec=%10.4f\n" j a b v
-
-  putStrLn "\n--- per-grad 時間 (ms・median of 50) ---"
-  putStrLn "ad=現行スカラtape / vec=解析勾配(下限) / bp=backprop(案A) / hr=自作tape(案B)\n"
-  forM_ [2, 4, 8, 16, 32] $ \nG -> do
-    let pr = genProb nG 12
-        c  = compile pr
-        t0 = theta0 pr
-        probe = sum . map abs
-    (tA, _)  <- timeitIO 50 probe (\_ -> pure (gradAD pr t0))
-    (tB, _)  <- timeitIO 50 probe (\_ -> pure (gradVec c t0))
-    (tC, _)  <- timeitIO 50 probe (\_ -> pure (gradBackprop c t0))
-    (tD, _)  <- timeitIO 50 probe (\_ -> pure (gradHandroll c t0))
-    printf "nG=%-3d p=%-3d n=%-4d | ad=%7.4f | vec=%7.4f | bp=%7.4f | hr=%7.4f | ad/bp ×%.1f | ad/hr ×%.1f | hr/vec ×%.1f\n"
-      nG (paramLen pr) (nG * 12) tA tB tC tD (tA / tC) (tA / tD) (tD / tB)
-
-  putStrLn "\n(vec=tape-free 解析勾配=汎用ベクトル化 AD の時間下限。"
-  putStrLn " ad/bp・ad/hr = 案A・案B が現行 ad を何倍速くするか (判断ゲート: ≥5× で 54.4 本実装へ)。"
-  putStrLn " hr/vec = 案B が下限からどれだけ離れているか = 自前 tape のオーバヘッド)"
diff --git a/bench/haskell/BenchHBMVecIRProf.hs b/bench/haskell/BenchHBMVecIRProf.hs
deleted file mode 100644
--- a/bench/haskell/BenchHBMVecIRProf.hs
+++ /dev/null
@@ -1,283 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Phase 85.1: radon 相関モデルの gradVecIR per-eval 内訳プロファイル。
---
--- Phase 84 ベンチで radon (919 obs 相関階層) の per-eval が ~160µs =
--- XLA (numpyro) 比 ~5-10× 遅いと確定した。 本ベンチはその 160µs の内訳を
--- **成分分解で実測**する (推測するな計測せよ):
---
---   full compileGradUV closure per-eval
---   = pc 変換 (unconstrained→constrained・VS.generate)
---   + gradVecIR
---       = forwardArena (arena 確保 'VSM.unsafeNew' + forward 解釈)
---       + guard 検査 ('arenaGuardsOK')
---       + backward ('gradVecIRGo' = adj 確保 'VSM.replicate' + 逆伝播)
---   + 残差 prior の ad ('mPriorGrad'・radon で残るかも本ベンチで確定)
---   + chain rule (constrained→unconstrained・list ベース)
---
--- 各成分は下位経路 (forwardArena 単独・alloc 単独 等) を直接呼んで計測し、
--- 直接測れない成分 (guard・backward 純分) は差分で推定する。 併せて
--- 命令列 mix (命令種別 × 本数 × セル数) を静的に出す = forward/backward の
--- どこが重いか (gather / elementwise / Σ) の見当を付ける。
---
--- 実行: taskset -c 0 で 1 コア固定 (Phase 84 と同条件)。
---   cabal run bench-hbm-vecir-prof --project-file=cabal.project.plot -f benches
-module Main where
-
-import           Control.Monad                    (when)
-import           Control.Monad.ST                 (runST)
-import qualified Data.Map.Strict                  as Map
-import qualified Data.Set                         as Set
-import qualified Data.Text                        as T
-import qualified Data.Vector                      as BV
-import qualified Data.Vector.Storable             as VS
-import qualified Data.Vector.Storable.Mutable     as VSM
-import qualified Data.Vector.Unboxed              as VU
-import           Numeric.AD.Mode.Reverse.Double   (grad)
-import           Text.Printf                      (printf)
-
-import           Hanalyze.Model.HBM               (ModelP, sampleNames)
-import           Hanalyze.Fit                     (designHBMProgram)
-import           Hanalyze.Stat.Distribution       (Transform)
-import qualified Hanalyze.Model.HBM.Gradient      as G
-import qualified Hanalyze.Model.HBM.IR            as IR
-
-import           BenchUtil                        (timeitTastyIO)
-
--- ---------------------------------------------------------------------------
--- radon モデル (BenchHBMScaling と同一定義・同一 CSV)
--- ---------------------------------------------------------------------------
-
--- | Radon 生 CSV を読む (BenchHBMScaling.readRadon と同一)。
-readRadon :: IO ([[Double]], [Int], [Double], [Double])
-readRadon = do
-  txt <- readFile "bench/data/radon.csv"
-  let recs = map parseRow (drop 1 (lines txt))
-      parseRow ln = case splitComma ln of
-        (_c : ci : fl : lr : lu : _) ->
-          (read ci :: Int, read fl :: Double, read lr :: Double, read lu :: Double)
-        _ -> error ("readRadon: 列不足 " ++ ln)
-      cidx    = [ c | (c, _, _, _) <- recs ]
-      floors  = [ f | (_, f, _, _) <- recs ]
-      ys      = [ y | (_, _, y, _) <- recs ]
-      designX = [ [1.0, f, u] | (_, f, _, u) <- recs ]
-  return (designX, cidx, floors, ys)
-
-splitComma :: String -> [String]
-splitComma s = case break (== ',') s of
-  (a, ',' : rest) -> a : splitComma rest
-  (a, _)          -> [a]
-
--- | Radon 相関 varying intercept+slope (BenchHBMScaling.radonModel と同一)。
-radonModel :: [[Double]] -> [Int] -> [Double] -> [Double] -> ModelP ()
-radonModel designX cidx floorCol ys =
-  designHBMProgram designX ["(Intercept)", "floor", "uranium"]
-                   [(cidx, nCounties, [floorCol])] ys
-  where nCounties = if null cidx then 0 else maximum cidx + 1
-
--- ---------------------------------------------------------------------------
--- 残差 prior の ad (compileGradUV の mPriorGrad と同一構成)
--- ---------------------------------------------------------------------------
-
--- | 'G.compileGradUV' 内部の @grad (fExcl exclNames)@ を同一式で再現する
--- (radon の per-eval に残差 ad が乗っている場合、 その単独コストを測る)。
-residGrad
-  :: [[Double]] -> [Int] -> [Double] -> [Double]
-  -> [T.Text] -> [Transform] -> Set.Set T.Text
-  -> [Double] -> [Double]
-residGrad dX cidx fl ys names trans excl = grad f
-  where
-    f us =
-      let paramsC = Map.fromList
-            [ (n, G.invTransformF t u) | (n, t, u) <- zip3 names trans us ]
-          logJac  = sum [ G.logJacF t u | (t, u) <- zip trans us ]
-      in G.logJointExclBlocks excl (radonModel dX cidx fl ys) paramsC + logJac
-
--- ---------------------------------------------------------------------------
--- 命令列 mix (静的)
--- ---------------------------------------------------------------------------
-
--- | (命令種別, 本数, 総セル数 = Σ max 1 len)。 forward/backward の作業量の
--- 静的な見当 (gather / elementwise / Σ の比率)。
-instrMix :: IR.VecProgram -> [(String, Int, Int)]
-instrMix prog =
-  let instrs = BV.toList (IR.vpInstrs prog)
-      lens   = VU.toList (IR.vpLen prog)
-      keyOf ins = case ins of
-        IR.VIK{}     -> "VIK   (スカラ定数)"
-        IR.VIKV{}    -> "VIKV  (ベクトル定数)"
-        IR.VILeafS{} -> "VILeafS (scalar leaf)"
-        IR.VILeafV{} -> "VILeafV (vector leaf)"
-        IR.VIGath{}  -> "VIGath (gather)"
-        IR.VIUn{}    -> "VIUn  (elementwise 単項)"
-        IR.VIBin{}   -> "VIBin (elementwise 二項)"
-        IR.VISum{}   -> "VISum (Σ 縮約)"
-        IR.VIAxpy{}  -> "VIAxpy (a+s·v 融合)"
-        IR.VIAxpyC{} -> "VIAxpyC (a+s·const 融合)"
-        IR.VISumSqD{} -> "VISumSqD (Σ(x−m)² 融合)"
-        IR.VISumSqC{} -> "VISumSqC (Σ(c−m)² 融合)"
-        IR.VIMulG{}   -> "VIMulG (s·gather 融合)"
-        IR.VIAxpyG{}  -> "VIAxpyG (a+s·gather 融合)"
-        IR.VIMulVC{}  -> "VIMulVC (s·v⊙c 融合)"
-        IR.VISumSqC2{} -> "VISumSqC2 (Σ(c−m1−m2)² 融合)"
-      accum m (ins, l) =
-        Map.insertWith (\(c1, e1) (c2, e2) -> (c1 + c2, e1 + e2))
-          (keyOf ins) (1 :: Int, max 1 l) m
-      mixed = foldl accum Map.empty (zip instrs lens)
-  in [ (k, c, e) | (k, (c, e)) <- Map.toList mixed ]
-
--- ---------------------------------------------------------------------------
-
-usOf :: Double -> Double
-usOf ms = ms * 1000
-
-main :: IO ()
-main = do
-  putStrLn "=== Phase 85.1: radon gradVecIR per-eval 内訳プロファイル ==="
-  (dX, cidx, fl, ys) <- readRadon
-  let m :: ModelP ()
-      m = radonModel dX cidx fl ys
-      names  = sampleNames m
-      trans  = [ Map.findWithDefault (error "transform missing") n tmap
-               | n <- names ]
-      tmap   = G.getTransforms m
-      nP     = length names
-      nObs   = length ys
-
-  -- (a) vecIR 経路の compile (compileGradUV の IR branch と同一手順)
-  let (gbs, _) = G.gaussLMBlocksAuto m
-  when (not (null gbs)) $
-    putStrLn "★注意: gaussLMBlocksAuto が非空 = radon は IR branch でなく hybrid branch"
-  case IR.synthVecIR m of
-    Nothing -> error "synthVecIR = Nothing (radon が vecIR に乗っていない)"
-    Just (gs, fams, sObs) -> do
-      let ixOf   = Map.fromList (zip names [0 :: Int ..])
-          cvi    = IR.compileVecIR ixOf gs fams
-          prog   = IR.cvProg cvi
-          famSet = Set.fromList (concat [ ms | (ms, _, _) <- fams ])
-          cps    = G.constPriorsOf m famSet
-          exclNames = sObs `Set.union` famSet
-                      `Set.union` Set.fromList (map fst cps)
-          noResid = G.residualFreeOfDensity exclNames m
-          transB  = BV.fromList trans
-          sz      = IR.vpSize prog
-          objOff  = IR.vpOff prog `VU.unsafeIndex` IR.vpObj prog
-
-      -- ---- 静的サマリ ----
-      printf "obs=%d  nP=%d  instrs=%d  vpSize(arena セル)=%d  guards=%d\n"
-        nObs nP (BV.length (IR.vpInstrs prog)) sz (length (IR.vpGuards prog))
-      printf "residual ad (mPriorGrad): %s  (constPriors=%d, excl=%d/%d)\n"
-        (if noResid then "なし (noResid)" else "★あり = per-eval に ad が乗る" :: String)
-        (length cps) (Set.size exclNames) nP
-      putStrLn "\n--- 命令列 mix (種別 / 本数 / 総セル数) ---"
-      mapM_ (\(k, c, e) -> printf "  %-24s %5d 本  %8d セル\n" k c e)
-            (instrMix prog)
-
-      -- 85.3-ii: 実命令列 dump (superinstruction パターン選定用)
-      putStrLn "\n--- 命令列 listing (slot: 命令 [len]) ---"
-      BV.imapM_ (\i ins -> do
-        let l = IR.vpLen prog `VU.unsafeIndex` i
-            s = case ins of
-                  IR.VIK v        -> printf "VIK %.4g" v :: String
-                  IR.VIKV v       -> printf "VIKV (n=%d)" (VS.length v)
-                  IR.VILeafS p    -> printf "VILeafS p%d" p
-                  IR.VILeafV p    -> printf "VILeafV p%d" p
-                  IR.VIGath p _ n -> printf "VIGath p%d (n=%d)" p n
-                  IR.VIUn o x     -> printf "VIUn %s s%d" (show o) x
-                  IR.VIBin o x y  -> printf "VIBin %s s%d s%d" (show o) x y
-                  IR.VISum x      -> printf "VISum s%d" x
-                  IR.VIAxpy a sc v  -> printf "VIAxpy s%d s%d s%d" a sc v
-                  IR.VIAxpyC a sc c ->
-                    printf "VIAxpyC s%d s%d (n=%d)" a sc (VS.length c)
-                  IR.VISumSqD x m -> printf "VISumSqD s%d s%d" x m
-                  IR.VISumSqC c m ->
-                    printf "VISumSqC (n=%d) s%d" (VS.length c) m
-                  IR.VIMulG sc p _ n ->
-                    printf "VIMulG s%d gath(p%d,n=%d)" sc p n
-                  IR.VIAxpyG a sc p _ n ->
-                    printf "VIAxpyG s%d s%d gath(p%d,n=%d)" a sc p n
-                  IR.VIMulVC sc v c ->
-                    printf "VIMulVC s%d s%d (n=%d)" sc v (VS.length c)
-                  IR.VISumSqC2 c m1 m2 ->
-                    printf "VISumSqC2 (n=%d) s%d s%d" (VS.length c) m1 m2
-        printf "  s%-3d [%4d] %s\n" i l s) (IR.vpInstrs prog)
-      printf "  obj=s%d  guards=%s\n" (IR.vpObj prog)
-        (show [ sl | (_, sl) <- IR.vpGuards prog ])
-
-      -- ---- 計測点 (16 変種で CSE 回避・guard 域内) ----
-      let uvs = BV.fromList
-            [ VS.generate nP (\k -> 1e-3 * fromIntegral ((j * 31 + k) `mod` 17))
-            | j <- [0 :: Int .. 15] ]
-          pcOf uv = VS.generate nP $ \i ->
-            G.invTransformF (transB BV.! i) (uv `VS.unsafeIndex` i)
-          pcs = BV.map pcOf uvs
-          uvAt i = uvs BV.! (i `mod` 16)
-          pcAt i = pcs BV.! (i `mod` 16)
-      -- pcs を先に強制 (計測に混ぜない)
-      mapM_ (\pc -> pure $! VS.sum pc) (BV.toList pcs)
-      let v0 = IR.vecIRValue cvi (pcAt 0)
-      printf "\nvecIRValue @probe = %.6f (有限であること)\n" v0
-
-      -- ---- per-eval 計測 (tasty-bench 適応・µs) ----
-      let gv = G.compileGradUV m names trans
-      (tFull, _) <- timeitTastyIO id $ \i ->
-        pure $! VS.unsafeIndex (gv (uvAt i)) (i `mod` nP)
-      (tPc, _) <- timeitTastyIO id $ \i ->
-        pure $! VS.unsafeIndex (pcOf (uvAt i)) (i `mod` nP)
-      (tValue, _) <- timeitTastyIO id $ \i ->
-        pure $! IR.vecIRValue cvi (pcAt i)
-      (tGradIR, _) <- timeitTastyIO id $ \i ->
-        pure $! runST (do
-          mg <- VSM.replicate nP 0
-          ok <- IR.gradVecIR cvi (pcAt i) mg
-          if ok then VSM.unsafeRead mg (i `mod` nP) else pure (0 / 0))
-      (tForward, _) <- timeitTastyIO id $ \i ->
-        pure $! runST (do
-          ar <- IR.forwardArena cvi (pcAt i)
-          VSM.unsafeRead ar objOff)
-      (tFwGuard, _) <- timeitTastyIO id $ \i ->
-        pure $! runST (do
-          ar <- IR.forwardArena cvi (pcAt i)
-          ok <- IR.arenaGuardsOK prog ar
-          if ok then VSM.unsafeRead ar objOff else pure (0 / 0))
-      (tAllocFw, _) <- timeitTastyIO id $ \i ->
-        pure $! runST (do
-          ar <- VSM.unsafeNew sz
-          VSM.unsafeWrite ar 0 (fromIntegral i :: Double)
-          VSM.unsafeRead ar 0)
-      (tAllocAdj, _) <- timeitTastyIO id $ \i ->
-        pure $! runST (do
-          adj <- VSM.replicate sz (0 :: Double)
-          VSM.unsafeWrite adj 0 (fromIntegral i)
-          VSM.unsafeRead adj (sz - 1))
-      tResid <- if noResid
-        then pure Nothing
-        else do
-          let rg = residGrad dX cidx fl ys names trans exclNames
-          (t, _) <- timeitTastyIO id $ \i ->
-            pure $! sum (rg (VS.toList (uvAt i)))
-          pure (Just t)
-
-      -- ---- 内訳表 (µs・差分成分は推定) ----
-      let us = usOf
-          fwInterp  = tForward - tAllocFw
-          guardT    = tFwGuard - tForward
-          backward  = tGradIR - tFwGuard - tAllocAdj
-          accounted = tPc + tGradIR + maybe 0 id tResid
-          chainRest = tFull - accounted
-          pct t = 100 * t / tFull
-      putStrLn "\n--- per-eval 内訳 (µs・full 比 %) ---"
-      printf "full compileGradUV closure      : %9.2f µs (100.0%%)\n" (us tFull)
-      printf "├ pc 変換 (VS.generate)         : %9.2f µs (%5.1f%%)\n" (us tPc) (pct tPc)
-      printf "├ gradVecIR (fw+guard+bw)       : %9.2f µs (%5.1f%%)\n" (us tGradIR) (pct tGradIR)
-      printf "│  ├ forward arena 確保 (New)   : %9.2f µs (%5.1f%%)\n" (us tAllocFw) (pct tAllocFw)
-      printf "│  ├ forward 解釈 (差分)        : %9.2f µs (%5.1f%%)\n" (us fwInterp) (pct fwInterp)
-      printf "│  ├ guard 検査 (差分)          : %9.2f µs (%5.1f%%)\n" (us guardT) (pct guardT)
-      printf "│  ├ adj arena 確保 (replicate) : %9.2f µs (%5.1f%%)\n" (us tAllocAdj) (pct tAllocAdj)
-      printf "│  └ backward 逆伝播 (差分)     : %9.2f µs (%5.1f%%)\n" (us backward) (pct backward)
-      case tResid of
-        Nothing -> printf "├ 残差 prior ad                 : なし (noResid)\n"
-        Just t  -> printf "├ 残差 prior ad (grad fExcl)    : %9.2f µs (%5.1f%%)\n" (us t) (pct t)
-      printf "└ chain rule + 残り (差分)      : %9.2f µs (%5.1f%%)\n" (us chainRest) (pct chainRest)
-      printf "(参考) vecIRValue (値のみ)      : %9.2f µs (%5.1f%%)\n" (us tValue) (pct tValue)
diff --git a/bench/haskell/BenchHBMVecIRSpike.hs b/bench/haskell/BenchHBMVecIRSpike.hs
deleted file mode 100644
--- a/bench/haskell/BenchHBMVecIRSpike.hs
+++ /dev/null
@@ -1,353 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Phase 54.11 spike: 非線形 μ の「ベクトル式 IR」 feasibility (計測先行)。
---
--- 54.9 の prof で M5/M6 (非 affine μ) の負けは「per-obs スカラ AD」 帰着が
--- ~90% (logDensityObs ~52% + μ の AD 演算 ~25% + tape 管理 ~12%) と確定した。
--- 54.11 本実装 = 「ベクトル式 IR を構築する追跡 interpreter」 の前に、
--- **手組みの vec-tape (VecAD + 54.11 追加の elementwise op)** で M5/M6 の
--- 勾配カーネルがどこまで速いかを実測し、 ゲート (実経路 `gradADU` 比 ≥3×)
--- を判定する。
---
---   M5: μ_i = a·exp(-b·x_i) + c,  y_i ~ N(μ_i, σ)        (n=100, θ=4)
---   M6: μ_i = a_{g(i)}·exp(-b·x_i), a_g ~ N(μ_a, τ_a)    (n=96, nG=8, θ=12)
---
--- 比較 3 通り (全て同一の unconstrained 全勾配・中心差分/相互で検証後に計測):
---   (a)  RevD.grad (多相 logp 直書き)   — スカラ tape の下限 (walk 無し)
---   (a') HBM.gradADU (per-obs 手書き)   — 実経路 (Free walk + ad fallback) = NUTS が払う値
---   (b)  VecAD 手組み tape              — ベクトル式 IR 化の到達見込み (per-call 構築込み)
---
--- ⚠ (b) は手組み = IR 追跡 interpreter のオーバヘッドを含まない楽観側。
--- 「PyMC 同等」 とは言わない。 ゲート判定にのみ使う。
-module Main where
-
-import           Control.Monad                  (forM_)
-import           Control.Monad.ST               (ST)
-import           Data.List                      (foldl')
-import qualified Data.Map.Strict                as Map
-import qualified Data.Text                      as T
-import qualified Data.Vector                    as V
-import qualified Data.Vector.Storable           as VS
-import qualified Data.Vector.Unboxed            as VU
-import qualified System.Random.MWC              as MWC
-import           System.Random.MWC.Distributions (standard)
-import           Text.Printf                    (printf)
-
-import qualified Numeric.AD.Mode.Reverse.Double as RevD
-
-import           Hanalyze.Model.HBM             (Distribution (..), ModelP,
-                                                 sample, observe, gradADU,
-                                                 sampleNames, getTransforms)
-import           Hanalyze.Model.HBM.VecAD
-
-import           BenchUtil                      (timeitIO)
-
--- ---------------------------------------------------------------------------
--- データ (BenchHBMScaling と同一 DGP・seed)
--- ---------------------------------------------------------------------------
-
-normals :: Int -> Int -> IO [Double]
-normals seed k = do
-  g <- MWC.initialize (V.singleton (fromIntegral seed))
-  mapM (const (standard g)) [1 .. k]
-
-nM5 :: Int
-nM5 = 100
-
-genM5 :: IO ([Double], [Double])
-genM5 = do
-  let (a, b, c, s) = (2.5, 1.2, 0.5, 0.3)
-  ez <- normals 51 nM5
-  let xs = [ 3.0 * (fromIntegral i + 0.5) / fromIntegral nM5
-           | i <- [0 .. nM5 - 1] ]
-      ys = [ a * exp (negate b * x) + c + s * e | (x, e) <- zip xs ez ]
-  return (xs, ys)
-
-nGroups, perGroup :: Int
-nGroups  = 8
-perGroup = 12
-
-genM6 :: IO ([Double], [Int], [Double])
-genM6 = do
-  let (muA, tauA, b, s) = (2.0, 0.5, 1.0, 0.3)
-      n = nGroups * perGroup
-  ez <- normals 61 n
-  az <- normals 62 nGroups
-  let as   = [ muA + tauA * z | z <- az ]
-      gids = [ i `div` perGroup | i <- [0 .. n - 1] ]
-      xs   = [ 3.0 * (fromIntegral (i `mod` perGroup) + 0.5)
-                   / fromIntegral perGroup
-             | i <- [0 .. n - 1] ]
-      ys   = [ (as !! g) * exp (negate b * x) + s * e
-             | (x, g, e) <- zip3 xs gids ez ]
-  return (xs, gids, ys)
-
--- ---------------------------------------------------------------------------
--- モデル (BenchHBMScaling と同一・(a') gradADU 用)
--- ---------------------------------------------------------------------------
-
-m5Model :: [Double] -> [Double] -> ModelP ()
-m5Model xs ys = do
-  a <- sample "a" (Normal 0 10)
-  b <- sample "b" (HalfNormal 2)
-  c <- sample "c" (Normal 0 10)
-  s <- sample "sigma" (Exponential 1)
-  forM_ (zip3 [0 :: Int ..] xs ys) $ \(i, x, y) ->
-    observe (T.pack ("y_" ++ show i))
-      (Normal (a * exp (negate b * realToFrac x) + c) s) [y]
-
-m6Model :: [Double] -> [Int] -> [Double] -> ModelP ()
-m6Model xs gids ys = do
-  let nG = if null gids then 0 else maximum gids + 1
-  muA  <- sample "mu_a"  (Normal 0 10)
-  tauA <- sample "tau_a" (HalfNormal 2)
-  as   <- mapM (\j -> sample (T.pack ("a_" ++ show j)) (Normal muA tauA))
-               [0 .. nG - 1]
-  b    <- sample "b" (HalfNormal 2)
-  s    <- sample "sigma" (Exponential 1)
-  forM_ (zip3 [0 :: Int ..] (zip xs gids) ys) $ \(i, (x, g), y) ->
-    observe (T.pack ("y_" ++ show i))
-      (Normal ((as !! g) * exp (negate b * realToFrac x)) s) [y]
-
--- ---------------------------------------------------------------------------
--- (a) 多相 logp 直書き (RevD.grad の対象・unconstrained 全勾配)
--- ---------------------------------------------------------------------------
-
-logN :: Floating a => a -> a -> a -> a
-logN x m s = -0.5 * log (2 * pi) - log s - 0.5 * ((x - m) / s) ^ (2 :: Int)
-
-logHalfNormal :: Floating a => a -> a -> a
-logHalfNormal x s = 0.5 * log (2 / pi) - log s - 0.5 * (x / s) ^ (2 :: Int)
-
--- θ = [a, log b, c, log σ] (sampleNames 順・b/σ は PositiveT)。
-logp5 :: forall a. Floating a => [Double] -> [Double] -> [a] -> a
-logp5 xs ys [ua, ub, uc, us] =
-  let b   = exp ub
-      sig = exp us
-      pri = logN ua 0 10 + logHalfNormal b 2 + ub
-            + logN uc 0 10 + (negate sig + us)
-      ll  = sum [ logN (realToFrac y) (ua * exp (negate b * realToFrac x) + uc) sig
-                | (x, y) <- zip xs ys ]
-  in pri + ll
-logp5 _ _ _ = error "logp5: θ shape"
-
--- θ = [μ_a, log τ_a, a_0..a_{nG-1}, log b, log σ]。
-logp6 :: forall a. Floating a => [Double] -> [Int] -> [Double] -> [a] -> a
-logp6 xs gids ys theta =
-  let nG  = nGroups
-      ma  = theta !! 0
-      ut  = theta !! 1
-      as  = take nG (drop 2 theta)
-      ub  = theta !! (2 + nG)
-      us  = theta !! (3 + nG)
-      tau = exp ut
-      b   = exp ub
-      sig = exp us
-      pri = logN ma 0 10 + logHalfNormal tau 2 + ut
-            + sum [ logN aj ma tau | aj <- as ]
-            + logHalfNormal b 2 + ub + (negate sig + us)
-      ll  = sum [ logN (realToFrac y) ((as !! g) * exp (negate b * realToFrac x)) sig
-                | (x, g, y) <- zip3 xs gids ys ]
-  in pri + ll
-
--- ---------------------------------------------------------------------------
--- (b) VecAD 手組み tape (per-call 構築込み)
--- ---------------------------------------------------------------------------
-
-negS :: Ctx s -> Rval -> ST s Rval
-negS ctx = mulConstS ctx (-1)
-
--- | M5 の unconstrained 全勾配 (θ=4)。
-gradVec5 :: VS.Vector Double -> VS.Vector Double -> [Double] -> [Double]
-gradVec5 xC yC [ua0, ub0, uc0, us0] =
-  let n  = VS.length xC
-      gs = runTape $ \ctx -> do
-        ua <- inputScal ctx ua0
-        ub <- inputScal ctx ub0
-        uc <- inputScal ctx uc0
-        us <- inputScal ctx us0
-        b   <- expS ctx ub
-        sig <- expS ctx us
-        xs  <- constVec ctx xC
-        ys  <- constVec ctx yC
-        nb  <- negS ctx b
-        t1  <- scaleHR ctx nb xs            -- -b·x
-        t2  <- vexpHR ctx t1                -- exp(-b·x)
-        t3  <- scaleHR ctx ua t2            -- a·exp(-b·x)
-        mu  <- bcastAddHR ctx uc t3         -- + c
-        r   <- vsubHR ctx ys mu
-        sr2 <- dotHR ctx r r
-        -- loglik = -n/2·log2π - n·logσ - sr2/(2σ²)   (logσ = us)
-        s2  <- mulS ctx sig sig
-        den <- mulConstS ctx 2 s2
-        q   <- divByS ctx sr2 den
-        nls <- mulConstS ctx (fromIntegral n) us
-        ll0 <- addS ctx nls q               -- n·logσ + sr2/(2σ²)
-        ll  <- mulConstS ctx (-1) ll0
-        -- priors: a,c ~ N(0,10); b ~ HalfNormal 2 (+jac ub); σ ~ Exp 1 (+jac us)
-        aa  <- mulS ctx ua ua
-        pa  <- mulConstS ctx (negate (1 / 200)) aa
-        cc  <- mulS ctx uc uc
-        pc  <- mulConstS ctx (negate (1 / 200)) cc
-        bb  <- mulS ctx b b
-        pb0 <- mulConstS ctx (negate (1 / 8)) bb
-        pb  <- addS ctx pb0 ub
-        nsg <- negS ctx sig
-        ps  <- addS ctx nsg us
-        tot <- foldAddS ctx [ll, pa, pc, pb, ps]
-        pure (tot, [ua, ub, uc, us])
-  in map VS.head gs
-gradVec5 _ _ _ = error "gradVec5: θ shape"
-
--- | M6 の unconstrained 全勾配 (θ = 4 + nG)。
-gradVec6 :: VS.Vector Double -> VU.Vector Int -> VS.Vector Double
-         -> [Double] -> [Double]
-gradVec6 xC gids yC theta =
-  let n   = VS.length xC
-      nG  = nGroups
-      ma0 = theta !! 0
-      ut0 = theta !! 1
-      as0 = VS.fromList (take nG (drop 2 theta))
-      ub0 = theta !! (2 + nG)
-      us0 = theta !! (3 + nG)
-      gs = runTape $ \ctx -> do
-        ma  <- inputScal ctx ma0
-        ut  <- inputScal ctx ut0
-        av  <- inputVec ctx as0
-        ub  <- inputScal ctx ub0
-        us  <- inputScal ctx us0
-        tau <- expS ctx ut
-        b   <- expS ctx ub
-        sig <- expS ctx us
-        xs  <- constVec ctx xC
-        ys  <- constVec ctx yC
-        nb  <- negS ctx b
-        t1  <- scaleHR ctx nb xs
-        t2  <- vexpHR ctx t1                -- exp(-b·x)
-        ag  <- gatherHR ctx gids nG av      -- a_{g(i)}
-        mu  <- hadamardHR ctx ag t2         -- a_g·exp(-b·x)
-        r   <- vsubHR ctx ys mu
-        sr2 <- dotHR ctx r r
-        s2  <- mulS ctx sig sig
-        den <- mulConstS ctx 2 s2
-        q   <- divByS ctx sr2 den
-        nls <- mulConstS ctx (fromIntegral n) us
-        ll0 <- addS ctx nls q
-        ll  <- mulConstS ctx (-1) ll0
-        -- prior a_j ~ N(μ_a, τ): -nG·logτ - Σ(a_j-μ_a)²/(2τ²)   (logτ = ut)
-        zsC <- constVec ctx (VS.replicate nG 0)
-        mab <- bcastAddHR ctx ma zsC        -- μ_a broadcast (長さ nG)
-        ra  <- vsubHR ctx av mab
-        sra <- dotHR ctx ra ra
-        t2a <- mulS ctx tau tau
-        dna <- mulConstS ctx 2 t2a
-        qa  <- divByS ctx sra dna
-        nlt <- mulConstS ctx (fromIntegral nG) ut
-        pa0 <- addS ctx nlt qa
-        pa  <- mulConstS ctx (-1) pa0
-        -- μ_a ~ N(0,10); τ_a ~ HalfNormal 2 (+jac ut); b ~ HalfNormal 2 (+jac ub);
-        -- σ ~ Exp 1 (+jac us)
-        mm  <- mulS ctx ma ma
-        pm  <- mulConstS ctx (negate (1 / 200)) mm
-        tt  <- mulS ctx tau tau
-        pt0 <- mulConstS ctx (negate (1 / 8)) tt
-        pt  <- addS ctx pt0 ut
-        bb  <- mulS ctx b b
-        pb0 <- mulConstS ctx (negate (1 / 8)) bb
-        pb  <- addS ctx pb0 ub
-        nsg <- negS ctx sig
-        ps  <- addS ctx nsg us
-        tot <- foldAddS ctx [ll, pa, pm, pt, pb, ps]
-        pure (tot, [ma, ut, av, ub, us])
-  in case gs of
-       [gma, gut, gav, gub, gus] ->
-         VS.head gma : VS.head gut : VS.toList gav ++ [VS.head gub, VS.head gus]
-       _ -> error "gradVec6: leaf shape"
-
--- | スカラノード列を addS で畳む。
-foldAddS :: Ctx s -> [Rval] -> ST s Rval
-foldAddS _   []       = error "foldAddS: empty"
-foldAddS _   [x]      = pure x
-foldAddS ctx (x:y:xs) = addS ctx x y >>= \z -> foldAddS ctx (z : xs)
-
--- ---------------------------------------------------------------------------
--- 検証 + 計測
--- ---------------------------------------------------------------------------
-
-closeVec :: Double -> [Double] -> [Double] -> Bool
-closeVec tol u v =
-  length u == length v
-  && and [ abs (a - b) <= tol * (1 + max (abs a) (abs b)) | (a, b) <- zip u v ]
-
-centralDiff :: ([Double] -> Double) -> [Double] -> [Double]
-centralDiff f th =
-  [ (f (bump i h) - f (bump i (negate h))) / (2 * h) | i <- [0 .. length th - 1] ]
-  where
-    h = 1e-5
-    bump i d = [ if j == i then t + d else t | (j, t) <- zip [0 ..] th ]
-
--- K 回の勾配呼出を 1 計測にまとめる (1 call ~0.1ms 級のタイマ分解能対策)。
-benchGrad :: String -> Int -> ([Double] -> [Double]) -> [Double] -> IO Double
-benchGrad tag k f th0 = do
-  let run i = pure $! foldl' (\ !acc j ->
-                 let th = [ t + 1e-9 * fromIntegral (i + j) | t <- th0 ]
-                 in acc + sum (f th)) 0 [1 .. k]
-  (ms, _) <- timeitIO 7 id run
-  let per = ms / fromIntegral k
-  printf "  %-28s %8.4f ms/grad (%d calls median)\n" tag per k
-  pure per
-
-main :: IO ()
-main = do
-  putStrLn "== Phase 54.11 spike: 非線形 μ の vec-tape (手組み IR) =="
-  (x5, y5)     <- genM5
-  (x6, g6, y6) <- genM6
-
-  -- ---- M5 ----
-  let m5 :: ModelP ()
-      m5 = m5Model x5 y5
-      n5names = sampleNames m5
-      n5trans = [ getTransforms m5 Map.! nm | nm <- n5names ]
-      th5  = [0.8, log 0.9, 0.3, log 0.4]
-      x5C  = VS.fromList x5
-      y5C  = VS.fromList y5
-      gAd5  = RevD.grad (logp5 x5 y5) th5
-      gAdu5 = gradADU m5 n5names n5trans th5
-      gVec5 = gradVec5 x5C y5C th5
-      gCd5  = centralDiff (logp5 x5 y5) th5
-  putStrLn "M5 検証 (RevD / gradADU / vec-tape / 中心差分):"
-  printf "  RevD vs gradADU: %s\n" (show (closeVec 1e-9 gAd5 gAdu5))
-  printf "  vec  vs RevD:    %s\n" (show (closeVec 1e-9 gVec5 gAd5))
-  printf "  vec  vs 中心差分: %s\n" (show (closeVec 1e-4 gVec5 gCd5))
-  putStrLn "M5 計測:"
-  pa5  <- benchGrad "(a)  RevD.grad (logp 直書き)" 200 (RevD.grad (logp5 x5 y5)) th5
-  pa5' <- benchGrad "(a') gradADU (実経路 walk+ad)" 200 (gradADU m5 n5names n5trans) th5
-  pb5  <- benchGrad "(b)  vec-tape 手組み" 200 (gradVec5 x5C y5C) th5
-  printf "  → (a')/(b) = %.1fx / (a)/(b) = %.1fx (ゲート ≥3×)\n\n"
-    (pa5' / pb5) (pa5 / pb5)
-
-  -- ---- M6 ----
-  let m6 :: ModelP ()
-      m6 = m6Model x6 g6 y6
-      n6names = sampleNames m6
-      n6trans = [ getTransforms m6 Map.! nm | nm <- n6names ]
-      th6  = [1.5, log 0.6] ++ replicate nGroups 1.8 ++ [log 0.9, log 0.4]
-      x6C  = VS.fromList x6
-      y6C  = VS.fromList y6
-      g6U  = VU.fromList g6
-      gAd6  = RevD.grad (logp6 x6 g6 y6) th6
-      gAdu6 = gradADU m6 n6names n6trans th6
-      gVec6 = gradVec6 x6C g6U y6C th6
-      gCd6  = centralDiff (logp6 x6 g6 y6) th6
-  putStrLn "M6 検証 (RevD / gradADU / vec-tape / 中心差分):"
-  printf "  RevD vs gradADU: %s\n" (show (closeVec 1e-9 gAd6 gAdu6))
-  printf "  vec  vs RevD:    %s\n" (show (closeVec 1e-9 gVec6 gAd6))
-  printf "  vec  vs 中心差分: %s\n" (show (closeVec 1e-4 gVec6 gCd6))
-  putStrLn "M6 計測:"
-  pa6  <- benchGrad "(a)  RevD.grad (logp 直書き)" 200 (RevD.grad (logp6 x6 g6 y6)) th6
-  pa6' <- benchGrad "(a') gradADU (実経路 walk+ad)" 200 (gradADU m6 n6names n6trans) th6
-  pb6  <- benchGrad "(b)  vec-tape 手組み" 200 (gradVec6 x6C g6U y6C) th6
-  printf "  → (a')/(b) = %.1fx / (a)/(b) = %.1fx (ゲート ≥3×)\n"
-    (pa6' / pb6) (pa6 / pb6)
diff --git a/bench/haskell/BenchHBMVecSpike.hs b/bench/haskell/BenchHBMVecSpike.hs
deleted file mode 100644
--- a/bench/haskell/BenchHBMVecSpike.hs
+++ /dev/null
@@ -1,177 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Phase 54.0 feasibility spike (計測先行・推測するな計測せよ)。
---
--- Phase 54 の本実装に入る前に、 2 つの不確実点を実測で確かめる小実験:
---
---   (Q1) AD over Vector の勾配保存:
---        `Numeric.AD.Mode.Reverse.Double.grad` が、 観測尤度を
---        ① list 内包 (現状の obsLogSum 形)・② 非ボックス Vector 上の fold・
---        ③ 十分統計量による fused 閉形式 (O(1)) で書いた log-density に対し、
---        いずれも **中心差分と一致する勾配** を返すか。
---        → 一致すれば 54.2 (観測尤度ベクトル化) の AD 前提が成立。
---
---   (Q2) ベクトル化/融合の per-grad 改善率:
---        ①②③ の 1 勾配あたり時間を `timeitIO` で計測し、 scalar 版 (①) 比の
---        改善率を出す。 ③ が桁で速ければ 54.2 の「vector-mean observe →
---        fused 配列 log-density」 の利得見込みを定量化できる。
---
--- 対象は Gaussian 線形回帰 y_i ~ Normal(a + b x_i, σ) (= m1Model 相当)。
--- σ は unconstrained u = log σ で持ち、 prior は a,b~Normal(0,10)・σ~Exp(1)
--- (HBM の logJointUnconstrained と同じ「prior + jacobian + 観測和」 構造)。
---
--- ★この spike は HBM 本体を一切いじらない。 観測和の 3 表現が AD で同値かつ
---   どれだけ速いかだけを切り出して測る独立実験。
-module Main where
-
-import           Control.Monad                  (forM_)
-import qualified Data.Vector.Storable           as VS
-import           Text.Printf                    (printf)
-
-import qualified Numeric.AD.Mode.Reverse.Double as RevD
-
-import           BenchUtil                      (timeitIO)
-
--- ---------------------------------------------------------------------------
--- 対数尤度 3 表現 (param = [a, b, u], σ = exp u)
--- ---------------------------------------------------------------------------
-
--- 共通の prior + jacobian (3 表現で同一)。
-logPriorPart :: Floating a => a -> a -> a -> a
-logPriorPart a b u =
-  let s          = exp u
-      lnNormal mu sig x = -0.5 * log (2 * pi) - log sig
-                          - 0.5 * ((x - mu) / sig) ^ (2 :: Int)
-      priorA     = lnNormal 0 10 a
-      priorB     = lnNormal 0 10 b
-      -- σ ~ Exponential 1: logDensity = log 1 - 1*σ = -σ。 jacobian dσ/du = σ → +u
-      priorSigma = (-s) + u
-  in priorA + priorB + priorSigma
-
--- 観測の 1 項 (Normal): -0.5 log(2π) - log s - 0.5 ((y - μ)/s)^2
-obsTerm :: Floating a => a -> a -> a -> Double -> Double -> a
-obsTerm a b s x y =
-  let mu = a + b * realToFrac x
-  in -0.5 * log (2 * pi) - log s
-     - 0.5 * ((realToFrac y - mu) / s) ^ (2 :: Int)
-{-# INLINE obsTerm #-}
-
--- ① scalar list 内包 (現状 obsLogSum と同じ形)。
-logLikScalar :: Floating a => [Double] -> [Double] -> [a] -> a
-logLikScalar xs ys ps =
-  let (a : b : u : _) = ps
-      s = exp u
-  in logPriorPart a b u
-     + sum [ obsTerm a b s x y | (x, y) <- zip xs ys ]
-
--- ② 非ボックス Storable Vector 上の手動 fold (list alloc を排除)。
---    データは VS.Vector Double (unboxed)、 累算器のみ AD スカラ (boxed)。
-logLikVec :: Floating a => VS.Vector Double -> VS.Vector Double -> [a] -> a
-logLikVec xs ys ps =
-  let (a : b : u : _) = ps
-      s = exp u
-      n = VS.length xs
-      go !acc i
-        | i >= n    = acc
-        | otherwise = go (acc + obsTerm a b s (xs `VS.unsafeIndex` i)
-                                              (ys `VS.unsafeIndex` i)) (i + 1)
-  in logPriorPart a b u + go 0 0
-
--- ③ 十分統計量による fused 閉形式 (O(1) per eval)。
---    Σ_i (y_i - a - b x_i)^2 = Syy - 2a Sy - 2b Sxy + n a^2 + 2ab Sx + b^2 Sxx
---    の 6 つの和は Double 定数として 1 回だけ前計算 → eval は a,b,s の多項式。
-data SuffStat = SuffStat
-  { ssN :: !Double, ssSx :: !Double, ssSy :: !Double
-  , ssSxx :: !Double, ssSxy :: !Double, ssSyy :: !Double }
-
-mkSuffStat :: [Double] -> [Double] -> SuffStat
-mkSuffStat xs ys = SuffStat
-  { ssN   = fromIntegral (length xs)
-  , ssSx  = sum xs
-  , ssSy  = sum ys
-  , ssSxx = sum (map (\x -> x * x) xs)
-  , ssSxy = sum (zipWith (*) xs ys)
-  , ssSyy = sum (map (\y -> y * y) ys)
-  }
-
-logLikFused :: Floating a => SuffStat -> [a] -> a
-logLikFused ss ps =
-  let (a : b : u : _) = ps
-      s  = exp u
-      n  = realToFrac (ssN ss)
-      sx = realToFrac (ssSx ss); sy = realToFrac (ssSy ss)
-      sxx = realToFrac (ssSxx ss); sxy = realToFrac (ssSxy ss)
-      syy = realToFrac (ssSyy ss)
-      -- Σ resid^2 を展開した閉形式
-      sse = syy - 2 * a * sy - 2 * b * sxy
-            + n * a * a + 2 * a * b * sx + b * b * sxx
-      obsSum = n * (-0.5 * log (2 * pi) - log s) - 0.5 / (s * s) * sse
-  in logPriorPart a b u + obsSum
-
--- ---------------------------------------------------------------------------
--- 中心差分 (ground truth)
--- ---------------------------------------------------------------------------
-
-centralDiff :: ([Double] -> Double) -> [Double] -> [Double]
-centralDiff f ps =
-  [ let h    = 1e-6 * (abs (ps !! j) + 1e-3)
-        plus = f (bump j h)
-        minu = f (bump j (-h))
-    in (plus - minu) / (2 * h)
-  | j <- [0 .. length ps - 1] ]
-  where bump j d = [ if k == j then p + d else p | (k, p) <- zip [0 ..] ps ]
-
-relErr :: [Double] -> [Double] -> Double
-relErr g1 g2 = maximum
-  [ abs (x - y) / (abs y + 1e-8) | (x, y) <- zip g1 g2 ]
-
--- ---------------------------------------------------------------------------
--- データ生成 (BenchHBMADModes と同形)
--- ---------------------------------------------------------------------------
-
-genData :: Int -> ([Double], [Double])
-genData n =
-  let xs = [ 2.0 * sin (0.7 * fromIntegral i) | i <- [0 .. n - 1] ]
-      ys = [ 2.0 + 1.5 * x + 0.3 * cos (1.3 * fromIntegral i)
-           | (i, x) <- zip [0 :: Int ..] xs ]
-  in (xs, ys)
-
--- ---------------------------------------------------------------------------
--- main
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  putStrLn "=== Phase 54.0 feasibility spike: 観測尤度ベクトル化 × Reverse.Double ===\n"
-  let ps0 = [1.8, 1.4, log 0.35]   -- [a, b, u=log σ] (真値近傍)
-
-  putStrLn "--- (Q1) 勾配の数値一致 (RevD.grad vs 中心差分・rel err) ---"
-  forM_ [50, 200, 1000] $ \n -> do
-    let (xs, ys) = genData n
-        xv = VS.fromList xs; yv = VS.fromList ys
-        ss = mkSuffStat xs ys
-        gScalar = RevD.grad (logLikScalar xs ys) ps0
-        gVec    = RevD.grad (logLikVec xv yv)    ps0
-        gFused  = RevD.grad (logLikFused ss)     ps0
-        gCD     = centralDiff (logLikScalar xs ys) ps0
-    printf "n=%-5d | scalar=%.3e | vec=%.3e | fused=%.3e (各 vs 中心差分)\n"
-      n (relErr gScalar gCD) (relErr gVec gCD) (relErr gFused gCD)
-    -- AD 同士の一致も確認 (3 表現が同一勾配か)
-    printf "         | vec-vs-scalar=%.3e | fused-vs-scalar=%.3e (AD 同士)\n"
-      (relErr gVec gScalar) (relErr gFused gScalar)
-
-  putStrLn "\n--- (Q2) per-grad 時間 (ms・median of 50) と scalar 比 ---"
-  forM_ [50, 200, 1000, 5000] $ \n -> do
-    let (xs, ys) = genData n
-        xv = VS.fromList xs; yv = VS.fromList ys
-        ss = mkSuffStat xs ys
-        probe = sum . map abs
-    (tS, _) <- timeitIO 50 probe (\_ -> pure (RevD.grad (logLikScalar xs ys) ps0))
-    (tV, _) <- timeitIO 50 probe (\_ -> pure (RevD.grad (logLikVec xv yv)    ps0))
-    (tF, _) <- timeitIO 50 probe (\_ -> pure (RevD.grad (logLikFused ss)     ps0))
-    printf "n=%-5d | scalar=%8.4f | vec=%8.4f (×%.2f) | fused=%8.4f (×%.1f)\n"
-      n tS tV (tS / tV) tF (tS / tF)
-
-  putStrLn "\n(×N = scalar 比の速度向上。 fused は O(1) ゆえ n 増で差が拡大する想定)"
diff --git a/bench/haskell/BenchKernel.hs b/bench/haskell/BenchKernel.hs
deleted file mode 100644
--- a/bench/haskell/BenchKernel.hs
+++ /dev/null
@@ -1,213 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Kernel / GP benchmarks (B2).
-
-module Main where
-
-import qualified Numeric.LinearAlgebra   as LA
-import qualified System.Random.MWC       as MWC
-import qualified Data.Vector             as V
-
-import qualified Hanalyze.Model.KernelRegression            as Kn
-import qualified Hanalyze.Model.GP                as GP
-import qualified Hanalyze.Model.RFF               as RFF
-import qualified Hanalyze.Model.GPRobust          as GPR
-
-import           BenchUtil
-
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  rows <- mconcat <$> sequence
-    [ benchGram     "bench/data/kernel_n500_p5.csv"  "GramMV_n500_p5"
-    , benchGram     "bench/data/kernel_n1000_p5.csv" "GramMV_n1000_p5"
-    , benchGram     "bench/data/kernel_n2000_p5.csv" "GramMV_n2000_p5"
-    , benchGram     "bench/data/kernel_n4000_p5.csv" "GramMV_n4000_p5"
-    , benchKR       "bench/data/kernel_n500_p5.csv"  "KR_n500_p5"
-    , benchKR       "bench/data/kernel_n1000_p5.csv" "KR_n1000_p5"
-    , benchKR       "bench/data/kernel_n2000_p5.csv" "KR_n2000_p5"
-    , benchKR       "bench/data/kernel_n4000_p5.csv" "KR_n4000_p5"
-    , benchNW       "bench/data/kernel_n1000_p5.csv" "NW_n1000_p5"
-    , benchRFF      "bench/data/kernel_n1000_p5.csv" "RFF_n1000_D256_p5"  256
-    , benchRFF      "bench/data/kernel_n2000_p5.csv" "RFF_n2000_D256_p5"  256
-    , benchGPFit    "bench/data/kernel_n500_p5.csv"  "GP_fit_n500_p5"
-    , benchGPFit    "bench/data/kernel_n1000_p5.csv" "GP_fit_n1000_p5"
-    , benchGPFit    "bench/data/kernel_n2000_p5.csv" "GP_fit_n2000_p5"
-    , benchGPOpt    "bench/data/kernel_n500_p5.csv"  "GP_opt_n500_p5"
-    , benchGPRobust "bench/data/kernel_n500_p5.csv"  "GPRobust_n500_p5"
-    ]
-  writeRows "bench/results/haskell/kernel.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/kernel.csv"
-
--- ---------------------------------------------------------------------------
--- 共通設定: Gaussian RBF, h = 1.0, λ = 1e-3
-h0 :: Double
-h0 = 1.0
-
-lam0 :: Double
-lam0 = 1e-3
-
--- ---------------------------------------------------------------------------
--- Gram matrix (BLAS pairwise dist + cmap)
--- ---------------------------------------------------------------------------
-
-{-# NOINLINE gramPhantom #-}
-gramPhantom :: Int -> Kn.Kernel -> Double
-            -> LA.Matrix Double -> LA.Matrix Double
-gramPhantom _ k h x = Kn.gramMatrixMV k h x
-
-benchGram :: FilePath -> String -> IO [BenchRow]
-benchGram path name = do
-  (x, _) <- readCsvXY path
-  (ms, g) <- timeitTastyIO LA.sumElements
-               (\i -> return $! gramPhantom i Kn.Gaussian h0 x)
-  return [ BenchRow "haskell" "kernel" name ms 0 0
-            ("gramMatrixMV BLAS, n=" ++ show (LA.rows g)) ]
-
--- ---------------------------------------------------------------------------
--- Kernel Ridge fit (multi-input)
--- ---------------------------------------------------------------------------
-
-{-# NOINLINE krPhantom #-}
-krPhantom :: Int -> LA.Matrix Double -> LA.Matrix Double -> Kn.KernelRidgeFitMV
-krPhantom _ x ym = Kn.kernelRidgeMV Kn.Gaussian h0 lam0 x ym
-
-benchKR :: FilePath -> String -> IO [BenchRow]
-benchKR path name = do
-  (x, y) <- readCsvXY path
-  let yMat = LA.asColumn y
-  (ms, fit) <- timeitTastyIO (\f -> LA.sumElements (Kn.krmvAlpha f))
-                 (\i -> return $! krPhantom i x yMat)
-  let yhat = LA.flatten (Kn.fittedKernelRidgeMV fit LA.¿ [0])
-      r2v  = computeR2 y yhat
-  return [ BenchRow "haskell" "kernel" name ms r2v
-                     (sqrt (LA.sumElements ((y - yhat) ** 2)
-                            / fromIntegral (LA.size y)))
-                     ("kernelRidgeMV Gaussian h=1 λ=1e-3") ]
-
--- ---------------------------------------------------------------------------
--- Nadaraya-Watson
--- ---------------------------------------------------------------------------
-
-{-# NOINLINE nwPhantom #-}
-nwPhantom :: Int -> LA.Matrix Double -> LA.Matrix Double -> LA.Matrix Double
-nwPhantom _ x ym = Kn.nwRegressionMV Kn.Gaussian h0 x ym x
-
-benchNW :: FilePath -> String -> IO [BenchRow]
-benchNW path name = do
-  (x, y) <- readCsvXY path
-  let yMat = LA.asColumn y
-  (ms, yhatMat) <- timeitTastyIO LA.sumElements
-                     (\i -> return $! nwPhantom i x yMat)
-  let yhat = LA.flatten (yhatMat LA.¿ [0])
-      r2v  = computeR2 y yhat
-  return [ BenchRow "haskell" "kernel" name ms r2v
-                     (sqrt (LA.sumElements ((y - yhat) ** 2)
-                            / fromIntegral (LA.size y)))
-                     "nwRegressionMV Gaussian h=1" ]
-
--- ---------------------------------------------------------------------------
--- RFF Ridge (multivariate input)
--- ---------------------------------------------------------------------------
-
-{-# NOINLINE rffPhantom #-}
-rffPhantom :: Int -> RFF.RFFFeaturesMV -> LA.Matrix Double
-           -> LA.Matrix Double -> RFF.RFFRidgeFitMVMO
-rffPhantom _ feats x ym = RFF.rffRidgeMVMulti feats x ym lam0
-
-benchRFF :: FilePath -> String -> Int -> IO [BenchRow]
-benchRFF path name d = do
-  (x, y) <- readCsvXY path
-  let ym = LA.asColumn y
-      p  = LA.cols x
-  gen <- MWC.createSystemRandom
-  feats <- RFF.sampleRFFRBFMV p d 1.0 1.0 gen
-  (ms, _) <- timeitTastyIO (\f -> LA.sumElements (RFF.rffrmvmWeights f))
-                (\i -> return $! rffPhantom i feats x ym)
-  let yhatMat = RFF.predictRFFRidgeMVMulti
-                  (rffPhantom 0 feats x ym) x
-      yhat = LA.flatten (yhatMat LA.¿ [0])
-      r2v  = computeR2 y yhat
-  return [ BenchRow "haskell" "kernel" name ms r2v
-                     (sqrt (LA.sumElements ((y - yhat) ** 2)
-                            / fromIntegral (LA.size y)))
-                     ("RFFFeaturesMV D=" ++ show d) ]
-
--- ---------------------------------------------------------------------------
--- GP fit (HP fixed)
--- ---------------------------------------------------------------------------
-
-{-# NOINLINE gpFitPhantom #-}
-gpFitPhantom :: Int -> GP.GPModel
-             -> LA.Matrix Double -> LA.Vector Double -> LA.Matrix Double
-             -> GP.GPResultMV
-gpFitPhantom _ mdl x y t = GP.fitGPMV mdl x y t
-
-benchGPFit :: FilePath -> String -> IO [BenchRow]
-benchGPFit path name = do
-  (x, y) <- readCsvXY path
-  let mdl = GP.GPModel GP.RBF (GP.GPParams 1.0 1.0 0.05 1.0 Nothing)
-  (ms, res) <- timeitTastyIO (\r -> LA.sumElements (GP.gpmvMean r)
-                                + LA.sumElements (GP.gpmvVar r))
-                 (\i -> return $! gpFitPhantom i mdl x y x)
-  let yhat = GP.gpmvMean res
-      r2v  = computeR2 y yhat
-  return [ BenchRow "haskell" "kernel" name ms r2v
-                     (sqrt (LA.sumElements ((y - yhat) ** 2)
-                            / fromIntegral (LA.size y)))
-                     "fitGPMV RBF (HP fixed)" ]
-
--- ---------------------------------------------------------------------------
--- GP HP optimization (L-BFGS over log marginal likelihood)
--- ---------------------------------------------------------------------------
-
-{-# NOINLINE gpOptPhantom #-}
-gpOptPhantom :: Int -> LA.Matrix Double -> LA.Vector Double -> GP.GPParams
-gpOptPhantom _ x y = GP.optimizeGPMV GP.RBF x y
-                       (GP.GPParams 0.5 1.0 0.05 1.0 Nothing)
-
-benchGPOpt :: FilePath -> String -> IO [BenchRow]
-benchGPOpt path name = do
-  (x, y) <- readCsvXY path
-  (ms, p) <- timeitTastyIO (\pr -> GP.gpLengthScale pr + GP.gpSignalVar pr
-                              + GP.gpNoiseVar pr)
-               (\i -> return $! gpOptPhantom i x y)
-  let mdl = GP.GPModel GP.RBF p
-      res = GP.fitGPMV mdl x y x
-      yhat = GP.gpmvMean res
-      r2v  = computeR2 y yhat
-  return [ BenchRow "haskell" "kernel" name ms r2v (GP.gpLengthScale p)
-                     "optimizeGPMV (L-BFGS / log marginal likelihood)" ]
-
--- ---------------------------------------------------------------------------
--- GPRobust IRLS (Student-t)
--- ---------------------------------------------------------------------------
-
-{-# NOINLINE gprPhantom #-}
-gprPhantom :: Int -> LA.Matrix Double -> LA.Vector Double
-           -> GPR.RobustGPFitMV
-gprPhantom _ x y = GPR.fitGPRobustMV GP.RBF
-                      (GP.GPParams 1.0 1.0 0.05 1.0 Nothing)
-                      (GPR.RStudentT 4.0 0.1)
-                      x y
-
-benchGPRobust :: FilePath -> String -> IO [BenchRow]
-benchGPRobust path name = do
-  (x, y) <- readCsvXY path
-  (ms, fit) <- timeitTastyIO (\f -> LA.sumElements (GPR.rgpmvAlpha f))
-                 (\i -> return $! gprPhantom i x y)
-  let (mu, _) = GPR.predictGPRobustMV fit x
-      r2v    = computeR2 y mu
-  return [ BenchRow "haskell" "kernel" name ms r2v (fromIntegral (GPR.rgpmvIters fit))
-                     "fitGPRobustMV StudentT(4, 0.1)" ]
-
--- ---------------------------------------------------------------------------
-
-computeR2 :: LA.Vector Double -> LA.Vector Double -> Double
-computeR2 y yhat =
-  let mu  = LA.sumElements y / fromIntegral (LA.size y)
-      sst = LA.sumElements ((y - LA.konst mu (LA.size y)) ** 2)
-      sse = LA.sumElements ((y - yhat) ** 2)
-  in if sst == 0 then 0 else 1 - sse / sst
diff --git a/bench/haskell/BenchM2Iso.hs b/bench/haskell/BenchM2Iso.hs
deleted file mode 100644
--- a/bench/haskell/BenchM2Iso.hs
+++ /dev/null
@@ -1,89 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Phase 85.5: M2 (random intercept) 単独 NUTS の A/B 計測ドライバ。
---
--- Phase 85.3 の回帰ガードで M2 の wall が Phase 84 基準比 ~1.5× 悪化して
--- 3 run 再現した (M3-M8 は非回帰・per-eval A/B は +10-18% のみ・iter 増で
--- 悪化率上昇 = GC/heap の疑い)。 本ドライバは BenchHBMScaling の M2 を
--- データ・init・config 込みで単独再現し、 `-rtsopts` 付きでビルドして
--- `+RTS -s` の alloc / GC / MUT 時間を旧 lib (worktree) と直接比較する。
---
---   cabal run bench-m2-iso -f benches -- 1600 +RTS -s
-module Main where
-
-import           Control.Monad                    (forM_)
-import qualified Data.Map.Strict                  as Map
-import qualified Data.Text                        as T
-import qualified Data.Vector                      as V
-import qualified System.Random.MWC                as MWC
-import           System.Random.MWC.Distributions  (standard)
-import           System.Environment               (getArgs)
-import           Text.Printf                      (printf)
-
-import           Hanalyze.Model.HBM               (ModelP, glmmRandomIntercept,
-                                                   GlmmFamily (..))
-import           Hanalyze.MCMC.NUTS               (NUTSConfig (..),
-                                                   defaultNUTSConfig, nuts)
-import           Hanalyze.MCMC.Core               (Chain, posteriorMean)
-
-import           BenchUtil                        (timeitIO)
-
--- BenchHBMScaling と同一の M2 DGP (8 群 × 12 = 96 obs・決定的)
-nGroups, perGroup :: Int
-nGroups  = 8
-perGroup = 12
-
-normals :: Int -> Int -> IO [Double]
-normals seed k = do
-  g <- MWC.initialize (V.singleton (fromIntegral seed))
-  mapM (const (standard g)) [1 .. k]
-
-genM2Data :: IO ([[Double]], [Int], [Double])
-genM2Data = do
-  let (b0, b1, tauU, s) = (1.0, 0.8, 1.5, 1.0)
-      n = nGroups * perGroup
-  xz <- normals 21 n
-  ez <- normals 22 n
-  uz <- normals 23 nGroups
-  let us   = map (* tauU) uz
-      gids = [ i `div` perGroup | i <- [0 .. n - 1] ]
-      xs   = map (* 2.0) xz
-      ys   = [ b0 + b1 * x + (us !! g) + s * e
-             | (x, g, e) <- zip3 xs gids ez ]
-      xRows = [ [1.0, x] | x <- xs ]
-  return (xRows, gids, ys)
-
-mkConfig :: Int -> NUTSConfig
-mkConfig iters = defaultNUTSConfig
-  { nutsIterations    = iters
-  , nutsBurnIn        = 500
-  , nutsStepSize      = 0.1
-  , nutsMaxDepth      = 10
-  , nutsAdaptStepSize = True
-  , nutsTargetAccept  = 0.8
-  , nutsAdaptMass     = True
-  }
-
-main :: IO ()
-main = do
-  args <- getArgs
-  let iters = case args of
-        (a : _) -> read a
-        _       -> 1600
-  (xr, gids, ys) <- genM2Data
-  let m2 :: ModelP ()
-      m2 = glmmRandomIntercept GlmmGaussian xr gids ys
-      names = ["beta_0", "beta_1", "tau_u", "sigma"]
-              ++ [ T.pack ("u_" ++ show j) | j <- [0 .. nGroups - 1] ]
-      initP = Map.fromList $
-        [ ("beta_0", 1.0), ("beta_1", 0.8), ("tau_u", 1.5), ("sigma", 1.0) ]
-        ++ [ (T.pack ("u_" ++ show j), 0.0) | j <- [0 .. nGroups - 1] ]
-      probe ch = sum [ maybe 0 id (posteriorMean p ch) | p <- names ]
-      run :: Int -> IO Chain
-      run i = do
-        g <- MWC.initialize (V.singleton (fromIntegral (42 + i)))
-        nuts m2 (mkConfig iters) initP g
-  (ms, ch) <- timeitIO 5 probe run
-  printf "M2_iso iter=%d warmup=500 reps=5 median=%.1f ms  beta_1=%.4f\n"
-    iters ms (maybe 0 id (posteriorMean "beta_1" ch))
diff --git a/bench/haskell/BenchMCMCB7.hs b/bench/haskell/BenchMCMCB7.hs
deleted file mode 100644
--- a/bench/haskell/BenchMCMCB7.hs
+++ /dev/null
@@ -1,147 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | MCMC benchmarks (B7).
---
--- Compares hanalyze's MCMC.{HMC, NUTS} against PyMC / blackjax /
--- numpyro on a shared 8-schools-style hierarchical normal model.
--- Outputs the unified BenchRow CSV at @bench/results/haskell/mcmc.csv@.
---
--- Model:
---   mu      ~ Normal(0, 100)
---   tau     ~ Exponential(0.1)
---   theta_j ~ Normal(mu, tau)         (j = 1..3)
---   y_ij    ~ Normal(theta_j, sigma=5)
---
--- Iterations: warmup=500, samples=1000 (single chain, deterministic
--- starting point) — chosen so the bench finishes in seconds.
-module Main where
-
-import qualified Data.Map.Strict         as Map
-import qualified Data.Text               as T
-import qualified System.Random.MWC       as MWC
-
-import           Hanalyze.Model.HBM               (Distribution (..), ModelP, sample,
-                                          observe)
-import           Hanalyze.MCMC.Core               (Chain, chainAccepted, chainTotal,
-                                          chainVals, posteriorMean,
-                                          posteriorSD)
-import           Hanalyze.MCMC.HMC                (HMCConfig (..), defaultHMCConfig, hmc)
-import           Hanalyze.MCMC.NUTS               (NUTSConfig (..), defaultNUTSConfig,
-                                          nuts)
-import           Hanalyze.Stat.MCMC               (ess)
-
-import           BenchUtil
-
--- ---------------------------------------------------------------------------
--- Shared model
--- ---------------------------------------------------------------------------
-
-schoolData :: [[Double]]
-schoolData =
-  [ [72, 68, 75, 71]
-  , [85, 88, 82, 90]
-  , [61, 65, 58, 63]
-  ]
-
-sigmaY :: Double
-sigmaY = 5.0
-
-schoolModel :: ModelP ()
-schoolModel = do
-  mu  <- sample "mu"  (Normal 0 100)
-  tau <- sample "tau" (Exponential 0.1)
-  mapM_ (\(j, ys) -> do
-    theta <- sample (T.pack ("theta_" ++ show (j :: Int)))
-                    (Normal mu tau)
-    observe (T.pack ("y_" ++ show j))
-            (Normal theta (realToFrac sigmaY)) ys)
-    (zip [1 ..] schoolData)
-
-initParams :: Map.Map T.Text Double
-initParams = Map.fromList
-  [ ("mu",      73.0)
-  , ("tau",     10.0)
-  , ("theta_1", 71.5)
-  , ("theta_2", 86.25)
-  , ("theta_3", 61.75)
-  ]
-
-paramNames :: [T.Text]
-paramNames = ["mu", "tau", "theta_1", "theta_2", "theta_3"]
-
--- Probe forces the full chain by summing posterior means + SDs.
-probeChain :: Chain -> Double
-probeChain ch =
-  sum [ maybe 0 id (posteriorMean p ch)
-      + maybe 0 id (posteriorSD   p ch)
-      | p <- paramNames ]
-
-acceptRate :: Chain -> Double
-acceptRate ch =
-  fromIntegral (chainAccepted ch) / max 1 (fromIntegral (chainTotal ch))
-
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  rows <- mconcat <$> sequence
-    [ benchHMC  "HMC_8schools_warm500_n1000"
-    , benchNUTS "NUTS_8schools_warm500_n1000"
-    ]
-  writeRows "bench/results/haskell/mcmc.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/mcmc.csv"
-
--- ---------------------------------------------------------------------------
--- HMC
--- ---------------------------------------------------------------------------
-
-benchHMC :: String -> IO [BenchRow]
-benchHMC name = do
-  let cfg = defaultHMCConfig
-              { hmcIterations    = 1000
-              , hmcBurnIn        = 500
-              , hmcStepSize      = 0.05
-              , hmcLeapfrogSteps = 30
-              }
-      run :: Int -> IO Chain
-      run _ = do
-        g <- MWC.create
-        hmc schoolModel cfg initParams g
-  (ms, ch) <- timeitTastyIO probeChain run
-  let muEss  = ess (chainVals "mu"  ch)
-      tauEss = ess (chainVals "tau" ch)
-      muMean = maybe 0 id (posteriorMean "mu" ch)
-      acc    = acceptRate ch
-  return [ BenchRow "haskell" "mcmc" name ms muMean muEss
-            ("HMC eps=0.05 L=30 accept=" ++ show acc
-             ++ " ess(mu)=" ++ show muEss
-             ++ " ess(tau)=" ++ show tauEss) ]
-
--- ---------------------------------------------------------------------------
--- NUTS
--- ---------------------------------------------------------------------------
-
-benchNUTS :: String -> IO [BenchRow]
-benchNUTS name = do
-  let cfg = defaultNUTSConfig
-              { nutsIterations    = 1000
-              , nutsBurnIn        = 500
-              , nutsStepSize      = 0.08
-              , nutsAdaptStepSize = True
-              , nutsAdaptMass     = True   -- B11: Stan-style multi-window
-              }
-      run :: Int -> IO Chain
-      run _ = do
-        g <- MWC.create
-        nuts schoolModel cfg initParams g
-  (ms, ch) <- timeitTastyIO probeChain run
-  let muEss  = ess (chainVals "mu"  ch)
-      tauEss = ess (chainVals "tau" ch)
-      muMean = maybe 0 id (posteriorMean "mu" ch)
-      acc    = acceptRate ch
-  return [ BenchRow "haskell" "mcmc" name ms muMean muEss
-            ("NUTS eps=0.08 dual-averaging mass-adapt accept=" ++ show acc
-             ++ " ess(mu)=" ++ show muEss
-             ++ " ess(tau)=" ++ show tauEss) ]
diff --git a/bench/haskell/BenchMCMCDiag.hs b/bench/haskell/BenchMCMCDiag.hs
deleted file mode 100644
--- a/bench/haskell/BenchMCMCDiag.hs
+++ /dev/null
@@ -1,148 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Diagnostic runner for the B7 MCMC bench (Phase B10b investigation).
---
--- Runs hanalyze NUTS on the 8-schools model with progressively
--- different configurations to localise the cause of poor ESS:
---
---   * default          : nutsStepSize=0.08, adapt=on (current)
---   * smaller-step     : nutsStepSize=0.02, adapt=off
---   * longer-warmup    : burnin 2000 (more dual-averaging)
---   * deeper-tree      : nutsMaxDepth=12 (default 10)
---
--- For each, prints accept rate, ESS(mu), ESS(tau), n samples > 1
--- distinct.
-module Main where
-
-import qualified Data.Map.Strict      as Map
-import qualified Data.Text            as T
-import qualified System.Random.MWC    as MWC
-import qualified Data.Time.Clock      as Time
-import           System.IO            (hSetBuffering, stdout, BufferMode (..))
-import           Text.Printf          (printf)
-
-import           Hanalyze.Model.HBM            (Distribution (..), ModelP, sample,
-                                       observe)
-import           Hanalyze.MCMC.Core            (Chain, chainAccepted, chainTotal,
-                                       chainVals, posteriorMean,
-                                       posteriorSD)
-import           Hanalyze.MCMC.NUTS            (NUTSConfig (..), defaultNUTSConfig,
-                                       nuts)
-import           Hanalyze.Stat.MCMC            (ess)
-
-schoolData :: [[Double]]
-schoolData =
-  [ [72, 68, 75, 71]
-  , [85, 88, 82, 90]
-  , [61, 65, 58, 63]
-  ]
-
-sigmaY :: Double
-sigmaY = 5.0
-
-schoolModel :: ModelP ()
-schoolModel = do
-  mu  <- sample "mu"  (Normal 0 100)
-  tau <- sample "tau" (Exponential 0.1)
-  mapM_ (\(j, ys) -> do
-    theta <- sample (T.pack ("theta_" ++ show (j :: Int)))
-                    (Normal mu tau)
-    observe (T.pack ("y_" ++ show j))
-            (Normal theta (realToFrac sigmaY)) ys)
-    (zip [1 ..] schoolData)
-
-initParams :: Map.Map T.Text Double
-initParams = Map.fromList
-  [ ("mu",      73.0)
-  , ("tau",     10.0)
-  , ("theta_1", 71.5)
-  , ("theta_2", 86.25)
-  , ("theta_3", 61.75)
-  ]
-
-runOne :: String -> NUTSConfig -> IO ()
-runOne label cfg = do
-  g <- MWC.create
-  t0 <- Time.getCurrentTime
-  ch <- nuts schoolModel cfg initParams g
-  t1 <- Time.getCurrentTime
-  let dt = realToFrac (Time.diffUTCTime t1 t0) :: Double
-  reportChain label dt ch
-
-reportChain :: String -> Double -> Chain -> IO ()
-reportChain label dt ch = do
-  let muV    = chainVals "mu"  ch
-      tauV   = chainVals "tau" ch
-      muEss  = ess muV
-      tauEss = ess tauV
-      acc    = fromIntegral (chainAccepted ch)
-             / max 1 (fromIntegral (chainTotal ch)) :: Double
-      muMean = maybe 0 id (posteriorMean "mu" ch)
-      muSD   = maybe 0 id (posteriorSD   "mu" ch)
-      tauMean = maybe 0 id (posteriorMean "tau" ch)
-      muDistinct  = length (uniq muV)
-      tauDistinct = length (uniq tauV)
-      uniq xs = go xs []
-        where go [] acc' = reverse acc'
-              go (x:xs') acc'
-                | x `elem` acc' = go xs' acc'
-                | otherwise     = go xs' (x:acc')
-  printf "=== %s ===\n" label
-  printf "  time         %.2f s\n" dt
-  printf "  accept       %.3f\n" acc
-  printf "  mu  mean=%.3f sd=%.3f ess=%.1f distinct=%d\n"
-    muMean muSD muEss muDistinct
-  printf "  tau mean=%.3f                ess=%.1f distinct=%d\n"
-    tauMean tauEss tauDistinct
-  putStrLn ""
-
-main :: IO ()
-main = do
-  hSetBuffering stdout LineBuffering
-  putStrLn "================================================="
-  putStrLn "  NUTS on 8-schools — diagnostic (B10b)"
-  putStrLn "  Goal: explain ESS(mu)=42 vs blackjax ess=810"
-  putStrLn "================================================="
-  putStrLn ""
-
-  let baseCfg = defaultNUTSConfig
-        { nutsIterations = 1000
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.08
-        , nutsMaxDepth   = 10
-        , nutsAdaptStepSize = True
-        , nutsTargetAccept  = 0.8
-        }
-
-  -- Reduced iterations for fast diagnostic. ESS scales linearly with
-  -- iterations so ratios stay informative.
-  let cfg = baseCfg { nutsIterations = 200, nutsBurnIn = 100 }
-
-  -- 1. baseline
-  runOne "baseline (eps=0.08, adapt=on)" cfg
-
-  -- 2. small step, no adapt
-  runOne "small-step (eps=0.02, adapt=off)"
-    cfg { nutsStepSize = 0.02, nutsAdaptStepSize = False }
-
-  -- 3. high target accept (forces smaller eps)
-  runOne "high-target (eps=0.08, target=0.95)"
-    cfg { nutsTargetAccept = 0.95 }
-
-  -- 4. shallower tree (limit search space)
-  runOne "shallow-tree (maxDepth=5)"
-    cfg { nutsMaxDepth = 5 }
-
-  -- 5. full-size 1000 samples WITH diagonal mass-matrix adaptation (B11)
-  runOne "full-size (1000 samples, mass adapt ON)" baseCfg
-    { nutsIterations = 1000, nutsBurnIn = 500, nutsAdaptMass = True }
-
-  -- 6. full-size with mass adapt OFF (baseline for comparison)
-  runOne "full-size (1000 samples, mass adapt OFF)" baseCfg
-    { nutsIterations = 1000, nutsBurnIn = 500, nutsAdaptMass = False }
-
-  -- 7. mass adapt ON, longer warmup (2000) — does multi-window
-  -- adaptation eventually converge given enough budget?
-  runOne "long-warmup (1000 samples, warmup=2000, mass ON)" baseCfg
-    { nutsIterations = 1000, nutsBurnIn = 2000, nutsAdaptMass = True }
diff --git a/bench/haskell/BenchMCMCExtras.hs b/bench/haskell/BenchMCMCExtras.hs
deleted file mode 100644
--- a/bench/haskell/BenchMCMCExtras.hs
+++ /dev/null
@@ -1,169 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | B7 残: Gibbs / ADVI / WAIC ベンチ。bench-mcmc-b7 (HMC/NUTS) の続編。
---
---   * Gibbs Beta-Binomial conjugate sampling, 10k iter
---   * ADVI on a small logistic regression posterior, 500 iter
---   * WAIC / PSIS-LOO on a (S=1000, N=200) log-likelihood matrix
---
--- 出力: bench/results/haskell/mcmc_extras.csv
-module Main where
-
-import qualified Data.Map.Strict        as Map
-import qualified Data.Text              as T
-import qualified System.Random.MWC      as MWC
-
-import           Hanalyze.Model.HBM              (Distribution (..), ModelP, sample,
-                                         observe)
-import           Hanalyze.MCMC.Core              (Chain, posteriorMean)
-import           Hanalyze.MCMC.Gibbs             (betaBinomial, gibbs,
-                                         gibbsBetaBinomial,
-                                         defaultGibbsConfig, GibbsConfig (..))
-import           Hanalyze.Stat.VI                (advi, defaultVIConfig, VIConfig (..),
-                                         VIResult (..))
-import           Hanalyze.Stat.ModelSelect       (waic, WAICResult (..),
-                                         loo, LOOResult (..))
-
-import           BenchUtil
-
--- ---------------------------------------------------------------------------
--- Gibbs: Beta-Binomial conjugate, 10k iterations.
--- ---------------------------------------------------------------------------
-
-benchGibbsBB :: IO [BenchRow]
-benchGibbsBB = do
-  let cfg = defaultGibbsConfig
-              { gibbsIterations = 10000
-              , gibbsBurnIn     = 0
-              }
-      -- Beta(2,2) prior × Binomial(20, p) with k=12 successes.
-      run :: Int -> IO Chain
-      run _ = do
-        g <- MWC.create
-        -- P37: specialised batched conjugate sampler.
-        -- Equivalent to @gibbs [betaBinomial "p" 2 2 20 12] cfg ...@
-        -- but skips the per-iter Map.insert / IORef / list-cons
-        -- (~0.56 ms total at n=10000) since Beta-Binomial draws are
-        -- i.i.d. — no chain dependency to maintain.
-        gibbsBetaBinomial "p" 2 2 20 12 cfg g
-      probe ch = maybe 0 id (posteriorMean "p" ch)
-  (ms, ch) <- timeitTastyIO probe run
-  let mu = probe ch
-  return [ BenchRow "haskell" "mcmc_extras"
-            "Gibbs_BetaBinomial_n10000" ms mu 0
-            ("Beta(2,2) | Binom(20,12); analytic E[p]=" ++ show ((2+12)/(2+2+20)::Double)) ]
-
--- ---------------------------------------------------------------------------
--- ADVI: 2D logistic regression posterior. 500 Adam iterations × 5 MC samples.
--- ---------------------------------------------------------------------------
-
-logisticData :: ([Double], [Double], [Double])
-logisticData =
-  -- 100 observations, true (β0, β1) = (-0.5, 1.2). Generated with seed = 0.
-  let n :: Int
-      n   = 60
-      xs  = [ 0.1 * fromIntegral i - 3.0 | i <- [0 .. n - 1] ]
-      lin = map (\x -> -0.5 + 1.2 * x) xs
-      probs = map (\z -> 1 / (1 + exp (-z))) lin
-      -- Deterministic 0/1 from prob > 0.5 (avoids RNG dependency in bench).
-      ys  = map (\p -> if p > 0.5 then 1 else 0) probs
-  in (xs, ys, probs)
-
-logisticModel :: ModelP ()
-logisticModel = do
-  beta0 <- sample "beta0" (Normal 0 5)
-  beta1 <- sample "beta1" (Normal 0 5)
-  let (xs, ys, _) = logisticData
-      logits = [ beta0 + beta1 * realToFrac x | x <- xs ]
-      probs  = [ 1 / (1 + exp (-z)) | z <- logits ]
-  mapM_ (\(i, (p, y)) ->
-            observe (T.pack ("y_" ++ show (i :: Int)))
-                    (Bernoulli p) [y])
-        (zip [0 ..] (zip probs ys))
-
-benchADVI :: IO [BenchRow]
-benchADVI = do
-  let cfg = defaultVIConfig
-              { viIterations   = 500
-              , viSamples      = 5
-              , viLearningRate = 0.05
-              , viNumDraws     = 200
-              }
-      run :: Int -> IO VIResult
-      run _ = do
-        g <- MWC.create
-        advi logisticModel cfg
-             (Map.fromList [("beta0", 0), ("beta1", 0)]) g
-      probe r =
-        maybe 0 id (Map.lookup "beta1" (viPostMeans r))
-  (ms, r) <- timeitTastyIO probe run
-  let b0  = maybe 0 id (Map.lookup "beta0" (viPostMeans r))
-      b1  = maybe 0 id (Map.lookup "beta1" (viPostMeans r))
-      lastElbo = case viElboHistory r of
-                   [] -> 0
-                   xs -> last xs
-  return [ BenchRow "haskell" "mcmc_extras"
-            "ADVI_logistic_n60_iter500" ms b1 b0
-            ("ADVI mean-field 500 iter; ELBO=" ++ show lastElbo
-             ++ " beta0=" ++ show b0 ++ " beta1=" ++ show b1) ]
-
--- ---------------------------------------------------------------------------
--- WAIC / LOO: synthetic log-lik matrix (S=1000, N=200).
--- ---------------------------------------------------------------------------
-
--- Generate a deterministic log-likelihood matrix that *roughly* mimics the
--- output of a Bayesian linear regression with mild dispersion across draws.
--- The values are stable across runs (no RNG) so we can compare WAIC across
--- implementations.
-makeLogLikMat :: Int -> Int -> [[Double]]
-makeLogLikMat s n =
-  [ [ baseLL i + 0.05 * sin (fromIntegral (i + j))
-        + 0.02 * cos (fromIntegral (3 * i + 7 * j))
-    | i <- [0 .. n - 1] ]
-  | j <- [0 .. s - 1] ]
-  where
-    baseLL i = -0.5 * (fromIntegral i / fromIntegral n - 0.5) ** 2 - 1.0
-
-benchWAIC :: IO [BenchRow]
-benchWAIC = do
-  let s    = 1000
-      n    = 200
-      ll   = makeLogLikMat s n
-      runW :: Int -> IO WAICResult
-      runW _ = return (waic ll)
-      probeW r = waicValue r
-  (ms, r) <- timeitTastyIO probeW runW
-  return [ BenchRow "haskell" "mcmc_extras"
-            "WAIC_S1000_N200" ms (waicValue r) (waicSE r)
-            ("lppd=" ++ show (waicLppd r)
-             ++ " p_waic=" ++ show (waicPwaic r)) ]
-
-benchLOO :: IO [BenchRow]
-benchLOO = do
-  let s    = 1000
-      n    = 200
-      ll   = makeLogLikMat s n
-      runL :: Int -> IO LOOResult
-      runL _ = return (loo ll)
-      probeL r = looValue r
-  (ms, r) <- timeitTastyIO probeL runL
-  return [ BenchRow "haskell" "mcmc_extras"
-            "LOO_PSIS_S1000_N200" ms (looValue r)
-            (fromIntegral (looKHatBad r))
-            ("elpd=" ++ show (looElpd r)
-             ++ " bad_k(>0.7)=" ++ show (looKHatBad r)) ]
-
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  rows <- mconcat <$> sequence
-    [ benchGibbsBB
-    , benchADVI
-    , benchWAIC
-    , benchLOO
-    ]
-  writeRows "bench/results/haskell/mcmc_extras.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/mcmc_extras.csv"
diff --git a/bench/haskell/BenchML.hs b/bench/haskell/BenchML.hs
deleted file mode 100644
--- a/bench/haskell/BenchML.hs
+++ /dev/null
@@ -1,143 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Classical-ML benchmarks (B6).
---
--- Compares hanalyze's Model.{PCA, Cluster, DecisionTree, RandomForest}
--- against scikit-learn on shared CSV inputs:
---
---   PCA       : lm_n10000_p50.csv (X only), 5 components
---   KMeans    : kernel_n2000_p5.csv (X only), k=5
---   DT / RF   : logistic_n10000_p20.csv (binary y), p=20
---
--- Outputs the unified BenchRow CSV at @bench/results/haskell/ml.csv@.
-module Main where
-
-import qualified Data.Vector             as V
-import qualified Numeric.LinearAlgebra   as LA
-import qualified System.Random.MWC       as MWC
-
-import qualified Hanalyze.Model.PCA               as PCA
-import qualified Hanalyze.Model.Cluster           as Cl
-import qualified Hanalyze.Model.DecisionTree      as DT
-import qualified Hanalyze.Model.RandomForest      as RF
-
-import           BenchUtil
-
--- ---------------------------------------------------------------------------
--- Phantom wrappers (defeat CSE across iterations)
--- ---------------------------------------------------------------------------
-
-{-# NOINLINE pcaPhantom #-}
-pcaPhantom :: Int -> Int -> LA.Matrix Double -> PCA.PCAResult
-pcaPhantom _ k x = PCA.pca PCA.CenterScale (Just k) x
-
-{-# NOINLINE kmeansPhantom #-}
-kmeansPhantom :: Int -> Int -> LA.Matrix Double -> MWC.GenIO -> IO Cl.KMeansResult
-kmeansPhantom _ k x gen = Cl.kMeans (Cl.defaultKMeansConfig k) x gen
-
-{-# NOINLINE dtPhantom #-}
-dtPhantom :: Int -> [[Double]] -> [Int] -> DT.DTree
-dtPhantom _ xs ys = DT.fitDT DT.defaultDTConfig xs ys
-
-{-# NOINLINE rfPhantom #-}
-rfPhantom :: Int -> [[Double]] -> [Double] -> MWC.GenIO -> IO RF.RandomForest
-rfPhantom _ xs ys gen =
-  RF.fitRF RF.defaultRFConfig { RF.rfTrees = 20 } xs ys gen
-
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  rows <- mconcat <$> sequence
-    [ benchPCA      "bench/data/lm_n10000_p50.csv"       "PCA_n10000_p50_k5"  5
-    , benchKMeans   "bench/data/kernel_n2000_p5.csv"     "KMeans_n2000_p5_k5" 5
-    -- DT/RF use list-based [[Double]] APIs internally; we cap at
-    -- n=2000 p=10 so the bench finishes in reasonable time. The Python
-    -- side uses the same fixture for fairness.
-    , benchDT       "bench/data/logistic_n2000_p10.csv"  "DT_n2000_p10"
-    , benchRF       "bench/data/logistic_n2000_p10.csv"  "RF_n2000_p10_t20"
-    ]
-  writeRows "bench/results/haskell/ml.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/ml.csv"
-
--- ---------------------------------------------------------------------------
--- PCA
--- ---------------------------------------------------------------------------
-
-benchPCA :: FilePath -> String -> Int -> IO [BenchRow]
-benchPCA path name k = do
-  (x, _y) <- readCsvXY path
-  (ms, res) <- timeitTastyIO probe
-                 (\i -> return $! pcaPhantom i k x)
-  let ratio = LA.sumElements (PCA.pcaExplainedRatio res)
-      sigma = LA.sumElements (PCA.pcaSingularValues res)
-  return [ BenchRow "haskell" "ml" name ms ratio sigma
-            ("Hanalyze.Model.PCA k=" ++ show k ++ " standardized") ]
-  where
-    probe r = LA.sumElements (PCA.pcaExplainedRatio r)
-            + LA.sumElements (PCA.pcaSingularValues r)
-
--- ---------------------------------------------------------------------------
--- KMeans
--- ---------------------------------------------------------------------------
-
-benchKMeans :: FilePath -> String -> Int -> IO [BenchRow]
-benchKMeans path name k = do
-  (x, _y) <- readCsvXY path
-  gen <- MWC.createSystemRandom
-  (ms, res) <- timeitTastyIO probe
-                 (\i -> kmeansPhantom i k x gen)
-  let inert = Cl.kmrInertia res
-      iters = fromIntegral (Cl.kmrIters res)
-  return [ BenchRow "haskell" "ml" name ms inert iters
-            ("Hanalyze.Model.Cluster.kMeans k=" ++ show k) ]
-  where
-    probe r = Cl.kmrInertia r
-
--- ---------------------------------------------------------------------------
--- DecisionTree (classification)
--- ---------------------------------------------------------------------------
-
-benchDT :: FilePath -> String -> IO [BenchRow]
-benchDT path name = do
-  (x, y) <- readCsvXY path
-  let xs   = LA.toLists x
-      ys   = map (round :: Double -> Int) (LA.toList y)
-  (ms, tree) <- timeitTastyIO probe
-                  (\i -> return $! dtPhantom i xs ys)
-  let acc = let preds = [ DT.predictDT tree row | row <- xs ]
-                hits  = length (filter id (zipWith (==) preds ys))
-            in fromIntegral hits / fromIntegral (length ys) :: Double
-  return [ BenchRow "haskell" "ml" name ms acc 0
-            "Hanalyze.Model.DecisionTree.fitDT default config" ]
-  where
-    -- Force tree by predicting on the first row.
-    probe t = case [ DT.predictDT t [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] ] of
-                (r:_) -> fromIntegral r
-                _     -> 0
-
--- ---------------------------------------------------------------------------
--- RandomForest (regression on binary y; same accuracy metric via threshold 0.5)
--- ---------------------------------------------------------------------------
-
-benchRF :: FilePath -> String -> IO [BenchRow]
-benchRF path name = do
-  (x, y) <- readCsvXY path
-  let xs = LA.toLists x
-      ys = LA.toList y
-  gen <- MWC.createSystemRandom
-  (ms, forest) <- timeitTastyIO probe
-                    (\i -> rfPhantom i xs ys gen)
-  let preds = map (RF.predictRF forest) xs
-      yi    = map (round :: Double -> Int) ys
-      pi'   = map (\p -> if p > 0.5 then 1 else 0 :: Int) preds
-      hits  = length (filter id (zipWith (==) pi' yi))
-      acc   = fromIntegral hits / fromIntegral (length ys) :: Double
-  return [ BenchRow "haskell" "ml" name ms acc 0
-            "Hanalyze.Model.RandomForest.fitRF (20 trees)" ]
-  where
-    probe forest = case xs of
-      (row:_) -> RF.predictRF forest row
-      _       -> 0
-      where xs = [[0.0 :: Double | _ <- [0 :: Int .. 19]]]
diff --git a/bench/haskell/BenchMO.hs b/bench/haskell/BenchMO.hs
deleted file mode 100644
--- a/bench/haskell/BenchMO.hs
+++ /dev/null
@@ -1,121 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Multi-objective optimization benchmarks (B4).
---
--- Runs NSGA-II from 'Hanalyze.Optim.NSGA' on ZDT1/2/3 (m=2, d=30) and DTLZ1/2
--- (m=3, d=10). Reports median wall time and the hypervolume of the
--- final approximation set against a reference point.
-
-module Main where
-
-import qualified Hanalyze.Optim.NSGA              as NSGA
-import qualified System.Random.MWC       as MWC
-import           Data.List               (sort)
-import           Control.Monad           (forM)
-
-import           BenchUtil
-
--- ---------------------------------------------------------------------------
--- Test problems (m = number of objectives, d = dimension)
--- ---------------------------------------------------------------------------
-
-data MOProblem = MOProblem
-  { mpName     :: String
-  , mpDim      :: Int
-  , mpObjs     :: Int
-  , mpFunc     :: [Double] -> [Double]
-  , mpBounds   :: [(Double, Double)]
-  , mpRefPoint :: [Double]                -- ^ reference for hypervolume
-  }
-
-zdt1, zdt2, zdt3 :: MOProblem
-zdt1 = MOProblem "ZDT1" 30 2 fn (replicate 30 (0,1)) [1.1, 1.1]
-  where
-    fn xs =
-      let f1 = head xs
-          g  = 1 + 9 * sum (tail xs) / fromIntegral (length xs - 1)
-          f2 = g * (1 - sqrt (f1 / g))
-      in [f1, f2]
-
-zdt2 = MOProblem "ZDT2" 30 2 fn (replicate 30 (0,1)) [1.1, 1.1]
-  where
-    fn xs =
-      let f1 = head xs
-          g  = 1 + 9 * sum (tail xs) / fromIntegral (length xs - 1)
-          f2 = g * (1 - (f1 / g) ** 2)
-      in [f1, f2]
-
-zdt3 = MOProblem "ZDT3" 30 2 fn (replicate 30 (0,1)) [1.1, 1.1]
-  where
-    fn xs =
-      let f1 = head xs
-          g  = 1 + 9 * sum (tail xs) / fromIntegral (length xs - 1)
-          f2 = g * (1 - sqrt (f1 / g) - (f1 / g) * sin (10 * pi * f1))
-      in [f1, f2]
-
-dtlz2_3 :: MOProblem
-dtlz2_3 = MOProblem "DTLZ2_3" 10 3 fn (replicate 10 (0, 1)) [1.5, 1.5, 1.5]
-  where
-    fn xs =
-      let m  = 3
-          k  = length xs - m + 1
-          x_m = drop (m - 1) xs
-          g  = sum [(xi - 0.5) ** 2 | xi <- x_m]
-          fAt i = (1 + g)
-                * product [ cos (xs!!j * pi / 2) | j <- [0 .. m - i - 2] ]
-                * (if i == 0 then 1 else sin (xs!!(m - i - 1) * pi / 2))
-      in [fAt i | i <- [0 .. m - 1]]
-
-problems :: [MOProblem]
-problems = [zdt1, zdt2, zdt3, dtlz2_3]
-
--- ---------------------------------------------------------------------------
--- Driver
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  rows <- fmap concat $ forM problems $ \p -> do
-    -- 1 seed: run NSGA once; export the Pareto set so that the Python
-    -- aggregator can score HV/IGD via pymoo (uniform metric for both
-    -- sides).
-    (ms, sols) <- runOne p
-    let pts = map NSGA.solObjectives sols
-    writePareto p pts
-    return [ BenchRow "haskell" "mo"
-              (mpName p ++ "/NSGA-II") ms 0 (fromIntegral (length pts))
-              ("Pareto written to bench/results/haskell/mo_pareto_"
-               ++ mpName p ++ ".csv") ]
-  writeRows "bench/results/haskell/mo.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/mo.csv"
-
-writePareto :: MOProblem -> [[Double]] -> IO ()
-writePareto p pts = do
-  let path = "bench/results/haskell/mo_pareto_" ++ mpName p ++ ".csv"
-      hdr  = unwords ["f" ++ show i | i <- [0 .. mpObjs p - 1]]
-  writeFile path (replaceSpaces hdr ++ "\n"
-                 ++ unlines [ commas (map show row) | row <- pts ])
-  where
-    replaceSpaces = map (\c -> if c == ' ' then ',' else c)
-    commas []     = ""
-    commas [x]    = x
-    commas (x:xs) = x ++ "," ++ commas xs
-
-{-# NOINLINE runOne #-}
-runOne :: MOProblem -> IO (Double, [NSGA.Solution])
-runOne p = do
-  gen <- MWC.createSystemRandom
-  let cfg = NSGA.defaultNSGAConfig
-              { NSGA.nsgaPopSize     = 100
-              , NSGA.nsgaGenerations = 100   -- pymoo と同条件 (N4 で per-gen も近接)
-              }
-  (ms, sols) <- timeitIO 1
-                  (\xs -> sum [head (NSGA.solObjectives s) | s <- xs])
-                  (\_ -> NSGA.nsga2 cfg (mpFunc p) (mpBounds p) gen)
-  return (ms, sols)
-
--- ---------------------------------------------------------------------------
-
-median :: Ord a => [a] -> a
-median xs = sort xs !! (length xs `div` 2)
diff --git a/bench/haskell/BenchMassiv.hs b/bench/haskell/BenchMassiv.hs
deleted file mode 100644
--- a/bench/haskell/BenchMassiv.hs
+++ /dev/null
@@ -1,240 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Standalone bench to compare hmatrix vs massiv for pairwise squared
--- distance computation. Tests the F4 plan's central question: can
--- massiv outperform hmatrix on the kernel-distance hot path?
---
--- All paths are pure Haskell (no unsafe*, no raw pointers).
-module Main where
-
-import qualified Numeric.LinearAlgebra as LA
-import qualified Hanalyze.Stat.KernelDist       as KD ()
-import qualified Hanalyze.Stat.KernelDist       as KD
-import qualified Data.Massiv.Array     as A
-import           Data.Massiv.Array     ( Array, Comp (..), Ix2 (..), Sz (..) )
-import           Data.Time.Clock       (getCurrentTime, diffUTCTime)
-import           Control.DeepSeq       (NFData, deepseq)
-
--- ---------------------------------------------------------------------------
--- hmatrix ↔ massiv conversion (safe API only)
--- ---------------------------------------------------------------------------
-
--- | hmatrix 'LA.Matrix' → massiv @Array S Ix2 Double@. Round-trips
--- through the row-major flat 'LA.Vector' (Storable) which both sides
--- understand, then resizes via massiv's 'A.resize''.
-hMatrixToMassiv :: LA.Matrix Double -> Array A.S Ix2 Double
-hMatrixToMassiv m =
-  let rs = LA.rows m
-      cs = LA.cols m
-      v  = LA.flatten m  -- LA.Vector Double (Storable, row-major)
-      arrFlat = A.fromStorableVector Seq v  -- Array S Ix1 Double
-  in A.resize' (Sz (rs :. cs)) arrFlat
-
--- | massiv @Array S Ix2 Double@ → hmatrix 'LA.Matrix'. Uses
--- 'A.toStorableVector' (no copy if storage matches) and reshapes.
-massivToHMatrix :: Array A.S Ix2 Double -> LA.Matrix Double
-massivToHMatrix a =
-  let Sz (_ :. cs) = A.size a
-      flat         = A.toStorableVector (A.flatten a)
-  in LA.reshape cs flat
-
--- ---------------------------------------------------------------------------
--- pairwiseSqDist via massiv
--- ---------------------------------------------------------------------------
-
--- | Pairwise squared distance using massiv. Uses the identity
--- @D[i,j] = ‖x_i‖² + ‖x_j‖² − 2 X·Xᵀ@.
-pairwiseSqDistMassiv :: LA.Matrix Double -> LA.Matrix Double
-pairwiseSqDistMassiv x =
-  let am   = hMatrixToMassiv x          -- n × p
-      Sz (n :. _p) = A.size am
-      -- (am * am) is element-wise square
-      sq  = A.compute (am A.!*! A.compute (A.transpose am)) :: Array A.U Ix2 Double
-      _   = sq
-      -- Easier: do the linear-algebra side via hmatrix BLAS, only the
-      -- elementwise piece in massiv.
-      sqVec = LA.fromList [ row `LA.dot` row | row <- LA.toRows x ]  -- placeholder
-      _ = sqVec
-      _ = n
-  in pairwiseSqDistMassiv2 x
-
--- | Cleaner version: keep matrix multiply in hmatrix BLAS (faster for
--- now), do only the elementwise +/- part in massiv.
-pairwiseSqDistMassiv2 :: LA.Matrix Double -> LA.Matrix Double
-pairwiseSqDistMassiv2 x =
-  let n     = LA.rows x
-      sq    = KD.rowSqNorms x                           -- length n
-      ones  = LA.konst 1 n :: LA.Vector Double
-      r2    = LA.outer sq ones                          -- n × n
-      c2    = LA.outer ones sq                          -- n × n
-      cross = x LA.<> LA.tr x                           -- BLAS GEMM
-      -- elementwise: r2 + c2 - 2*cross, then max 0, then zero diagonal
-      mr2   = hMatrixToMassiv r2
-      mc2   = hMatrixToMassiv c2
-      mcr   = hMatrixToMassiv cross
-      mDiff = A.computeAs A.S
-                (A.zipWith3
-                   (\a b c -> max 0 (a + b - 2 * c))
-                   mr2 mc2 mcr)
-      raw   = massivToHMatrix mDiff
-  in raw - LA.diag (LA.takeDiag raw) + LA.diagl (replicate n 0)
-
--- | Fusion-friendly version: skip the r2 / c2 outer-product
--- intermediates entirely. Build the result via massiv's index-based
--- 'A.makeArrayR': for each (i, j) read sq[i], sq[j] and cross[i, j]
--- in a single sweep — no per-position write to r2/c2.
-pairwiseSqDistMassiv3 :: LA.Matrix Double -> LA.Matrix Double
-pairwiseSqDistMassiv3 x =
-  let n     = LA.rows x
-      sq    = KD.rowSqNorms x                           -- length n (Storable)
-      cross = x LA.<> LA.tr x                           -- n × n, BLAS GEMM
-      sqA   = A.fromStorableVector Seq sq               -- Array S Ix1 Double
-      crA   = hMatrixToMassiv cross                     -- Array S Ix2 Double
-      raw   = A.computeAs A.S $
-                A.makeArrayR A.D Seq (Sz (n :. n)) $ \(i :. j) ->
-                  if i == j
-                    then 0
-                    else max 0 ( A.index' sqA i
-                               + A.index' sqA j
-                               - 2 * A.index' crA (i :. j) )
-      result = massivToHMatrix raw
-  in result
-
--- | Same as v3 but with parallel comp.
-pairwiseSqDistMassiv3Par :: LA.Matrix Double -> LA.Matrix Double
-pairwiseSqDistMassiv3Par x =
-  let n     = LA.rows x
-      sq    = KD.rowSqNorms x
-      cross = x LA.<> LA.tr x
-      sqA   = A.fromStorableVector Par sq
-      crA0  = hMatrixToMassiv cross
-      crA   = A.setComp Par crA0
-      raw   = A.computeAs A.S $
-                A.makeArrayR A.D Par (Sz (n :. n)) $ \(i :. j) ->
-                  if i == j
-                    then 0
-                    else max 0 ( A.index' sqA i
-                               + A.index' sqA j
-                               - 2 * A.index' crA (i :. j) )
-  in massivToHMatrix raw
-
--- ---------------------------------------------------------------------------
--- Benchmark loop
--- ---------------------------------------------------------------------------
-
-timeIt :: NFData a => String -> IO a -> IO a
-timeIt label act = do
-  -- Warm-up
-  !w <- act
-  w `deepseq` pure ()
-  t0 <- getCurrentTime
-  let n = 5
-  results <- mapM (\_ -> do { !r <- act; r `deepseq` pure r }) [1 .. n]
-  t1 <- getCurrentTime
-  let totalMs = realToFrac (diffUTCTime t1 t0) * 1000 :: Double
-      avgMs   = totalMs / fromIntegral n
-  putStrLn $ label ++ ": " ++ show avgMs ++ " ms (avg over " ++ show n ++ " runs)"
-  pure (head results)
-
-main :: IO ()
-main = do
-  let mkX seedBase n p =
-        LA.fromLists
-          [ [ sin (fromIntegral (seedBase * i + j))
-            | j <- [1 .. p] ]
-          | i <- [1 .. n] ]
-      x500   = mkX 1  500   20
-      x1000  = mkX 1  1000  20
-      x2000  = mkX 1  2000  20
-
-  putStrLn "=== Conversion overhead (hmatrix ↔ massiv ↔ hmatrix) ==="
-  _ <- timeIt "  conv only n=2000" $ pure $! massivToHMatrix (hMatrixToMassiv x2000)
-
-  putStrLn ""
-  putStrLn "=== pairwiseSqDist n=500 ==="
-  d1 <- timeIt "  hmatrix" $ pure $! KD.pairwiseSqDist x500
-  d2 <- timeIt "  massiv" $ pure $! pairwiseSqDistMassiv2 x500
-  let !diff1 = LA.norm_2 (LA.flatten (d1 - d2))
-  putStrLn $ "  numeric diff (should be 0): " ++ show diff1
-
-  putStrLn ""
-  putStrLn "=== pairwiseSqDist n=1000 ==="
-  d3 <- timeIt "  hmatrix" $ pure $! KD.pairwiseSqDist x1000
-  d4 <- timeIt "  massiv" $ pure $! pairwiseSqDistMassiv2 x1000
-  let !diff2 = LA.norm_2 (LA.flatten (d3 - d4))
-  putStrLn $ "  numeric diff (should be 0): " ++ show diff2
-
-  putStrLn ""
-  putStrLn "=== pairwiseSqDist n=2000 ==="
-  d5 <- timeIt "  hmatrix" $ pure $! KD.pairwiseSqDist x2000
-  d6 <- timeIt "  massiv v2 (zipWith3)" $ pure $! pairwiseSqDistMassiv2 x2000
-  d7 <- timeIt "  massiv v3 (makeArray)" $ pure $! pairwiseSqDistMassiv3 x2000
-  let !diff3 = LA.norm_2 (LA.flatten (d5 - d6))
-      !diff4 = LA.norm_2 (LA.flatten (d5 - d7))
-  putStrLn $ "  v2 numeric diff: " ++ show diff3
-  putStrLn $ "  v3 numeric diff: " ++ show diff4
-
-  putStrLn ""
-  putStrLn "=== Parallel comp (Par instead of Seq) for v3 ==="
-  d8 <- timeIt "  massiv v3 Par" $ pure $! pairwiseSqDistMassiv3Par x2000
-  let !diff5 = LA.norm_2 (LA.flatten (d5 - d8))
-  putStrLn $ "  numeric diff: " ++ show diff5
-
-  putStrLn ""
-  putStrLn "=== Par mode profitability sweep (pairwiseSqDist) ==="
-  let szs = [200, 500, 1000, 2000, 4000]
-  mapM_ (\sz -> do
-            let xs = mkX 1 sz 20
-            putStrLn $ "  n=" ++ show sz
-            _ <- timeIt "    Seq" $ pure $! pairwiseSqDistMassiv3 xs
-            _ <- timeIt "    Par" $ pure $! pairwiseSqDistMassiv3Par xs
-            pure ()
-        ) szs
-
-  putStrLn ""
-  putStrLn "=== Vector cmap exp (length n=10000, 10 calls) ==="
-  let !v10k = LA.fromList [sin (fromIntegral (i :: Int)) | i <- [1 .. 10000]] :: LA.Vector Double
-      goVH i = let m = LA.cmap (\s -> exp s + fromIntegral (i :: Int) * 1e-15) v10k
-               in LA.norm_2 m
-      goVM i = let arr = A.fromStorableVector A.Seq v10k
-                   m   = A.computeAs A.S
-                          (A.map (\s -> exp s + fromIntegral i * 1e-15) arr)
-               in LA.norm_2 (A.toStorableVector m)
-  _ <- timeItN "  hmatrix LA.cmap exp" 50 goVH
-  _ <- timeItN "  massiv A.map exp"    50 goVM
-
-  putStrLn ""
-  putStrLn "=== applyKernel-style cmap exp on n×n matrix ==="
-  let !d2K = KD.pairwiseSqDist x2000  -- 2000×2000 distance matrix
-      l2   = 1.0 :: Double
-      goH i = let sfI = 1.0 + fromIntegral (i :: Int) * 1e-15
-                  m = LA.cmap (\s -> sfI * exp (- s / (2 * l2))) d2K
-              in LA.norm_2 (LA.flatten m)
-      goM i = let sfI = 1.0 + fromIntegral (i :: Int) * 1e-15
-                  m = applyKernelMassiv sfI l2 d2K
-              in LA.norm_2 (LA.flatten m)
-  _ <- timeItN "  hmatrix cmap exp" 5 goH
-  _ <- timeItN "  massiv A.map exp" 5 goM
-  pure ()
-
--- | Time a function (Int -> a) over 'n' calls, each with distinct
--- input to defeat constant-folding.
-timeItN :: NFData a => String -> Int -> (Int -> a) -> IO ()
-timeItN label n f = do
-  let !w = f 0
-  w `deepseq` pure ()
-  t0 <- getCurrentTime
-  results <- mapM (\i -> let !r = f i in r `deepseq` pure r) [1 .. n]
-  t1 <- getCurrentTime
-  let totalMs = realToFrac (diffUTCTime t1 t0) * 1000 :: Double
-      avgMs   = totalMs / fromIntegral n
-  results `deepseq` pure ()
-  putStrLn $ label ++ ": " ++ show avgMs ++ " ms (avg over " ++ show n ++ " runs)"
-
--- | cmap-style RBF kernel via massiv map.
-applyKernelMassiv :: Double -> Double -> LA.Matrix Double -> LA.Matrix Double
-applyKernelMassiv sf l2 d2 =
-  let am  = hMatrixToMassiv d2
-      out = A.computeAs A.S (A.map (\s -> sf * exp (- s / (2 * l2))) am)
-  in massivToHMatrix out
diff --git a/bench/haskell/BenchMemAggregate.hs b/bench/haskell/BenchMemAggregate.hs
deleted file mode 100644
--- a/bench/haskell/BenchMemAggregate.hs
+++ /dev/null
@@ -1,44 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Memory audit Q2-B: Preprocess.groupBy* aggregation.
---
--- Suspected bug: 'collectInOrder' uses O(n²) lookup + 'vs ++ [v]' per
--- element. n=10⁴ rows × small group count should already be slow.
---
--- Usage:
---   ./bench-mem-aggregate <n_rows> <n_groups>  +RTS -s -M256m
-module Main where
-
-import qualified Data.Text                as T
-import qualified Data.Vector              as V
-import qualified DataFrame.Internal.Column    as DX
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified DataFrame.Operations.Core     as DX
-import           Data.Time.Clock          (getCurrentTime, diffUTCTime)
-import           System.Environment       (getArgs)
-import           System.IO                (hSetBuffering, BufferMode (..), stdout)
-
-import qualified Hanalyze.DataIO.Preprocess as PP
-
-main :: IO ()
-main = do
-  hSetBuffering stdout NoBuffering
-  args <- getArgs
-  let (n, ng) = case args of
-        [a]    -> (read a :: Int, 10 :: Int)
-        [a, b] -> (read a, read b)
-        _      -> (10000, 10)
-  putStrLn $ "BenchMemAggregate  n=" ++ show n ++ "  groups=" ++ show ng
-  let groupCol = DX.fromList
-                  ([ T.pack ("g" ++ show (i `mod` ng)) | i <- [0 .. n - 1] ] :: [T.Text])
-      valCol   = DX.fromList
-                  ([ sin (fromIntegral i / 7) :: Double | i <- [0 .. n - 1] ])
-      df       = DX.insertColumn "g" groupCol
-               $ DX.insertColumn "v" valCol DX.empty
-  V.length (V.fromList [(0::Int)]) `seq` return ()  -- silence vector import
-  t0 <- getCurrentTime
-  let !res = PP.groupByMean "g" "v" df
-  case res of
-    Nothing -> putStrLn "  groupByMean returned Nothing!"
-    Just r  -> putStrLn $ "  result rows=" ++ show (DX.dimensions r)
-  t1 <- getCurrentTime
-  putStrLn $ "  elapsed=" ++ show (diffUTCTime t1 t0)
diff --git a/bench/haskell/BenchMemBO.hs b/bench/haskell/BenchMemBO.hs
deleted file mode 100644
--- a/bench/haskell/BenchMemBO.hs
+++ /dev/null
@@ -1,55 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Memory audit Q2-B: Bayesian Optimization (1D + multi-D).
---
---   ./bench-mem-bo <iters>            # 1D Forrester
---   ./bench-mem-bo <iters> <dim>      # ND sphere
-module Main where
-
-import           Data.Time.Clock       (getCurrentTime, diffUTCTime)
-import           System.Environment    (getArgs)
-import           System.IO             (hSetBuffering, BufferMode (..), stdout)
-import           System.Random.MWC     (createSystemRandom)
-
-import           Hanalyze.Optim.BayesOpt
-                  (BayesOptConfig (..), defaultBayesOptConfig, bayesOpt,
-                   bayesOptND)
-
--- 1D Forrester (canonical BO benchmark).
-forrester :: Double -> IO Double
-forrester x =
-  let v = (6 * x - 2) ** 2 * sin (12 * x - 4)
-  in return v
-
-sphere :: [Double] -> IO Double
-sphere xs = return $ sum [ (x - 0.3) * (x - 0.3) | x <- xs ]
-
-main :: IO ()
-main = do
-  hSetBuffering stdout NoBuffering
-  args <- getArgs
-  gen <- createSystemRandom
-  case args of
-    [it]     -> do
-      let iters = read it :: Int
-          cfg = defaultBayesOptConfig { boIterations = iters }
-      putStrLn $ "BenchMemBO  iters=" ++ show iters ++ "  (1D Forrester)"
-      t0 <- getCurrentTime
-      (hist, (xb, yb)) <- bayesOpt cfg forrester (0.0, 1.0) gen
-      t1 <- getCurrentTime
-      putStrLn $ "  best=(" ++ show xb ++ ", " ++ show yb ++ ")"
-              ++ "  histLen=" ++ show (length hist)
-              ++ "  elapsed=" ++ show (diffUTCTime t1 t0)
-    [it, d] -> do
-      let iters = read it :: Int
-          dim   = read d  :: Int
-          cfg = defaultBayesOptConfig { boIterations = iters }
-          bs = replicate dim (-1.0 :: Double, 1.0 :: Double)
-      putStrLn $ "BenchMemBO  iters=" ++ show iters
-                          ++ "  dim=" ++ show dim ++ "  (sphere)"
-      t0 <- getCurrentTime
-      (hist, (xb, yb)) <- bayesOptND cfg 5 sphere bs gen
-      t1 <- getCurrentTime
-      putStrLn $ "  best=" ++ show (xb, yb)
-              ++ "  histLen=" ++ show (length hist)
-              ++ "  elapsed=" ++ show (diffUTCTime t1 t0)
-    _ -> putStrLn "usage: bench-mem-bo <iters> [dim]"
diff --git a/bench/haskell/BenchMemMCMC.hs b/bench/haskell/BenchMemMCMC.hs
deleted file mode 100644
--- a/bench/haskell/BenchMemMCMC.hs
+++ /dev/null
@@ -1,61 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes        #-}
--- | Memory audit Q2-B: MCMC samplers (MH / HMC / NUTS).
---
--- Suspected: 'modifyIORef'' samplesRef (Map.Strict... :)' uses Data.Map.Strict
--- so values are WHNF; chain length T × params K should grow linearly but
--- not leak. This bench confirms.
---
---   ./bench-mem-mcmc <sampler> <iters> <K>
---   sampler ∈ {mh, hmc, nuts}
-module Main where
-
-import           Control.Monad         (forM_)
-import qualified Data.Map.Strict       as Map
-import qualified Data.Text             as T
-import           Data.Time.Clock       (getCurrentTime, diffUTCTime)
-import           System.Environment    (getArgs)
-import           System.IO             (hSetBuffering, BufferMode (..), stdout)
-import           System.Random.MWC     (createSystemRandom)
-
-import           Hanalyze.Model.HBM
-import           Hanalyze.Stat.Distribution ()
-import           Hanalyze.MCMC.Core    (Chain (..), chainAccepted)
-import           Hanalyze.MCMC.MH      (MCMCConfig (..), defaultMCMCConfig, metropolis)
-import           Hanalyze.MCMC.HMC     (HMCConfig (..), defaultHMCConfig, hmc)
-import           Hanalyze.MCMC.NUTS    (NUTSConfig (..), defaultNUTSConfig, nuts)
-
-flatModel :: Int -> ModelP ()
-flatModel k = do
-  forM_ [1 .. k] $ \i -> do
-    let nm = T.pack ("p" ++ show i)
-    pi_ <- sample nm (Normal 0 1)
-    observe (T.pack ("y" ++ show i)) (Normal pi_ 1) [0.0]
-
-main :: IO ()
-main = do
-  hSetBuffering stdout NoBuffering
-  args <- getArgs
-  let (sampler, iters, k) = case args of
-        [s]         -> (s :: String, 1000 :: Int, 20 :: Int)
-        [s, it]     -> (s, read it, 20)
-        [s, it, kk] -> (s, read it, read kk)
-        _           -> ("mh", 1000, 20)
-  putStrLn $ "BenchMemMCMC  sampler=" ++ sampler
-                       ++ "  iters=" ++ show iters
-                       ++ "  K="     ++ show k
-  gen <- createSystemRandom
-  let initP = Map.fromList [ (T.pack ("p" ++ show i), 0.0) | i <- [1 .. k] ]
-  t0 <- getCurrentTime
-  ch <- case sampler of
-          "mh"   -> metropolis (flatModel k)
-                       ((defaultMCMCConfig (Map.keys initP))
-                          { mcmcIterations = iters }) initP gen
-          "hmc"  -> hmc  (flatModel k) (defaultHMCConfig  { hmcIterations  = iters }) initP gen
-          "nuts" -> nuts (flatModel k) (defaultNUTSConfig { nutsIterations = iters
-                                                          , nutsBurnIn     = iters `div` 4 }) initP gen
-          _      -> error "sampler ∈ {mh, hmc, nuts}"
-  t1 <- getCurrentTime
-  putStrLn $ "  samples=" ++ show (length (chainSamples ch))
-          ++ "  accepted=" ++ show (chainAccepted ch)
-          ++ "  elapsed=" ++ show (diffUTCTime t1 t0)
diff --git a/bench/haskell/BenchMemNSGA.hs b/bench/haskell/BenchMemNSGA.hs
deleted file mode 100644
--- a/bench/haskell/BenchMemNSGA.hs
+++ /dev/null
@@ -1,53 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Memory audit Q2-B: NSGA-II long generation count.
---
--- Suspected leak: 'generationLoop' recurses with a fresh 'newPop' that is
--- a thunk depending on the previous 'pop' (via 'pop ++ children' →
--- 'nonDominatedSort' → 'selectTopN'). Until forced, the entire ancestor
--- chain may be retained.
---
---   ./bench-mem-nsga2 <generations> <popSize> <dim>  +RTS -s -M256m
-module Main where
-
-import           Data.Time.Clock      (getCurrentTime, diffUTCTime)
-import           System.Environment   (getArgs)
-import           System.IO            (hSetBuffering, BufferMode (..), stdout)
-import           System.Random.MWC    (createSystemRandom)
-
-import           Hanalyze.Optim.NSGA  (NSGAConfig (..), defaultNSGAConfig,
-                                       nsga2, Solution (..))
-
--- ZDT1 (m=2, decision dim d): convex Pareto front in [0,1]^d.
-zdt1 :: [Double] -> [Double]
-zdt1 xs =
-  let f1 = head xs
-      g  = 1 + 9 * (sum (tail xs) / fromIntegral (length xs - 1))
-      f2 = g * (1 - sqrt (f1 / g))
-  in [f1, f2]
-
-main :: IO ()
-main = do
-  hSetBuffering stdout NoBuffering
-  args <- getArgs
-  let (gens, pop, d) = case args of
-        [a]       -> (read a :: Int, 100 :: Int, 10 :: Int)
-        [a, b]    -> (read a, read b, 10)
-        [a, b, c] -> (read a, read b, read c)
-        _         -> (200, 100, 10)
-  putStrLn $ "BenchMemNSGA  gens=" ++ show gens
-                       ++ "  pop=" ++ show pop
-                       ++ "  dim=" ++ show d
-  gen <- createSystemRandom
-  let cfg = defaultNSGAConfig
-              { nsgaPopSize     = pop
-              , nsgaGenerations = gens
-              }
-      bounds = replicate d (0.0, 1.0)
-  t0 <- getCurrentTime
-  front <- nsga2 cfg zdt1 bounds gen
-  t1 <- getCurrentTime
-  let nFront = length front
-      avgF1  = sum [ head (solObjectives s) | s <- front ] / fromIntegral nFront
-  putStrLn $ "  front=" ++ show nFront
-          ++ "  avgF1=" ++ show avgF1
-          ++ "  elapsed=" ++ show (diffUTCTime t1 t0)
diff --git a/bench/haskell/BenchMemVI.hs b/bench/haskell/BenchMemVI.hs
deleted file mode 100644
--- a/bench/haskell/BenchMemVI.hs
+++ /dev/null
@@ -1,68 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes        #-}
--- | Memory audit Q2-B-1: Stat.VI ADVI.
---
--- Looking for the chained-thunk leak in @Stat/VI.hs:176, 184@:
---
--- > writeIORef muRef (zipWith (+) mu dxMu)
---
--- After T iterations, @muRef@ holds @T@ levels of chained @zipWith@ that
--- are only forced post-loop. Expectation: alloc grows roughly linearly
--- with T, peak residency too.
---
--- Run e.g.:
---   ./bench-mem-vi 500  +RTS -s -t -M256m
---   ./bench-mem-vi 5000 +RTS -s -t -M512m
---
--- We use a synthetic flat-prior model with K parameters so the unconstrained
--- vector size is large enough to make any per-iter leak visible.
-module Main where
-
-import           Control.Monad         (forM_)
-import qualified Data.Map.Strict       as Map
-import qualified Data.Text             as T
-import           Data.Time.Clock       (getCurrentTime, diffUTCTime)
-import           System.Environment    (getArgs)
-import           System.IO             (hSetBuffering, BufferMode (..), stdout)
-import           System.Random.MWC     (createSystemRandom)
-
-import           Hanalyze.Model.HBM
-import           Hanalyze.Stat.Distribution ()
-import           Hanalyze.Stat.VI
-
--- | Synthetic model: K independent Normal latents with one Normal observation
--- each (data fixed at the prior mean). Larger K = larger variational vector
--- per iteration → leak is more visible per iter.
-flatModel :: Int -> ModelP ()
-flatModel k = do
-  forM_ [1 .. k] $ \i -> do
-    let nm = T.pack ("p" ++ show i)
-    pi_ <- sample nm (Normal 0 1)
-    observe (T.pack ("y" ++ show i)) (Normal pi_ 1) [0.0]
-
-main :: IO ()
-main = do
-  hSetBuffering stdout NoBuffering
-  args <- getArgs
-  let (iters, kParams) = case args of
-        [it]      -> (read it, 20)
-        [it, kk]  -> (read it, read kk)
-        _         -> (500, 20)
-  putStrLn $ "BenchMemVI  iters=" ++ show iters
-                       ++ "  K="  ++ show kParams
-  gen <- createSystemRandom
-  let initP = Map.fromList [ (T.pack ("p" ++ show i), 0.0)
-                           | i <- [1 .. kParams] ]
-      cfg   = defaultVIConfig
-                { viIterations = iters
-                , viSamples    = 5
-                , viNumDraws   = 100
-                }
-  t0 <- getCurrentTime
-  res <- advi (flatModel kParams) cfg initP gen
-  t1 <- getCurrentTime
-  let elboLast = case viElboHistory res of [] -> 0/0; xs -> last xs
-      muLen    = length (viMuU res)
-  putStrLn $ "  done elbo_last=" ++ show elboLast
-                  ++ "  |mu|=" ++ show muLen
-                  ++ "  elapsed=" ++ show (diffUTCTime t1 t0)
diff --git a/bench/haskell/BenchMultiOutput.hs b/bench/haskell/BenchMultiOutput.hs
deleted file mode 100644
--- a/bench/haskell/BenchMultiOutput.hs
+++ /dev/null
@@ -1,123 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | B12 Multi-output ベンチ。MultiLM / MultiGP を sklearn の
--- @MultiOutputRegressor@ と比較。
---
---   * MultiLM n=2000 p=10 q=5  → sklearn LinearRegression (multi-Y)
---   * MultiGP n=200 p=3  q=3   → sklearn GaussianProcessRegressor 多出力ループ
---
--- 出力: bench/results/haskell/multi_output.csv
-module Main where
-
-import qualified Numeric.LinearAlgebra as LA
-
-import           Hanalyze.Model.MultiLM         (fitMultiLM, predictMultiLM)
-import           Hanalyze.Model.MultiGP         (fitMultiGPMV, fitMultiGPMVIndep,
-                                        MultiGPResultMV (..))
-import           Hanalyze.Model.GP              (Kernel (..))
-
-import           BenchUtil
-
--- ---------------------------------------------------------------------------
--- Deterministic data generators (must match Python side).
--- ---------------------------------------------------------------------------
-
-designX :: Int -> Int -> LA.Matrix Double
-designX n p =
-  LA.fromLists
-    [ [ sin (fromIntegral i * 0.1 + fromIntegral j * 0.7)
-        + 0.3 * cos (fromIntegral i * 0.05 + fromIntegral j)
-      | j <- [0 .. p - 1] ]
-    | i <- [0 .. n - 1] ]
-
-multiY :: LA.Matrix Double -> Int -> LA.Matrix Double
-multiY x q =
-  let n = LA.rows x
-      p = LA.cols x
-      coefs = LA.fromLists
-                [ [ sin (fromIntegral (j * (k + 1)))
-                  | j <- [0 .. p - 1] ]
-                | k <- [0 .. q - 1] ]
-      y = x LA.<> LA.tr coefs
-      bump = LA.fromLists
-        [ [ 0.05 * sin (fromIntegral i * 0.3 + fromIntegral k)
-          | k <- [0 .. q - 1] ]
-        | i <- [0 .. n - 1] ]
-  in y + bump
-
--- ---------------------------------------------------------------------------
-
-benchMultiLM :: IO [BenchRow]
-benchMultiLM = do
-  let !n = 2000
-      !p = 10
-      !q = 5
-      !x = designX n p
-      !y = multiY x q
-      run :: Int -> IO Double
-      run _ = do
-        let mf   = fitMultiLM x y
-            yhat = predictMultiLM mf x
-            r    = yhat - y
-        return (LA.sumElements (LA.cmap (\d -> d * d) r))
-      probe = id
-  (ms, sse) <- timeitTastyIO probe run
-  let rmse = sqrt (sse / fromIntegral (n * q))
-  return [ BenchRow "haskell" "multi_output"
-            "MultiLM_n2000_p10_q5" ms rmse 0
-            ("MultiLM n=2000 p=10 q=5; RMSE=" ++ show rmse) ]
-
-benchMultiGP :: IO [BenchRow]
-benchMultiGP = do
-  let !n = 200
-      !p = 3
-      !q = 3
-      !x = designX n p
-      !y = multiY x q
-      yCols = [ LA.flatten (y LA.?? (LA.All, LA.Pos (LA.idxs [k])))
-              | k <- [0 .. q - 1] ]
-      run :: Int -> IO Double
-      run _ = do
-        let r = fitMultiGPMV x yCols x
-            s = sum [ LA.sumElements m | m <- mgpmvMean r ]
-        return s
-      probe = id
-  (ms, _) <- timeitTastyIO probe run
-  return [ BenchRow "haskell" "multi_output"
-            "MultiGP_n200_p3_q3" ms 0 0
-            "MultiGP RBF n=200 p=3 q=3 (shared HP, default API)" ]
-
-benchMultiGPIndep :: IO [BenchRow]
-benchMultiGPIndep = do
-  let !n = 200
-      !p = 3
-      !q = 3
-      !x = designX n p
-      !y = multiY x q
-      -- yCols: list of length q, each an LA.Vector of length n.
-      yCols = [ LA.flatten (y LA.?? (LA.All, LA.Pos (LA.idxs [k])))
-              | k <- [0 .. q - 1] ]
-      run :: Int -> IO Double
-      run _ = do
-        let r = fitMultiGPMVIndep RBF x yCols x
-            -- Sum of all per-output predicted means (forces full computation).
-            s = sum [ LA.sumElements m | m <- mgpmvMean r ]
-        return s
-      probe = id
-  (ms, _) <- timeitTastyIO probe run
-  return [ BenchRow "haskell" "multi_output"
-            "MultiGP_n200_p3_q3_indep" ms 0 0
-            "MultiGP RBF n=200 p=3 q=3 (per-output independent HPs)" ]
-
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  rows <- mconcat <$> sequence
-    [ benchMultiLM
-    , benchMultiGP
-    , benchMultiGPIndep
-    ]
-  writeRows "bench/results/haskell/multi_output.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/multi_output.csv"
diff --git a/bench/haskell/BenchOptim.hs b/bench/haskell/BenchOptim.hs
deleted file mode 100644
--- a/bench/haskell/BenchOptim.hs
+++ /dev/null
@@ -1,202 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Single-objective optimization benchmarks (B3).
---
--- Each (algorithm × test function) combination is run for 30 seeds and we
--- record:
---   - median wall time per run (ms),
---   - median final objective value @f(x*)@,
---   - success rate: @|f(x*)| < 1e-2@ (Sphere/Ackley/Levy: known optimum 0;
---     Rosenbrock: known min 0; Rastrigin: known min 0).
-
-module Main where
-
-import qualified System.Random.MWC       as MWC
-import           Data.List               (sort, minimumBy)
-import           Data.Ord                (comparing)
-import           Control.Monad           (forM)
-
-import qualified Hanalyze.Optim.NelderMead        as NM
-import qualified Hanalyze.Optim.LBFGS             as LB
-import qualified Hanalyze.Optim.LineSearch        as LS
-import qualified Hanalyze.Optim.DifferentialEvolution as DE
-import qualified Hanalyze.Optim.CMAES             as CM
-import qualified Hanalyze.Optim.SimulatedAnnealing    as SA
-import qualified Hanalyze.Optim.ParticleSwarm     as PS
-import qualified Hanalyze.Optim.Common            as OC
-
-import           BenchUtil
-
--- ---------------------------------------------------------------------------
--- Test functions (all minimization, true optimum f(x*) = 0)
--- ---------------------------------------------------------------------------
-
-rosenbrock, rastrigin, sphere, ackley, levy :: [Double] -> Double
-rosenbrock xs =
-  sum [ 100 * (xs!!(i+1) - xs!!i ** 2)^(2::Int)
-      + (1 - xs!!i)^(2::Int)
-      | i <- [0 .. length xs - 2] ]
-rastrigin xs =
-  10 * fromIntegral (length xs)
-    + sum [x*x - 10 * cos (2 * pi * x) | x <- xs]
-sphere = sum . map (^(2::Int))
-ackley xs =
-  let n  = fromIntegral (length xs) :: Double
-      s1 = sum (map (^(2::Int)) xs) / n
-      s2 = sum (map (\x -> cos (2*pi*x)) xs) / n
-  in - 20 * exp (- 0.2 * sqrt s1) - exp s2 + 20 + exp 1
-levy xs =
-  let w i = 1 + (xs!!i - 1) / 4
-      d   = length xs
-      sumMid = sum [ (w i - 1)^(2::Int) * (1 + 10 * sin (pi * w i + 1)^(2::Int))
-                   | i <- [0 .. d - 2] ]
-  in sin (pi * w 0)^(2::Int)
-   + sumMid
-   + (w (d-1) - 1)^(2::Int) * (1 + sin (2 * pi * w (d-1))^(2::Int))
-
--- | Griewank function: smooth multimodal with global min at 0.
--- f(x) = sum(x_i^2)/4000 - prod(cos(x_i/sqrt(i+1))) + 1
-griewank :: [Double] -> Double
-griewank xs =
-  let s = sum (map (^(2::Int)) xs) / 4000
-      p = product [ cos (x / sqrt (fromIntegral i))
-                  | (i, x) <- zip [1 :: Int ..] xs ]
-  in s - p + 1
-
--- | Schwefel function: very deceptive, global min near boundary at
--- x_i = 420.9687, f* = 0. Hard for local optimizers.
--- f(x) = 418.9829 d - sum(x_i sin(sqrt|x_i|))
-schwefel :: [Double] -> Double
-schwefel xs =
-  let d = length xs
-  in 418.9829 * fromIntegral d
-   - sum [ x * sin (sqrt (abs x)) | x <- xs ]
-
-testFns :: [(String, Int, [Double] -> Double)]
-testFns =
-  [ ("Rosenbrock_2D",  2, rosenbrock)
-  , ("Rosenbrock_10D", 10, rosenbrock)
-  , ("Rastrigin_10D",  10, rastrigin)
-  , ("Sphere_30D",     30, sphere)
-  , ("Ackley_10D",     10, ackley)
-  , ("Levy_10D",       10, levy)
-  , ("Griewank_10D",   10, griewank)
-  , ("Schwefel_5D",    5,  schwefel)
-  ]
-
--- ---------------------------------------------------------------------------
--- Adapters: each algorithm returns (final f(x*), wall-time ms)
--- ---------------------------------------------------------------------------
-
-data Algo = Algo
-  { algoName :: String
-  , algoRun  :: ([Double] -> Double) -> Int -> IO (Double, Double)
-                -- ^ given f and dim, returns (f_final, ms)
-  }
-
-algoNM, algoLBFGS, algoDE, algoCMA, algoSA, algoPSO :: Algo
-
-algoNM = Algo "NelderMead" $ \f d -> do
-  x0 <- initSeed d
-  (ms, r) <- timeitIO 1 OC.orValue (\_ -> NM.runNelderMead f x0)
-  return (OC.orValue r, ms)
-
--- | L-BFGS の勾配は中央差分で代用 (Numeric variant)。
-algoLBFGS = Algo "LBFGS" $ \f d -> do
-  x0 <- initSeed d
-  (ms, r) <- timeitIO 1 OC.orValue
-               (\_ -> LB.runLBFGSNumeric LB.defaultLBFGSConfig f x0)
-  return (OC.orValue r, ms)
-
-algoDE = Algo "DE" $ \f d -> do
-  let bs = replicate d (-5.0, 5.0)
-  gen <- MWC.createSystemRandom
-  (ms, r) <- timeitIO 1 OC.orValue (\_ -> DE.runDE bs f gen)
-  return (OC.orValue r, ms)
-
-algoCMA = Algo "CMAES" $ \f d -> do
-  x0 <- initSeed d
-  gen <- MWC.createSystemRandom
-  (ms, r) <- timeitIO 1 OC.orValue (\_ -> CM.runCMAES f x0 gen)
-  return (OC.orValue r, ms)
-
-algoSA = Algo "SA" $ \f d -> do
-  let bs = replicate d (-5.0, 5.0)
-  gen <- MWC.createSystemRandom
-  -- P42 (2026-05-07): the previous (20 runs × 10000 iter, LBFGS every
-  -- 10 iter) configuration spawned ~20K inner LBFGS refines × ~1000
-  -- numeric-gradient f calls each = ~20M f calls and dominated the
-  -- ~1900 ms wall-time.
-  --
-  -- Settled on (10 runs, every 20) which gives ~5K LBFGS refines
-  -- (1/4 of the baseline) — 3-7× faster across all benches with
-  -- equivalent quality. We did try (5 runs, every 50) but Rastrigin
-  -- collapsed to f≈0.99. We also tried (15 runs, every 20): 1.5×
-  -- slower for no measurable quality gain. Note: Rastrigin escape
-  -- rate fluctuates 23-70% across re-runs because the SA harness
-  -- uses MWC.createSystemRandom (non-deterministic seed) — single
-  -- 30-seed runs are noisy; the algorithm itself is robust.
-  let cfg = (SA.defaultSAConfig bs)
-              { SA.saProposal       = SA.Tsallis 2.62
-              , SA.saAccept         = SA.Boltzmann
-              , SA.saLocalMethod    = SA.LocalLBFGS
-              , SA.saLocalEvery     = Just 20
-              , SA.saInitTemp       = 5230.0
-              , SA.saRestartIfStuck = Nothing
-              , SA.saStop           = (SA.saStop (SA.defaultSAConfig bs))
-                                        { OC.stMaxIter = 10000 }
-              }
-      nRuns = 10 :: Int
-  (ms, r) <- timeitIO 1 OC.orValue $ \_ -> do
-    rs <- mapM (\_ -> do
-                   x0 <- initSeed d
-                   SA.runSAWith cfg f x0 gen) [1 .. nRuns]
-    return (minimumBy (comparing OC.orValue) rs)
-  return (OC.orValue r, ms)
-
-algoPSO = Algo "PSO" $ \f d -> do
-  let bs = replicate d (-5.0, 5.0)
-  gen <- MWC.createSystemRandom
-  (ms, r) <- timeitIO 1 OC.orValue (\_ -> PS.runPSO bs f gen)
-  return (OC.orValue r, ms)
-
-initSeed :: Int -> IO [Double]
-initSeed d = do
-  gen <- MWC.createSystemRandom
-  OC.sampleUniformIn (replicate d (-2.0, 2.0)) gen
-
-algos :: [Algo]
-algos = [algoNM, algoLBFGS, algoDE, algoCMA, algoSA, algoPSO]
-
--- ---------------------------------------------------------------------------
--- Run loop
--- ---------------------------------------------------------------------------
-
-nSeeds :: Int
-nSeeds = 30
-
-successThr :: Double
-successThr = 1e-2
-
-main :: IO ()
-main = do
-  rows <- fmap concat $ forM testFns $ \(fname, d, f) ->
-    forM algos $ \alg -> do
-      results <- mapM (\_ -> (algoRun alg) f d) [1 .. nSeeds]
-      let (fs, ts) = unzip results
-          medF  = median fs
-          medMs = median ts
-          succRate = fromIntegral
-                       (length (filter (\v -> abs v < successThr) fs))
-                     / fromIntegral nSeeds :: Double
-      return $ BenchRow "haskell" "optim"
-        (fname ++ "/" ++ algoName alg) medMs medF succRate
-        ("median over " ++ show nSeeds ++ " seeds")
-  writeRows "bench/results/haskell/optim.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/optim.csv"
-
-median :: Ord a => [a] -> a
-median xs =
-  let s = sort xs
-  in s !! (length s `div` 2)
diff --git a/bench/haskell/BenchOptimPlus.hs b/bench/haskell/BenchOptimPlus.hs
deleted file mode 100644
--- a/bench/haskell/BenchOptimPlus.hs
+++ /dev/null
@@ -1,132 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | B9 Optim+: Constrained / Adam / CMAESFull のベンチ。
---
---   * Constrained: 2D 問題 minimise (x-1)^2 + (y-2)^2 s.t. x+y=1
---     → Augmented Lagrangian (Hanalyze.Optim.Constrained)、scipy SLSQP / trust-constr
---   * Adam: 50D quadratic min ‖x‖^2 を 1000 step
---     → Hanalyze.Optim.Adam.runAdamMinimize、torch / scipy 自前
---   * CMAESFull: Rosenbrock 5D (full-rank covariance)
---     → Hanalyze.Optim.CMAESFull、cma library full-rank
---
--- 出力: bench/results/haskell/optim_plus.csv
-module Main where
-
-import qualified System.Random.MWC      as MWC
-
-import qualified Hanalyze.Optim.Common           as OC
-import qualified Hanalyze.Optim.Constrained      as Co
-import           Hanalyze.Optim.Adam             (defaultAdamConfig, AdamConfig (..),
-                                         runAdamMinimize)
-import           Hanalyze.Optim.CMAESFull        (defaultCMAESFConfig, CMAESFConfig (..),
-                                         runCMAESFullWith)
-
-import           BenchUtil
-
--- ---------------------------------------------------------------------------
--- Constrained: Augmented Lagrangian on a quadratic with linear equality.
--- ---------------------------------------------------------------------------
-
--- minimise (x-1)^2 + (y-2)^2 subject to x + y = 1.
--- Closed-form optimum: x* = 0, y* = 1, f* = 2.
-benchConstrained :: IO [BenchRow]
-benchConstrained = do
-  let f xs = case xs of
-        [x, y] -> (x - 1)^(2::Int) + (y - 2)^(2::Int)
-        _      -> error "expected 2D"
-      cs = Co.ConstraintSet
-            { Co.csEq   = [ \[x, y] -> x + y - 1 ]
-            , Co.csIneq = []
-            }
-      cfg = Co.defaultConstrainedConfig
-              { Co.ccOuterIter = 25
-              }
-      run :: Int -> IO ([Double], Double)
-      run _ = do
-        (r, _v) <- Co.runAugmentedLagrangian cfg f cs [0, 0]
-        return (OC.orBest r, OC.orValue r)
-      probe (xs, val) = case xs of
-        [_, _] -> val
-        _      -> 0
-  (ms, (xs, val)) <- timeitTastyIO probe run
-  let [x_, y_] = take 2 (xs ++ [0, 0])
-      err = sqrt ((x_ - 0)^(2::Int) + (y_ - 1)^(2::Int))
-  return [ BenchRow "haskell" "optim_plus"
-            "Constrained_Quad2D_eq" ms err val
-            ("x=" ++ show x_ ++ " y=" ++ show y_
-             ++ " f=" ++ show val ++ " err_to_opt=" ++ show err) ]
-
--- ---------------------------------------------------------------------------
--- Adam: minimise ‖x‖² in 50D, 1000 iterations, lr=0.05.
--- ---------------------------------------------------------------------------
-
-benchAdam :: IO [BenchRow]
-benchAdam = do
-  let n     = 50
-      x0    = replicate n 1.0   -- f(x0) = 50
-      grad  = map (* 2)         -- ∇‖x‖² = 2x
-      cfg   = defaultAdamConfig
-                { adamIterations   = 1000
-                , adamLearningRate = 0.05
-                }
-      run :: Int -> IO ([Double], Double)
-      run _ = do
-        let (xFinal, _hist) = runAdamMinimize cfg grad x0
-            f x = sum (map (\v -> v * v) x)
-        return (xFinal, f xFinal)
-      probe = snd
-  (ms, (_xFinal, fVal)) <- timeitTastyIO probe run
-  return [ BenchRow "haskell" "optim_plus"
-            "Adam_quad50D_iter1000" ms fVal 0
-            ("‖x‖² minimization 50D from x0=1; f_final=" ++ show fVal) ]
-
--- ---------------------------------------------------------------------------
--- CMAESFull: Rosenbrock 5D, 200 iterations.
--- ---------------------------------------------------------------------------
-
-rosenbrock :: [Double] -> Double
-rosenbrock xs =
-  sum [ 100 * (xs !! (i + 1) - (xs !! i)^(2::Int))^(2::Int)
-        + (1 - xs !! i)^(2::Int)
-      | i <- [0 .. length xs - 2] ]
-
-benchCMAESFull :: IO [BenchRow]
-benchCMAESFull = do
-  -- P3 fairness: give both sides the same convergence criterion
-  -- (tolfun = 1e-10) and a generous iter cap (1000), so both run "to
-  -- convergence" rather than getting cut off at an artificial maxiter.
-  -- Previously hanalyze stopped at 200 iter with f = 0.031 while cma
-  -- effectively converged in <200 iter to f ~ 5e-7; the unfair part
-  -- was hanalyze's tolfun never had a chance to fire.
-  let cfg = defaultCMAESFConfig
-              { cmfStop   = (cmfStop defaultCMAESFConfig)
-                              { OC.stMaxIter = 1000
-                              , OC.stTolFun  = 1e-10
-                              }
-              , cmfSigma0 = 0.5
-              }
-      x0  = replicate 5 (-1.5)
-      run :: Int -> IO Double
-      run _ = do
-        gen <- MWC.create
-        r <- runCMAESFullWith cfg rosenbrock x0 gen
-        return (OC.orValue r)
-      probe = id
-  (ms, fVal) <- timeitTastyIO probe run
-  return [ BenchRow "haskell" "optim_plus"
-            "CMAESFull_Rosenbrock5D_converge" ms fVal 0
-            ("CMAESFull σ₀=0.5 tolfun=1e-10 maxIter=1000 from x0=-1.5; "
-             ++ "f_final=" ++ show fVal) ]
-
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  rows <- mconcat <$> sequence
-    [ benchConstrained
-    , benchAdam
-    , benchCMAESFull
-    ]
-  writeRows "bench/results/haskell/optim_plus.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/optim_plus.csv"
diff --git a/bench/haskell/BenchPhase17.hs b/bench/haskell/BenchPhase17.hs
deleted file mode 100644
--- a/bench/haskell/BenchPhase17.hs
+++ /dev/null
@@ -1,408 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse -Wno-incomplete-uni-patterns #-}
--- | Phase 1-7 機能 (Spotfire/JMP gap) の Python/R 比較ベンチ (Haskell 側)。
---
--- 比較先:
---   * Weibull MLE          → scipy.stats.weibull_min.fit
---   * MANOVA (Wilks Λ)     → 自前 numpy 実装 (Haskell と同 algorithm)
---   * Hotelling T²         → 自前 numpy 実装
---   * Lasso/Ridge λ CV     → sklearn.linear_model.{LassoCV,RidgeCV}
---   * SpaceFilling LHS     → scipy.stats.qmc.LatinHypercube
---   * SpaceFilling Halton  → scipy.stats.qmc.Halton
---   * Augment Design       → Python 直接等価なし (Haskell-only 計測)
---   * DSD (Jones-Nachtsheim) → Python 直接等価なし (Haskell-only 計測)
---   * SPC X̄-R              → Python 直接等価なし (Haskell-only 計測)
---
--- 共通入力 CSV は本ファイルで生成 (Halton 列で再現性確保) し
--- @bench/data/@ に書き出す。 Python 側は同じ CSV を読む。
---
--- 出力: @bench/results/haskell/phase17.csv@ (unified BenchRow schema)。
-module Main where
-
-import qualified Data.Text               as T
-import qualified Data.Vector             as V
-import qualified Numeric.LinearAlgebra   as LA
-import qualified System.Random.MWC       as MWC
-import           System.Directory        (createDirectoryIfMissing)
-import           System.IO               (withFile, IOMode (..), hPutStrLn)
-import           Text.Printf             (printf)
-
-import qualified Hanalyze.Model.Weibull         as Wei
-import qualified Hanalyze.Model.Regularized     as Reg
-import qualified Hanalyze.Design.Optimal        as Opt
-import qualified Hanalyze.Design.SpaceFilling   as SF
-import qualified Hanalyze.Design.DSD            as DSD
-import qualified Hanalyze.Stat.SPC              as SPC
-import qualified Hanalyze.Stat.QuasiRandom      as QR
-import qualified Hanalyze.Stat.Test             as ST
-
-import           BenchUtil
-
--- ===========================================================================
--- 共通 helper
--- ===========================================================================
-
--- | Halton 1D 列で (0, 1) 上の n 個の deterministic uniform 値。
-haltonU1 :: Int -> Int -> [Double]
-haltonU1 prime n = [ QR.radicalInverse prime i | i <- [1 .. n] ]
-
--- ===========================================================================
--- Weibull MLE
--- ===========================================================================
-
-weibullSamples :: Int -> Double -> Double -> [Double]
-weibullSamples n k lam =
-  [ lam * (negate (log (1 - u))) ** (1 / k)
-  | u <- haltonU1 2 n
-  ]
-
-writeWeibullCSV :: FilePath -> [Double] -> IO ()
-writeWeibullCSV path xs = withFile path WriteMode $ \h -> do
-  hPutStrLn h "time"
-  mapM_ (hPutStrLn h . printf "%.10g") xs
-
-{-# NOINLINE fitWeibullPhantom #-}
-fitWeibullPhantom :: Int -> V.Vector Double -> Either T.Text Wei.WeibullFit
-fitWeibullPhantom _ = Wei.fitWeibullMLE
-
-benchWeibullMLE :: Int -> Double -> Double -> IO BenchRow
-benchWeibullMLE n trueK trueLam = do
-  let xs   = weibullSamples n trueK trueLam
-      vec  = V.fromList xs
-      name = "WeibullMLE_n" ++ show n
-  (medMs, result) <- timeitIO 7 forceFit (\i -> pure (fitWeibullPhantom i vec))
-  let (kErr, lamErr) = case result of
-        Right f -> ( abs (Wei.wfShape f - trueK) / trueK
-                   , abs (Wei.wfScale f - trueLam) / trueLam )
-        Left _  -> (1/0, 1/0)
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "phase17", brName = name
-    , brTimeMs = medMs, brAccMain = kErr, brAccAux = lamErr
-    , brExtra = printf "trueK=%g trueLam=%g" trueK trueLam
-    }
-  where
-    forceFit (Right f) = Wei.wfShape f + Wei.wfScale f
-    forceFit (Left _)  = 0
-
--- ===========================================================================
--- MANOVA
--- ===========================================================================
-
-generateManovaGroups :: Int -> Double -> [LA.Matrix Double]
-generateManovaGroups nPerGroup groupShift =
-  let centers = [(0, 0), (groupShift, 0), (0, groupShift)]
-      mkGroup (cx, cy) i0 =
-        LA.fromLists
-          [ [ cx + 2 * (QR.radicalInverse 2 (i0 + i) - 0.5)
-            , cy + 2 * (QR.radicalInverse 3 (i0 + i) - 0.5)
-            ]
-          | i <- [1 .. nPerGroup]
-          ]
-  in [ mkGroup c (g * 1000) | (g, c) <- zip [0..] centers ]
-
-writeManovaCSV :: FilePath -> [LA.Matrix Double] -> IO ()
-writeManovaCSV path groups = withFile path WriteMode $ \h -> do
-  hPutStrLn h "group,x1,x2"
-  let rows =
-        [ (g, x1, x2)
-        | (g, mat) <- zip [0 :: Int ..] groups
-        , row <- LA.toLists mat
-        , let [x1, x2] = row
-        ]
-  mapM_ (\(g, x1, x2) -> hPutStrLn h (printf "%d,%.10g,%.10g" g x1 x2)) rows
-
-{-# NOINLINE manovaPhantom #-}
-manovaPhantom :: Int -> [LA.Matrix Double] -> ST.TestResult
-manovaPhantom _ = ST.manova
-
-benchManova :: Int -> Double -> IO BenchRow
-benchManova nPerGroup groupShift = do
-  let groups = generateManovaGroups nPerGroup groupShift
-      name   = "MANOVA_3grp_n" ++ show nPerGroup
-  (medMs, result) <- timeitIO 7 forceTR (\i -> pure (manovaPhantom i groups))
-  let wilks = case ST.trEffect result of
-        Just (_, w) -> w
-        Nothing     -> 1/0
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "phase17", brName = name
-    , brTimeMs = medMs, brAccMain = wilks, brAccAux = ST.trPValue result
-    , brExtra = printf "groupShift=%g 2vars" groupShift
-    }
-  where
-    forceTR tr = ST.trStatistic tr + ST.trPValue tr
-
--- ===========================================================================
--- Hotelling T² (1-sample)
--- ===========================================================================
-
--- | n × 2 の deterministic データ、 中心が (shift, shift)。
-generateHotellingData :: Int -> Double -> LA.Matrix Double
-generateHotellingData n shift =
-  LA.fromLists
-    [ [ shift + 2 * (QR.radicalInverse 2 i - 0.5)
-      , shift + 2 * (QR.radicalInverse 3 i - 0.5)
-      ]
-    | i <- [1 .. n]
-    ]
-
-writeHotellingCSV :: FilePath -> LA.Matrix Double -> IO ()
-writeHotellingCSV path mat = withFile path WriteMode $ \h -> do
-  hPutStrLn h "x1,x2"
-  mapM_ (\[x1, x2] -> hPutStrLn h (printf "%.10g,%.10g" x1 x2)) (LA.toLists mat)
-
-{-# NOINLINE hotellingPhantom #-}
-hotellingPhantom :: Int -> LA.Matrix Double -> LA.Vector Double -> ST.TestResult
-hotellingPhantom _ = ST.hotellingsT2
-
-benchHotelling :: Int -> Double -> IO BenchRow
-benchHotelling n shift = do
-  let mat = generateHotellingData n shift
-      mu0 = LA.fromList [0.0, 0.0]
-      name = "HotellingT2_n" ++ show n
-  (medMs, result) <- timeitIO 7 forceTR (\i -> pure (hotellingPhantom i mat mu0))
-  let t2 = case ST.trEffect result of
-        Just (_, w) -> w
-        Nothing     -> 1/0
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "phase17", brName = name
-    , brTimeMs = medMs, brAccMain = t2, brAccAux = ST.trPValue result
-    , brExtra = printf "shift=%g 2vars mu0=(0,0)" shift
-    }
-  where
-    forceTR tr = ST.trStatistic tr + ST.trPValue tr
-
--- ===========================================================================
--- Lasso / Ridge λ CV
--- ===========================================================================
-
--- | LM-like data: y = X β_true + ε、 X は Halton 多次元、 ε は Halton-Box-Muller。
-generateLMData :: Int -> Int -> ([Double], LA.Matrix Double, LA.Vector Double)
-generateLMData n p =
-  let baseTrue = [2.0, 1.0, 0.5] ++ replicate (max 0 (p - 3)) 0.0
-      betaTrue = take p baseTrue
-      -- design matrix via Halton p-D
-      xMat = LA.fromLists
-        [ [ 2 * QR.radicalInverse (primes !! (j - 1)) i - 1
-          | j <- [1 .. p]
-          ]
-        | i <- [1 .. n]
-        ]
-      -- noise via Box-Muller from Halton (bases 11, 13)
-      noises =
-        [ let u1 = QR.radicalInverse 11 i
-              u2 = QR.radicalInverse 13 i
-          in 0.1 * sqrt (-2 * log (max 1e-10 u1)) * cos (2 * pi * u2)
-        | i <- [1 .. n]
-        ]
-      yVec = (xMat LA.#> LA.fromList betaTrue) + LA.fromList noises
-  in (betaTrue, xMat, yVec)
-  where
-    primes :: [Int]
-    primes = [ 2,  3,  5,  7, 11, 13, 17, 19, 23, 29
-             , 31, 37, 41, 43, 47, 53, 59, 61, 67, 71
-             , 73, 79, 83, 89, 97,101,103,107,109,113
-             ]
-
-writeLMCSV :: FilePath -> Int -> LA.Matrix Double -> LA.Vector Double -> IO ()
-writeLMCSV path p xMat yVec = withFile path WriteMode $ \h -> do
-  hPutStrLn h (intercalate "," ([ "x" ++ show j | j <- [1 .. p] ] ++ ["y"]))
-  let n = LA.rows xMat
-  mapM_ (\i ->
-            let xRow = LA.toLists (xMat LA.? [i]) !! 0
-                y    = yVec LA.! i
-                cells = [ printf "%.10g" x | x <- xRow ] ++ [ printf "%.10g" y ]
-            in hPutStrLn h (intercalate "," cells))
-        [0 .. n - 1]
-  where
-    intercalate sep = foldr1 (\a b -> a ++ sep ++ b)
-
-lambdaGrid :: [Double]
-lambdaGrid = [0.001, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0]
-
-benchRegCV :: Reg.PenaltyKind -> String -> Int -> Int -> IO BenchRow
-benchRegCV kind tag n p = do
-  let (_betaTrue, xMat, yVec) = generateLMData n p
-      name = tag ++ "CV_n" ++ show n ++ "_p" ++ show p
-  -- selectLambdaCV is IO で MWC.GenIO 要なので、 fresh gen を使う
-  gen <- MWC.create
-  (medMs, sel) <- timeitIO 5 (\s -> Reg.lsBestLambda s)
-                              (\_ -> Reg.selectLambdaCV 5 kind lambdaGrid xMat yVec gen)
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "phase17", brName = name
-    , brTimeMs = medMs, brAccMain = Reg.lsBestLambda sel
-    , brAccAux = Reg.lsOneSeLambda sel
-    , brExtra = printf "grid=%d folds=5" (length lambdaGrid)
-    }
-
--- ===========================================================================
--- SpaceFilling LHS / Halton
--- ===========================================================================
-
-benchLHS :: Int -> Int -> IO BenchRow
-benchLHS n d = do
-  let name = "LHS_n" ++ show n ++ "_d" ++ show d
-  (medMs, sfd) <- timeitIO 5 (\s -> SF.sfdMinDist s)
-                              (\_ -> do
-                                  gen <- MWC.create
-                                  SF.latinHypercube n d gen)
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "phase17", brName = name
-    , brTimeMs = medMs, brAccMain = SF.sfdMinDist sfd
-    , brAccAux = 0
-    , brExtra = "method=LHS"
-    }
-
-benchHalton :: Int -> Int -> IO BenchRow
-benchHalton n d = do
-  let name = "Halton_n" ++ show n ++ "_d" ++ show d
-      sfd  = SF.haltonDesign n d
-  (medMs, sfd2) <- timeitIO 5 (\s -> SF.sfdMinDist s)
-                               (\_ -> pure sfd)
-  let _ = sfd2
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "phase17", brName = name
-    , brTimeMs = medMs, brAccMain = SF.sfdMinDist sfd
-    , brAccAux = 0
-    , brExtra = "method=Halton"
-    }
-
--- ===========================================================================
--- Augment Design (Haskell-only、 Python 等価なし)
--- ===========================================================================
-
-benchAugment :: Int -> IO BenchRow
-benchAugment nNew = do
-  let cands = Opt.quadraticCandidates 2 3  -- 9 候補
-      existing =
-        [ [1, -1, -1, 1, 1,  1]
-        , [1,  1, -1, 1, 1, -1]
-        , [1, -1,  1, 1, 1, -1]
-        , [1,  0,  0, 0, 0,  0]
-        ]
-      name = "Augment_existing4_add" ++ show nNew
-  (medMs, res) <- timeitIO 7 (\r -> Opt.arFinalCrit r)
-                              (\_ -> pure (Opt.augmentDesign Opt.DOpt existing nNew cands 42))
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "phase17", brName = name
-    , brTimeMs = medMs, brAccMain = Opt.arFinalCrit res
-    , brAccAux = Opt.arInitialCrit res
-    , brExtra = "no_python_equivalent"
-    }
-
--- ===========================================================================
--- DSD (Haskell-only)
--- ===========================================================================
-
-benchDSD :: Int -> IO BenchRow
-benchDSD k = do
-  let name = "DSD_k" ++ show k
-  (medMs, res) <- timeitIO 7
-        (\eth -> case eth of
-            Right r -> fromIntegral (DSD.dsdNRuns r)
-            Left  _ -> 0)
-        (\_ -> pure (DSD.dsdDesign k))
-  let nRuns = case res of
-        Right r -> DSD.dsdNRuns r
-        Left  _ -> 0
-      hasOpt = case res of
-        Right r -> if DSD.dsdHasOptimal r then 1 else 0
-        Left  _ -> 0 :: Int
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "phase17", brName = name
-    , brTimeMs = medMs, brAccMain = fromIntegral nRuns
-    , brAccAux = fromIntegral hasOpt
-    , brExtra = "no_python_equivalent"
-    }
-
--- ===========================================================================
--- SPC X̄-R (Haskell-only)
--- ===========================================================================
-
-benchSPC :: Int -> IO BenchRow
-benchSPC nSubgroups = do
-  let subs = V.fromList
-        [ V.fromList
-            [ 10 + 0.5 * (QR.radicalInverse 2 (g * 5 + j) - 0.5)
-            | j <- [1 .. 5]
-            ]
-        | g <- [1 .. nSubgroups]
-        ]
-      name = "SPC_XR_subgroups" ++ show nSubgroups
-  (medMs, result) <- timeitIO 7 forceR (\_ -> pure (SPC.fitSPC SPC.XR (SPC.VarSubgroups subs)))
-  let center = case result of
-        Right (ch:_) -> SPC.spcCenter ch
-        _            -> 0
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "phase17", brName = name
-    , brTimeMs = medMs, brAccMain = center
-    , brAccAux = 0
-    , brExtra = "no_python_equivalent subgroupSize=5"
-    }
-  where
-    forceR (Right (ch:_)) = SPC.spcCenter ch
-    forceR _              = 0
-
--- ===========================================================================
--- Main
--- ===========================================================================
-
-main :: IO ()
-main = do
-  createDirectoryIfMissing True "bench/data"
-  createDirectoryIfMissing True "bench/results/haskell"
-
-  -- Weibull MLE
-  let trueK   = 2.0
-      trueLam = 10.0
-  mapM_ (\n ->
-            writeWeibullCSV (printf "bench/data/weibull_n%d.csv" n)
-                            (weibullSamples n trueK trueLam))
-        [100, 1000, 10000 :: Int]
-  weibullRows <- mapM (\n -> benchWeibullMLE n trueK trueLam)
-                      [100, 1000, 10000 :: Int]
-
-  -- MANOVA
-  let groupShift = 1.5
-  mapM_ (\nPer -> do
-            let groups = generateManovaGroups nPer groupShift
-            writeManovaCSV (printf "bench/data/manova_3grp_n%d.csv" nPer) groups)
-        [30, 100, 500 :: Int]
-  manovaRows <- mapM (\nPer -> benchManova nPer groupShift)
-                     [30, 100, 500 :: Int]
-
-  -- Hotelling T²
-  mapM_ (\n -> do
-            let mat = generateHotellingData n 0.5
-            writeHotellingCSV (printf "bench/data/hotelling_n%d.csv" n) mat)
-        [50, 200, 1000 :: Int]
-  hotellingRows <- mapM (\n -> benchHotelling n 0.5) [50, 200, 1000 :: Int]
-
-  -- Lasso / Ridge CV
-  mapM_ (\(n, p) -> do
-            let (_, xMat, yVec) = generateLMData n p
-            writeLMCSV (printf "bench/data/lm_n%d_p%d.csv" n p) p xMat yVec)
-        [(200, 10), (500, 20)]
-  lassoRows <- mapM (\(n, p) -> benchRegCV Reg.KindLasso "Lasso" n p)
-                    [(200, 10), (500, 20)]
-  ridgeRows <- mapM (\(n, p) -> benchRegCV Reg.KindRidge "Ridge" n p)
-                    [(200, 10), (500, 20)]
-
-  -- SpaceFilling
-  lhsRows    <- mapM (\(n, d) -> benchLHS n d) [(50, 2), (200, 3)]
-  haltonRows <- mapM (\(n, d) -> benchHalton n d) [(50, 2), (200, 3)]
-
-  -- Augment Design
-  augmentRows <- mapM benchAugment [2, 3, 4 :: Int]
-
-  -- DSD
-  dsdRows <- mapM benchDSD [4, 6, 8, 10 :: Int]
-
-  -- SPC
-  spcRows <- mapM benchSPC [10, 30, 100 :: Int]
-
-  writeRows "bench/results/haskell/phase17.csv"
-            (weibullRows ++ manovaRows ++ hotellingRows
-             ++ lassoRows ++ ridgeRows
-             ++ lhsRows ++ haltonRows
-             ++ augmentRows ++ dsdRows ++ spcRows)
-  putStrLn "✓ bench/results/haskell/phase17.csv written"
diff --git a/bench/haskell/BenchProfile.hs b/bench/haskell/BenchProfile.hs
deleted file mode 100644
--- a/bench/haskell/BenchProfile.hs
+++ /dev/null
@@ -1,78 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Focused profile runner for the highest-allocation benchmarks
--- identified by tasty-bench (KernelRidgeMV, gramMatrixMV, GLM_logit).
---
--- Build with profiling:
---
--- > cabal build --enable-profiling --enable-library-profiling bench-profile
---
--- Run for time / cost-center profile:
---
--- > OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 \
--- >   $(cabal list-bin --enable-profiling bench-profile) \
--- >     +RTS -p -RTS <target>
---
--- Run for heap profile (by cost-center):
---
--- > $(cabal list-bin --enable-profiling bench-profile) \
--- >     +RTS -hc -L80 -RTS <target>
--- > hp2ps -e8in -c bench-profile.hp
---
--- targets: kr | gram | glm | lasso | psd
-module Main where
-
-import           Control.DeepSeq         (deepseq)
-import           Control.Monad           (replicateM_)
-import qualified Numeric.LinearAlgebra   as LA
-import           System.Environment      (getArgs)
-
-import           Hanalyze.Model.Core              (coefficients)
-import           Hanalyze.Model.GLM               (Family (..), LinkFn (..), fitGLMFull)
-import qualified Hanalyze.Model.Regularized       as Reg
-import           Hanalyze.Model.Regularized       (Penalty (..), rfBeta)
-import qualified Hanalyze.Model.KernelRegression            as Kn
-import qualified Hanalyze.Stat.KernelDist         as KD
-
-import           BenchUtil               (readCsvXY)
-
--- Probe to a Double scalar so the entire result is forced through NF
--- by pulling a numeric field. (Some result types lack an NFData
--- instance so we evaluate the probe value to NF instead.)
-runN :: Int -> (a -> Double) -> IO a -> IO ()
-runN n force action =
-  replicateM_ n $ do
-    x <- action
-    let s = force x
-    s `deepseq` pure ()
-
-main :: IO ()
-main = do
-  args <- getArgs
-  let target = case args of
-        (t : _) -> t
-        _       -> "kr"
-  case target of
-    "kr" -> do
-      (xKR, yKR) <- readCsvXY "bench/data/kernel_n1000_p5.csv"
-      let yMat = LA.asColumn yKR
-      runN 30 (LA.sumElements . Kn.krmvAlpha) $
-        pure $! Kn.kernelRidgeMV Kn.Gaussian 1.0 1e-3 xKR yMat
-    "gram" -> do
-      (xKR, _) <- readCsvXY "bench/data/kernel_n1000_p5.csv"
-      runN 50 LA.sumElements $
-        pure $! Kn.gramMatrixMV Kn.Gaussian 1.0 xKR
-    "glm" -> do
-      (xL, yL) <- readCsvXY "bench/data/logistic_n10000_p20.csv"
-      runN 100 (LA.sumElements . coefficients) $
-        pure $! fst (fitGLMFull Binomial Logit xL yL)
-    "lasso" -> do
-      (xL, yL) <- readCsvXY "bench/data/lm_n10000_p50.csv"
-      runN 200 (LA.sumElements . rfBeta) $
-        pure $! Reg.fitRegularized (L1 0.1) xL yL
-    "psd" -> do
-      (xKR, _) <- readCsvXY "bench/data/kernel_n2000_p5.csv"
-      runN 30 LA.sumElements $
-        pure $! KD.pairwiseSqDist xKR
-    _ -> putStrLn $ "unknown target: " ++ target
-                 ++ "  (expected: kr | gram | glm | lasso | psd)"
diff --git a/bench/haskell/BenchRFFOOM.hs b/bench/haskell/BenchRFFOOM.hs
deleted file mode 100644
--- a/bench/haskell/BenchRFFOOM.hs
+++ /dev/null
@@ -1,54 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
--- | OOM regression bench for Phase 11b
--- (RFF.medianPairwiseDist + rbfKernelMat).
---
--- Pre-fix: n>=768 OOM-killed WSL2 (~7 GB+) inside maximizeMarginalLikRBFMV
--- because both internals built O(n²) Haskell-list intermediates with
--- @rows !! i@ index walks. Post-fix expectation:
---
---   * n=200  : sub-second, ~10 MB alloc
---   * n=400  : ~1 s,        ~50 MB alloc
---   * n=768  : few seconds, ~200 MB alloc (grid evals dominate; was OOM)
---
--- @maximizeMarginalLikRBFMV@ exercises both fixed paths heavily:
---
---   * 'medianPairwiseDist' once for the @ℓ@ centre.
---   * 'rbfKernelMat' inside @logMarginalLikRBFMV@ for every grid point.
-module Main where
-
-import qualified Numeric.LinearAlgebra as LA
-import qualified Hanalyze.Model.RFF    as RFF
-import           Data.Time.Clock        (getCurrentTime, diffUTCTime)
-import           System.IO              (hSetBuffering, BufferMode (..), stdout)
-import           System.Environment     (getArgs)
-
-mkX :: Int -> Int -> LA.Matrix Double
-mkX n p =
-  LA.reshape p
-    (LA.fromList [ sin (fromIntegral (i * 31 + j * 7)) / 3
-                 | i <- [0 .. n - 1], j <- [0 .. p - 1] ])
-
--- Tiny grid (3,2,2) so we evaluate logMarginalLikRBFMV / rbfKernelMat
--- only 24 times — enough to surface OOM behaviour but not enough to drown
--- the timing.
-benchOne :: Int -> IO ()
-benchOne n = do
-  let x = mkX n 8
-      y = LA.fromList [ sin (fromIntegral i / 5) | i <- [0 .. n - 1] ]
-  t0 <- getCurrentTime
-  let !r = RFF.maximizeMarginalLikRBFMV x y (Just (3, 2, 2))
-  t1 <- getCurrentTime
-  putStrLn $ "  n=" ++ show n
-          ++ "  ml=" ++ show (RFF.mlLogMlik r)
-          ++ "  ell=" ++ show (RFF.mlEll r)
-          ++ "  elapsed=" ++ show (diffUTCTime t1 t0)
-
-main :: IO ()
-main = do
-  hSetBuffering stdout NoBuffering
-  args <- getArgs
-  let ns = case args of
-             [] -> [50, 100, 200]
-             _  -> map read args
-  putStrLn "=== maximizeMarginalLikRBFMV (Stage1+2 with tiny grid 3*2*2) ==="
-  mapM_ benchOne ns
diff --git a/bench/haskell/BenchRegression.hs b/bench/haskell/BenchRegression.hs
deleted file mode 100644
--- a/bench/haskell/BenchRegression.hs
+++ /dev/null
@@ -1,190 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Regression benchmarks (B1).
---
--- LM, Logistic GLM, Poisson GLM, Gaussian LME (GLMM), Ridge, Lasso,
--- ElasticNet on the shared @bench/data/*.csv@ files. Outputs the unified
--- BenchRow CSV at @bench/results/haskell/regression.csv@.
-
-module Main where
-
-import qualified Data.Vector             as V
-import qualified Data.Text               as T
-import qualified Numeric.LinearAlgebra   as LA
-
-import           Hanalyze.Model.Core              (FitResult (..))
-import           Hanalyze.Model.LM                (fitLMVec)
-import           Hanalyze.Model.GLM               (Family (..), LinkFn (..), fitGLMFull)
-import qualified Hanalyze.Model.GLMM              as GLMM
-import qualified Hanalyze.Model.Regularized       as Reg
-
-import           BenchUtil
-
--- ---------------------------------------------------------------------------
--- Memoization-defeating wrappers.
---
--- GHC will common-subexpression-eliminate @fitLMVec xWith1 y@ across
--- iterations because the function is pure and the inputs are bound once
--- outside the loop. Each wrapper takes the iteration index as a phantom
--- argument and is marked NOINLINE so the optimizer cannot see through it.
--- ---------------------------------------------------------------------------
-
-{-# NOINLINE fitLMVecPhantom #-}
-fitLMVecPhantom :: Int -> LA.Matrix Double -> LA.Vector Double -> FitResult
-fitLMVecPhantom _ x y = fitLMVec x y
-
-{-# NOINLINE fitGLMFullPhantom #-}
-fitGLMFullPhantom :: Int -> Family -> LinkFn
-                  -> LA.Matrix Double -> LA.Vector Double -> FitResult
-fitGLMFullPhantom _ fam link x y = fst (fitGLMFull fam link x y)
-
-{-# NOINLINE fitLMEPhantom #-}
-fitLMEPhantom :: Int -> LA.Matrix Double -> LA.Vector Double
-              -> V.Vector Int -> V.Vector T.Text -> V.Vector Int
-              -> GLMM.GLMMResult
-fitLMEPhantom _ x y idx labels sizes = GLMM.fitLME x y idx labels sizes
-
-{-# NOINLINE fitRegPhantom #-}
-fitRegPhantom :: Int -> Reg.Penalty
-              -> LA.Matrix Double -> LA.Vector Double -> Reg.RegFit
--- Match the Python-side bench's @max_iter=200, tol=1e-4@. The previous
--- run used hanalyze's hardcoded @1000 / 1e-7@ which made tol 1000×
--- stricter than sklearn's bench setting and gave an unfair speed
--- comparison.
-fitRegPhantom _ pen x y = Reg.fitRegularizedWith 200 1e-4 pen x y
-
-main :: IO ()
-main = do
-  rows <- mconcat <$> sequence
-    [ benchLM "bench/data/lm_n1000_p5.csv"      "LM_n1000_p5"
-    , benchLM "bench/data/lm_n10000_p50.csv"    "LM_n10000_p50"
-    , benchLM "bench/data/lm_n100000_p100.csv"  "LM_n100000_p100"
-    , benchLogistic "bench/data/logistic_n2000_p10.csv"  "GLM_logit_n2000_p10"
-    , benchLogistic "bench/data/logistic_n10000_p20.csv" "GLM_logit_n10000_p20"
-    , benchPoisson  "bench/data/poisson_n2000_p10.csv"   "GLM_poisson_n2000_p10"
-    , benchPoisson  "bench/data/poisson_n10000_p20.csv"  "GLM_poisson_n10000_p20"
-    , benchLME "bench/data/glmm_n2000_p5_g20.csv"    "LME_n2000_p5_g20"
-    , benchLME "bench/data/glmm_n10000_p10_g50.csv"  "LME_n10000_p10_g50"
-    , benchRidge "bench/data/lm_n1000_p5.csv"     "Ridge_n1000_p5"      1.0
-    , benchRidge "bench/data/lm_n10000_p50.csv"   "Ridge_n10000_p50"    1.0
-    , benchLasso "bench/data/lm_n1000_p5.csv"     "Lasso_n1000_p5"      0.05
-    , benchLasso "bench/data/lm_n10000_p50.csv"   "Lasso_n10000_p50"    0.05
-    , benchEN    "bench/data/lm_n1000_p5.csv"     "EN_n1000_p5"         0.05 0.05
-    , benchEN    "bench/data/lm_n10000_p50.csv"   "EN_n10000_p50"       0.05 0.05
-    ]
-  writeRows "bench/results/haskell/regression.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/regression.csv"
-
--- ---------------------------------------------------------------------------
--- LM (OLS)
--- ---------------------------------------------------------------------------
-
-benchLM :: FilePath -> String -> IO [BenchRow]
-benchLM path name = do
-  (x, y) <- readCsvXY path
-  let xWith1 = LA.fromBlocks [[ LA.konst 1 (LA.rows x, 1), x ]]
-  (ms, fr) <- timeitTastyIO forceFR
-                (\i -> return $! fitLMVecPhantom i xWith1 y)
-  let yhat = LA.flatten (fitted fr LA.¿ [0])
-      r2   = computeR2 y yhat
-      rmse = sqrt (LA.sumElements ((y - yhat) ** 2) / fromIntegral (LA.size y))
-  return [ BenchRow "haskell" "regression" name ms r2 rmse "fitLM (OLS)" ]
-
--- ---------------------------------------------------------------------------
--- GLM (Logistic / Poisson, IRLS)
--- ---------------------------------------------------------------------------
-
-benchLogistic :: FilePath -> String -> IO [BenchRow]
-benchLogistic = benchGLM Binomial Logit "fitGLM Binomial/Logit"
-
-benchPoisson :: FilePath -> String -> IO [BenchRow]
-benchPoisson = benchGLM Poisson Log "fitGLM Poisson/Log"
-
-benchGLM
-  :: Family -> LinkFn -> String -> FilePath -> String -> IO [BenchRow]
-benchGLM fam link extra path name = do
-  (x, y) <- readCsvXY path
-  let xWith1 = LA.fromBlocks [[ LA.konst 1 (LA.rows x, 1), x ]]
-  (ms, fr) <- timeitTastyIO forceFR
-                (\i -> return $! fitGLMFullPhantom i fam link xWith1 y)
-  let yhat = LA.flatten (fitted fr LA.¿ [0])
-      r2   = computeR2 y yhat
-      rmse = sqrt (LA.sumElements ((y - yhat) ** 2) / fromIntegral (LA.size y))
-  return [ BenchRow "haskell" "regression" name ms r2 rmse extra ]
-
--- ---------------------------------------------------------------------------
--- LME (Gaussian, exact EM)
--- ---------------------------------------------------------------------------
-
-benchLME :: FilePath -> String -> IO [BenchRow]
-benchLME path name = do
-  (x, gIdxs, y) <- readCsvXYG path
-  let xWith1 = LA.fromBlocks [[ LA.konst 1 (LA.rows x, 1), x ]]
-      uniq   = uniqueInts (V.toList gIdxs)
-      labels = V.fromList (map (T.pack . ('g' :) . show) uniq)
-      sizes  = V.fromList
-        [ length (filter (== g) (V.toList gIdxs)) | g <- uniq ]
-  (ms, fit) <- timeitTastyIO (\f -> LA.sumElements (coefficients (GLMM.glmmFixed f)))
-                (\i -> return $! fitLMEPhantom i xWith1 y gIdxs labels sizes)
-  let yhat = LA.flatten (fitted (GLMM.glmmFixed fit) LA.¿ [0])
-      r2   = computeR2 y yhat
-  return
-    [ BenchRow "haskell" "regression" name ms r2 (GLMM.glmmICC fit)
-        "fitLME exact EM" ]
-  where
-    uniqueInts = foldr (\a acc -> if a `elem` acc then acc else a : acc) []
-
--- ---------------------------------------------------------------------------
--- Ridge / Lasso / ElasticNet
--- ---------------------------------------------------------------------------
-
-benchRidge :: FilePath -> String -> Double -> IO [BenchRow]
-benchRidge = benchPenalty (\lam -> Reg.L2 lam)
-                          (\lam -> "fitRidge lambda=" ++ show lam)
-
-benchLasso :: FilePath -> String -> Double -> IO [BenchRow]
-benchLasso = benchPenalty (\lam -> Reg.L1 lam)
-                          (\lam -> "fitLasso lambda=" ++ show lam ++ " (CD)")
-
-benchEN :: FilePath -> String -> Double -> Double -> IO [BenchRow]
-benchEN path name lam1 lam2 = do
-  (x, y) <- readCsvXY path
-  (ms, fr) <- timeitTastyIO forceReg
-                (\i -> return $! fitRegPhantom i (Reg.ElasticNet lam1 lam2) x y)
-  let yhat = Reg.predictRegularized fr x
-      r2   = computeR2 y yhat
-      rmse = sqrt (LA.sumElements ((y - yhat) ** 2) / fromIntegral (LA.size y))
-  return [ BenchRow "haskell" "regression" name ms r2 rmse
-                     ("fitElasticNet lam1=" ++ show lam1
-                      ++ " lam2=" ++ show lam2) ]
-
-benchPenalty
-  :: (Double -> Reg.Penalty)
-  -> (Double -> String)
-  -> FilePath -> String -> Double -> IO [BenchRow]
-benchPenalty mkPen mkExtra path name lam = do
-  (x, y) <- readCsvXY path
-  (ms, fr) <- timeitTastyIO forceReg
-                (\i -> return $! fitRegPhantom i (mkPen lam) x y)
-  let yhat = Reg.predictRegularized fr x
-      r2   = computeR2 y yhat
-      rmse = sqrt (LA.sumElements ((y - yhat) ** 2) / fromIntegral (LA.size y))
-  return [ BenchRow "haskell" "regression" name ms r2 rmse (mkExtra lam) ]
-
--- ---------------------------------------------------------------------------
-
-forceFR :: FitResult -> Double
-forceFR fr = LA.sumElements (coefficients fr)
-           + LA.sumElements (residuals fr)
-
-forceReg :: Reg.RegFit -> Double
-forceReg fr = LA.sumElements (Reg.rfBeta fr)
-            + LA.sumElements (Reg.rfYHat fr)
-
-computeR2 :: LA.Vector Double -> LA.Vector Double -> Double
-computeR2 y yhat =
-  let mu  = LA.sumElements y / fromIntegral (LA.size y)
-      sst = LA.sumElements ((y - LA.konst mu (LA.size y)) ** 2)
-      sse = LA.sumElements ((y - yhat) ** 2)
-  in if sst == 0 then 0 else 1 - sse / sst
diff --git a/bench/haskell/BenchRegrid.hs b/bench/haskell/BenchRegrid.hs
deleted file mode 100644
--- a/bench/haskell/BenchRegrid.hs
+++ /dev/null
@@ -1,43 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | B13 Regrid ベンチ。
---
--- @data/io/potential_long_jagged.csv@ (21 dose × ~80 z 点、name で id) を
--- 共通 grid (N=30) に揃える。Python 側は pandas + scipy.interpolate で
--- 同等処理を合成して比較。
---
--- 出力: bench/results/haskell/regrid.csv
-module Main where
-
-import qualified DataFrame.Operations.Core     as DX
-import qualified Hanalyze.DataIO.Preprocess          as Pre
-import qualified Hanalyze.Stat.Interpolate           as IL
-import qualified Hanalyze.Stat.AdaptiveGrid          as AG
-import           Hanalyze.DataIO.CSV                 (loadAuto)
-
-import           BenchUtil
-
-main :: IO ()
-main = do
-  -- Load once (the load itself is not what we benchmark).
-  edf <- loadAuto "data/io/potential_long_jagged.csv"
-  case edf of
-    Left err -> error ("regrid bench: failed to load: " ++ show err)
-    Right df -> do
-      let opts = Pre.defaultRegridOpts
-                   { Pre.roInterp     = IL.PCHIP
-                   , Pre.roGridKind   = AG.Adaptive
-                   , Pre.roN          = 30
-                   , Pre.roZBoundsMode = Pre.ZIntersection
-                   }
-          run :: Int -> IO Pre.RegridResult
-          run _ = return (Pre.regridLong "name" "z" "y" opts df)
-          probe r =
-            -- Force the full regridded DataFrame by counting rows.
-            fromIntegral (DX.nRows (Pre.rrDataFrame r))
-      (ms, _r) <- timeitTastyIO probe run
-      let row = BenchRow "haskell" "regrid"
-                  "Regrid_long_jagged_PCHIP_N30" ms 0 0
-                  "regridLong PCHIP+Adaptive N=30 ZIntersection on potential_long_jagged"
-      writeRows "bench/results/haskell/regrid.csv" [row]
-      putStrLn "wrote 1 row → bench/results/haskell/regrid.csv"
diff --git a/bench/haskell/BenchStatUtil.hs b/bench/haskell/BenchStatUtil.hs
deleted file mode 100644
--- a/bench/haskell/BenchStatUtil.hs
+++ /dev/null
@@ -1,188 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | B10 Stat util ベンチ。
---
---   * Bootstrap CI: B=1000 resamples on n=1000 sample mean
---   * Welch's t-test: two samples n=500 each
---   * Mann-Whitney U: two samples n=500 each
---   * Multiple testing (BH): 1000 p-values
---   * Halton sequence: n=10000 d=5
---   * AUC + log-loss: n=10000 binary predictions
---   * k-fold split: 5-fold on n=1000
---
--- 出力: bench/results/haskell/stat_util.csv
-module Main where
-
-import qualified Data.Vector             as V
-import qualified Data.Vector.Unboxed     as VU
-import qualified Numeric.LinearAlgebra   as LA
-import qualified System.Random.MWC       as MWC
-
-import           Hanalyze.Stat.Bootstrap          (bootstrapMeanCI)
-import           Hanalyze.Stat.Test               (Alternative (..),
-                                          tTestWelch, mannWhitneyU,
-                                          kolmogorovSmirnovNormal,
-                                          TestResult (..))
-import           Hanalyze.Stat.MultipleTesting    (benjaminiHochbergV)
-import           Hanalyze.Stat.QuasiRandom        (haltonMatrix)
-import           Hanalyze.Stat.ClassMetrics       (auc, logLoss)
-import           Hanalyze.Stat.CV                 (kFold)
-
-import           BenchUtil
-
--- ---------------------------------------------------------------------------
--- Deterministic data generators (no RNG dependency on values)
--- ---------------------------------------------------------------------------
-
--- Sin-of-i deterministic-ish vector ~ N(0, 1) in distribution.
-syntheticVec :: Int -> Int -> LA.Vector Double
-syntheticVec n offset =
-  LA.fromList
-    [ sin (fromIntegral (i + offset) * 0.71)
-        + 0.4 * sin (fromIntegral (3 * i + offset))
-    | i <- [0 .. n - 1] ]
-
-shifted :: Double -> LA.Vector Double -> LA.Vector Double
-shifted c v = v + LA.scalar c
-
--- ---------------------------------------------------------------------------
-
-benchBootstrap :: IO [BenchRow]
-benchBootstrap = do
-  let xs = syntheticVec 1000 0
-      run :: Int -> IO (Double, Double)
-      run _ = do
-        gen <- MWC.create
-        -- P40 (2026-05-07): specialised mean-bootstrap path. Generic
-        -- bootstrapCI invokes the statistic per resample (B times)
-        -- against a freshly-frozen length-n Vector; this version
-        -- uses one (B×n) buffer and one BLAS GEMV for all B means.
-        bootstrapMeanCI 1000 0.95 xs gen
-      probe (lo, hi) = hi - lo
-  (ms, (lo, hi)) <- timeitTastyIO probe run
-  return [ BenchRow "haskell" "stat_util"
-            "Bootstrap_mean_n1000_B1000" ms (hi - lo) lo
-            ("95% CI for mean of n=1000 with B=1000; ["
-             ++ show lo ++ ", " ++ show hi ++ "]") ]
-
-benchTTestWelch :: IO [BenchRow]
-benchTTestWelch = do
-  let xs = syntheticVec 500 0
-      ys = shifted 0.3 (syntheticVec 500 1000)
-      run :: Int -> IO TestResult
-      run _ = return (tTestWelch xs ys TwoSided)
-      probe r = trStatistic r
-  (ms, r) <- timeitTastyIO probe run
-  return [ BenchRow "haskell" "stat_util"
-            "Welch_ttest_n500x500" ms (trStatistic r) (trPValue r)
-            ("Welch's two-sample t-test n=500+500; t="
-             ++ show (trStatistic r) ++ " p=" ++ show (trPValue r)) ]
-
-benchMannWhitney :: IO [BenchRow]
-benchMannWhitney = do
-  let xs = syntheticVec 500 0
-      ys = shifted 0.3 (syntheticVec 500 1000)
-      run :: Int -> IO TestResult
-      run _ = return (mannWhitneyU xs ys TwoSided)
-      probe r = trStatistic r
-  (ms, r) <- timeitTastyIO probe run
-  return [ BenchRow "haskell" "stat_util"
-            "MannWhitneyU_n500x500" ms (trStatistic r) (trPValue r)
-            ("Mann-Whitney U n=500+500; U=" ++ show (trStatistic r)
-             ++ " p=" ++ show (trPValue r)) ]
-
-benchKS :: IO [BenchRow]
-benchKS = do
-  let xs = syntheticVec 1000 0
-      run :: Int -> IO TestResult
-      run _ = return (kolmogorovSmirnovNormal xs)
-      probe r = trStatistic r
-  (ms, r) <- timeitTastyIO probe run
-  return [ BenchRow "haskell" "stat_util"
-            "KS_normal_n1000" ms (trStatistic r) (trPValue r)
-            ("KS test against Normal(μ̂, σ̂); D="
-             ++ show (trStatistic r) ++ " p=" ++ show (trPValue r)) ]
-
-benchBH :: IO [BenchRow]
-benchBH = do
-  -- Mix of "true null" (uniform) and "alternative" (small) p-values.
-  -- P39 (2026-05-07): pre-construct the input as a 'VU.Vector Double'
-  -- and call 'benjaminiHochbergV' directly. Matches Python's harness
-  -- (which has a pre-built @np.array@ of p-values), so the timer only
-  -- captures the BH algorithm itself rather than @[Double]@↔Vector
-  -- conversion overhead.
-  let n  = 1000
-      psV = VU.generate n $ \i ->
-              if i < 100 then 0.001 + 0.0001 * fromIntegral i
-                         else 0.5 + 0.4 * sin (fromIntegral i)
-      run :: Int -> IO (VU.Vector Double)
-      run _ = return (benjaminiHochbergV psV)
-      probe r = VU.sum r / fromIntegral (VU.length r)
-  (ms, adjV) <- timeitTastyIO probe run
-  let nSig = VU.length (VU.filter (< 0.05) adjV)
-  return [ BenchRow "haskell" "stat_util"
-            "BH_pAdjust_n1000" ms (fromIntegral nSig) 0
-            ("BH on n=1000 p-values (VU API); significant="
-             ++ show nSig) ]
-
-benchHalton :: IO [BenchRow]
-benchHalton = do
-  -- P41 (2026-05-07): use the flat Matrix API to match Python's
-  -- @ndarray@ baseline (scipy returns @(n, d)@ ndarray, summed via
-  -- @pts.sum()@). The legacy @[[Double]]@ form added ~1.3 ms of
-  -- @n × d@ list-cell + boxed-Double allocation.
-  let run :: Int -> IO (LA.Matrix Double)
-      run _ = return (haltonMatrix 10000 5)
-      -- Force every element via BLAS sumElements (same as np.sum).
-      probe = LA.sumElements
-  (ms, mat) <- timeitTastyIO probe run
-  return [ BenchRow "haskell" "stat_util"
-            "Halton_n10000_d5" ms (fromIntegral (LA.rows mat)) 5
-            "Halton quasi-random n=10000 d=5 (flat Matrix)" ]
-
-benchAUC :: IO [BenchRow]
-benchAUC = do
-  let n      = 10000
-      -- Deterministic logits from sin(i); labels = (logit > 0).
-      logits = [ sin (fromIntegral i * 0.31) | i <- [0 .. n - 1] ]
-      probs  = [ 1 / (1 + exp (-z)) | z <- logits ]
-      labels = [ if z > 0 then 1 else 0 :: Int | z <- logits ]
-      run :: Int -> IO (Double, Double)
-      run _ = return (auc labels probs, logLoss labels probs)
-      probe = fst
-  (ms, (a, ll)) <- timeitTastyIO probe run
-  return [ BenchRow "haskell" "stat_util"
-            "AUC_LogLoss_n10000" ms a ll
-            ("AUC=" ++ show a ++ " logLoss=" ++ show ll) ]
-
-benchKFold :: IO [BenchRow]
-benchKFold = do
-  let run :: Int -> IO Int
-      run _ = do
-        gen <- MWC.create
-        folds <- kFold 5 1000 gen
-        -- Force the full list of fold indices.
-        return $! sum [ length (fst f) + length (snd f) | f <- folds ]
-      probe = fromIntegral
-  (ms, k) <- timeitTastyIO probe run
-  return [ BenchRow "haskell" "stat_util"
-            "KFold_5_n1000" ms (fromIntegral k) 0
-            ("k-fold split: 5 folds on n=1000") ]
-
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  rows <- mconcat <$> sequence
-    [ benchBootstrap
-    , benchTTestWelch
-    , benchMannWhitney
-    , benchKS
-    , benchBH
-    , benchHalton
-    , benchAUC
-    , benchKFold
-    ]
-  writeRows "bench/results/haskell/stat_util.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/stat_util.csv"
diff --git a/bench/haskell/BenchSurvTS.hs b/bench/haskell/BenchSurvTS.hs
deleted file mode 100644
--- a/bench/haskell/BenchSurvTS.hs
+++ /dev/null
@@ -1,174 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Survival / time-series / quantile / GAM / spline benchmarks (B8).
---
--- Compares hanalyze against statsmodels (ARIMA, quantile regression),
--- lifelines (Cox PH, Kaplan-Meier), pygam (GAM), and scipy.interpolate
--- (1D spline interpolation).
---
--- Outputs the unified BenchRow CSV at @bench/results/haskell/survts.csv@.
-module Main where
-
-import qualified Data.Vector             as V
-import qualified Numeric.LinearAlgebra   as LA
-import           System.Random           (mkStdGen, randomR)
-
-import qualified Hanalyze.Model.TimeSeries        as TS
-import qualified Hanalyze.Model.Survival          as Surv
-import qualified Hanalyze.Model.Quantile          as QR
-import qualified Hanalyze.Model.GAM               as GAM
-import qualified Hanalyze.Stat.Interpolate        as Interp
-
-import           BenchUtil
-
--- ---------------------------------------------------------------------------
--- Synthetic data generators (deterministic seeds)
--- ---------------------------------------------------------------------------
-
--- | AR(1) series with phi=0.7 and Gaussian noise.
-genAR1 :: Int -> LA.Vector Double
-genAR1 n = LA.fromList (go (mkStdGen 42) 0.0 [])
-  where
-    go _ _ acc | length acc >= n = reverse acc
-    go g x acc =
-      let (z, g') = randomR (-3.0, 3.0) g
-          x'     = 0.7 * x + 0.3 * z
-      in go g' x' (x' : acc)
-
--- | Survival data: exponential time + 30% censoring.
-genSurv :: Int -> ([LA.Vector Double], [Surv.SurvSample])
-genSurv n =
-  let g0 = mkStdGen 7
-      (rows, _) = foldr step ([], g0) [1 .. n]
-      step _ (acc, g) =
-        let (x1, g1) = randomR (-1.0 :: Double, 1.0) g
-            (x2, g2) = randomR (-1.0 :: Double, 1.0) g1
-            (u,  g3) = randomR (0.01 :: Double, 1.0) g2
-            t        = -log u / exp (0.5 * x1 - 0.3 * x2)
-            (c,  g4) = randomR (0.0 :: Double, 1.0) g3
-            ev       = if c < 0.7 then Surv.Observed else Surv.Censored
-        in ((LA.fromList [x1, x2], Surv.SurvSample t ev) : acc, g4)
-  in unzip rows
-
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  rows <- mconcat <$> sequence
-    [ benchARIMA   "ARIMA_n1000_pdq111"
-    , benchCoxPH   "CoxPH_n2000_p2_30pct_censor"
-    , benchKM      "KM_n2000"
-    , benchQuant   "Quantile_n10000_p20_tau0.5"
-    , benchGAM     "GAM_n2000_p2_d3_k5"
-    , benchSpline  "Spline_PCHIP_n1000"
-    ]
-  writeRows "bench/results/haskell/survts.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/survts.csv"
-
--- ---------------------------------------------------------------------------
--- ARIMA(1,1,1)
--- ---------------------------------------------------------------------------
-
-benchARIMA :: String -> IO [BenchRow]
-benchARIMA name = do
-  let y = genAR1 1000
-      run :: Int -> IO TS.ARIMAFit
-      run _ = pure $! TS.fitARIMA 1 1 1 y
-  (ms, fit) <- timeitTastyIO probe run
-  return [ BenchRow "haskell" "survts" name ms 0 0
-            ("Hanalyze.Model.TimeSeries.fitARIMA p=1 d=1 q=1 n=1000") ]
-  where
-    probe f = LA.sumElements (TS.forecastARIMA f 10)
-
--- ---------------------------------------------------------------------------
--- Cox PH
--- ---------------------------------------------------------------------------
-
-benchCoxPH :: String -> IO [BenchRow]
-benchCoxPH name = do
-  let (xs, samples) = genSurv 2000
-      run :: Int -> IO Surv.CoxFit
-      run _ = pure $! Surv.coxPH xs samples
-  (ms, fit) <- timeitTastyIO probe run
-  let beta = Surv.coxBeta fit
-      b1 = beta LA.! 0
-      b2 = beta LA.! 1
-  return [ BenchRow "haskell" "survts" name ms b1 b2
-            ("Hanalyze.Model.Survival.coxPH n=2000 p=2 (Newton-Raphson)") ]
-  where
-    probe f = LA.sumElements (Surv.coxBeta f)
-
--- ---------------------------------------------------------------------------
--- Kaplan-Meier
--- ---------------------------------------------------------------------------
-
-benchKM :: String -> IO [BenchRow]
-benchKM name = do
-  let (_, samples) = genSurv 2000
-      run :: Int -> IO Surv.KMResult
-      run _ = pure $! Surv.kaplanMeier samples
-  (ms, res) <- timeitTastyIO probe run
-  let ts   = Surv.kmrTimes res
-      surv = Surv.kmrSurvival res
-      tEnd = if null ts then 0 else last ts
-      sEnd = if null surv then 1 else last surv
-  return [ BenchRow "haskell" "survts" name ms tEnd sEnd
-            ("Hanalyze.Model.Survival.kaplanMeier n=2000") ]
-  where
-    probe r = sum (Surv.kmrSurvival r)
-
--- ---------------------------------------------------------------------------
--- Quantile regression (median, tau=0.5)
--- ---------------------------------------------------------------------------
-
-benchQuant :: String -> IO [BenchRow]
-benchQuant name = do
-  (x, y) <- readCsvXY "bench/data/lm_n10000_p50.csv"
-  -- Use first 20 columns for fair comparison with Python.
-  let xCut = LA.takeColumns 20 x
-      run :: Int -> IO QR.QRFit
-      run _ = pure $! QR.fitQuantile 0.5 xCut y
-  (ms, fit) <- timeitTastyIO probe run
-  return [ BenchRow "haskell" "survts" name ms 0 0
-            ("Hanalyze.Model.Quantile.fitQuantile tau=0.5 n=10000 p=20") ]
-  where
-    probe f = LA.sumElements (QR.qfBeta f)
-
--- ---------------------------------------------------------------------------
--- GAM (degree=3, knots=5, two predictors)
--- ---------------------------------------------------------------------------
-
-benchGAM :: String -> IO [BenchRow]
-benchGAM name = do
-  (x, y) <- readCsvXY "bench/data/kernel_n2000_p5.csv"
-  let cols = LA.toColumns x
-      x1   = V.fromList (LA.toList (head cols))
-      x2   = V.fromList (LA.toList (cols !! 1))
-      yV   = V.fromList (LA.toList y)
-      run :: Int -> IO GAM.GAMFit
-      run _ = pure $! GAM.fitGAM 3 5 1.0 [x1, x2] yV
-  (ms, fit) <- timeitTastyIO probe run
-  return [ BenchRow "haskell" "survts" name ms 0 0
-            ("Hanalyze.Model.GAM.fitGAM degree=3 knots=5 lambda=1.0 n=2000 p=2") ]
-  where
-    probe f = LA.sumElements (GAM.gamYHat f)
-
--- ---------------------------------------------------------------------------
--- 1D spline interpolation (PCHIP)
--- ---------------------------------------------------------------------------
-
-benchSpline :: String -> IO [BenchRow]
-benchSpline name = do
-  let n = 1000
-      xs = [fromIntegral i / fromIntegral (n - 1) | i <- [0 .. n - 1]]
-      ys = map (\xi -> sin (3 * xi) + 0.1 * cos (15 * xi)) xs
-      pts = zip xs ys
-      f = Interp.interp1d Interp.PCHIP pts
-      -- Evaluate at 5000 query points.
-      qs = [fromIntegral i / 4999.0 | i <- [0 .. 4999 :: Int]]
-      run :: Int -> IO Double
-      run _ = pure $! sum [f q | q <- qs]
-  (ms, total) <- timeitTastyIO id run
-  return [ BenchRow "haskell" "survts" name ms total 0
-            ("Hanalyze.Stat.Interpolate.interp1d PCHIP, build n=1000 + eval @5000 pts") ]
diff --git a/bench/haskell/BenchTSExtras.hs b/bench/haskell/BenchTSExtras.hs
deleted file mode 100644
--- a/bench/haskell/BenchTSExtras.hs
+++ /dev/null
@@ -1,134 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | B8 残: Holt-Winters / GAM / Spline ベンチ。
---
---   * Holt-Winters seasonal n=500 period=12 (Additive)
---   * GAM n=2000 splines=10 (1D)
---   * Interp1d (Linear / NaturalSpline / PCHIP) on n=1000 grid → eval 5000 pts
---
--- 出力: bench/results/haskell/ts_extras.csv
-module Main where
-
-import qualified Data.Vector             as V
-import qualified Numeric.LinearAlgebra   as LA
-
-import           Hanalyze.Model.TimeSeries        (HWMode (..), holtWinters, hwFitted)
-import           Hanalyze.Model.GAM               (fitGAM, gamYHat)
-import           Hanalyze.Stat.Interpolate        (InterpKind (..), interp1d)
-
-import           BenchUtil
-
--- ---------------------------------------------------------------------------
--- Data generators (deterministic, no RNG).
--- ---------------------------------------------------------------------------
-
--- Seasonal series of length n with period 12: y_t = trend + sin(2π t/12) + ε
--- where ε is small deterministic noise (sinusoidal with different period).
-seasonalSeries :: Int -> LA.Vector Double
-seasonalSeries n =
-  LA.fromList
-    [ 0.05 * fromIntegral t
-        + 2.0 * sin (2 * pi * fromIntegral t / 12.0)
-        + 0.1 * sin (fromIntegral t * 0.7)
-    | t <- [0 .. n - 1] ]
-
--- 1D smooth-with-bumps function for GAM / interpolation.
-smoothFn :: Double -> Double
-smoothFn x = sin (2 * x) + 0.5 * x + 0.3 * sin (5 * x)
-
-gamData :: Int -> ([V.Vector Double], V.Vector Double)
-gamData n =
-  let xs = V.fromList [ -3.0 + 6.0 * fromIntegral i / fromIntegral (n - 1)
-                       | i <- [0 .. n - 1] ]
-      ys = V.map smoothFn xs
-  in ([xs], ys)
-
--- Returns (knots, fineEvalXs) — scattered knot data + evaluation grid.
-interpData :: Int -> Int -> ([(Double, Double)], [Double])
-interpData nKnots nEval =
-  let knots = [ (xi, smoothFn xi)
-              | i <- [0 .. nKnots - 1]
-              , let xi = -3.0 + 6.0 * fromIntegral i / fromIntegral (nKnots - 1) ]
-      grid  = [ -2.9 + 5.8 * fromIntegral i / fromIntegral (nEval - 1)
-              | i <- [0 .. nEval - 1] ]
-  in (knots, grid)
-
--- ---------------------------------------------------------------------------
--- Holt-Winters
--- ---------------------------------------------------------------------------
-
-benchHW :: IO [BenchRow]
-benchHW = do
-  let !y = seasonalSeries 500
-      run :: Int -> IO Double
-      run _ = do
-        let fit  = holtWinters HWAdditive 12 y
-            yhat = hwFitted fit
-            r    = yhat - y
-            err  = LA.sumElements (LA.cmap (\d -> d * d) r)
-        return err
-      probe = id
-  (ms, e) <- timeitTastyIO probe run
-  let n = LA.size y
-      rmse = sqrt (e / fromIntegral n)
-  return [ BenchRow "haskell" "ts_extras"
-            "HW_seasonal_n500_p12_additive" ms rmse 0
-            ("Holt-Winters additive period=12 RMSE=" ++ show rmse) ]
-
--- ---------------------------------------------------------------------------
--- GAM
--- ---------------------------------------------------------------------------
-
-benchGAM :: IO [BenchRow]
-benchGAM = do
-  let (xss, !y) = gamData 2000
-      yLA = LA.fromList (V.toList y)
-      run :: Int -> IO Double
-      run _ = do
-        let fit  = fitGAM 3 10 1e-3 xss y
-            yhat = gamYHat fit
-            r    = yhat - yLA
-            err  = LA.sumElements (LA.cmap (\d -> d * d) r)
-        return err
-      probe = id
-  (ms, e) <- timeitTastyIO probe run
-  let n = V.length y
-      rmse = sqrt (e / fromIntegral n)
-  return [ BenchRow "haskell" "ts_extras"
-            "GAM_n2000_splines10_1D" ms rmse 0
-            ("GAM degree=3 nKnots=10 λ=1e-3 RMSE=" ++ show rmse) ]
-
--- ---------------------------------------------------------------------------
--- Spline interpolation: Linear / NaturalSpline / PCHIP each evaluated on 5000 pts
--- ---------------------------------------------------------------------------
-
-benchInterp :: InterpKind -> String -> IO [BenchRow]
-benchInterp kind label = do
-  let nKnots = 1000
-      nEval  = 5000
-      (knots, grid) = interpData nKnots nEval
-      run :: Int -> IO Double
-      run _ = do
-        let f = interp1d kind knots
-            ys = map f grid
-        return (sum ys)
-      probe = id
-  (ms, _) <- timeitTastyIO probe run
-  return [ BenchRow "haskell" "ts_extras"
-            ("Interp1D_" ++ label ++ "_knots1000_eval5000") ms 0 0
-            (label ++ " interpolation knots=1000 eval=5000") ]
-
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  rows <- mconcat <$> sequence
-    [ benchHW
-    , benchGAM
-    , benchInterp Linear        "Linear"
-    , benchInterp NaturalSpline "NatSpline"
-    , benchInterp PCHIP         "PCHIP"
-    ]
-  writeRows "bench/results/haskell/ts_extras.csv" rows
-  putStrLn $ "wrote " ++ show (length rows)
-          ++ " rows → bench/results/haskell/ts_extras.csv"
diff --git a/bench/haskell/BenchTasty.hs b/bench/haskell/BenchTasty.hs
deleted file mode 100644
--- a/bench/haskell/BenchTasty.hs
+++ /dev/null
@@ -1,73 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | tasty-bench based microbenchmarks for hot paths affected by
--- Phase 1-7 perf optimizations (-O2, StrictData, INLINE).
---
--- Run with:
---
--- > OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 \
--- >   cabal run bench-tasty -- --csv bench/results/tasty.csv
---
--- The CSV output is comparable across builds (same data fixtures and
--- single-thread BLAS). Use @--baseline=path.csv --fail-if-slower=N@ to
--- guard against regressions.
-module Main where
-
-import qualified Numeric.LinearAlgebra   as LA
-import           Test.Tasty.Bench
-
-import           Hanalyze.Model.Core              (coefficients)
-import           Hanalyze.Model.GLM               (Family (..), LinkFn (..), fitGLMFull)
-import qualified Hanalyze.Model.Regularized       as Reg
-import           Hanalyze.Model.Regularized       (Penalty (..), rfBeta)
-import qualified Hanalyze.Model.KernelRegression            as Kn
-import qualified Hanalyze.Stat.Cholesky           as Chol
-import qualified Hanalyze.Stat.KernelDist         as KD
-import           Data.Maybe              (fromMaybe)
-
-import           BenchUtil               (readCsvXY)
-
-makeSpd :: Int -> LA.Matrix Double
-makeSpd n =
-  let g = LA.build (n, n)
-            (\i j -> exp (-(((i - j) * (i - j)) / fromIntegral n)))
-  in g + LA.scale 1e-3 (LA.ident n)
-
-main :: IO ()
-main = do
-  (xLogi, yLogi) <- readCsvXY "bench/data/logistic_n10000_p20.csv"
-  (xKR,   yKR)   <- readCsvXY "bench/data/kernel_n1000_p5.csv"
-  (xLas,  yLas)  <- readCsvXY "bench/data/lm_n10000_p50.csv"
-  let yKRMat   = LA.asColumn yKR
-      spd500   = makeSpd 500
-      spdRhs   = LA.asColumn (LA.fromList (replicate 500 1.0))
-      kdInput2k = LA.fromLists
-        [[fromIntegral i + 0.1 * fromIntegral j | j <- [0 .. 4]] | i <- [0 .. 1999]]
-
-  defaultMain
-    [ bgroup "regression"
-        [ bench "GLM_logit_n10000_p20" $
-            nf (\() -> LA.sumElements
-                         (coefficients (fst (fitGLMFull Binomial Logit
-                                              xLogi yLogi)))) ()
-        , bench "Lasso_n10000_p50_lam0.1" $
-            nf (\() -> LA.sumElements
-                         (rfBeta (Reg.fitRegularized (L1 0.1) xLas yLas))) ()
-        ]
-    , bgroup "kernel"
-        [ bench "KernelRidgeMV_n1000_p5_RBF" $
-            nf (\() -> LA.sumElements
-                         (Kn.krmvAlpha (Kn.kernelRidgeMV Kn.Gaussian 1.0 1e-3
-                                          xKR yKRMat))) ()
-        , bench "pairwiseSqDist_n2000_p5" $
-            nf (LA.sumElements . KD.pairwiseSqDist) kdInput2k
-        , bench "gramMatrixMV_n1000_p5_RBF" $
-            nf (\() -> LA.sumElements (Kn.gramMatrixMV Kn.Gaussian 1.0 xKR)) ()
-        ]
-    , bgroup "cholesky"
-        [ bench "cholSolve_n500" $
-            nf (\() -> LA.sumElements
-                         (fromMaybe (LA.scalar 0)
-                                    (Chol.cholSolve spd500 spdRhs))) ()
-        ]
-    ]
diff --git a/bench/haskell/BenchTier12.hs b/bench/haskell/BenchTier12.hs
deleted file mode 100644
--- a/bench/haskell/BenchTier12.hs
+++ /dev/null
@@ -1,563 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse -Wno-incomplete-uni-patterns #-}
--- | Phase 9-16 (Tier 1 + Tier 2) 機能の Python 比較ベンチ (Haskell 側)。
---
--- Python 比較対象あり:
---   * PLS            → sklearn.cross_decomposition.PLSRegression
---   * LDA / QDA      → sklearn.discriminant_analysis.{Linear,Quadratic}DA
---   * HCluster Ward  → scipy.cluster.hierarchy.linkage(method='ward')
---   * Friedman       → scipy.stats.friedmanchisquare
---   * RFClassifier   → sklearn.ensemble.RandomForestClassifier
---   * MLPRegressor   → sklearn.neural_network.MLPRegressor
---
--- Haskell-only (Python 直接等価なし or 重い依存):
---   * TOST、 AFT (lifelines は重)、 EWMA / CUSUM、 GaugeRR、
---     ProcessCapability 非正規、 DoE 診断、 I/E-optimal、 Kalman (statsmodels
---     入れない方針)、 Fit Y by X (wrapper)
---
--- 共通入力 CSV は本ファイルで Halton 列から生成し bench/data/tier12_*.csv に
--- 書き出す。 Python 側は同じ CSV を読む。
-module Main where
-
-import qualified Data.Text               as T
-import qualified Data.Vector             as V
-import qualified Data.Vector.Unboxed     as VU
-import qualified Numeric.LinearAlgebra   as LA
-import qualified System.Random.MWC       as MWC
-import           System.Directory        (createDirectoryIfMissing)
-import           System.IO               (withFile, IOMode (..), hPutStrLn)
-import           Text.Printf             (printf)
-
-import qualified Hanalyze.Model.PLS                    as PLS
-import qualified Hanalyze.Model.Discriminant           as Disc
-import qualified Hanalyze.Model.HierarchicalCluster    as HC
-import qualified Hanalyze.Model.AFT                    as AFT
-import qualified Hanalyze.Model.RandomForestClassifier as RFC
-import qualified Hanalyze.Model.NeuralNetwork          as NN
-import qualified Hanalyze.Model.StateSpace             as SS
-import qualified Hanalyze.Design.GaugeRR               as GRR
-import qualified Hanalyze.Design.Quality               as Quality
-import qualified Hanalyze.Design.Diagnostics           as DDiag
-import qualified Hanalyze.Design.Optimal               as Opt
-import qualified Hanalyze.Stat.SPC                     as SPC
-import qualified Hanalyze.Stat.Test                    as ST
-import qualified Hanalyze.Stat.QuasiRandom             as QR
-import qualified Hanalyze.Model.Weibull                as Wei
-
-import           BenchUtil
-
--- ===========================================================================
--- 共通 helper
--- ===========================================================================
-
--- 決定的 N(0,1)風列 (Box-Muller via Halton)
-gaussianHalton :: Int -> Int -> Int -> [Double]
-gaussianHalton primeU primeV n =
-  let us = [ QR.radicalInverse primeU i | i <- [1 .. n] ]
-      vs = [ QR.radicalInverse primeV i | i <- [1 .. n] ]
-  in zipWith (\u v -> sqrt (-2 * log (u + 1e-12)) * cos (2 * pi * v)) us vs
-
-writeMatrixCSV :: FilePath -> [String] -> [[Double]] -> IO ()
-writeMatrixCSV path header rows = withFile path WriteMode $ \h -> do
-  hPutStrLn h (commaJoin header)
-  mapM_ (\r -> hPutStrLn h (commaJoin (map (printf "%.10g") r))) rows
-  where
-    commaJoin :: [String] -> String
-    commaJoin = foldr1 (\a b -> a ++ "," ++ b)
-
--- ===========================================================================
--- PLS
--- ===========================================================================
-
--- y = x1 + 0.5*x2 (+ noise),  p = 10 (8 ノイズ列)
-plsData :: Int -> Int -> (LA.Matrix Double, LA.Vector Double)
-plsData n p =
-  let cols = [ LA.fromList (gaussianHalton (primeAt j) (primeAt (j + 1)) n)
-             | j <- [0 .. p - 1] ]
-      x    = LA.fromColumns cols
-      x0   = LA.flatten (x LA.¿ [0])
-      x1   = LA.flatten (x LA.¿ [1])
-      eps  = LA.fromList (gaussianHalton 19 23 n)
-      y    = x0 + LA.scale 0.5 x1 + LA.scale 0.1 eps
-  in (x, y)
-  where
-    primes :: [Int]
-    primes = [2,3,5,7,11,13,17,19,23,29,31,37,41,43]
-    primeAt :: Int -> Int
-    primeAt k = primes !! (k `mod` length primes)
-
-writePLSCSV :: FilePath -> LA.Matrix Double -> LA.Vector Double -> IO ()
-writePLSCSV path x y =
-  let p = LA.cols x
-      header = [ "x" ++ show j | j <- [0 .. p - 1] ] ++ ["y"]
-      rows = [ [ LA.atIndex x (i, j) | j <- [0 .. p - 1] ]
-                 ++ [LA.atIndex y i]
-             | i <- [0 .. LA.rows x - 1] ]
-  in writeMatrixCSV path header rows
-
-{-# NOINLINE plsPhantom #-}
-plsPhantom :: Int -> LA.Matrix Double -> LA.Vector Double -> Either String PLS.PLSFit
-plsPhantom _ x y = case PLS.fitPLS1 (PLS.defaultPLSConfig { PLS.plsN_Components = 3 }) x y of
-  Left e  -> Left (show e)
-  Right f -> Right f
-
-benchPLS :: Int -> Int -> IO BenchRow
-benchPLS n p = do
-  let (x, y) = plsData n p
-  (medMs, res) <- timeitIO 5 forceR (\i -> pure (plsPhantom i x y))
-  let nrmse = case res of
-        Right f ->
-          let yhat = PLS.predictPLS1 f x
-              d = yhat - y
-          in sqrt (LA.sumElements (d * d) / fromIntegral n)
-                / (LA.maxElement y - LA.minElement y + 1e-12)
-        Left _  -> 1/0
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "tier12"
-    , brName = printf "PLS_n%d_p%d" n p
-    , brTimeMs = medMs, brAccMain = nrmse, brAccAux = 0
-    , brExtra = "k=3"
-    }
-  where
-    forceR (Right f) = LA.atIndex (PLS.plsCoef f) (0, 0)
-    forceR _         = 0
-
--- ===========================================================================
--- LDA / QDA
--- ===========================================================================
-
-ldaData :: Int -> Int -> Int -> (LA.Matrix Double, V.Vector Int)
-ldaData nPerClass p k =
-  let totN = nPerClass * k
-      base = [ LA.fromList (gaussianHalton (primeAt j) (primeAt (j + 1)) totN)
-             | j <- [0 .. p - 1] ]
-      x0   = LA.fromColumns base
-      labels = V.fromList (concat [ replicate nPerClass c | c <- [0 .. k - 1] ])
-      -- shift by class on first 2 dims
-      shift i =
-        let c = labels V.! i
-        in LA.fromList ([fromIntegral c * 3, fromIntegral c * 2]
-                        ++ replicate (p - 2) 0)
-      shifted = LA.fromRows
-        [ LA.flatten (x0 LA.? [i]) + shift i | i <- [0 .. totN - 1] ]
-  in (shifted, labels)
-  where
-    primes :: [Int]
-    primes = [2,3,5,7,11,13,17,19,23,29]
-    primeAt :: Int -> Int
-    primeAt j = primes !! (j `mod` length primes)
-
-writeLDACSV :: FilePath -> LA.Matrix Double -> V.Vector Int -> IO ()
-writeLDACSV path x y =
-  let p = LA.cols x
-      n = LA.rows x
-      header = [ "x" ++ show j | j <- [0 .. p - 1] ] ++ ["class"]
-      rows = [ [ LA.atIndex x (i, j) | j <- [0 .. p - 1] ]
-                 ++ [fromIntegral (y V.! i)]
-             | i <- [0 .. n - 1] ]
-  in writeMatrixCSV path header rows
-
-{-# NOINLINE ldaPhantom #-}
-ldaPhantom :: Int -> LA.Matrix Double -> V.Vector Int -> Disc.DiscriminantFit
-ldaPhantom _ x y = case Disc.fitLDA x y of
-  Right f -> f
-  Left _  -> error "LDA fit failed"
-
-{-# NOINLINE qdaPhantom #-}
-qdaPhantom :: Int -> LA.Matrix Double -> V.Vector Int -> Disc.DiscriminantFit
-qdaPhantom _ x y = case Disc.fitQDA x y of
-  Right f -> f
-  Left _  -> error "QDA fit failed"
-
-benchLDA :: Int -> Int -> Int -> IO BenchRow
-benchLDA nPerClass p k = do
-  let (x, y) = ldaData nPerClass p k
-  (medMs, fit) <- timeitIO 5 (\f -> LA.atIndex (Disc.dfPriors f) 0)
-                              (\i -> pure (ldaPhantom i x y))
-  let (preds, _) = Disc.predictDiscriminant fit x
-      correct = length [ () | i <- [0 .. V.length y - 1], preds V.! i == y V.! i ]
-      acc = fromIntegral correct / fromIntegral (V.length y)
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "tier12"
-    , brName = printf "LDA_n%d_p%d_k%d" (nPerClass * k) p k
-    , brTimeMs = medMs, brAccMain = acc, brAccAux = 0
-    , brExtra = ""
-    }
-
-benchQDA :: Int -> Int -> Int -> IO BenchRow
-benchQDA nPerClass p k = do
-  let (x, y) = ldaData nPerClass p k
-  (medMs, fit) <- timeitIO 5 (\f -> LA.atIndex (Disc.dfPriors f) 0)
-                              (\i -> pure (qdaPhantom i x y))
-  let (preds, _) = Disc.predictDiscriminant fit x
-      correct = length [ () | i <- [0 .. V.length y - 1], preds V.! i == y V.! i ]
-      acc = fromIntegral correct / fromIntegral (V.length y)
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "tier12"
-    , brName = printf "QDA_n%d_p%d_k%d" (nPerClass * k) p k
-    , brTimeMs = medMs, brAccMain = acc, brAccAux = 0
-    , brExtra = ""
-    }
-
--- ===========================================================================
--- Hierarchical Cluster Ward
--- ===========================================================================
-
-hcData :: Int -> LA.Matrix Double
-hcData n = fst (plsData n 5)
-
-writeHCData :: FilePath -> LA.Matrix Double -> IO ()
-writeHCData path x =
-  let p = LA.cols x
-      n = LA.rows x
-      header = [ "x" ++ show j | j <- [0 .. p - 1] ]
-      rows = [ [ LA.atIndex x (i, j) | j <- [0 .. p - 1] ]
-             | i <- [0 .. n - 1] ]
-  in writeMatrixCSV path header rows
-
-{-# NOINLINE hcPhantom #-}
-hcPhantom :: Int -> LA.Matrix Double -> HC.HClusterFit
-hcPhantom _ = HC.fitHierarchical HC.Ward
-
-benchHC :: Int -> IO BenchRow
-benchHC n = do
-  let x = hcData n
-  (medMs, fit) <- timeitIO 3 (\f -> head (HC.hcHeights f))
-                              (\i -> pure (hcPhantom i x))
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "tier12"
-    , brName = printf "HClusterWard_n%d" n
-    , brTimeMs = medMs
-    , brAccMain = last (HC.hcHeights fit)
-    , brAccAux = 0
-    , brExtra = ""
-    }
-
--- ===========================================================================
--- Friedman
--- ===========================================================================
-
-friedmanData :: Int -> LA.Matrix Double
-friedmanData nBlocks =
-  let xs = [ [ fromIntegral b + fromIntegral t * 0.5
-                 + (gaussianHalton (2 + t) (3 + t) nBlocks !! b) * 0.1
-             | t <- [0 .. 2] ]
-           | b <- [0 .. nBlocks - 1] ]
-  in LA.fromLists xs
-
-writeFriedmanCSV :: FilePath -> LA.Matrix Double -> IO ()
-writeFriedmanCSV path m =
-  let header = [ "t0", "t1", "t2" ]
-      rows = [ [ LA.atIndex m (i, j) | j <- [0 .. 2] ]
-             | i <- [0 .. LA.rows m - 1] ]
-  in writeMatrixCSV path header rows
-
-{-# NOINLINE friedmanPhantom #-}
-friedmanPhantom :: Int -> LA.Matrix Double -> ST.TestResult
-friedmanPhantom _ = ST.friedmanTest
-
-benchFriedman :: Int -> IO BenchRow
-benchFriedman n = do
-  let m = friedmanData n
-  (medMs, tr) <- timeitIO 7 ST.trStatistic (\i -> pure (friedmanPhantom i m))
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "tier12"
-    , brName = printf "Friedman_n%d" n
-    , brTimeMs = medMs
-    , brAccMain = ST.trStatistic tr
-    , brAccAux = ST.trPValue tr
-    , brExtra = ""
-    }
-
--- ===========================================================================
--- RF Classifier
--- ===========================================================================
-
-benchRFC :: Int -> Int -> Int -> IO BenchRow
-benchRFC n p k = do
-  gen <- MWC.create
-  let (x, y) = ldaData (n `div` k) p k
-      yU = VU.fromList (V.toList y)
-  (medMs, fit) <- timeitIO 3 RFC.rfcOOBError
-                              (\_ -> RFC.fitRFClassifier
-                                       (RFC.defaultRFCConfig { RFC.rfcNTrees = 50 })
-                                       x yU gen)
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "tier12"
-    , brName = printf "RFC_n%d_p%d_k%d" n p k
-    , brTimeMs = medMs
-    , brAccMain = 1 - RFC.rfcOOBError fit
-    , brAccAux = 0
-    , brExtra = "trees=50"
-    }
-
--- ===========================================================================
--- MLP Regressor
--- ===========================================================================
-
-benchMLP :: Int -> Int -> IO BenchRow
-benchMLP n p = do
-  gen <- MWC.create
-  let (x, y) = plsData n p
-      cfg = NN.defaultMLP
-              { NN.mlpHidden = [16]
-              , NN.mlpEpochs = 100
-              , NN.mlpBatch  = 16
-              , NN.mlpLR     = 0.01
-              }
-  (medMs, fit) <- timeitIO 3 (\f -> last (NN.mlpLossHist f))
-                              (\_ -> NN.fitMLPRegressor cfg x y gen)
-  let preds = LA.flatten (NN.predictMLP fit x)
-      d = preds - y
-      mse = LA.sumElements (d * d) / fromIntegral n
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "tier12"
-    , brName = printf "MLPRegressor_n%d_p%d" n p
-    , brTimeMs = medMs
-    , brAccMain = mse
-    , brAccAux = 0
-    , brExtra = "hidden=16 epochs=100"
-    }
-
--- ===========================================================================
--- Haskell-only benches
--- ===========================================================================
-
-benchTOST :: Int -> IO BenchRow
-benchTOST n = do
-  let xs = LA.fromList (take n (gaussianHalton 2 3 (n * 2)))
-      ys = LA.fromList (take n (drop n (gaussianHalton 2 3 (n * 2))))
-      delta = 0.5
-  (medMs, tr) <- timeitIO 7 ST.trPValue
-                              (\_ -> pure (ST.tostWelch xs ys delta))
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "tier12"
-    , brName = printf "TOSTWelch_n%d" n
-    , brTimeMs = medMs
-    , brAccMain = ST.trPValue tr
-    , brAccAux = ST.trStatistic tr
-    , brExtra = "no_python_equivalent"
-    }
-
-benchAFT :: Int -> IO BenchRow
-benchAFT n = do
-  let ts = LA.fromList [ exp (1 + (gaussianHalton 5 7 n !! i) * 0.3)
-                       | i <- [0 .. n - 1] ]
-      delta_ = V.fromList (replicate n True)
-      x1 = LA.fromColumns [LA.fromList (replicate n 1.0)]
-  (medMs, r) <- timeitIO 3 (\_ -> 1.0)
-                            (\_ -> AFT.fitAFT AFT.AFTLogNormal x1 ts delta_)
-  let sc = case r of
-        Right f -> AFT.aftScale f
-        Left _  -> 1/0
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "tier12"
-    , brName = printf "AFTLogNormal_n%d" n
-    , brTimeMs = medMs
-    , brAccMain = sc
-    , brAccAux = 0
-    , brExtra = "no_python_equivalent"
-    }
-
-{-# NOINLINE ewmaPhantom #-}
-ewmaPhantom :: Int -> V.Vector Double -> Either T.Text [SPC.SPCChartResult]
-ewmaPhantom _ xs = SPC.fitSPC SPC.EWMAChart (SPC.EWMAInput xs 0.2 3.0 0 1.0)
-
-benchEWMA :: Int -> IO BenchRow
-benchEWMA n = do
-  let xs = LA.fromList (gaussianHalton 11 13 n)
-      xsV = V.fromList (LA.toList xs)
-  (medMs, res) <- timeitIO 7 forceEWMA (\i -> pure (ewmaPhantom i xsV))
-  let lastZ = case res of
-        Right (ch:_) -> V.last (SPC.spcPoints ch)
-        _            -> 0
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "tier12"
-    , brName = printf "EWMA_n%d" n
-    , brTimeMs = medMs, brAccMain = lastZ, brAccAux = 0
-    , brExtra = "no_python_equivalent"
-    }
-  where
-    forceEWMA (Right (ch:_)) = V.sum (SPC.spcPoints ch)
-    forceEWMA _              = 0
-
-{-# NOINLINE cusumPhantom #-}
-cusumPhantom :: Int -> V.Vector Double -> Either T.Text [SPC.SPCChartResult]
-cusumPhantom _ xs = SPC.fitSPC SPC.CUSUMChart (SPC.CUSUMInput xs 0 1.0 0.5 4.0)
-
-benchCUSUM :: Int -> IO BenchRow
-benchCUSUM n = do
-  let xs = LA.fromList (gaussianHalton 11 13 n)
-      xsV = V.fromList (LA.toList xs)
-  (medMs, res) <- timeitIO 7 forceCUSUM (\i -> pure (cusumPhantom i xsV))
-  let lastC = case res of
-        Right (cp:_) -> V.last (SPC.spcPoints cp)
-        _            -> 0
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "tier12"
-    , brName = printf "CUSUM_n%d" n
-    , brTimeMs = medMs, brAccMain = lastC, brAccAux = 0
-    , brExtra = "no_python_equivalent"
-    }
-  where
-    forceCUSUM (Right (cp:_)) = V.sum (SPC.spcPoints cp)
-    forceCUSUM _              = 0
-
-{-# NOINLINE gaugeRRPhantom #-}
-gaugeRRPhantom :: Int -> V.Vector Int -> V.Vector Int -> V.Vector Double
-               -> Either T.Text GRR.GaugeRRResult
-gaugeRRPhantom _ ops parts ys = GRR.gaugeRRCrossed ops parts ys
-
-benchGaugeRR :: IO BenchRow
-benchGaugeRR = do
-  let parts = V.fromList (concat (replicate 9 [0, 1, 2]))
-      ops   = V.fromList (concatMap (\o -> replicate 9 o) [0, 1, 2])
-      ys    = V.fromList
-        [ fromIntegral (parts V.! i) * 2
-          + fromIntegral (ops V.! i) * 0.1
-          + (gaussianHalton 17 19 27 !! i) * 0.05
-        | i <- [0 .. 26] ]
-  (medMs, res) <- timeitIO 7 forceGRR
-                              (\i -> pure (gaugeRRPhantom i ops parts ys))
-  let pctGRR = case res of
-        Right r -> GRR.grrPctGRR r
-        _       -> 0
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "tier12"
-    , brName = "GaugeRRCrossed_3p3o3r"
-    , brTimeMs = medMs, brAccMain = pctGRR, brAccAux = 0
-    , brExtra = "no_python_equivalent"
-    }
-  where
-    forceGRR (Right r) = GRR.grrTotalVar r + GRR.grrPartVar r
-    forceGRR _         = 0
-
-benchProcessCapWeibull :: IO BenchRow
-benchProcessCapWeibull = do
-  let wf = Wei.WeibullFit 2.0 100.0 0 0 0 (0, 0, 0)
-  (medMs, cap) <- timeitIO 100 Quality.capCp
-    (\_ -> pure (Quality.processCapabilityWeibull wf 10 300))
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "tier12"
-    , brName = "ProcCapWeibull"
-    , brTimeMs = medMs, brAccMain = Quality.capCp cap, brAccAux = Quality.capCpk cap
-    , brExtra = "no_python_equivalent"
-    }
-
-benchDoEDiag :: IO BenchRow
-benchDoEDiag = do
-  let x = LA.fromLists
-            [ [1, x1, x2, x1 * x2, x1 * x1, x2 * x2]
-            | x1 <- [-1, 0, 1], x2 <- [-1, 0, 1] ]
-  (medMs, dd) <- timeitIO 30 DDiag.ddDEff
-                              (\_ -> pure (DDiag.diagnostics x))
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "tier12"
-    , brName = "DoEDiagnostics_n9p6"
-    , brTimeMs = medMs
-    , brAccMain = DDiag.ddDEff dd
-    , brAccAux = DDiag.ddAEff dd
-    , brExtra = "no_python_equivalent"
-    }
-
-{-# NOINLINE optimalPhantom #-}
-optimalPhantom :: Int -> Opt.OptCriterion -> [[Double]] -> Int -> ([Int], [[Double]])
-optimalPhantom _ crit cands seed = Opt.optimalDesign crit cands 6 seed
-
-benchIEOptimal :: Opt.OptCriterion -> String -> IO BenchRow
-benchIEOptimal crit label = do
-  let cands = [ [1, x1, x2, x1 * x2] | x1 <- [-1, 0, 1], x2 <- [-1, 0, 1] ]
-  (medMs, (idxs, _)) <- timeitIO 5 forceOpt
-    (\i -> pure (optimalPhantom i crit cands (42 + i)))
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "tier12"
-    , brName = label ++ "_n9_6"
-    , brTimeMs = medMs
-    , brAccMain = fromIntegral (sum idxs)
-    , brAccAux = 0
-    , brExtra = "no_python_equivalent"
-    }
-  where
-    forceOpt (idxs, _) = fromIntegral (sum idxs)
-
-benchKalman :: Int -> IO BenchRow
-benchKalman tT = do
-  let obs = LA.fromLists
-              [ [ fromIntegral i * 0.1 + (gaussianHalton 5 7 tT !! i) * 0.1
-                | i <- [0 .. tT - 1] ] ]
-      ssm = SS.StateSpaceModel
-        { SS.ssF  = LA.fromLists [[1]]
-        , SS.ssH  = LA.fromLists [[1]]
-        , SS.ssQ  = LA.fromLists [[0.01]]
-        , SS.ssR  = LA.fromLists [[0.1]]
-        , SS.ssX0 = LA.fromList  [0]
-        , SS.ssP0 = LA.fromLists [[1.0]]
-        }
-  (medMs, kr) <- timeitIO 5 SS.krLogLik
-                              (\_ -> pure (SS.kalmanFilter ssm obs))
-  pure BenchRow
-    { brSystem = "haskell", brSuite = "tier12"
-    , brName = printf "KalmanFilter_T%d" tT
-    , brTimeMs = medMs
-    , brAccMain = SS.krLogLik kr
-    , brAccAux = 0
-    , brExtra = "no_python_equivalent"
-    }
-
--- ===========================================================================
--- Main
--- ===========================================================================
-
-main :: IO ()
-main = do
-  createDirectoryIfMissing True "bench/data"
-  createDirectoryIfMissing True "bench/results/haskell"
-
-  -- 共通 CSV を書き出し (Python 側が読む)
-  let plsParams = [(100, 10), (500, 10)]
-  mapM_ (\(n, p) -> do
-            let (x, y) = plsData n p
-            writePLSCSV (printf "bench/data/tier12_pls_n%d_p%d.csv" n p) x y)
-        plsParams
-
-  let ldaParams = [(30, 5, 3), (100, 5, 3)]
-  mapM_ (\(nc, p, k) -> do
-            let (x, y) = ldaData nc p k
-            writeLDACSV (printf "bench/data/tier12_lda_n%d_p%d_k%d.csv" (nc * k) p k)
-                        x y)
-        ldaParams
-
-  mapM_ (\n -> writeFriedmanCSV (printf "bench/data/tier12_friedman_n%d.csv" n)
-                                 (friedmanData n))
-        [10, 30, 100 :: Int]
-
-  mapM_ (\n -> writeHCData (printf "bench/data/tier12_hc_n%d.csv" n) (hcData n))
-        [20, 50 :: Int]
-
-  plsRows  <- mapM (\(n, p) -> benchPLS n p) plsParams
-  ldaRows  <- mapM (\(nc, p, k) -> benchLDA nc p k) ldaParams
-  qdaRows  <- mapM (\(nc, p, k) -> benchQDA nc p k) ldaParams
-  hcRows   <- mapM benchHC [20, 50 :: Int]
-  friRows  <- mapM benchFriedman [10, 30, 100 :: Int]
-  -- RFC は LDA と同じ data CSV を使うので n_total = nc*k を渡す
-  rfcRows  <- mapM (\(nc, p, k) -> benchRFC (nc * k) p k) ldaParams
-  mlpRows  <- mapM (\(n, p) -> benchMLP n p) plsParams
-  -- Haskell-only
-  tostRows <- mapM benchTOST [50, 200 :: Int]
-  aftRows  <- mapM benchAFT [50, 200 :: Int]
-  ewmaRows <- mapM benchEWMA [100, 500 :: Int]
-  cusRows  <- mapM benchCUSUM [100, 500 :: Int]
-  grrRow   <- benchGaugeRR
-  pcRow    <- benchProcessCapWeibull
-  diagRow  <- benchDoEDiag
-  iOptRow  <- benchIEOptimal Opt.IOpt "IOptimal"
-  eOptRow  <- benchIEOptimal Opt.EOpt "EOptimal"
-  kfRows   <- mapM benchKalman [50, 200 :: Int]
-
-  writeRows "bench/results/haskell/tier12.csv"
-    (concat [ plsRows, ldaRows, qdaRows, hcRows, friRows
-            , rfcRows, mlpRows
-            , tostRows, aftRows, ewmaRows, cusRows
-            , [grrRow, pcRow, diagRow, iOptRow, eOptRow]
-            , kfRows ])
-  putStrLn "✓ bench/results/haskell/tier12.csv written"
diff --git a/bench/haskell/BenchUtil.hs b/bench/haskell/BenchUtil.hs
deleted file mode 100644
--- a/bench/haskell/BenchUtil.hs
+++ /dev/null
@@ -1,167 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Shared helpers used by the Haskell side of the benchmarks.
--- Writes a uniform per-row CSV layout consumed by the Python aggregator.
-module BenchUtil
-  ( BenchRow (..)
-  , writeRows
-  , timeit
-  , timeitIO
-  , timeitTasty
-  , timeitTastyIO
-  , readCsvXY
-  , readCsvXYG
-  ) where
-
-import           Data.IORef                 (newIORef, readIORef, writeIORef)
-
-import qualified Data.ByteString.Lazy.Char8 as BL
-import qualified Data.ByteString            as BS
-import qualified Data.Vector                as V
-import qualified Numeric.LinearAlgebra      as LA
-import           Data.Csv                   (decode, HasHeader (..))
-import           Data.Time.Clock            (getCurrentTime, diffUTCTime)
-import           Text.Printf                (printf)
-
-import qualified Test.Tasty.Bench           as TB
-import           Test.Tasty                 (Timeout (NoTimeout))
-import           System.IO                  (withFile, IOMode (..), hPutStrLn,
-                                             hSetBuffering, BufferMode (..))
-import           Control.Exception          (evaluate)
-
--- | One benchmark observation. The aggregator joins (system, suite, name)
--- across @bench/results/haskell/*.csv@ and @bench/results/python/*.csv@.
-data BenchRow = BenchRow
-  { brSystem  :: String   -- ^ "haskell" or "python".
-  , brSuite   :: String   -- ^ "regression", "kernel", "optim", "mo", "bo", ...
-  , brName    :: String   -- ^ Stable benchmark name (e.g. "LM_n1000_p5").
-  , brTimeMs  :: Double   -- ^ Median wall time per call in milliseconds.
-  , brAccMain :: Double   -- ^ Primary accuracy metric (R², HV, |x*-x_true| etc.).
-  , brAccAux  :: Double   -- ^ Secondary metric (RMSE, IGD, runs to optimum, ...).
-  , brExtra   :: String   -- ^ Free-form note (e.g. "kernel=Gaussian h=0.5").
-  } deriving Show
-
--- | Write a list of rows to a CSV file (with header).
-writeRows :: FilePath -> [BenchRow] -> IO ()
-writeRows path rows = withFile path WriteMode $ \h -> do
-  hSetBuffering h LineBuffering
-  hPutStrLn h "system,suite,name,time_ms,acc_main,acc_aux,extra"
-  mapM_ (\r -> hPutStrLn h
-          (printf "%s,%s,%s,%.6g,%.6g,%.6g,%s"
-            (brSystem r) (brSuite r) (brName r)
-            (brTimeMs r) (brAccMain r) (brAccAux r)
-            (escapeCsv (brExtra r)))) rows
-
-escapeCsv :: String -> String
-escapeCsv s
-  | any (`elem` (",\"\n" :: String)) s =
-      '"' : concatMap (\c -> if c == '"' then "\"\"" else [c]) s ++ "\""
-  | otherwise = s
-
--- | Run a fresh recomputation @n@ times, return the median wall-time in
--- milliseconds plus the value from the last invocation. The caller passes
--- a per-iteration builder @runIt :: Int -> IO a@ (the index defeats GHC's
--- common-subexpression elimination so the work is actually re-run each
--- time) and a probe @force :: a -> Double@ that pulls a scalar out of the
--- result, forcing the underlying Matrix / Vector computation via
--- 'evaluate'.
-timeitIO :: Int -> (a -> Double) -> (Int -> IO a) -> IO (Double, a)
-timeitIO n force runIt = do
-  -- IORef は per-iteration の runtime 依存を作る (GHC が CSE しないように)。
-  ref <- newIORef (0 :: Int)
-  ts <- mapM (\i -> do
-                writeIORef ref i
-                _  <- readIORef  ref
-                t0 <- getCurrentTime
-                x  <- runIt i
-                _  <- evaluate (force x)
-                t1 <- getCurrentTime
-                return (1000.0 * realToFrac (diffUTCTime t1 t0))) [1 .. n]
-  x <- runIt 0
-  _ <- evaluate (force x)
-  let sorted = quickSort ts
-      med    = sorted !! (length sorted `div` 2)
-  return (med, x)
-
--- | Convenience wrapper: the action does not actually depend on the
--- iteration index. Provided for backwards compatibility — please use
--- 'timeitIO' for new code.
-timeit :: Int -> (a -> Double) -> IO a -> IO (Double, a)
-timeit n force action = timeitIO n force (\_ -> action)
-
--- | tasty-bench based timer (Phase 13).
---
--- Adaptive iteration count converges to a stable mean. Returns
--- (mean wall-time in ms, last result). The relative standard
--- deviation cap is 5% (much tighter than the default 10%).
---
--- Use this for new code; 'timeit' / 'timeitIO' kept for backwards
--- compatibility while the migration is in progress.
-timeitTastyIO :: (a -> Double) -> (Int -> IO a) -> IO (Double, a)
-timeitTastyIO force runIt = do
-  -- Build a Benchmarkable that depends on a counter so GHC cannot
-  -- common-subexpression-eliminate across iterations.
-  ref <- newIORef (0 :: Int)
-  let bm = TB.nfIO $ do
-             i <- readIORef ref
-             writeIORef ref (i + 1)
-             x <- runIt i
-             _ <- evaluate (force x)
-             pure ()
-  -- 0.05 = 5% relative stdev target. NoTimeout = run as long as
-  -- needed for convergence (typical < 1 s for ms-range benchmarks).
-  secs <- TB.measureCpuTime NoTimeout 0.05 bm
-  -- Probe value to return alongside the timing.
-  x <- runIt 0
-  _ <- evaluate (force x)
-  return (1000.0 * secs, x)
-
--- | Like 'timeitTastyIO' but the action does not depend on the
--- iteration index.
-timeitTasty :: (a -> Double) -> IO a -> IO (Double, a)
-timeitTasty force action = timeitTastyIO force (\_ -> action)
-
-quickSort :: Ord a => [a] -> [a]
-quickSort []     = []
-quickSort (p:xs) = quickSort [y | y <- xs, y < p]
-                ++ [p]
-                ++ quickSort [y | y <- xs, y >= p]
-  where
-    quickSort []     = []
-    quickSort (p:xs) = quickSort [y | y <- xs, y < p]
-                    ++ [p]
-                    ++ quickSort [y | y <- xs, y >= p]
-
--- ---------------------------------------------------------------------------
--- CSV input (small, header-fronted, all-numeric)
--- ---------------------------------------------------------------------------
-
--- | Read a CSV with header @x0,x1,...,x{p-1},y@ into @(X, y)@.
-readCsvXY :: FilePath -> IO (LA.Matrix Double, LA.Vector Double)
-readCsvXY path = do
-  bytes <- BL.fromStrict <$> BS.readFile path
-  case decode HasHeader bytes :: Either String (V.Vector (V.Vector Double)) of
-    Left err -> error ("readCsvXY: " ++ path ++ ": " ++ err)
-    Right rs ->
-      let n = V.length rs
-          p = V.length (rs V.! 0) - 1
-          xs = LA.fromLists
-                 [ [ rs V.! i V.! j | j <- [0 .. p - 1] ]
-                 | i <- [0 .. n - 1] ]
-          ys = LA.fromList [ rs V.! i V.! p | i <- [0 .. n - 1] ]
-      in return (xs, ys)
-
--- | Read a CSV with header @x0,...,x{p-1},group,y@ into @(X, group_idx, y)@.
-readCsvXYG :: FilePath -> IO (LA.Matrix Double, V.Vector Int, LA.Vector Double)
-readCsvXYG path = do
-  bytes <- BL.fromStrict <$> BS.readFile path
-  case decode HasHeader bytes :: Either String (V.Vector (V.Vector Double)) of
-    Left err -> error ("readCsvXYG: " ++ path ++ ": " ++ err)
-    Right rs ->
-      let n = V.length rs
-          p = V.length (rs V.! 0) - 2
-          xs = LA.fromLists
-                 [ [ rs V.! i V.! j | j <- [0 .. p - 1] ]
-                 | i <- [0 .. n - 1] ]
-          gs = V.fromList [ round (rs V.! i V.! p)        :: Int | i <- [0 .. n - 1] ]
-          ys = LA.fromList [ rs V.! i V.! (p + 1)                  | i <- [0 .. n - 1] ]
-      in return (xs, gs, ys)
diff --git a/bench/haskell/BenchWarmupProf.hs b/bench/haskell/BenchWarmupProf.hs
deleted file mode 100644
--- a/bench/haskell/BenchWarmupProf.hs
+++ /dev/null
@@ -1,140 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
--- | Phase 85.6a: warmup 固定費の内訳プロファイル (radon)。
---
--- radon の wall は warmup 500 draw の固定費 (~3.3s = 6.6ms/draw) が支配し、
--- 本サンプリング (~1.4ms/draw) の ~5 倍/draw。 本ドライバは 'nutsStream' の
--- per-iteration callback ('seTreeDepth' = Phase 85.6 追加) で warmup 中の
--- tree depth / ε の推移を採取し、 固定費の主因 (適応初期の深い tree?) を
--- 確定する。 leapfrog 数 ≈ 2^depth で勾配評価回数を近似する。
---
---   cabal run bench-warmup-prof --project-file=cabal.project.plot -f benches
-module Main where
-
-import           Data.IORef                       (modifyIORef', newIORef,
-                                                   readIORef)
-import qualified Data.Map.Strict                  as Map
-import qualified Data.Text                        as T
-import qualified Data.Vector                      as V
-import qualified System.Random.MWC                as MWC
-import           System.Environment               (getArgs)
-import           Text.Printf                      (printf)
-
-import           Hanalyze.Model.HBM               (ModelP)
-import           Hanalyze.Fit                     (designHBMProgram)
-import           Hanalyze.MCMC.NUTS               (NUTSConfig (..),
-                                                   SampleEvent (..),
-                                                   defaultNUTSConfig,
-                                                   nutsStream)
-
--- ---------------------------------------------------------------------------
--- radon モデル (BenchHBMScaling と同一)
--- ---------------------------------------------------------------------------
-
-readRadon :: IO ([[Double]], [Int], [Double], [Double])
-readRadon = do
-  txt <- readFile "bench/data/radon.csv"
-  let recs = map parseRow (drop 1 (lines txt))
-      parseRow ln = case splitComma ln of
-        (_c : ci : fl : lr : lu : _) ->
-          (read ci :: Int, read fl :: Double, read lr :: Double, read lu :: Double)
-        _ -> error ("readRadon: 列不足 " ++ ln)
-      cidx    = [ c | (c, _, _, _) <- recs ]
-      floors  = [ f | (_, f, _, _) <- recs ]
-      ys      = [ y | (_, _, y, _) <- recs ]
-      designX = [ [1.0, f, u] | (_, f, _, u) <- recs ]
-  return (designX, cidx, floors, ys)
-
-splitComma :: String -> [String]
-splitComma s = case break (== ',') s of
-  (a, ',' : rest) -> a : splitComma rest
-  (a, _)          -> [a]
-
-radonModel :: [[Double]] -> [Int] -> [Double] -> [Double] -> ModelP ()
-radonModel designX cidx floorCol ys =
-  designHBMProgram designX ["(Intercept)", "floor", "uranium"]
-                   [(cidx, nCounties, [floorCol])] ys
-  where nCounties = if null cidx then 0 else maximum cidx + 1
-
-warmupN, sampleN :: Int
-warmupN = 500
-sampleN = 100  -- 既定。 第 2 引数で上書き可 (Phase 87.2 profiling 用)
-
-mkConfig :: Int -> NUTSConfig
-mkConfig nSamp = defaultNUTSConfig
-  { nutsIterations    = nSamp
-  , nutsBurnIn        = warmupN
-  , nutsStepSize      = 0.1
-  , nutsMaxDepth      = 10
-  , nutsAdaptStepSize = True
-  , nutsTargetAccept  = 0.8
-  , nutsAdaptMass     = True
-  }
-
-main :: IO ()
-main = do
-  -- Phase 86 着手時計測: seed 分散を見るため第 1 引数で seed 指定可 (既定 42)。
-  args <- getArgs
-  let seed = case args of { (s : _) -> read s; [] -> 42 } :: Int
-      nSamp = case args of { (_ : n : _) -> read n; _ -> sampleN } :: Int
-  printf "=== Phase 85.6a: radon warmup 内訳プロファイル (seed=%d) ===\n" seed
-  (dX, cidx, fl, ys) <- readRadon
-  let m :: ModelP ()
-      m = radonModel dX cidx fl ys
-      initP = Map.fromList
-        [ ("(Intercept)", 1.3), ("floor", -0.6), ("uranium", 0.7)
-        , ("sigma", 0.7)
-        , ("tau_g0_0", 0.5), ("tau_g0_1", 0.3), ("Lcorr_g0_u1_0", 0.5) ]
-  evRef <- newIORef ([] :: [(Int, Bool, Int, Double, Double)])
-  g <- MWC.initialize (V.singleton (fromIntegral seed))
-  _ <- nutsStream m (mkConfig nSamp) initP g $ \ev ->
-    modifyIORef' evRef
-      ((seIter ev, seIsBurnIn ev, seTreeDepth ev, seStepSize ev, seAcceptStat ev) :)
-  evs <- fmap reverse (readIORef evRef)
-
-  let leap d = (2 :: Int) ^ d
-      warm = [ e | e@(_, True,  _, _, _) <- evs ]
-      samp = [ e | e@(_, False, _, _, _) <- evs ]
-      -- Stan windows (W=500): init buffer 75・windows 75-450・term 450-500
-      seg lo hi = [ (d, eps, al) | (i, _, d, eps, al) <- warm, i >= lo, i < hi ]
-      segs = [ ("init buf   [  0, 75)", seg 0 75)
-             , ("window 25  [ 75,100)", seg 75 100)
-             , ("window 50  [100,150)", seg 100 150)
-             , ("window 100 [150,250)", seg 150 250)
-             , ("window 200 [250,450)", seg 250 450)
-             , ("term buf   [450,500)", seg 450 500) ]
-      stats xs =
-        let ds = [ d | (d, _, _) <- xs ]
-            n  = max 1 (length ds)
-            meanD = fromIntegral (sum ds) / fromIntegral n :: Double
-            lps   = sum (map leap ds)
-            epsL  = case xs of { [] -> 0 / 0; _ -> (\(_, e, _) -> e) (last xs) }
-            meanA = sum [ a | (_, _, a) <- xs ] / fromIntegral n
-        in (meanD, maximum (0 : ds), lps, epsL, meanA)
-  putStrLn "\n--- warmup 区間別 (depth 平均 / 最大 / leapfrog 総数 / 区間末ε / 平均α) ---"
-  mapM_ (\(tag, xs) ->
-    let (md, mx, lp, eps, ma) = stats xs
-    in printf "  %-22s depth=%5.2f max=%2d  leapfrogs=%7d  ε=%.4g  α=%.3f\n"
-         (tag :: String) md mx lp eps ma) segs
-
-  let (mdS, mxS, lpS, epsS, maS) = stats [ (d, e, a) | (_, _, d, e, a) <- samp ]
-      lpW = sum [ leap d | (_, _, d, _, _) <- warm ]
-  printf "\n--- sampling %d draw: depth=%.2f max=%d leapfrogs=%d ε̄=%.4g α=%.3f ---\n"
-    nSamp mdS mxS lpS epsS maS
-  printf "\nleapfrog 総数: warmup=%d (%.1f/draw)  sampling=%d (%.1f/draw)\n"
-    lpW (fromIntegral lpW / fromIntegral warmupN :: Double)
-    lpS (fromIntegral lpS / fromIntegral nSamp :: Double)
-  printf "warmup 支配率 (grad 評価ベース) = %.1f%%\n"
-    (100 * fromIntegral lpW / fromIntegral (lpW + lpS) :: Double)
-  -- ε 推移の先頭 20 draw (初期 ε=0.1 の適否)
-  putStrLn "\n--- 先頭 20 draw の (depth, ε, α) ---"
-  mapM_ (\(i, _, d, eps, al) -> printf "  iter=%3d depth=%2d ε=%.5f α=%.3f\n" i d eps al)
-        (take 20 evs)
-  -- Phase 87.1: term buffer の DA 収束 trace (最終 window 末 450 の再較正 anchor
-  -- から ε がどう動き、 ε̄ がどこに着地するかの診断)。
-  putStrLn "\n--- term buffer [445,500) + sampling 先頭 5 の (depth, ε, α) ---"
-  mapM_ (\(i, b, d, eps, al) ->
-          printf "  iter=%3d %s depth=%2d ε=%.5f α=%.3f\n"
-            i (if b then "W" else "S" :: String) d eps al)
-        [ e | e@(i, _, _, _, _) <- evs, i >= 445, i < 505 ]
diff --git a/bench/haskell/ProfNUTS.hs b/bench/haskell/ProfNUTS.hs
deleted file mode 100644
--- a/bench/haskell/ProfNUTS.hs
+++ /dev/null
@@ -1,245 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | NUTS コストセンタ・プロファイル用の最小実行体 (Phase 53 追加調査)。
---
--- 引数でモデルを選び 1 chain (warmup 500 + 800 draws) を走らせ、
--- @+RTS -p@ で `prof-nuts.prof` を吐かせ per-draw ボトルネックを局在化する:
---   m2 (既定) = 階層 random intercept (glmm helper・高速経路の代表)
---   m3        = random intercept+slope per-obs 手書き (中間: u は REff 昇格・
---               v は dense 列 + v-prior が residual ad walk 残留)
---   m5        = パラメタ非線形 a·exp(-b·x)+c per-obs 手書き (54.8 合成不可 =
---               walk + ad fallback。 Phase 54.9 の本命)
---   m6        = 階層 × 非線形 a_g·exp(-b·x) per-obs 手書き (M5 と同型 fallback・
---               差分 = 階層 prior。 54.9 では差分確認のみ)
---   m7        = Poisson 回帰 exp(a+b·x) per-obs 手書き (非 Gaussian 観測 =
---               高速経路対象外。 Phase 55.1 の支配項確定用)
---   m8        = logistic 回帰 invLogit(a+b·x) per-obs 手書き (同上)
--- モデル定義・DGP は `BenchHBMScaling.hs` と同一 (seed も同じ・CSV は書かない)。
-module Main where
-
-import           Control.Monad                    (forM_)
-import qualified Data.Map.Strict                  as Map
-import qualified Data.Text                        as T
-import qualified Data.Vector                      as V
-import           System.Environment               (getArgs)
-import qualified System.Random.MWC                as MWC
-import           System.Random.MWC.Distributions  (standard)
-
-import           Hanalyze.Model.HBM               (Distribution (..), ModelP,
-                                                   sample, observe, sampleDist,
-                                                   glmmRandomIntercept,
-                                                   GlmmFamily (..))
-import           Hanalyze.MCMC.Core               (chainVals, posteriorMean)
-import           Hanalyze.MCMC.NUTS               (NUTSConfig (..),
-                                                   defaultNUTSConfig, nuts)
-
--- ---------------------------------------------------------------------------
--- DGP (BenchHBMScaling.hs と同一 seed・同一式)
--- ---------------------------------------------------------------------------
-
-normals :: Int -> Int -> IO [Double]
-normals seed k = do
-  g <- MWC.initialize (V.singleton (fromIntegral seed))
-  mapM (const (standard g)) [1 .. k]
-
-nGroups, perGroup :: Int
-nGroups  = 8
-perGroup = 12
-
-genM2 :: IO ([[Double]], [Int], [Double])
-genM2 = do
-  let n = nGroups * perGroup
-  xz <- normals 21 n
-  ez <- normals 22 n
-  uz <- normals 23 nGroups
-  let us   = map (* 1.5) uz
-      gids = [ i `div` perGroup | i <- [0 .. n - 1] ]
-      xs   = map (* 2.0) xz
-      ys   = [ 1.0 + 0.8 * x + (us !! g) + e | (x, g, e) <- zip3 xs gids ez ]
-      xRows = [ [1.0, x] | x <- xs ]
-  return (xRows, gids, ys)
-
-genM3 :: IO ([Double], [Int], [Double])
-genM3 = do
-  let (b0, b1, tauU, tauV, s) = (1.0, 0.8, 1.0, 0.5, 1.0)
-      n = nGroups * perGroup
-  xz <- normals 31 n
-  ez <- normals 32 n
-  uz <- normals 33 nGroups
-  vz <- normals 34 nGroups
-  let us   = map (* tauU) uz
-      vs   = map (* tauV) vz
-      gids = [ i `div` perGroup | i <- [0 .. n - 1] ]
-      xs   = map (* 2.0) xz
-      ys   = [ b0 + b1 * x + (us !! g) + (vs !! g) * x + s * e
-             | (x, g, e) <- zip3 xs gids ez ]
-  return (xs, gids, ys)
-
-genM5 :: IO ([Double], [Double])
-genM5 = do
-  let (a, b, c, s) = (2.5, 1.2, 0.5, 0.3)
-      nM5 = 100 :: Int
-  ez <- normals 51 nM5
-  let xs = [ 3.0 * (fromIntegral i + 0.5) / fromIntegral nM5
-           | i <- [0 .. nM5 - 1] ]
-      ys = [ a * exp (negate b * x) + c + s * e | (x, e) <- zip xs ez ]
-  return (xs, ys)
-
-genM6 :: IO ([Double], [Int], [Double])
-genM6 = do
-  let (muA, tauA, b, s) = (2.0, 0.5, 1.0, 0.3)
-      n = nGroups * perGroup
-  ez <- normals 61 n
-  az <- normals 62 nGroups
-  let as   = [ muA + tauA * z | z <- az ]
-      gids = [ i `div` perGroup | i <- [0 .. n - 1] ]
-      xs   = [ 3.0 * (fromIntegral (i `mod` perGroup) + 0.5)
-                   / fromIntegral perGroup
-             | i <- [0 .. n - 1] ]
-      ys   = [ (as !! g) * exp (negate b * x) + s * e
-             | (x, g, e) <- zip3 xs gids ez ]
-  return (xs, gids, ys)
-
-genM7 :: IO ([Double], [Double])
-genM7 = do
-  let (a, b) = (0.5, 0.8)
-      nM7 = 100 :: Int
-  xs <- normals 71 nM7
-  g  <- MWC.initialize (V.singleton 72)
-  ys <- mapM (\x -> sampleDist (Poisson (exp (a + b * x))) g) xs
-  return (xs, ys)
-
-genM8 :: IO ([Double], [Double])
-genM8 = do
-  let (a, b) = (0.3, 1.2)
-      nM8 = 100 :: Int
-  xs <- normals 81 nM8
-  g  <- MWC.initialize (V.singleton 82)
-  ys <- mapM (\x -> sampleDist
-                      (Bernoulli (1 / (1 + exp (negate (a + b * x))))) g) xs
-  return (xs, ys)
-
--- ---------------------------------------------------------------------------
--- モデル定義 (BenchHBMScaling.hs と同一)
--- ---------------------------------------------------------------------------
-
-m2Model :: [[Double]] -> [Int] -> [Double] -> ModelP ()
-m2Model xRows gids ys = glmmRandomIntercept GlmmGaussian xRows gids ys
-
-m3Model :: [Double] -> [Int] -> [Double] -> ModelP ()
-m3Model xs gids ys = do
-  let nG = if null gids then 0 else maximum gids + 1
-  b0 <- sample "beta_0" (Normal 0 5)
-  b1 <- sample "beta_1" (Normal 0 5)
-  tu <- sample "tau_u"  (HalfNormal 5)
-  tv <- sample "tau_v"  (HalfNormal 5)
-  us <- mapM (\j -> sample (T.pack ("u_" ++ show j)) (Normal 0 tu)) [0 .. nG - 1]
-  vs <- mapM (\j -> sample (T.pack ("v_" ++ show j)) (Normal 0 tv)) [0 .. nG - 1]
-  s  <- sample "sigma" (Exponential 1)
-  forM_ (zip3 [0 :: Int ..] (zip xs gids) ys) $ \(i, (x, g), y) ->
-    observe (T.pack ("y_" ++ show i))
-      (Normal (b0 + b1 * realToFrac x + us !! g + (vs !! g) * realToFrac x) s) [y]
-
-m6Model :: [Double] -> [Int] -> [Double] -> ModelP ()
-m6Model xs gids ys = do
-  let nG = if null gids then 0 else maximum gids + 1
-  muA  <- sample "mu_a"  (Normal 0 10)
-  tauA <- sample "tau_a" (HalfNormal 2)
-  as   <- mapM (\j -> sample (T.pack ("a_" ++ show j)) (Normal muA tauA))
-               [0 .. nG - 1]
-  b    <- sample "b" (HalfNormal 2)
-  s    <- sample "sigma" (Exponential 1)
-  forM_ (zip3 [0 :: Int ..] (zip xs gids) ys) $ \(i, (x, g), y) ->
-    observe (T.pack ("y_" ++ show i))
-      (Normal ((as !! g) * exp (negate b * realToFrac x)) s) [y]
-
-m5Model :: [Double] -> [Double] -> ModelP ()
-m5Model xs ys = do
-  a <- sample "a" (Normal 0 10)
-  b <- sample "b" (HalfNormal 2)
-  c <- sample "c" (Normal 0 10)
-  s <- sample "sigma" (Exponential 1)
-  forM_ (zip3 [0 :: Int ..] xs ys) $ \(i, x, y) ->
-    observe (T.pack ("y_" ++ show i))
-      (Normal (a * exp (negate b * realToFrac x) + c) s) [y]
-
-m7Model :: [Double] -> [Double] -> ModelP ()
-m7Model xs ys = do
-  a <- sample "a" (Normal 0 5)
-  b <- sample "b" (Normal 0 5)
-  forM_ (zip3 [0 :: Int ..] xs ys) $ \(i, x, y) ->
-    observe (T.pack ("y_" ++ show i))
-      (Poisson (exp (a + b * realToFrac x))) [y]
-
-m8Model :: [Double] -> [Double] -> ModelP ()
-m8Model xs ys = do
-  a <- sample "a" (Normal 0 5)
-  b <- sample "b" (Normal 0 5)
-  forM_ (zip3 [0 :: Int ..] xs ys) $ \(i, x, y) ->
-    observe (T.pack ("y_" ++ show i))
-      (Bernoulli (1 / (1 + exp (negate (a + b * realToFrac x))))) [y]
-
--- ---------------------------------------------------------------------------
--- 実行 (warmup 500 + 800 draws・seed 42・BenchHBMScaling の mkConfig と同一)
--- ---------------------------------------------------------------------------
-
-profConfig :: NUTSConfig
-profConfig = defaultNUTSConfig
-  { nutsIterations = 800, nutsBurnIn = 500
-  , nutsStepSize = 0.1, nutsMaxDepth = 10
-  , nutsAdaptStepSize = True, nutsTargetAccept = 0.8, nutsAdaptMass = True }
-
-groupNames :: String -> [T.Text]
-groupNames pre = [ T.pack (pre ++ show j) | j <- [0 .. nGroups - 1] ]
-
-runProf :: ModelP () -> Map.Map T.Text Double -> [T.Text] -> IO ()
-runProf mdl initP names = do
-  gen <- MWC.initialize (V.singleton 42)
-  ch <- nuts mdl profConfig initP gen
-  -- 全 chain を force (プロファイル対象を確実に評価)
-  let s = sum [ maybe 0 id (posteriorMean p ch) | p <- names ]
-          + sum (map (\p -> sum (chainVals p ch)) names) * 0
-  print (s :: Double)
-
-main :: IO ()
-main = do
-  args <- getArgs
-  let which = case args of { (w : _) -> w; [] -> "m2" }
-  case which of
-    "m3" -> do
-      (xs, gids, ys) <- genM3
-      let initP = Map.fromList $
-            [ ("beta_0", 1.0), ("beta_1", 0.8), ("tau_u", 1.0), ("tau_v", 0.5)
-            , ("sigma", 1.0) ]
-            ++ [ (u, 0.0) | u <- groupNames "u_" ++ groupNames "v_" ]
-          names = ["beta_0", "beta_1", "tau_u", "tau_v", "sigma"]
-                  ++ groupNames "u_" ++ groupNames "v_"
-      runProf (m3Model xs gids ys) initP names
-    "m6" -> do
-      (xs, gids, ys) <- genM6
-      let initP = Map.fromList $
-            [ ("mu_a", 2.0), ("tau_a", 0.5), ("b", 1.0), ("sigma", 0.3) ]
-            ++ [ (a, 2.0) | a <- groupNames "a_" ]
-          names = ["mu_a", "tau_a", "b", "sigma"] ++ groupNames "a_"
-      runProf (m6Model xs gids ys) initP names
-    "m5" -> do
-      (xs, ys) <- genM5
-      let initP = Map.fromList
-            [("a", 2.5), ("b", 1.2), ("c", 0.5), ("sigma", 0.3)]
-          names = ["a", "b", "c", "sigma"]
-      runProf (m5Model xs ys) initP names
-    "m7" -> do
-      (xs, ys) <- genM7
-      runProf (m7Model xs ys)
-        (Map.fromList [("a", 0.5), ("b", 0.8)]) ["a", "b"]
-    "m8" -> do
-      (xs, ys) <- genM8
-      runProf (m8Model xs ys)
-        (Map.fromList [("a", 0.3), ("b", 1.2)]) ["a", "b"]
-    _ -> do
-      (xRows, gids, ys) <- genM2
-      let initP = Map.fromList $
-            [ ("beta_0", 1.0), ("beta_1", 0.8), ("tau_u", 1.5), ("sigma", 1.0) ]
-            ++ [ (u, 0.0) | u <- groupNames "u_" ]
-          names = ["beta_0", "beta_1", "tau_u", "sigma"] ++ groupNames "u_"
-      runProf (m2Model xRows gids ys) initP names
diff --git a/bench/posteriordb/01-glm-poisson/Model.hs b/bench/posteriordb/01-glm-poisson/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/01-glm-poisson/Model.hs
+++ /dev/null
@@ -1,103 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | GLM_Poisson_Data-GLM_Poisson_model (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89: posteriordb 横断ベンチマーク。BPA (Kery & Schaub 2011, Ch.3) の
--- 個体数カウントデータ (n=40年) を3次多項式 Poisson 回帰でモデル化する。
--- Stan 原典の "暗黙の一様事前分布" (bounded, no explicit prior) を
--- 'Uniform' distribution で忠実に移植する。
---
--- 高レベル API (`df |-> hbm`) を使用: データは 'dataNamedX'/'dataNamedObs' で
--- df から束縛し、 反復は 'plateForM_' で書く (docs/api-guide/03-bayesian-hbm.md
--- の規約どおり)。 診断図は hgg の 'dashboardFullOf' (構造 DAG /
--- forest / PPC / energy の 2x2 + param ごと [事後分布|trace]) を 1 枚の PNG
--- (rasterific backend) として出力する (PyMC 側の合成ダッシュボード
--- `_common.make_pymc_dashboard` と対)。 chain 数等の設定は PyMC 側
--- (`model.py`) と同じくコード中の定数 (= 4) で揃える (CLI 引数化しない)。
---
--- reference_posterior_name = null (posteriordb に公式 reference posterior 無し)。
---
--- ビルド: cabal build --project-file=cabal.project.plot posteriordb-glm-poisson
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observe,
-                                    dataNamedX, dataNamedObs, plateForM_)
-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)
-
--- | posteriordb の @GLM_Poisson_Data.json@ 形状 ({"year":[...], "C":[...], "n":40})。
-data GlmPoissonData = GlmPoissonData
-  { year :: [Double]
-  , c    :: [Int]
-  }
-
-instance FromJSON GlmPoissonData where
-  parseJSON = withObject "GlmPoissonData" $ \v ->
-    GlmPoissonData <$> v .: "year" <*> v .: "C"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/01-glm-poisson/data/GLM_Poisson_Data.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/01-glm-poisson/figures"
-
-readData :: IO ([Double], [Int])
-readData = do
-  d <- either fail pure =<< eitherDecodeFileStrict dataPath
-  pure (year d, c d)
-
--- | 3次多項式 Poisson 回帰 (Stan 原典と同一構造)。 データは df 経由で束縛
--- ('dataNamedX'/'dataNamedObs')・反復は 'plateForM_' (docs/api-guide 規約)。
-glmPoissonModel :: ModelP ()
-glmPoissonModel = do
-  alpha <- sample "alpha" (Uniform (-20) 20)
-  beta1 <- sample "beta1" (Uniform (-10) 10)
-  beta2 <- sample "beta2" (Uniform (-10) 10)
-  beta3 <- sample "beta3" (Uniform (-10) 10)
-  years <- dataNamedX   "year" []
-  cs    <- dataNamedObs "C"    []
-  plateForM_ "obs" (zip years cs) $ \(y, c) ->
-    let y2 = y * y
-        y3 = y2 * y
-        logLambda = alpha + beta1 * y + beta2 * y2 + beta3 * y3
-    in observe "C" (Poisson (exp logLambda)) [c]
-
-main :: IO ()
-main = do
-  (years, cs) <- readData
-  let df = [ ("year", NumData (V.fromList years))
-           , ("C",    NumData (V.fromList (map fromIntegral cs)))
-           ] :: [(T.Text, ColData)]
-      -- PyMC 側 (model.py) と同じ設定を定数で揃える (draws/tune=1000×4chain)。
-      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                        , hbmWarmup = 1000, hbmSeed = Just 1 }
-      m = df |-> hbm cfg glmPoissonModel
-
-  -- Phase 96 A2: 勾配経路 = compileGradUV が実際に選ぶ経路 (束縛済
-  -- hbmModelSpec で判定・Phase 91 A4 と同型)。
-  putStrLn $ "勾配経路 = " ++ gradPathLabel (hbmModelSpec m)
-
-  -- 診断図 (hgg・PNG・SVG は param 数×draw 数でファイルが重くなる
-  -- ため rasterific backend を使う)。 dashboardFullOf 1 枚 (構造+推定+PPC+
-  -- 健全性の 2x2 + param ごと [事後分布|trace])。 PyMC 側の
-  -- py_dashboard_full.png と対 (trace は dashboardFullOf に必ず含まれる)。
-  -- figures/ は事前に用意されている前提 (git 管理下・新規モデル作成時に
-  -- 1 度だけ作る) — 実行のたびに作り直さない。
-  savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-    (noDf |>> dashboardFullOf m "C" :: BoundPlot)
-
-  -- 事後要約 (az.summary 相当・Common.summarize)。
-  printSummary $ summarize ["alpha", "beta1", "beta2", "beta3"] (hbmChainsR m)
diff --git a/bench/posteriordb/02-dogs/Model.hs b/bench/posteriordb/02-dogs/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/02-dogs/Model.hs
+++ /dev/null
@@ -1,122 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | dogs-dogs (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89: posteriordb 横断ベンチマーク。Solomon & Wynne (1953) の犬の
--- 回避学習実験 (30 匹 × 25 試行、Gelman & Hill 2006 Ch.24 の ARM 本例) を、
--- 累積の回避/被ショック回数を共変量とするロジスティック回帰でモデル化する。
--- Stan 原典の transformed parameters (n_avoid/n_shock) は beta 非依存の
--- 純粋な y (観測データ) の累積和なので、サンプリング前の前処理として
--- 1 度だけ計算する (Stan は反復ごとに再計算するが数学的には同一)。
---
--- 高レベル API (`df |-> hbm`) を使用: データは 'dataNamedX'/'dataNamedObs' で
--- df から束縛し、 反復は 'plateForM_' で書く (docs/api-guide/03-bayesian-hbm.md
--- の規約どおり)。 診断図は hgg の 'dashboardFullOf' を 1 枚の PNG
--- (rasterific backend) として出力する。 chain 数等の設定は PyMC 側
--- (`model.py`) と同じくコード中の定数 (= 4) で揃える (CLI 引数化しない)。
---
--- reference_posterior_name = null (posteriordb に公式 reference posterior 無し)。
---
--- ビルド: cabal build --project-file=cabal.project.plot posteriordb-dogs
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import Data.List (intercalate)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observe,
-                                    dataNamedX, dataNamedObs, plateForM_)
-import Hanalyze.MCMC.Core (Chain, chainVals)
-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)
-
--- | posteriordb の @dogs_data.json@ 形状 ({"n_dogs":30,"n_trials":25,"y":[[...]]})。
-data DogsData = DogsData
-  { yMatrix :: [[Int]]  -- ^ 30匹 × 25試行の 0/1 (0=回避成功, 1=ショック)。
-  }
-
-instance FromJSON DogsData where
-  parseJSON = withObject "DogsData" $ \v ->
-    DogsData <$> v .: "y"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/02-dogs/data/dogs_data.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/02-dogs/figures"
-
-readData :: IO [[Int]]
-readData = do
-  d <- either fail pure =<< eitherDecodeFileStrict dataPath
-  pure (yMatrix d)
-
--- | 1匹分の回避/被ショック累積回数 (beta 非依存・y のみに依存する前処理)。
--- @n_avoid[0] = n_shock[0] = 0@、以降は前試行までの累積。
-cumulativeCounts :: [Int] -> ([Double], [Double])
-cumulativeCounts ys =
-  ( scanl (\a y -> a + 1 - fromIntegral y) 0 (init ys)
-  , scanl (\s y -> s + fromIntegral y) 0 (init ys)
-  )
-
--- | 累積回避/被ショック回数を共変量とするロジスティック回帰 (Stan 原典と
--- 同一構造)。 データは df 経由で束縛 ('dataNamedX'/'dataNamedObs')・
--- 反復は 'plateForM_' (docs/api-guide 規約)。
-dogsModel :: ModelP ()
-dogsModel = do
-  beta1 <- sample "beta1" (Normal 0 100)
-  beta2 <- sample "beta2" (Normal 0 100)
-  beta3 <- sample "beta3" (Normal 0 100)
-  navoid <- dataNamedX   "n_avoid" []
-  nshock <- dataNamedX   "n_shock" []
-  ys     <- dataNamedObs "y"       []
-  plateForM_ "obs" (zip3 navoid nshock ys) $ \(na, ns, yi) ->
-    let logitP = beta1 + beta2 * na + beta3 * ns
-        p      = 1 / (1 + exp (negate logitP))
-    in observe "y" (Bernoulli p) [yi]
-
-main :: IO ()
-main = do
-  rows <- readData
-  let (avoidRows, shockRows) = unzip (map cumulativeCounts rows)
-      yFlat     = map fromIntegral (concat rows) :: [Double]
-      avoidFlat = concat avoidRows
-      shockFlat = concat shockRows
-      df = [ ("n_avoid", NumData (V.fromList avoidFlat))
-           , ("n_shock", NumData (V.fromList shockFlat))
-           , ("y",       NumData (V.fromList yFlat))
-           ] :: [(T.Text, ColData)]
-      -- PyMC 側 (model.py) と同じ設定を定数で揃える (draws/tune=1000×4chain)。
-      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                        , hbmWarmup = 1000, hbmSeed = Just 1 }
-      m = df |-> hbm cfg dogsModel
-      pars = ["beta1", "beta2", "beta3"]
-
-  -- Phase 96 A2: 勾配経路 = compileGradUV が実際に選ぶ経路 (束縛済
-  -- hbmModelSpec で判定・Phase 91 A4 と同型)。
-  putStrLn $ "勾配経路 = " ++ gradPathLabel (hbmModelSpec m)
-
-  printSummary $ summarize pars (hbmChainsR m)
-  writeDrawsCSV "bench/posteriordb/02-dogs/hs_draws.csv" pars (hbmChainsR m)
-
-  savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-    (noDf |>> dashboardFullOf m "y" :: BoundPlot)
-
--- | arviz 独立検証用の生 draw ダンプ (chain,draw,<param>...) — hanalyze 自身の
--- 診断コードは使わず、外部 (arviz) で bulk/tail ESS・R-hat を再計算するため。
-writeDrawsCSV :: FilePath -> [T.Text] -> [Chain] -> IO ()
-writeDrawsCSV path pars chains = writeFile path (unlines (header : rows))
-  where
-    header = intercalate "," ("chain" : "draw" : map T.unpack pars)
-    rows = [ intercalate "," (show ci : show di : [ show (chainVals p ch !! di) | p <- pars ])
-           | (ci, ch) <- zip [0 :: Int ..] chains
-           , di <- [0 .. length (chainVals (head pars) ch) - 1] ]
diff --git a/bench/posteriordb/03-garch11/Model.hs b/bench/posteriordb/03-garch11/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/03-garch11/Model.hs
+++ /dev/null
@@ -1,119 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | garch-garch11 (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89: posteriordb 横断ベンチマーク。GARCH(1,1) 時系列モデル (T=200)。
--- 分散 sigma[t] がパラメータ依存の逐次再帰 (sigma[t-1] から計算) という、
--- これまでの2モデル (GLM-Poisson・dogs) とは異なる構造。dogs の累積和は
--- observed data のみに依存する前処理だったが、本モデルの再帰は
--- alpha0/alpha1/beta1/mu という **サンプリング対象のパラメータに依存**
--- するため前処理不可 (毎回の勾配評価で再計算)。 'synthVecIR' がこの
--- パラメータ依存再帰にも対応することを事前に小規模トイモデルで実測確認済み
--- (`cabal repl` + 'synthVecIR' 直接呼び出し・`Just` を確認)。
---
--- mu/alpha0 は Stan の暗黙improper flat prior (無制約 real / 下限のみ) を
--- そのまま移植できない (hanalyze の Uniform は有限区間が必要)。実用上の
--- proper prior (mu~Normal(0,10)・alpha0~HalfNormal(5)) で代替する
--- (README「既知の課題」に記載)。alpha1/beta1 は Stan 原典の有界一様事前
--- 分布 (beta1 の上限が alpha1 に依存する動的境界) を忠実に移植する。
---
--- 高レベル API (`df |-> hbm`) を使用: データは 'dataNamedObs' で df から
--- 束縛する (説明変数無し・時系列そのもの)。 診断図は hgg の
--- 'dashboardFullOf' を 1 枚の PNG (rasterific backend) として出力する。
--- chain 数等の設定は PyMC 側 (`model.py`) と同じくコード中の定数 (= 4) で
--- 揃える (CLI 引数化しない)。
---
--- reference_posterior_name = "garch-garch11" — posteriordb に公式 reference
--- posterior あり (hanalyze vs PyMC vs 公式referenceの3者比較が可能)。
---
--- ビルド: cabal build --project-file=cabal.project.plot posteriordb-garch11
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import Data.List (intercalate)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observe,
-                                    dataNamedObs, plateForM_)
-import Hanalyze.MCMC.Core (Chain, chainVals)
-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)
-
--- | posteriordb の @garch_data.json@ 形状 ({"T":200,"y":[...],"sigma1":0.5})。
-data GarchData = GarchData
-  { yTS    :: [Double]
-  , sigma1 :: Double  -- ^ sigma[1] の固定初期値 (データの一部)。
-  }
-
-instance FromJSON GarchData where
-  parseJSON = withObject "GarchData" $ \v ->
-    GarchData <$> v .: "y" <*> v .: "sigma1"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/03-garch11/data/garch_data.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/03-garch11/figures"
-
-readData :: IO ([Double], Double)
-readData = do
-  d <- either fail pure =<< eitherDecodeFileStrict dataPath
-  pure (yTS d, sigma1 d)
-
--- | GARCH(1,1): sigma[t] はパラメータ依存の逐次再帰 (Stan 原典と同一構造)。
--- @sigma[1] = sigma1@ (データの固定初期値)、@sigma[t] = sqrt(alpha0 +
--- alpha1*(y[t-1]-mu)^2 + beta1*sigma[t-1]^2)@。 データは df 経由で束縛
--- ('dataNamedObs')・観測は 'plateForM_' (docs/api-guide 規約)。
-garch11Model :: Double -> ModelP ()
-garch11Model s1 = do
-  mu     <- sample "mu"     (Normal 0 10)
-  alpha0 <- sample "alpha0" (HalfNormal 5)
-  alpha1 <- sample "alpha1" (Uniform 0 1)
-  beta1  <- sample "beta1"  (Uniform 0 (1 - alpha1))
-  ys <- dataNamedObs "y" []
-  let sigmas = scanl (\sPrev yPrev ->
-                         sqrt (alpha0 + alpha1 * (realToFrac yPrev - mu) ^ (2 :: Int)
-                               + beta1 * sPrev ^ (2 :: Int)))
-                      (realToFrac s1) (init ys)
-  plateForM_ "obs" (zip sigmas ys) $ \(s, yi) ->
-    observe "y" (Normal mu s) [yi]
-
-main :: IO ()
-main = do
-  (ys, s1) <- readData
-  let df = [ ("y", NumData (V.fromList ys)) ] :: [(T.Text, ColData)]
-      -- PyMC 側 (model.py) と同じ設定を定数で揃える (draws/tune=1000×4chain)。
-      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                        , hbmWarmup = 1000, hbmSeed = Just 1 }
-      m = df |-> hbm cfg (garch11Model s1)
-      pars = ["mu", "alpha0", "alpha1", "beta1"]
-
-  -- Phase 96 A2: 勾配経路 = compileGradUV が実際に選ぶ経路 (束縛済
-  -- hbmModelSpec で判定・Phase 91 A4 と同型)。
-  putStrLn $ "勾配経路 = " ++ gradPathLabel (hbmModelSpec m)
-
-  printSummary $ summarize pars (hbmChainsR m)
-  writeDrawsCSV "bench/posteriordb/03-garch11/hs_draws.csv" pars (hbmChainsR m)
-
-  savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-    (noDf |>> dashboardFullOf m "y" :: BoundPlot)
-
--- | arviz 独立検証用の生 draw ダンプ (chain,draw,<param>...) — hanalyze 自身の
--- 診断コードは使わず、外部 (arviz) で bulk/tail ESS・R-hat を再計算するため。
-writeDrawsCSV :: FilePath -> [T.Text] -> [Chain] -> IO ()
-writeDrawsCSV path pars chains = writeFile path (unlines (header : rows))
-  where
-    header = intercalate "," ("chain" : "draw" : map T.unpack pars)
-    rows = [ intercalate "," (show ci : show di : [ show (chainVals p ch !! di) | p <- pars ])
-           | (ci, ch) <- zip [0 :: Int ..] chains
-           , di <- [0 .. length (chainVals (head pars) ch) - 1] ]
diff --git a/bench/posteriordb/04-low-dim-gauss-mix/Model.hs b/bench/posteriordb/04-low-dim-gauss-mix/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/04-low-dim-gauss-mix/Model.hs
+++ /dev/null
@@ -1,118 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | low_dim_gauss_mix-low_dim_gauss_mix (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89/90: posteriordb 横断ベンチマーク + Phase 90 A3 (vecIR ギャップ
--- 解消: 04-low-dim-gauss-mix = 2成分Normal混合)。
---
--- Stan 原典 (posteriordb `models/stan/low_dim_gauss_mix.stan`。 N=1000):
---   parameters { ordered[2] mu; array[2] real<lower=0> sigma;
---                real<lower=0,upper=1> theta; }
---   model {
---     sigma ~ normal(0, 2); mu ~ normal(0, 2); theta ~ beta(5, 5);
---     for (n in 1:N)
---       target += log_mix(theta, normal_lpdf(y[n]|mu[1],sigma[1]),
---                                 normal_lpdf(y[n]|mu[2],sigma[2]));
---   }
---
--- reference_posterior_name = "low_dim_gauss_mix-low_dim_gauss_mix"
--- (posteriordb に公式 reference posterior あり・3者比較可能)。
---
--- Phase 90 A3: `Mixture [w1,w2] [Normal mu1 sg1, Normal mu2 sg2]` を新規
--- vecIR family (`VGMixNorm2`) で高速経路に載せた (`IR.hs`)。Stan の
--- `ordered[2] mu` 制約 (label switching 回避) は hanalyze 側に対応する
--- 順序制約プリミティブが無いため実装せず、mu1<mu2 を確認できた場合のみ
--- 素直に採用する (後述「既知の課題」参照)。
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observe,
-                                    dataNamedObs, plateForM_, withData)
-import Hanalyze.Model.HBM (gradPathLabel)
-import Hanalyze.Plot (hbmModelSpec)
-import Hanalyze.Plot (HBMConfig (..), defaultHBM, hbm, (|->),
-                              dashboardFullOf, hbmChainsR)
-import Hanalyze.MCMC.Core (Chain (..))
-import Hgg.Plot.Spec (ColData (..))
-import Hgg.Plot.Frame (BoundPlot, (|>>))
-import Hgg.Plot.Backend.Rasterific (savePNGBound)
-import qualified Data.Map.Strict as Map
-
-import Common (summarize, printSummary, timeSamplingMs)
-import Text.Printf (printf)
-
--- | posteriordb の @low_dim_gauss_mix.json@ 形状 ({"N":1000, "y":[...]})。
-newtype MixData = MixData { y :: [Double] }
-
-instance FromJSON MixData where
-  parseJSON = withObject "MixData" $ \v -> MixData <$> v .: "y"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/04-low-dim-gauss-mix/data/low_dim_gauss_mix.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/04-low-dim-gauss-mix/figures"
-
-readData :: IO [Double]
-readData = do
-  d <- either fail pure =<< eitherDecodeFileStrict dataPath
-  pure (y d)
-
--- | 2成分 Normal 混合 (Phase 90 A3 で追加した `VGMixNorm2` vecIR family を使用)。
-lowDimGaussMixModel :: ModelP ()
-lowDimGaussMixModel = do
-  mu1    <- sample "mu1"    (Normal 0 2)
-  mu2    <- sample "mu2"    (Normal 0 2)
-  sigma1 <- sample "sigma1" (HalfNormal 2)
-  sigma2 <- sample "sigma2" (HalfNormal 2)
-  theta  <- sample "theta"  (Beta 5 5)
-  ys     <- dataNamedObs "y" []
-  plateForM_ "obs" ys $ \yi ->
-    observe "y" (Mixture [theta, 1 - theta]
-                          [Normal mu1 sigma1, Normal mu2 sigma2]) [yi]
-
--- | ラベルスイッチング補正 (Stan 原典の `ordered[2] mu` 制約に相当)。
--- 2成分混合は mu1/mu2 を入れ替えても尤度が不変 (非識別) なので、posterior
--- draw ごとに mu1 > mu2 なら (mu1,sigma1,theta) <-> (mu2,sigma2,1-theta) を
--- 入れ替えて mu1 < mu2 に正規化する。 hanalyze は各 chain が個別の順序に
--- 収束しやすく (chain 内 ESS は健全・chain 間で符号が割れて R-hat が数十に
--- 跳ねる)、 この後処理で PyMC/公式referenceと比較可能な要約になる。
-orderedChains :: [Chain] -> [Chain]
-orderedChains = map orderChain
-  where
-    orderChain c = c { chainSamples = map orderDraw (chainSamples c) }
-    orderDraw ps = case (Map.lookup "mu1" ps, Map.lookup "mu2" ps) of
-      (Just m1, Just m2) | m1 > m2 -> swapDraw ps
-      _                            -> ps
-    swapDraw ps = Map.union (Map.fromList
-      [ ("mu1", ps Map.! "mu2"), ("mu2", ps Map.! "mu1")
-      , ("sigma1", ps Map.! "sigma2"), ("sigma2", ps Map.! "sigma1")
-      , ("theta", 1 - ps Map.! "theta")
-      ]) ps
-
-main :: IO ()
-main = do
-  ys <- readData
-  let df = [ ("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 lowDimGaussMixModel
-
-  -- 勾配経路 = 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
-
-  savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-    (noDf |>> dashboardFullOf m "y" :: BoundPlot)
-
-  printSummary $ summarize ["mu1", "mu2", "sigma1", "sigma2", "theta"]
-                           (orderedChains (hbmChainsR m))
diff --git a/bench/posteriordb/05-mh/Model.hs b/bench/posteriordb/05-mh/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/05-mh/Model.hs
+++ /dev/null
@@ -1,145 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Mh_data-Mh_model (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89/90: posteriordb 横断ベンチマーク + Phase 90 A3 (vecIR ギャップ
--- 解消: 05-mh = capture-recapture・ZeroInflatedBinomial + 個体ごとの
--- ランダム効果)。
---
--- Stan 原典 (posteriordb `models/stan/Mh_model.stan`。 BPA本 Ch.6・
--- M=385個体・T=5回のサンプリング機会):
---   parameters { real<lower=0,upper=1> omega; real<lower=0,upper=1> mean_p;
---                real<lower=0,upper=5> sigma; vector[M] eps_raw; }
---   transformed parameters { vector[M] eps = logit(mean_p) + sigma*eps_raw; }
---   model {
---     eps_raw ~ normal(0, 1);
---     for (i in 1:M) {
---       if (y[i] > 0)
---         target += bernoulli_lpmf(1|omega) + binomial_logit_lpmf(y[i]|T,eps[i]);
---       else
---         target += log_sum_exp(bernoulli_lpmf(1|omega)+binomial_logit_lpmf(0|T,eps[i]),
---                                bernoulli_lpmf(0|omega));
---     }
---   }
---
--- 05-mh/README.md (Phase 89) で代数的に導出済みのとおり、この尤度は
--- hanalyze の `ZeroInflatedBinomial n ψ p` (ψ=1-omega, p=invlogit(eps)) と
--- 厳密に一致する。 reference_posterior_name = null (2者比較のみ)。
---
--- Phase 90 A3: `SDZIBinom`/`VGZIBinom`/`VOZIBinom` (新設 vecIR family) を
--- 使用。 eps_i (個体ごとのランダム効果) は `plateI` で M 個の latent を
--- 宣言し、 族 gather (`!!`) 経由で観測式に取り込む (`docs/api-guide` の
--- eight-schools 型パターンと同型)。 トイモデルで `synthVecIR` = `Just` を
--- 実測確認済み (個体ごとの logit-link random effect を含む形でも通る)。
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observe,
-                                    dataNamedObs, plateI, plateForM_, (.#), withData)
-import Hanalyze.Model.HBM (gradPathLabel)
-import Hanalyze.Plot (hbmModelSpec)
-import Hanalyze.Plot (HBMConfig (..), defaultHBM, hbm, (|->),
-                              dashboardOf, hbmChainsR)
-import Hgg.Plot.Spec (ColData (..))
-import Hgg.Plot.Frame (BoundPlot, (|>>))
-import Hgg.Plot.Backend.Rasterific (savePNGBound)
-
-import Common (summarize, printSummary, timeSamplingMs)
-import Control.Monad (unless)
-import Data.List (group, sort)
-import System.Environment (getArgs)
-import Text.Printf (printf)
-
-import Hanalyze.MCMC.Core (chainTreeDepths)
-
--- | posteriordb の @Mh_data.json@ 形状 ({"M":385, "T":5, "y":[...]})。
-data MhData = MhData { yObs :: [Double], tOccasions :: Int }
-
-instance FromJSON MhData where
-  parseJSON = withObject "MhData" $ \v ->
-    MhData <$> v .: "y" <*> v .: "T"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/05-mh/data/Mh_data.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/05-mh/figures"
-
-readData :: IO ([Double], Int)
-readData = do
-  d <- either fail pure =<< eitherDecodeFileStrict dataPath
-  pure (yObs d, tOccasions d)
-
--- | capture-recapture (Mh モデル)。 T (サンプリング機会数) はデータ由来の
--- 固定定数として closure で渡す (ctor 引数化・GLM-Poisson 等と同じ流儀)。
-mhModel :: Int -> ModelP ()
-mhModel t = do
-  -- Stan 原典は omega/mean_p ~ Uniform(0,1) (暗黙一様事前分布) だが、
-  -- hanalyze の `Uniform` は制約変換が現状 unconstrained 扱い
-  -- (`01-glm-poisson/README.md` に既知の課題として記載済) のため、
-  -- vecIR probe (2点評価) で mean_p が (0,1) 域外の値を取り
-  -- `log(meanP/(1-meanP))` が NaN 化して誤フォールバックする実害が
-  -- M=385 (個体ごとの random effect) という多latentモデルで発覚した。
-  -- Beta(1,1) は Uniform(0,1) と数学的に同一の分布で、hanalyze では
-  -- 実際に (0,1) へ写す変換を持つため確率的に等価かつ probe 安全。
-  omega  <- sample "omega"  (Beta 1 1)
-  meanP  <- sample "mean_p" (Beta 1 1)
-  sigma  <- sample "sigma"  (Uniform 0 5)
-  ys     <- dataNamedObs "y" []
-  epsRaws <- plateI "ind" (length ys) $ \i -> sample ("eps_raw" .# i) (Normal 0 1)
-  let logitMeanP = log (meanP / (1 - meanP))
-  plateForM_ "obs" (zip [0 ..] ys) $ \(i, yi) ->
-    let eps = logitMeanP + sigma * (epsRaws !! i)
-        p   = 1 / (1 + exp (negate eps))
-    in observe "y" (ZeroInflatedBinomial t (1 - omega) p) [yi]
-
-main :: IO ()
-main = do
-  (ys, t) <- readData
-  -- Phase 96 A1: `prof` 引数で図出力 skip (Phase 102 A1 の dugongs/radon と
-  -- 同型。Rasterific が cost centre を汚さないため)。サンプリング設定は
-  -- 本番のまま据え置く。
-  args <- getArgs
-  let profRun = elem "prof" args
-  let df = [ ("y", NumData (V.fromList ys)) ] :: [(T.Text, ColData)]
-      -- PyMC 側 (model.py) と同じ設定を定数で揃える。
-      -- Phase 96 A5: M=I 期間 (init buffer + 第1 window) の deep tree 抑制。
-      -- 無効時は init 期が 61.9 steps/iter (avg depth≈6・ε 中央値 0.107 vs
-      -- 収束後 0.242 の鋸歯) で warmup の 32% を浪費する。Just 4 で warmup
-      -- evals −25〜28% (116-121k → 85-91k)・seed 1/2/3 で posterior 統計一致・
-      -- ess/s 分散も縮小 (実測 root: experiments/phase96-mh-reconfirm/)。
-      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                        , hbmWarmup = 1000, hbmSeed = Just 1
-                        , hbmWarmupInitMaxDepth = Just 4 }
-      m = df |-> hbm cfg (mhModel t)
-
-  -- 勾配経路 = 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
-
-  -- Phase 96 A4: draws 区間の軌道長を nutpie の n_steps(draws) と同一区間で
-  -- 突合するための tree depth 統計 (leapfrog 数 ≈ 2^depth − 1)。
-  let depths = concatMap chainTreeDepths (hbmChainsR m)
-      hist   = map (\g -> (head g, length g)) (group (sort depths))
-      nLeap  = sum [ (2 :: Int) ^ d - 1 | d <- depths ]
-  printf "tree depth (draws): hist=%s  leapfrog~=%d  steps/draw~=%.1f\n"
-         (show hist) nLeap
-         (fromIntegral nLeap / fromIntegral (max 1 (length depths)) :: Double)
-
-  -- M=385 個体分の eps_raw latent を含むため 'dashboardFullOf' (全 param の
-  -- [事後分布|trace] グリッド) は 52MB 超の巨大画像になり非実用的 (実測)。
-  -- 健全性 2x2 パネル (DAG/forest/PPC/energy) のみの 'dashboardOf' を使う
-  -- (forest には全 latent が出るが 1 行/param のコンパクト表示なので実用的)。
-  unless profRun $
-    savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-      (noDf |>> dashboardOf m "y" :: BoundPlot)
-
-  printSummary $ summarize ["omega", "mean_p", "sigma"] (hbmChainsR m)
diff --git a/bench/posteriordb/06-irt-2pl/Model.hs b/bench/posteriordb/06-irt-2pl/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/06-irt-2pl/Model.hs
+++ /dev/null
@@ -1,116 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | irt_2pl-irt_2pl (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89/90: posteriordb 横断ベンチマーク + Phase 90 A5 (vecIR ギャップ
--- 解消: 06-irt-2pl = 独立2ラテント配列を跨ぐ乗算項 (a[i]*(theta[j]-b[i])))。
---
--- Stan 原典 (posteriordb `models/stan/irt_2pl.stan`。 I=20項目×J=100人):
---   parameters { real<lower=0> sigma_theta; vector[J] theta;
---                real<lower=0> sigma_a; vector<lower=0>[I] a;
---                real mu_b; real<lower=0> sigma_b; vector[I] b; }
---   model {
---     sigma_theta ~ cauchy(0,2); theta ~ normal(0, sigma_theta);
---     sigma_a ~ cauchy(0,2); a ~ lognormal(0, sigma_a);
---     mu_b ~ normal(0,5); sigma_b ~ cauchy(0,2); b ~ normal(mu_b, sigma_b);
---     for (i in 1:I) y[i] ~ bernoulli_logit(a[i] * (theta - b[i]));
---   }
---
--- reference_posterior_name = null (posteriordb に公式 reference 無し・
--- hanalyze vs PyMC の2者比較のみ。 06-irt-2pl/README.md の旧記述「あり」は
--- Phase 89 時点の誤りで Phase 90 A5 で実測訂正した)。
---
--- Phase 90 A5: 06-irt-2pl は Phase 89 で「分類未確定」のまま保留されて
--- いたが、A1 実測調査で真因が「独立2ラテント配列を跨ぐ積」自体ではなく
--- `IR.hs` の `famOf` が family absorb (prior のベクトル化) を
--- Normal(m,τ) 構造限定にしていたこと (`a` の LogNormal 事前分布が family
--- 不成立で group ごと丸ごと Nothing になっていた) と判明。A5 で `tryGroup`
--- を「family absorb 失敗は fams から除外するだけ・likelihood 側の吸収は
--- 継続」という fault-tolerant 設計に修正し解消した (`theta`/`b` は
--- Normal 階層事前分布で family absorb、`a` の LogNormal 事前分布は
--- 既存の `constPriorsOf`/残差 AD 経路にフォールバック)。
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observe,
-                                    dataNamedObs, plateI, plateForM_, (.#), withData)
-import Hanalyze.Model.HBM (gradPathLabel)
-import Hanalyze.Plot (hbmModelSpec)
-import Hanalyze.Plot (HBMConfig (..), defaultHBM, hbm, (|->),
-                              dashboardOf, hbmChainsR)
-import Hgg.Plot.Spec (ColData (..))
-import Hgg.Plot.Frame (BoundPlot, (|>>))
-import Hgg.Plot.Backend.Rasterific (savePNGBound)
-
-import Common (summarize, printSummary, timeSamplingMs)
-import Text.Printf (printf)
-
--- | posteriordb の @irt_2pl.json@ 形状 ({"I":20, "J":100, "y":[[0/1,...]×20]})。
-data IrtData = IrtData { yMat :: [[Double]] }
-
-instance FromJSON IrtData where
-  parseJSON = withObject "IrtData" $ \v -> IrtData <$> v .: "y"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/06-irt-2pl/data/irt_2pl.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/06-irt-2pl/figures"
-
-readData :: IO [[Double]]
-readData = do
-  d <- either fail pure =<< eitherDecodeFileStrict dataPath
-  pure (yMat d)
-
--- | IRT 2PLモデル。theta (J人) / a,b (I項目) はそれぞれ族 gather
--- (`plateI` + `!!`) で宣言し、観測は (item,person) の全ペアで
--- `plateForM_` する (2000観測・`y`は行優先でフラット化して束縛)。
-irt2plModel :: Int -> Int -> ModelP ()
-irt2plModel nI nJ = do
-  sigmaTheta <- sample "sigma_theta" (HalfCauchy 2)
-  thetas <- plateI "person" nJ $ \j -> sample ("theta" .# j) (Normal 0 sigmaTheta)
-  sigmaA <- sample "sigma_a" (HalfCauchy 2)
-  as <- plateI "item" nI $ \i -> sample ("a" .# i) (LogNormal 0 sigmaA)
-  muB <- sample "mu_b" (Normal 0 5)
-  sigmaB <- sample "sigma_b" (HalfCauchy 2)
-  bs <- plateI "item2" nI $ \i -> sample ("b" .# i) (Normal muB sigmaB)
-  ys <- dataNamedObs "y" []
-  let pairs = [ (i, j) | i <- [0 .. nI - 1], j <- [0 .. nJ - 1] ]
-  plateForM_ "obs" (zip pairs ys) $ \((i, j), yij) ->
-    let logit = (as !! i) * ((thetas !! j) - (bs !! i))
-    in observe "y" (Bernoulli (1 / (1 + exp (negate logit)))) [yij]
-
-main :: IO ()
-main = do
-  rows <- readData
-  let nI = length rows
-      nJ = length (head rows)
-      ysFlat = concat rows   -- 行優先 (item-major) フラット化・Model.hs の pairs と対応
-      df = [ ("y", NumData (V.fromList ysFlat)) ] :: [(T.Text, ColData)]
-      -- PyMC 側 (model.py) と同じ設定を定数で揃える。
-      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                        , hbmWarmup = 1000, hbmSeed = Just 1
-                        -- Phase 105 A2: warmup init buffer (M=I 期) の深掘り抑制。
-                        -- seed 1/2/3 で wall −1.8〜−11.7%・ESS/s(mu_b) 全 seed 改善
-                        -- (05-mh Phase 96 A5 と同型・実測 root = experiments/phase105-*)
-                        , hbmWarmupInitMaxDepth = Just 4 }
-      m = df |-> hbm cfg (irt2plModel nI nJ)
-
-  -- 勾配経路 = 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
-
-  -- theta(100)+a(20)+b(20) = 140 latent。dashboardFullOf は大規模化するため
-  -- 05-mh と同じ理由で dashboardOf (健全性パネルのみ) を使う。
-  savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-    (noDf |>> dashboardOf m "y" :: BoundPlot)
-
-  printSummary $ summarize ["sigma_theta", "sigma_a", "mu_b", "sigma_b"] (hbmChainsR m)
diff --git a/bench/posteriordb/07-gp-regr/Model.hs b/bench/posteriordb/07-gp-regr/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/07-gp-regr/Model.hs
+++ /dev/null
@@ -1,136 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | gp_pois_regr-gp_regr (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89/90: posteriordb 横断ベンチマーク + Phase 90 A2 (vecIR ギャップ
--- 解消: 07-gp-regr = GP カーネル + Cholesky分解)。
---
--- Stan 原典 (posteriordb `models/stan/gp_regr.stan` — data_name は
--- `gp_pois_regr` だが実際に走るのは `gp_regr` モデル・Gaussian 尤度):
---   parameters { real<lower=0> rho; real<lower=0> alpha; real<lower=0> sigma; }
---   model {
---     matrix[N,N] cov = gp_exp_quad_cov(x, alpha, rho)
---                       + diag_matrix(rep_vector(sigma, N));
---     matrix[N,N] L_cov = cholesky_decompose(cov);
---     rho ~ gamma(25, 4); alpha ~ normal(0, 2); sigma ~ normal(0, 1);
---     y ~ multi_normal_cholesky(rep_vector(0, N), L_cov);
---   }
---
--- reference_posterior_name = "gp_pois_regr-gp_regr" (posteriordb に公式
--- reference posterior あり・3者比較可能)。
---
--- Phase 90 A2: `Hanalyze.Model.HBM.gpExpQuadCov` (Model.hs 追加) で
--- 共分散行列を構築し、既存の `MvNormal` distribution (`obsLogSum` が AD対応
--- 'mvNormalLogDensity' = 'choleskyL' 経由) にそのまま渡す。GP 専用の新しい
--- distribution 型は不要 — 密行列は vecIR に構造的に載らない (Phase 90 A1
--- 調査で判定済み) が、legacy walk+ad 経路でそのまま動く。
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import System.Environment (getArgs)
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observeMV,
-                                    dataNamedX, dataNamedObs)
-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 Text.Printf (printf)
-
-import Common (summarize, printSummary, timeSamplingMs)
-
--- | posteriordb の @gp_pois_regr.json@ 形状 ({"N":11, "x":[...], "y":[...], "k":[...]})。
--- `gp_regr` モデルが使うのは x/y のみ (k は gp_pois_regr モデル専用・未使用)。
-data GpRegrData = GpRegrData
-  { x :: [Double]
-  , y :: [Double]
-  }
-
-instance FromJSON GpRegrData where
-  parseJSON = withObject "GpRegrData" $ \v ->
-    GpRegrData <$> v .: "x" <*> v .: "y"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/07-gp-regr/data/gp_pois_regr.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/07-gp-regr/figures"
-
-readData :: IO ([Double], [Double])
-readData = do
-  d <- either fail pure =<< eitherDecodeFileStrict dataPath
-  pure (x d, y d)
-
--- | Phase 95 A4: N-scaling ベンチ用の合成データ (PyMC scaling script と同一式)。
--- x = linspace(-10,10,N)・y = 2 + sin(x/2) + 0.3 cos(3x) (決定的・GP 相当の滑らかさ)。
-syntheticData :: Int -> ([Double], [Double])
-syntheticData n = (xs, ys)
-  where
-    xs = [ -10 + 20 * fromIntegral i / fromIntegral (n - 1) | i <- [0 .. n - 1] ]
-    ys = [ 2 + sin (xi / 2) + 0.3 * cos (3 * xi) | xi <- xs ]
-
--- | GP 回帰 (RBF カーネル + Gaussian 尤度)。Phase 95 B-dsl: カーネル役割
--- (x/α/ρ/σ) を型で明示保持する 'MvNormalGpRBF' で 1 個の N 次元同時観測として
--- 尤度評価する (Stan の multi_normal_cholesky と等価・zero mean)。共分散
--- Σ = α² exp(-0.5 d²/ρ²) + (1e-10 + σ)·I は Distribution 側が内部構築し、勾配は
--- 閉形式随伴 ('gpRBFAnalyticVG'・Cholesky を AD tape に載せない) で高速評価される。
-gpRegrModel :: ModelP ()
-gpRegrModel = do
-  rho   <- sample "rho"   (Gamma 25 4)
-  alpha <- sample "alpha" (HalfNormal 2)
-  sigma <- sample "sigma" (HalfNormal 1)
-  xs <- dataNamedX   "x" []
-  ys <- dataNamedObs "y" []
-  observeMV "y" (MvNormalGpRBF xs alpha rho sigma) [ys]
-
-main :: IO ()
-main = do
-  args <- getArgs
-  case args of
-    -- Phase 95 A4: `scale <N>` = 合成データで N-scaling ベンチ (図出力なし・
-    -- sampling wall と posterior 平均のみ・PyMC scaling script と対比)。
-    ("scale" : nStr : _) -> do
-      let n = read nStr :: Int
-          (xs, ys) = syntheticData n
-          df = [ ("x", NumData (V.fromList xs)), ("y", NumData (V.fromList ys)) ]
-               :: [(T.Text, ColData)]
-          cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                            , hbmWarmup = 1000, hbmSeed = Just 1 }
-          m = df |-> hbm cfg gpRegrModel
-      -- Phase 96 A2: 勾配経路 (束縛済 hbmModelSpec で判定・Phase 91 A4 と同型)。
-      putStrLn $ "勾配経路 = " ++ gradPathLabel (hbmModelSpec m)
-      (_, samplingMs) <- timeSamplingMs (hbmChainsR m)
-      printf "N=%d  sampling wall = %.1f ms (draws only)\n" n samplingMs
-      printSummary $ summarize ["rho", "alpha", "sigma"] (hbmChainsR m)
-    _ -> do
-      (xs, ys) <- readData
-      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 gpRegrModel
-
-      -- Phase 96 A2: 勾配経路 (束縛済 hbmModelSpec で判定・Phase 91 A4 と同型)。
-      putStrLn $ "勾配経路 = " ++ gradPathLabel (hbmModelSpec m)
-
-      -- サンプリング**のみ**の壁時計 (PyMC run_pymc_matrix.py の t0=perf_counter();
-      -- pm.sample() と対応させる・Common.timeSamplingMs 参照)。dashboardFullOf 等
-      -- 後続処理は hbmChainsR の thunk が既に強制済みのものを再利用するので、
-      -- 二重計算にはならない。
-      (_, samplingMs) <- timeSamplingMs (hbmChainsR m)
-      printf "sampling wall = %.1f ms (draws only, no dashboard/startup)\n" samplingMs
-
-      savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-        (noDf |>> dashboardFullOf m "y" :: BoundPlot)
-
-      printSummary $ summarize ["rho", "alpha", "sigma"] (hbmChainsR m)
diff --git a/bench/posteriordb/09-eight-schools/Model.hs b/bench/posteriordb/09-eight-schools/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/09-eight-schools/Model.hs
+++ /dev/null
@@ -1,110 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | eight_schools-eight_schools_noncentered (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89: posteriordb 横断ベンチマーク。階層モデルの正準例 8-schools
--- (Rubin 1981・8校の補習授業効果) を non-centered パラメタ化で実装する
--- (`docs/api-guide/03-bayesian-hbm.md` §階層モデルの例に準拠)。
---
--- Stan 原典 (posteriordb `models/stan/eight_schools_noncentered.stan`):
---   parameters { vector[J] theta_trans; real mu; real<lower=0> tau; }
---   transformed parameters { theta = theta_trans * tau + mu; }
---   model {
---     theta_trans ~ normal(0, 1);
---     y ~ normal(theta, sigma);   // sigma は既知データ (観測誤差)
---     mu ~ normal(0, 5);
---     tau ~ cauchy(0, 5);
---   }
---
--- reference_posterior_name = "eight_schools-eight_schools_noncentered"
--- (posteriordb に公式 reference posterior あり・hanalyze vs PyMC vs 公式referenceの
--- 3者比較が可能)。
---
--- 高レベル API (`df |-> hbm`) を使用: y/sigma は 'dataNamedObs'/'dataNamedX' で
--- df から束縛し、 学校ごとの潜在変数 eta は 'plateI' (index 版・結果保持) で
--- 8個宣言してから 'plateForM_' で観測に束ねる (api-guide の gather パターン)。
---
--- ビルド: cabal build --project-file=cabal.project.plot posteriordb-eight-schools
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observe,
-                                    dataNamedX, dataNamedObs, plateI, plateForM_,
-                                    (.#))
-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 Text.Printf (printf)
-
-import Common (summarize, printSummary, timeSamplingMs)
-
--- | posteriordb の @eight_schools.json@ 形状 ({"J":8, "y":[...], "sigma":[...]})。
-data EightSchoolsData = EightSchoolsData
-  { y     :: [Double]
-  , sigma :: [Double]
-  }
-
-instance FromJSON EightSchoolsData where
-  parseJSON = withObject "EightSchoolsData" $ \v ->
-    EightSchoolsData <$> v .: "y" <*> v .: "sigma"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/09-eight-schools/data/eight_schools.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/09-eight-schools/figures"
-
-readData :: IO ([Double], [Double])
-readData = do
-  d <- either fail pure =<< eitherDecodeFileStrict dataPath
-  pure (y d, sigma d)
-
--- | Non-centered 階層モデル: eta_j ~ Normal(0,1)・theta_j = mu + tau*eta_j
--- (`docs/api-guide/03-bayesian-hbm.md` の eightSchools 例と同型・sigma は
--- 観測ごとに既知の定数 (Stan 原典の `sigma` データ) なので dataNamedX で束縛する)。
-eightSchoolsModel :: ModelP ()
-eightSchoolsModel = do
-  mu  <- sample "mu"  (Normal 0 5)
-  tau <- sample "tau" (HalfCauchy 5)
-  sigmas <- dataNamedX   "sigma" []
-  ys     <- dataNamedObs "y"     []
-  etas <- plateI "school" 8 $ \j -> sample ("eta" .# j) (Normal 0 1)
-  plateForM_ "school_obs" (zip3 [0 ..] sigmas ys) $ \(j, sg, yi) ->
-    observe ("y" .# j) (Normal (mu + tau * etas !! j) sg) [yi]
-
-main :: IO ()
-main = do
-  (ys, sigmas) <- readData
-  let df = [ ("y",     NumData (V.fromList ys))
-           , ("sigma", NumData (V.fromList sigmas))
-           ] :: [(T.Text, ColData)]
-      -- PyMC 側 (model.py) と同じ設定を定数で揃える。
-      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                        , hbmWarmup = 1000, hbmSeed = Just 1 }
-      m = df |-> hbm cfg eightSchoolsModel
-
-  -- Phase 96 A2: 勾配経路 = compileGradUV が実際に選ぶ経路 (束縛済
-  -- hbmModelSpec で判定・Phase 91 A4 と同型)。
-  putStrLn $ "勾配経路 = " ++ gradPathLabel (hbmModelSpec m)
-
-  -- サンプリング**のみ**の壁時計 (PyMC run_pymc_matrix.py の t0=perf_counter();
-  -- pm.sample() と対応させる・Common.timeSamplingMs 参照)。dashboardFullOf 等
-  -- 後続処理は hbmChainsR の thunk が既に強制済みのものを再利用するので、
-  -- 二重計算にはならない。
-  (_, samplingMs) <- timeSamplingMs (hbmChainsR m)
-  printf "sampling wall = %.1f ms (draws only, no dashboard/startup)\n" samplingMs
-
-  savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-    (noDf |>> dashboardFullOf m "y" :: BoundPlot)
-
-  printSummary $ summarize ["mu", "tau"] (hbmChainsR m)
diff --git a/bench/posteriordb/10-rats/Model.hs b/bench/posteriordb/10-rats/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/10-rats/Model.hs
+++ /dev/null
@@ -1,136 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | rats_data-rats_model (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89: posteriordb 横断ベンチマーク。BUGS 古典例「ラットの成長曲線」
--- (30匹 × 5時点の体重・縦断的階層線形回帰)。ラットごとに独立な切片
--- alpha[i]・傾き beta[i] を持ち、両方とも部分プーリングされる
--- (mu_alpha/sigma_alpha, mu_beta/sigma_beta)。
---
--- Stan 原典 (posteriordb `models/stan/rats_model.stan`。"Model simplified"
--- 版・sigma_y/sigma_alpha/sigma_beta は真に improper flat prior):
---   parameters { array[N] real alpha; array[N] real beta;
---                real mu_alpha; real mu_beta;
---                real<lower=0> sigma_y; real<lower=0> sigma_alpha; real<lower=0> sigma_beta; }
---   model {
---     mu_alpha ~ normal(0, 100); mu_beta ~ normal(0, 100);
---     alpha ~ normal(mu_alpha, sigma_alpha); beta ~ normal(mu_beta, sigma_beta);
---     y[n] ~ normal(alpha[rat[n]] + beta[rat[n]] * (x[n] - xbar), sigma_y);
---   }
---
--- improper flat prior (下限0のみ・上限なし) は hanalyze に表現できないため、
--- HalfCauchy(25) (09-eight-schools の tau ~ HalfCauchy(5) と同じ流儀の
--- 緩い weakly-informative prior) に置換する。
---
--- ★実測で判明した罠: 当初 Uniform(0,100) で試したところ、全 warmup で
--- acceptanceRate=0.0・chainEnergy=Infinity 全 draw で発散し、事後平均が
--- 全パラメータで厳密に 0.0000 に凍りつく現象が発生した。原因は
--- `Distribution.hs:309-310` の既知の制約:
--- 「Uniform の真の制約変換 (logit-on-(lo,hi)) は現状未実装・unconstrained
--- 扱い」。unconstrained 初期値は raw=0 だが、これが Uniform(lo,hi) では
--- 変換なしにそのまま値 0 として使われる ("下限ちょうど" ではなく
--- "変換前 0" が直接使われる)。sigma_y のような Normal 尤度の SD に
--- Uniform(0,X) を直接使うと、初期値 sigma_y=0 で
--- `Normal(mu, 0)` が退化し log-density が -Infinity になり、HMC が
--- 初手から発散し続けて一切回復しない (01-glm-poisson の alpha/beta の
--- ような「内部の有界パラメータ」なら raw=0 は無害だが、SD パラメータでは
--- 致命的)。`HalfCauchy`/`HalfNormal` は `PositiveT` 変換 (exp 系) を持ち
--- raw=0 が安全な内点にマップされるため、この罠を回避できる
--- (実測: acceptanceRate 0.0 → 0.9 に復帰・確認は cabal repl トイモデルで
--- 実施)。PyMC 側 (model.py) も同じ HalfCauchy(25) に揃える (両者比較可能
--- にするため・Stan 原典の改変幅は同一なので公平性は保たれる)。
---
--- reference_posterior_name = null (posteriordb に公式 reference 無し・2者比較のみ)。
---
--- ラット番号 (rat[]) は df 列ではなく、Mh モデルの T と同じ流儀でモデル関数へ
--- 直接 closure 引数として渡す (df|->hbm の対象データではなく構造情報)。
--- ラットごとの alpha[i]/beta[i] latent は eight-schools 型 plateI + gather
--- パターンで宣言する。
---
--- ビルド: cabal build --project-file=cabal.project.plot posteriordb-rats
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import Text.Printf (printf)
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observe,
-                                    dataNamedX, dataNamedObs, plateI, plateForM_, (.#))
-import Hanalyze.Model.HBM (gradPathLabel)
-import Hanalyze.Plot (hbmModelSpec)
-import Hanalyze.Plot (HBMConfig (..), defaultHBM, hbm, (|->),
-                              dashboardOf, hbmChainsR)
-import Hgg.Plot.Spec (ColData (..))
-import Hgg.Plot.Frame (BoundPlot, (|>>))
-import Hgg.Plot.Backend.Rasterific (savePNGBound)
-
-import Common (summarize, printSummary, timeSamplingMs)
-
--- | posteriordb の @rats_data.json@ 形状
--- ({"N":30, "Npts":150, "rat":[...], "x":[...], "y":[...], "xbar":22})。
-data RatsData = RatsData
-  { nRats :: Int
-  , rat   :: [Int]
-  , xArr  :: [Double]
-  , yArr  :: [Double]
-  , xbar  :: Double
-  }
-
-instance FromJSON RatsData where
-  parseJSON = withObject "RatsData" $ \v ->
-    RatsData <$> v .: "N" <*> v .: "rat" <*> v .: "x" <*> v .: "y" <*> v .: "xbar"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/10-rats/data/rats_data.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/10-rats/figures"
-
-readData :: IO RatsData
-readData = either fail pure =<< eitherDecodeFileStrict dataPath
-
--- | 縦断的階層線形回帰 (ラットごとの切片/傾き)。ratIdx (1始まり)・xbar は
--- データ由来の固定構造として closure で渡す (Mh モデルの T と同じ流儀)。
-ratsModel :: Int -> [Int] -> Double -> ModelP ()
-ratsModel n ratIdx center = do
-  muAlpha    <- sample "mu_alpha"    (Normal 0 100)
-  muBeta     <- sample "mu_beta"     (Normal 0 100)
-  sigmaY     <- sample "sigma_y"     (HalfCauchy 25)
-  sigmaAlpha <- sample "sigma_alpha" (HalfCauchy 25)
-  sigmaBeta  <- sample "sigma_beta"  (HalfCauchy 25)
-  alphas <- plateI "rat_alpha" n $ \i -> sample ("alpha" .# i) (Normal muAlpha sigmaAlpha)
-  betas  <- plateI "rat_beta"  n $ \i -> sample ("beta"  .# i) (Normal muBeta  sigmaBeta)
-  xs <- dataNamedX   "x" []
-  ys <- dataNamedObs "y" []
-  let centerA = realToFrac center
-  plateForM_ "obs" (zip3 ratIdx xs ys) $ \(r, xi, yi) ->
-    let mu = (alphas !! (r - 1)) + (betas !! (r - 1)) * (xi - centerA)
-    in observe "y" (Normal mu sigmaY) [yi]
-
-main :: IO ()
-main = do
-  d <- readData
-  let df = [ ("x", NumData (V.fromList (xArr d)))
-           , ("y", NumData (V.fromList (yArr d)))
-           ] :: [(T.Text, ColData)]
-      -- PyMC 側 (model.py) と同じ設定を定数で揃える。
-      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                        , hbmWarmup = 1000, hbmSeed = Just 1 }
-      m = df |-> hbm cfg (ratsModel (nRats d) (rat d) (xbar d))
-
-  -- 勾配経路 = 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
-
-  -- N=30 匹分の alpha/beta latent を含むため 'dashboardFullOf' は肥大化する
-  -- (05-mh と同じ判断)。健全性 2x2 パネル (DAG/forest/PPC/energy) のみ。
-  savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-    (noDf |>> dashboardOf m "y" :: BoundPlot)
-
-  printSummary $ summarize ["mu_alpha", "mu_beta", "sigma_y", "sigma_alpha", "sigma_beta"] (hbmChainsR m)
diff --git a/bench/posteriordb/11-seeds/Model.hs b/bench/posteriordb/11-seeds/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/11-seeds/Model.hs
+++ /dev/null
@@ -1,126 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | seeds_data-seeds_model (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89: posteriordb 横断ベンチマーク。BUGS 古典例「種子発芽実験」
--- (Crowder 1978・I=21プレート・2種の種子×2種の根の抽出物の2x2要因計画+
--- overdispersion 用のプレートごとランダム切片)。
---
--- Stan 原典 (posteriordb `models/stan/seeds_model.stan`):
---   parameters { real alpha0,alpha1,alpha2,alpha12; real<lower=0> tau;
---                vector[I] b; }
---   transformed parameters { sigma = 1/sqrt(tau); }
---   model {
---     alpha0,alpha1,alpha2,alpha12 ~ normal(0, 1000);
---     tau ~ gamma(1e-3, 1e-3);
---     b ~ normal(0, sigma);
---     n ~ binomial_logit(N, alpha0 + alpha1*x1 + alpha2*x2 + alpha12*x1*x2 + b);
---   }
---
--- hanalyze に `binomial_logit` 相当は無いため `Binomial N p` +
--- `p = invlogit(eta)` へ手動展開する (05-mh の ZeroInflatedBinomial と
--- 同じ流儀)。`tau ~ Gamma(1e-3,1e-3)` は hanalyze の `Gamma` が `PositiveT`
--- 変換 (exp系) を持つため 10-rats で踏んだ「Uniform を SD に使う罠」は
--- 発生しない (既知の一様事前 sd パラメータ発散パターン)。
---
--- reference_posterior_name = null (posteriordb に公式 reference 無し・2者比較のみ)。
---
--- N (試行数、プレートごとに既知の固定整数) は df 列ではなく、Mh モデルの T
--- と同じ流儀でモデル関数へ直接 closure 引数として渡す。
---
--- ビルド: cabal build --project-file=cabal.project.plot posteriordb-seeds
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import Data.List (zip4)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import Text.Printf (printf)
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observe,
-                                    dataNamedX, dataNamedObs, plateI, plateForM_, (.#))
-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 の @seeds_data.json@ 形状
--- ({"I":21, "n":[...], "N":[...], "x1":[...], "x2":[...]})。
-data SeedsData = SeedsData
-  { nI   :: Int
-  , nObs :: [Int]
-  , nCap :: [Int]
-  , x1v  :: [Double]
-  , x2v  :: [Double]
-  }
-
-instance FromJSON SeedsData where
-  parseJSON = withObject "SeedsData" $ \v ->
-    SeedsData <$> v .: "I" <*> v .: "n" <*> v .: "N" <*> v .: "x1" <*> v .: "x2"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/11-seeds/data/seeds_data.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/11-seeds/figures"
-
-readData :: IO SeedsData
-readData = either fail pure =<< eitherDecodeFileStrict dataPath
-
--- | 2x2要因計画のロジスティック回帰 + プレートごとランダム切片。
--- i (プレート数)・trials (試行数 N、プレートごとの固定整数) は
--- データ由来の固定構造として closure で渡す (Mh モデルの T と同じ流儀)。
---
--- Phase 94 A4-2: **非中心化** (non-centered)。 Stan 原典は centered
--- (@b_k ~ Normal(0, σ)@) だが、 σ=1/√τ が小さい領域に funnel の首ができ、
--- chain が凍結して tau ESS を潰す (§A4-1 で実測: centered は 1/4 chain 崩壊・
--- ess(tau)=11)。 @z_k ~ Normal(0,1)@ + @b_k = z_k·σ@ に置換すると z が τ と
--- decouple し首が消える (Stan/PyMC の hierarchical 定番)。 事後は同一。
--- 非中心化後: 崩壊 0・ess(tau)=837 (§A4-2)。
-seedsModel :: Int -> [Int] -> ModelP ()
-seedsModel i trials = do
-  alpha0  <- sample "alpha0"  (Normal 0 1000)
-  alpha1  <- sample "alpha1"  (Normal 0 1000)
-  alpha2  <- sample "alpha2"  (Normal 0 1000)
-  alpha12 <- sample "alpha12" (Normal 0 1000)
-  tau     <- sample "tau"     (Gamma 1.0e-3 1.0e-3)
-  let sigma = 1 / sqrt tau
-  zs <- plateI "plate" i $ \k -> sample ("z" .# k) (Normal 0 1)   -- 非中心化 latent (b_k = z_k·σ)
-  x1s <- dataNamedX   "x1" []
-  x2s <- dataNamedX   "x2" []
-  ns  <- dataNamedObs "n"  []
-  plateForM_ "obs" (zip [0 :: Int ..] (zip4 trials x1s x2s ns)) $ \(k, (capK, x1k, x2k, nk)) ->
-    let eta = alpha0 + alpha1 * x1k + alpha2 * x2k + alpha12 * x1k * x2k
-                + (zs !! k) * sigma
-        p   = 1 / (1 + exp (negate eta))
-    in observe "n" (Binomial capK p) [nk]
-
-main :: IO ()
-main = do
-  d <- readData
-  let df = [ ("x1", NumData (V.fromList (x1v d)))
-           , ("x2", NumData (V.fromList (x2v d)))
-           , ("n",  NumData (V.fromList (map fromIntegral (nObs d))))
-           ] :: [(T.Text, ColData)]
-      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                        , hbmWarmup = 1000, hbmSeed = Just 1 }
-      m = df |-> hbm cfg (seedsModel (nI d) (nCap d))
-
-  -- 勾配経路 = 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
-
-  savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-    (noDf |>> dashboardFullOf m "n" :: BoundPlot)
-
-  printSummary $ summarize ["alpha0", "alpha1", "alpha2", "alpha12", "tau"] (hbmChainsR m)
diff --git a/bench/posteriordb/12-ark/Model.hs b/bench/posteriordb/12-ark/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/12-ark/Model.hs
+++ /dev/null
@@ -1,114 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | arK-arK (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89: posteriordb 横断ベンチマーク。AR(K) (K次自己回帰) 時系列モデル
--- (K=5・T=200)。GARCH (03-garch11) と異なり **分散ではなく平均のみが過去に
--- 依存する**ため、全ての y は既知データであり、モデルとしては「K個のラグ
--- 特徴量を使った静的な線形回帰」に帰着する (潜在変数間の自己参照的な
--- 再帰は存在しない)。
---
--- Stan 原典 (posteriordb `models/stan/arK.stan`):
---   parameters { real alpha; array[K] real beta; real<lower=0> sigma; }
---   model {
---     alpha ~ normal(0, 10); beta ~ normal(0, 10); sigma ~ cauchy(0, 2.5);
---     for (t in (K+1):T) {
---       mu = alpha + sum_{k=1}^{K} beta[k]*y[t-k];
---       y[t] ~ normal(mu, sigma);
---     }
---   }
---
--- `sigma ~ cauchy(0, 2.5)` (下限0の半コーシー) は hanalyze の `HalfCauchy`
--- (`PositiveT` 変換) にそのまま対応する (10-rats の Uniform-SD 罠に
--- 該当しない・09-eight-schools の tau と同型)。
---
--- **reference_posterior_name = "arK-arK"** (posteriordb に公式 reference
--- あり・hanalyze vs PyMC vs 公式referenceの3者比較が可能)。
---
--- K 個のラグ特徴量 (@lag1@..@lagK@) を Haskell 側で事前計算し、df 列として
--- 束縛する (dataNamedX を K 回呼ぶ)。
---
--- ビルド: cabal build --project-file=cabal.project.plot posteriordb-ark
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import Text.Printf (printf)
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observe,
-                                    dataNamedX, dataNamedObs, plateI, plateForM_, (.#),
-                                    gradPathLabel)
-import Hanalyze.Plot (HBMConfig (..), defaultHBM, hbm, (|->),
-                              dashboardFullOf, hbmChainsR, hbmModelSpec)
-import Hgg.Plot.Spec (ColData (..))
-import Hgg.Plot.Frame (BoundPlot, (|>>))
-import Hgg.Plot.Backend.Rasterific (savePNGBound)
-
-import Common (summarize, printSummary, timeSamplingMs)
-
--- | posteriordb の @arK.json@ 形状 ({"K":5, "T":200, "y":[...]})。
-data ArKData = ArKData { kLag :: Int, tLen :: Int, yArr :: [Double] }
-
-instance FromJSON ArKData where
-  parseJSON = withObject "ArKData" $ \v ->
-    ArKData <$> v .: "K" <*> v .: "T" <*> v .: "y"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/12-ark/data/arK.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/12-ark/figures"
-
-readData :: IO ArKData
-readData = either fail pure =<< eitherDecodeFileStrict dataPath
-
--- | ラグ特徴量 (t=K..T-1 の各観測に対し @y[t-1]..y[t-K]@) と目的変数
--- (@y[K]..y[T-1]@) を事前計算する。0始まりのインデックス。
-lagDesign :: Int -> [Double] -> ([[Double]], [Double])
-lagDesign k ys =
-  let yv = V.fromList ys
-      n  = V.length yv
-      obsIdx = [k .. n - 1]
-      targets = [ yv V.! t | t <- obsIdx ]
-      lags = [ [ yv V.! (t - lg) | t <- obsIdx ] | lg <- [1 .. k] ]  -- lags!!(lg-1)
-  in (lags, targets)
-
--- | AR(K) 静的線形回帰 (ラグ特徴量は全て既知データ)。
-arKModel :: Int -> ModelP ()
-arKModel k = do
-  alpha <- sample "alpha" (Normal 0 10)
-  betas <- plateI "beta" k $ \j -> sample ("beta" .# (j + 1)) (Normal 0 10)
-  sigma <- sample "sigma" (HalfCauchy 2.5)
-  lagCols <- mapM (\lg -> dataNamedX (T.pack ("lag" ++ show lg)) []) [1 .. k]
-  ys <- dataNamedObs "y_obs" []
-  plateForM_ "obs" (zip [0 ..] ys) $ \(i, yi) ->
-    let mu = alpha + sum [ (betas !! (lg - 1)) * ((lagCols !! (lg - 1)) !! i) | lg <- [1 .. k] ]
-    in observe "y_obs" (Normal mu sigma) [yi]
-
-main :: IO ()
-main = do
-  d <- readData
-  let (lags, targets) = lagDesign (kLag d) (yArr d)
-      lagDf = [ (T.pack ("lag" ++ show lg), NumData (V.fromList col))
-              | (lg, col) <- zip [1 :: Int ..] lags ]
-      df = ("y_obs", NumData (V.fromList targets)) : lagDf
-      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                        , hbmWarmup = 1000, hbmSeed = Just 1 }
-      m = df |-> hbm cfg (arKModel (kLag d))
-
-  -- 勾配経路 = compileGradUV が実際に選ぶ経路 (束縛済モデルで判定)。
-  -- Phase 91 A4: AR(K) は静的ラグ線形回帰 = Gaussian LM 閉形式ブロックに吸収。
-  -- ★生モデルを synthVecIR に渡すと data 空で Nothing と誤表示するため
-  --   'hbmModelSpec m' (df 束縛済) を 'gradPathLabel' に渡す。
-  putStrLn $ "勾配経路 = " ++ gradPathLabel (hbmModelSpec m)
-
-  (_, samplingMs) <- timeSamplingMs (hbmChainsR m)
-  printf "sampling wall = %.1f ms (draws only, no dashboard/startup)\n" samplingMs
-
-  savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-    (noDf |>> dashboardFullOf m "y_obs" :: BoundPlot)
-
-  printSummary $ summarize ["alpha", "beta_1", "beta_2", "beta_3", "beta_4", "beta_5", "sigma"] (hbmChainsR m)
diff --git a/bench/posteriordb/13-traffic-accident-nyc/Model.hs b/bench/posteriordb/13-traffic-accident-nyc/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/13-traffic-accident-nyc/Model.hs
+++ /dev/null
@@ -1,260 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | traffic_accident_nyc-bym2_offset_only (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89: posteriordb 横断ベンチマーク。BYM2 空間疫学モデル (Morris et al.
--- 2019) — NYC 交通事故データ (N=1921地域・N_edges=5461隣接ペア)。ICAR
--- (intrinsic conditional autoregressive) 事前分布を「隣接ペアごとの
--- 差分ペナルティ」(`target += -0.5*dot_self(phi[node1]-phi[node2])`) で
--- 表現する Stan の標準的な BYM2 実装 (N×N 精度行列の陽な構築/逆行列計算を
--- 回避する定石)。
---
--- Stan 原典 (posteriordb `models/stan/bym2_offset_only.stan`):
---   parameters { real beta0; real<lower=0> sigma; real<lower=0,upper=1> rho;
---                vector[N] theta; vector[N] phi; }
---   transformed parameters {
---     convolved_re = sqrt(1-rho)*theta + sqrt(rho/scaling_factor)*phi;
---   }
---   model {
---     y ~ poisson_log(log_E + beta0 + convolved_re*sigma);
---     target += -0.5 * dot_self(phi[node1] - phi[node2]);  -- ICAR pairwise
---     beta0 ~ normal(0,1); theta ~ normal(0,1); sigma ~ normal(0,1);
---     rho ~ beta(0.5,0.5);
---     sum(phi) ~ normal(0, 0.001*N);                        -- soft sum-to-zero
---   }
---
--- ★`phi` は Stan 原典で **固有の (marginal) prior を持たない** (`theta`とは
--- 対照的)。ICAR ペナルティ + ソフトゼロ和制約のみが phi の情報源であり、
--- 事実上「improper flat」を前提にしている。hanalyze には improper flat
--- distribution が無いため、他モデル (01-glm-poisson の Uniform 箱等) と
--- 同じ流儀で **`Normal 0 1000` という極めて diffuse な近似**を phi 自身の
--- 周辺成分に与える (ICAR ペナルティが実質的に支配的なので事後への影響は
--- 無視できる想定・要実測確認)。
---
--- `target += ...` の非標準尤度項は `potential` (PyMC `pm.Potential` 相当)
--- で表現する。ソフトゼロ和制約 (`sum(phi) ~ normal(...)`) は
--- `logDensity (Normal 0 sd) (sum phis)` を `potential` に渡す形で実装する。
---
--- reference_posterior_name = null (posteriordb に公式 reference 無し・2者比較のみ)。
---
--- ★N=1921 (theta+phi=3842 latent) + N_edges=5461 という大規模モデル。
--- Phase 90 A10-1 実測 (2026-07-11) により: Poisson 尤度 + theta/phi 族は
--- **vecIR に吸収済み (synthVecIR = Just)**。 ただし `potential` 2 項
--- (icar / sum_zero) は vecIR 非対応で残差 ad に落ち、 これが勾配 1 回
--- ~0.13s の 93% を占める (potential 無し対照 = 0.009s)。 詳細は
--- specification/phases/phase-90-vecir-gap-extensions.md §A10-1。
---
--- ビルド: cabal build --project-file=cabal.project.plot posteriordb-bym2
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import qualified Data.Map.Strict as Map
-import qualified Data.Set as Set
-import qualified Data.Text as T
-import qualified Data.Vector as BV
-import qualified Data.Vector as V
-import qualified Data.Vector.Unboxed as VU
-import System.Environment (getArgs)
-import System.Exit (exitSuccess)
-import System.IO (BufferMode (..), hSetBuffering, stdout)
-import Text.Printf (printf)
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observe,
-                                    dataNamedX, dataNamedObs, plateI, plateForM_,
-                                    potential, logDensity, (.#), sampleNames)
-import qualified Hanalyze.Model.HBM.Gradient as G
-import qualified Hanalyze.Model.HBM.IR as IR
-import Hanalyze.Model.HBM (gradPathLabel)
-import Hanalyze.Plot (hbmModelSpec)
-import Hanalyze.Plot (HBMConfig (..), defaultHBM, hbm, (|->),
-                              dashboardOf, hbmChainsR)
-import Hgg.Plot.Spec (ColData (..))
-import Hgg.Plot.Frame (BoundPlot, (|>>))
-import Hgg.Plot.Backend.Rasterific (savePNGBound)
-
-import Common (summarize, printSummary, timeSamplingMs)
-
--- | posteriordb の @traffic_accident_nyc.json@ 形状
--- ({"N":1921, "N_edges":5461, "node1":[...], "node2":[...], "y":[...],
---   "E":[...], "scaling_factor":0.7137})。
-data TrafficData = TrafficData
-  { nAreas   :: Int
-  , nEdges   :: Int
-  , node1v   :: [Int]
-  , node2v   :: [Int]
-  , yObs     :: [Int]
-  , eOffset  :: [Double]
-  , scalingF :: Double
-  }
-
-instance FromJSON TrafficData where
-  parseJSON = withObject "TrafficData" $ \v ->
-    TrafficData <$> v .: "N" <*> v .: "N_edges" <*> v .: "node1" <*> v .: "node2"
-                <*> v .: "y" <*> v .: "E" <*> v .: "scaling_factor"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/13-traffic-accident-nyc/data/traffic_accident_nyc.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/13-traffic-accident-nyc/figures"
-
-readData :: IO TrafficData
-readData = either fail pure =<< eitherDecodeFileStrict dataPath
-
--- | BYM2 空間疫学モデル。edges (0始まりに変換済のnode1/node2ペア)・
--- scalingFactor はデータ由来の固定構造として closure で渡す。
---
--- ★Phase 90 A10-1: data list は引数で直接束縛する (`df |->` は同名同値の
--- 束縛なので挙動不変)。 旧実装は `dataNamedX "log_E" []` (空 list) だったため
--- `main` の `synthVecIR` 診断が「観測行 0 → Nothing」 を印字してしまい、
--- 実サンプラ (df 束縛済) が vecIR = Just で走っている事実を隠していた。
-bym2Model :: Int -> [(Int, Int)] -> Double -> [Double] -> [Double] -> ModelP ()
-bym2Model n edges scalingFactor logEsIn ysIn = do
-  beta0 <- sample "beta0" (Normal 0 1)
-  sigma <- sample "sigma" (HalfNormal 1)
-  rho   <- sample "rho"   (Beta 0.5 0.5)
-  thetas <- plateI "theta" n $ \i -> sample ("theta" .# i) (Normal 0 1)
-  phis   <- plateI "phi"   n $ \i -> sample ("phi"   .# i) (Normal 0 1000)
-  logEs <- dataNamedX   "log_E" logEsIn
-  ys    <- dataNamedObs "y"     ysIn
-  let scalingA = realToFrac scalingFactor
-      convolved i = sqrt (1 - rho) * (thetas !! i) + sqrt (rho / scalingA) * (phis !! i)
-  plateForM_ "obs" (zip3 [0 ..] logEs ys) $ \(i, logEi, yi) ->
-    let eta = logEi + beta0 + convolved i * sigma
-    in observe "y" (Poisson (exp eta)) [yi]
-  -- ICAR ペア差分ペナルティ (Stan の `target += -0.5*dot_self(phi[n1]-phi[n2])`)。
-  let icarPenalty = negate 0.5 * sum
-        [ (phis !! a - phis !! b) * (phis !! a - phis !! b) | (a, b) <- edges ]
-  potential "icar" icarPenalty
-  -- ソフトゼロ和制約 (`sum(phi) ~ normal(0, 0.001*N)`)。
-  let sdSumZero = realToFrac (0.001 * fromIntegral n :: Double)
-  potential "sum_zero" (logDensity (Normal 0 sdSumZero) (sum phis))
-
--- ===========================================================================
--- Phase 99 A2: vecIR arena 命令列の静的 dump (ICAR の融合度を実測)
--- ===========================================================================
-
--- | @synthVecIR@ → @compileVecIR@ でコンパイルし、命令種別の内訳と長さ分布を
--- 出力する。ICAR (5461 edge の二次形式) が融合ベクトル op か scalar 展開かを
--- 目視判定するための静的解析 (BenchHBMVecIRProf.instrMix と同型)。
-dumpIR :: ModelP () -> IO ()
-dumpIR m = do
-  let names = sampleNames m
-      nP    = length names
-  putStrLn $ "sampleNames nP = " ++ show nP
-  case IR.synthVecIR m of
-    Nothing -> putStrLn "synthVecIR = Nothing (vecIR に乗っていない)"
-    Just (gs, fams, sObs) -> do
-      let ixOf = Map.fromList (zip names [0 :: Int ..])
-          cvi  = IR.compileVecIR ixOf gs fams
-          prog = IR.cvProg cvi
-          instrs = BV.toList (IR.vpInstrs prog)
-          lens   = VU.toList (IR.vpLen prog)
-          keyOf ins = case ins of
-            IR.VIK{}       -> "VIK   (スカラ定数)"
-            IR.VIKV{}      -> "VIKV  (ベクトル定数)"
-            IR.VILeafS{}   -> "VILeafS (scalar leaf)"
-            IR.VILeafV{}   -> "VILeafV (vector leaf)"
-            IR.VIGath{}    -> "VIGath (gather)"
-            IR.VIUn{}      -> "VIUn  (elementwise 単項)"
-            IR.VIBin{}     -> "VIBin (elementwise 二項)"
-            IR.VISum{}     -> "VISum (Σ 縮約)"
-            IR.VIAxpy{}    -> "VIAxpy (a+s·v 融合)"
-            IR.VIAxpyC{}   -> "VIAxpyC (a+s·const 融合)"
-            IR.VISumSqD{}  -> "VISumSqD (Σ(x−m)² 融合)"
-            IR.VISumSqC{}  -> "VISumSqC (Σ(c−m)² 融合)"
-            IR.VIMulG{}    -> "VIMulG (s·gather 融合)"
-            IR.VIAxpyG{}   -> "VIAxpyG (a+s·gather 融合)"
-            IR.VIMulVC{}   -> "VIMulVC (s·v⊙c 融合)"
-            IR.VISumSqC2{} -> "VISumSqC2 (Σ(c−m1−m2)² 融合)"
-            IR.VISumSqDGG{} -> "VISumSqDGG (Σ(gath−gath)² 融合 = ICAR)"
-          accum mp (ins, l) =
-            Map.insertWith (\(c1, e1) (c2, e2) -> (c1 + c2, e1 + e2))
-              (keyOf ins) (1 :: Int, max 1 l) mp
-          mixed = [ (k, c, e) | (k, (c, e)) <- Map.toList (foldl accum Map.empty (zip instrs lens)) ]
-                    :: [(String, Int, Int)]
-          famSet = Set.fromList (concat [ ms | (ms, _, _) <- fams ])
-          cps    = G.constPriorsOf m famSet
-          exclNames = sObs `Set.union` famSet
-                      `Set.union` Set.fromList (map fst cps)
-          noResid = G.residualFreeOfDensity exclNames m
-      printf "instrs=%d  vpSize(arena セル)=%d  guards=%d\n"
-        (BV.length (IR.vpInstrs prog)) (IR.vpSize prog)
-        (length (IR.vpGuards prog))
-      printf "residual ad (mPriorGrad): %s  (constPriors=%d, excl=%d/%d)\n"
-        (if noResid then "なし (noResid)" else "★あり = per-eval に ad が乗る" :: String)
-        (length cps) (Set.size exclNames) nP
-      putStrLn "\n--- 命令列 mix (種別 / 本数 / 総セル数) ---"
-      mapM_ (\(k, c, e) -> printf "  %-28s %5d 本  %8d セル\n" k c e) mixed
-
-main :: IO ()
-main = do
-  hSetBuffering stdout NoBuffering
-  d0 <- readData
-  -- ★A9 プローブ用の一時トランケーション: 引数に N を与えると先頭 N 地域 +
-  --   両端点が N 未満の edge だけに縮小する (スケーリング実測用)。
-  args <- getArgs
-  let d = case args of
-            (nStr:_) | [(nn, "")] <- reads nStr ->
-              let keep = [ (a, b) | (a, b) <- zip (node1v d0) (node2v d0)
-                                  , a <= nn, b <= nn ]
-              in d0 { nAreas = nn, nEdges = length keep
-                    , node1v = map fst keep, node2v = map snd keep
-                    , yObs = take nn (yObs d0), eOffset = take nn (eOffset d0) }
-            _ -> d0
-  putStrLn $ "N = " ++ show (nAreas d) ++ ", N_edges = " ++ show (nEdges d)
-  let n = nAreas d
-      edges = [ (a - 1, b - 1) | (a, b) <- zip (node1v d) (node2v d) ]  -- 0始まりに変換
-      logEArr = map log (eOffset d)
-      df = [ ("log_E", NumData (V.fromList logEArr))
-           , ("y",     NumData (V.fromList (map fromIntegral (yObs d))))
-           ] :: [(T.Text, ColData)]
-  let ysD = map fromIntegral (yObs d)
-
-  -- ★Phase 99 A2: `dumpir` = vecIR arena の静的命令列を dump し、ICAR ペア差分
-  --   二次形式が (a) 融合 op (VIGath+VISumSqD 等) か (b) 5461 個の scalar op に
-  --   展開されているかを実測する (A2a の prize サイズ判定・推測するな計測せよ)。
-  case args of
-    ["dumpir"] -> do
-      let rawM :: ModelP ()
-          rawM = bym2Model n edges (scalingF d) logEArr ysD
-      dumpIR rawM
-      exitSuccess
-    _ -> pure ()
-
-  -- ★N=1921・N_edges=5461 の大規模モデル。本番計測 (4chain×warmup1000+
-  -- draws1000) の前に、まず縮小設定 (1chain×warmup3+draws3) でタイミング
-  -- プローブを行い、フル run の所要時間を見積もってから判断すること
-  -- (03-garch11/08-hudson-lynx-hare と同じ「保留判断」の慎重さで臨む)。
-  -- probe モード: 引数 <N> = 縮小 cfg (1chain・3+3) / <N> <warmup> <draws>
-  -- = 1chain で指定サイズ (Phase 90 A11 のプロファイリング run 用)。
-  let cfg = case args of
-        (_:wStr:dStr:_)
-          | [(w, "")] <- reads wStr, [(dd, "")] <- reads dStr ->
-              defaultHBM { hbmChains = 1, hbmSamples = dd
-                         , hbmWarmup = w, hbmSeed = Just 1 }
-        (_:_) -> defaultHBM { hbmChains = 1, hbmSamples = 3
-                            , hbmWarmup = 3, hbmSeed = Just 1 }
-        []    -> defaultHBM { hbmChains = 4, hbmSamples = 1000
-                            , hbmWarmup = 1000, hbmSeed = Just 1 }
-      m = df |-> hbm cfg (bym2Model n edges (scalingF d) logEArr ysD)
-
-  -- 勾配経路 = compileGradUV が実際に選ぶ経路 (束縛済 hbmModelSpec で判定・
-  -- Phase 91 A4: BYM2 は vecIR に吸収済。旧診断は生モデルを synthVecIR に渡し
-  -- 観測 0 行 → Nothing と誤表示していた ので hbmModelSpec m に差替)。
-  putStrLn $ "勾配経路 = " ++ gradPathLabel (hbmModelSpec m)
-
-  (_, samplingMs) <- timeSamplingMs (hbmChainsR m)
-  printf "sampling wall = %.1f ms (draws only, no dashboard/startup)\n" samplingMs
-
-  -- N=1921×2 latent のため dashboardFullOf は非実用サイズ (05-mh と同じ
-  -- 判断)。健全性2x2パネル (DAG/forest/PPC/energy) のみ。
-  -- (A9 プローブのトランケーション時は figure を汚さないためスキップ)
-  case args of
-    (_:_) -> pure ()
-    []    -> savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-               (noDf |>> dashboardOf m "y" :: BoundPlot)
-
-  printSummary $ summarize ["beta0", "sigma", "rho"] (hbmChainsR m)
diff --git a/bench/posteriordb/14-hmm-example/Model.hs b/bench/posteriordb/14-hmm-example/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/14-hmm-example/Model.hs
+++ /dev/null
@@ -1,193 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | hmm_example-hmm_example (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89: posteriordb 横断ベンチマーク。単純な隠れマルコフモデル
--- (K=2状態・N=100観測・1次元Gaussian放出)。離散潜在状態は NUTS で直接
--- サンプリングできないため、Stan 原典どおり forward algorithm で状態列を
--- 周辺化 (marginalize) した対数尤度を使う。
---
--- Stan 原典 (posteriordb `models/stan/hmm_example.stan`):
---   parameters { simplex[K] theta1; simplex[K] theta2; positive_ordered[K] mu; }
---   model {
---     mu[1] ~ normal(3,1); mu[2] ~ normal(10,1);
---     // forward algorithm (状態列の周辺化)
---     gamma[1,k] = normal_lpdf(y[1]|mu[k],1);  -- pi0 項なし (暗黙一様)
---     gamma[t,k] = log_sum_exp_j(gamma[t-1,j] + log(theta[j,k])) + normal_lpdf(y[t]|mu[k],1);
---     target += log_sum_exp(gamma[N]);
---   }
---
--- hanalyze には既に `hmmForwardLogLik` (log-space forward recursion・
--- Rabiner 1989) と `dirichlet` helper が実装済み (Phase 39-A4)。
--- `theta1`/`theta2` (simplex・Stan原典は暗黙一様事前分布) は
--- `dirichlet name [1,1]` (= Dirichlet(1,1) = simplex上一様) で移植する。
--- `pi0` は Stan 原典に明示的な項が無い (`gamma[1,k]` に log π_0 が加算
--- されない) ため、`hmmForwardLogLik` の pi0 引数には `replicate k 1`
--- (= log 1 = 0、実質「項なし」と等価) を渡す。
---
--- ★`positive_ordered[K]` (mu[1] < mu[2] の順序制約) に対応する分布は
--- hanalyze に無い。★実測で判明: 順序制約なしで `mu_1 ~ Normal(3,1)`・
--- `mu_2 ~ Normal(10,1)` を独立にサンプリングしたところ、chain間で
--- ラベルスイッチング (mu_1/mu_2 の意味がchainごとに入れ替わる) が発生し
--- r_hat が 17台という壊滅的な値になった (両事前分布が7σ以上離れていても、
--- 制約が全く無いと exchangeability により初期値次第でどちらのラベル
--- 付けにも収束しうる)。
---
--- 解決: `mu_2 = mu_1 + gap` (gap>0) という**加算的な順序制約**を導入し、
--- gap 自身の sample 事前分布 (HalfNormal) の寄与を `potential` で正確に
--- 打ち消して `Normal(10,1)` の寄与に置き換える (数学的に厳密・近似では
--- ない): 総 log-density 寄与 = [HalfNormal(gap) の寄与] + [potential] =
--- logDensity(HalfNormal, gap) + (logDensity(Normal 10 1, mu2) −
--- logDensity(HalfNormal, gap)) = logDensity(Normal 10 1, mu2)。
--- Stan の `positive_ordered` 変換のヤコビアンは加算シフトのため 1
--- (寄与なし) なので、この構成は Stan 原典と数学的に等価。
---
--- **reference_posterior_name = "hmm_example-hmm_example"** (posteriordb
--- に公式 reference あり・hanalyze vs PyMC vs 公式referenceの3者比較可能)。
---
--- ビルド: cabal build --project-file=cabal.project.plot posteriordb-hmm
-module Main (main) where
-
-import Control.Monad (unless)
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import Data.List (group, intercalate, sort, transpose)
-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, dataNamedX,
-                                    dirichlet, potential, logDensity, observeMV,
-                                    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 Hanalyze.MCMC.Core (Chain, chainAccepted, chainDivergences,
-                                    chainTotal, chainTreeDepths, chainVals)
-
-import Common (summarize, printSummary, timeSamplingMs)
-
--- | posteriordb の @hmm_example.json@ 形状 ({"N":100, "K":2, "y":[...]})。
-data HmmData = HmmData { nObsHmm :: Int, kStates :: Int, yArr :: [Double] }
-
-instance FromJSON HmmData where
-  parseJSON = withObject "HmmData" $ \v ->
-    HmmData <$> v .: "N" <*> v .: "K" <*> v .: "y"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/14-hmm-example/data/hmm_example.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/14-hmm-example/figures"
-
-readData :: IO HmmData
-readData = either fail pure =<< eitherDecodeFileStrict dataPath
-
--- | K=2 状態 HMM (forward algorithm で状態列を周辺化)。
---
--- Phase 92 A2: 尤度を @potential (hmmForwardLogLik ...)@ から構造化 primitive
--- 'HmmForwardNormal' + 'observeMV' へ移行 (密度は同値・'obsLogSum' が同じ
--- forward recursion を呼ぶ)。 役割 (π_0/遷移行/emission 平均/σ) が型で見える
--- ため、 勾配コンパイラが forward-backward の閉形式随伴 ('hmmAnalyticVG'・
--- AD tape ゼロ) を選べる。 dataNamedX "y" は dashboard の実データ参照用に残す。
-hmmModel :: Int -> [Double] -> ModelP ()
-hmmModel k ysRaw = do
-  mu1 <- sample "mu_1" (Normal 3 1)
-  gap <- sample "gap"  (HalfNormal 5)
-  mu2 <- deterministic "mu_2" (mu1 + gap)
-  potential "mu2_prior" (logDensity (Normal 10 1) mu2 - logDensity (HalfNormal 5) gap)
-  theta1 <- dirichlet "theta1" (replicate k 1)
-  theta2 <- dirichlet "theta2" (replicate k 1)
-  _ys <- dataNamedX "y" []
-  let mus   = [mu1, mu2]
-      trans = [theta1, theta2]
-      pi0   = replicate k 1
-  observeMV "y_seq" (HmmForwardNormal pi0 trans mus 1) [ysRaw]
-
-main :: IO ()
-main = do
-  d <- readData
-  -- Phase 92 A1d: `reduced` 引数で prof 用縮小 run (1chain・warmup200+draws200・
-  -- 図出力 skip = Rasterific が cost centre を汚さないため)。既定は本番設定。
-  args <- getArgs
-  -- Phase 92 ess/draw 調査: `seed N` で乱数 seed を差し替え可能に (ess 推定の
-  -- seed 感度確認用)。既定 = 1 (従来記録と bit 一致)。
-  let reduced = elem "reduced" args
-      seedArg = case dropWhile (/= "seed") args of
-                  (_ : s : _) -> read s
-                  _           -> 1  -- hbmSeed は Word32
-  let df = [ ("y", NumData (V.fromList (yArr d))) ] :: [(T.Text, ColData)]
-      cfg | reduced   = defaultHBM { hbmChains = 1, hbmSamples = 200
-                                    , hbmWarmup = 200, hbmSeed = Just seedArg }
-          | otherwise = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                                    , hbmWarmup = 1000, hbmSeed = Just seedArg }
-      m = df |-> hbm cfg (hmmModel (kStates d) (yArr d))
-
-  -- 勾配経路 = 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
-
-  -- seed≠1 の ess 感度 run では確定図 (seed1 前提) を上書きしない。
-  unless (reduced || seedArg /= 1) $
-    savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-      (noDf |>> dashboardFullOf m "y" :: BoundPlot)
-
-  -- theta1_*/theta2_*/mu_2 は deterministic (dirichlet の棒折り変換・
-  -- mu2の加算的順序制約) のため、summarize の前に augmentChainWithDeterministic
-  -- で Chain へ注入する (無いと chainVals が空リストを返し NaN/ess=0 になる)。
-  let chainsAug = map (augmentChainWithDeterministic (hmmModel (kStates d) (yArr d))) (hbmChainsR m)
-  printSummary $ summarize ["mu_1", "mu_2", "gap", "theta1_0", "theta1_1", "theta2_0", "theta2_1"] chainsAug
-
-  -- Phase 92 ess/draw 調査: per-draw NUTS 診断 (nutpie sample_stats との
-  -- 突き合わせ用)。depth は post-warmup・draw 順 (Core.chainTreeDepths)。
-  -- accept は burn-in 込みの粗い受理率 (Chain には per-draw accept-stat が
-  -- 無いため参考値)。leapfrog/draw ≈ 2^depth。
-  putStrLn "\n== per-chain NUTS diagnostics (depth = post-warmup) =="
-  printDiagnostics (hbmChainsR m)
-
-  -- 全 chain の post-warmup draw を CSV 化し、Python 側 (arviz) で PyMC と
-  -- 同一指標 (rank-normalized ess_bulk・4 chain) の ESS を計算する。
-  -- Common.summarize の ess は chain 0 のみ + Geyer IMSE (tau 下限 1 クランプで
-  -- n 頭打ち) のため nutpie の ess_bulk と直接比較できない。
-  writeDrawsCSV "bench/posteriordb/14-hmm-example/hmm_draws_postwarmup.csv"
-    ["mu_1", "mu_2", "gap", "theta1_0", "theta1_1", "theta2_0", "theta2_1"]
-    chainsAug
-
-printDiagnostics :: [Chain] -> IO ()
-printDiagnostics chains = do
-  mapM_ printOne (zip [0 :: Int ..] chains)
-  let allDepths = concatMap chainTreeDepths chains
-      histo = map (\g -> (head g, length g)) . group . sort $ allDepths
-  putStrLn $ "depth histogram (all chains): "
-          ++ intercalate ", " [ show dep ++ ":" ++ show c | (dep, c) <- histo ]
-  where
-    printOne (i, ch) = do
-      let ds    = chainTreeDepths ch
-          nd    = fromIntegral (length ds) :: Double
-          meanD = fromIntegral (sum ds) / nd
-          leap  = sum [ 2 ^ dep | dep <- ds ] :: Int
-          acc   = fromIntegral (chainAccepted ch) / fromIntegral (chainTotal ch) :: Double
-      printf "chain %d: mean_depth=%.2f  est_leapfrog/draw=%.1f  div=%d  accept(incl-warmup)=%.3f\n"
-        i meanD (fromIntegral leap / nd :: Double) (length (chainDivergences ch)) acc
-
-writeDrawsCSV :: FilePath -> [T.Text] -> [Chain] -> IO ()
-writeDrawsCSV path pars chains = do
-  let rows = concat
-        [ [ show ci ++ "," ++ show di ++ ","
-              ++ intercalate "," (map (printf "%.17g") vals)
-          | (di, vals) <- zip [0 :: Int ..] (transpose cols) ]
-        | (ci, ch) <- zip [0 :: Int ..] chains
-        , let cols = [ chainVals p ch | p <- pars ] ]
-      header = "chain,draw," ++ intercalate "," (map T.unpack pars)
-  writeFile path (unlines (header : rows))
-  putStrLn $ "draws CSV -> " ++ path
diff --git a/bench/posteriordb/15-dugongs/Model.hs b/bench/posteriordb/15-dugongs/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/15-dugongs/Model.hs
+++ /dev/null
@@ -1,137 +0,0 @@
-{-# 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
diff --git a/bench/posteriordb/16-lda/Model.hs b/bench/posteriordb/16-lda/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/16-lda/Model.hs
+++ /dev/null
@@ -1,150 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | three_men1-ldaK2 (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89: posteriordb 横断ベンチマーク。LDAトピックモデル (K=2固定・
--- V=249語彙・M=6文書・N=4999語インスタンス)。離散潜在トピック割当を
--- 周辺化 (collapsed) した対数尤度を使う (Stan 原典と同型)。
---
--- Stan 原典 (posteriordb `models/stan/ldaK2.stan`):
---   transformed data { int K=2; vector[K] alpha=[1,1]; vector[V] beta=[1,...,1]; }
---   parameters { array[M] simplex[K] theta; array[K] simplex[V] phi; }
---   model {
---     theta[m] ~ dirichlet(alpha); phi[k] ~ dirichlet(beta);
---     for (n in 1:N) {
---       gamma[k] = log(theta[doc[n],k]) + log(phi[k,w[n]]);
---       target += log_sum_exp(gamma);
---     }
---   }
---
--- hanalyze の 'dirichlet' helper (stick-breaking・Phase 39-A4) を使い
--- theta[m]/phi[k] を simplex latent として作る。周辺化尤度は
--- 'Hanalyze.Model.HBM.Util.logSumExpA' (K-way log-sum-exp) を
--- 'potential' で加算する (14-hmm-example の forward algorithm と同系統の
--- 「observe を使わず potential で尤度を直書きする」パターン)。
---
--- ★スケールへの配慮: V=249 の 'dirichlet' は内部で stick-breaking の
--- O(V) 演算 (scanl/scanr + (!!)) を行うため呼び出しコストが V に対して
--- 効くが K=2 回のみ (V に対して 1 回・K に対する繰り返しではない)。
--- 尤度ループ (N=4999) 側の theta/phi 参照は 'dirichlet' が返す素の Haskell
--- リストのまま `!!` で引くと O(V) の索引コストが 4999 回積み上がるため、
--- 'Data.Vector' に変換してから索引する (O(1))。 w/doc (posteriordb は
--- 1-based) は微分対象ではない構造的な添字データなので、`df`/`dataNamedX`
--- 経由の束縛ではなく素の @[Int]@ をクロージャで直接渡す (05-mh の @T@ と
--- 同じ流儀)。
---
--- ★posteriordbの keywords に "multimodal" とある: トピックは交換可能
--- (a priori に θ/φ のラベルに区別が無い) なため、chain ごとに異なる
--- トピックラベルへ収束するラベルスイッチングが起き得る (04-low-dim-gauss-mix
--- と同種の既知の課題)。 14-hmm-example のような加算的順序制約は θ/φ が
--- 同時に入れ替わる構造のため単純には適用できず、本モデルでは対処しない
--- (現象が出たら記録するに留める)。
---
--- reference_posterior_name = null (posteriordb に公式 reference posterior 無し)。
---
--- ★実測で判明: 本番設定 (4chain・warmup1000+draws1000) は 30分timeoutで
--- 完走せず (2026-07-11)。中規模probe (1chain・warmup100+draws100=200
--- iteration) は 325.1秒で完走・値は正常収束 (r_hat≈0.99-1.00・NaN無し)
--- だったため実装自体にバグは無いが、~510次元 (M*(K-1)+K*(V-1)=6+496) の
--- legacy walk+ad (vecIR非対応) では 1 iteration あたり平均 1.6秒
--- (単純外挿で本番 2000 iteration/chain ≈ 54分) かかり、13-traffic-accident-nyc
--- (N=1921×2 latent) と同様に実用外の規模と判断・保留 (user 確認済)。
--- 詳細は `16-lda/README.md` 参照。
---
--- ビルド: cabal build --project-file=cabal.project.plot posteriordb-lda
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import Text.Printf (printf)
-
-import Hanalyze.Model.HBM (ModelP, potential, plateI, plateForM_,
-                                    dirichlet, (.#), augmentChainWithDeterministic)
-import Hanalyze.Model.HBM.Util (logSumExpA)
-import Hanalyze.Model.HBM (gradPathLabel)
-import Hanalyze.Plot (hbmModelSpec)
-import Hanalyze.Plot (HBMConfig (..), defaultHBM, hbm, (|->),
-                              dashboardOf, hbmChainsR)
-import Hgg.Plot.Spec (ColData (..))
-import Hgg.Plot.Frame (BoundPlot, (|>>))
-import Hgg.Plot.Backend.Rasterific (savePNGBound)
-
-import Common (summarize, printSummary, timeSamplingMs)
-
--- | posteriordb の @three_men1.json@ 形状
--- ({"V":249, "M":6, "N":4999, "w":[...], "doc":[...]}・1-based 添字)。
-data LdaData = LdaData
-  { ldaV   :: Int
-  , ldaM   :: Int
-  , ldaW   :: [Int]
-  , ldaDoc :: [Int]
-  }
-
-instance FromJSON LdaData where
-  parseJSON = withObject "LdaData" $ \v ->
-    LdaData <$> v .: "V" <*> v .: "M" <*> v .: "w" <*> v .: "doc"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-kTopics :: Int
-kTopics = 2
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/16-lda/data/three_men1.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/16-lda/figures"
-
-readData :: IO LdaData
-readData = either fail pure =<< eitherDecodeFileStrict dataPath
-
--- | LDA (K=2固定・周辺化尤度)。 vVoc/mDocs/ws/docs はデータ由来の固定定数
--- として closure で渡す (05-mh の @T@ と同じ流儀・w/doc は微分対象ではない
--- ため df 経由で束縛しない)。
-ldaModel :: Int -> Int -> [Int] -> [Int] -> ModelP ()
-ldaModel vVoc mDocs ws docs = do
-  thetaLists <- plateI "doc"   mDocs   $ \i -> dirichlet ("theta" .# i) (replicate kTopics 1)
-  phiLists   <- plateI "topic" kTopics $ \k -> dirichlet ("phi"   .# k) (replicate vVoc 1)
-  let thetaV = V.fromList (map V.fromList thetaLists)  -- M x K (O(1) 索引)
-      phiV   = V.fromList (map V.fromList phiLists)     -- K x V (O(1) 索引)
-  plateForM_ "obs" (zip docs ws) $ \(d, w) ->
-    let gammas = [ log ((thetaV V.! (d - 1)) V.! kk) + log ((phiV V.! kk) V.! (w - 1))
-                 | kk <- [0 .. kTopics - 1] ]
-    in potential "lda_loglik" (logSumExpA gammas)
-
-main :: IO ()
-main = do
-  d <- readData
-  let vVoc  = ldaV d
-      mDocs = ldaM d
-      ws    = ldaW d
-      docs  = ldaDoc d
-      -- ダッシュボード用の名目 obs 列 (14-hmm-example と同様、実際の尤度は
-      -- potential で直書きするため観測ノードは存在しない・PPCパネルは空)。
-      df = [ ("w", NumData (V.fromList (map fromIntegral ws))) ] :: [(T.Text, ColData)]
-      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                        , hbmWarmup = 1000, hbmSeed = Just 1 }
-      model :: ModelP ()
-      model = ldaModel vVoc mDocs ws docs
-      m = df |-> hbm cfg model
-
-  -- 勾配経路 = 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
-
-  -- M*K + K*V = 12+498 = 510 latent (deterministic込み) と大規模なため、
-  -- 05-mh/10-rats と同様 dashboardFullOf でなく dashboardOf (健全性2x2パネル
-  -- のみ・forestに全latentがコンパクト表示) を使う。
-  savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-    (noDf |>> dashboardOf m "w" :: BoundPlot)
-
-  -- theta_i_j は dirichlet の deterministic (stick-breaking) のため
-  -- augmentChainWithDeterministic で Chain へ注入してから summarize する。
-  let chainsAug = map (augmentChainWithDeterministic model) (hbmChainsR m)
-      thetaNames = [ "theta_" <> T.pack (show i) <> "_" <> T.pack (show k)
-                   | i <- [0 .. mDocs - 1], k <- [0 .. kTopics - 1] ]
-  printSummary $ summarize thetaNames chainsAug
diff --git a/bench/posteriordb/17-nes/Model.hs b/bench/posteriordb/17-nes/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/17-nes/Model.hs
+++ /dev/null
@@ -1,149 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | nes1972-nes (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89: posteriordb 横断ベンチマーク。ARM本 (Gelman & Hill 2006) Ch.4
--- の政党支持度回帰 (National Election Studies 1972年調査・N=1330)。
--- 9変数の線形回帰 (イデオロギー・人種・年齢層3ダミー・教育・性別・収入)。
---
--- Stan 原典 (posteriordb `models/stan/nes.stan`):
---   transformed data {
---     age30_44[n] = age_discrete[n]==2; age45_64[n] = age_discrete[n]==3;
---     age65up[n]  = age_discrete[n]==4;  // 年齢層ファクタをダミー化
---   }
---   parameters { vector[9] beta; real<lower=0> sigma; }
---   model {
---     partyid7 ~ normal(beta[1] + beta[2]*real_ideo + beta[3]*race_adj
---                      + beta[4]*age30_44 + beta[5]*age45_64 + beta[6]*age65up
---                      + beta[7]*educ1 + beta[8]*gender + beta[9]*income, sigma);
---   }
---
--- Stan 原典に明示的な prior 行は無い (暗黙の flat/improper prior)。
--- 01-glm-poisson/10-rats と同じ流儀で diffuse な代替を与える:
--- beta_i ~ Normal(0,1000) (回帰係数・UnconstrainedT なので unconstrained
--- 初期値問題なし)・sigma ~ HalfCauchy(25) (10-rats で確立した「Uniform(0,X)
--- 境界外初期値の罠」を回避する SD 事前分布・PositiveT変換)。
---
--- age30_44/age45_64/age65up は Stan の transformed data 相当として
--- readData 後に Haskell 側でダミー化する (df に個別列として渡す)。
--- N=1330×9変数の標準的な線形回帰は vecIR 高速経路が期待できる規模
--- (01-glm-poisson と同型の構造)。
---
--- reference_posterior_name = "nes1972-nes" (posteriordb に公式 reference
--- あり・hanalyze vs PyMC vs 公式reference の3者比較可能)。
---
--- ビルド: cabal build --project-file=cabal.project.plot posteriordb-nes
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import Text.Printf (printf)
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observe,
-                                    dataNamedX, dataNamedObs, plateI_,
-                                    gradPathLabel)
-import Hanalyze.Plot (HBMConfig (..), defaultHBM, hbm, (|->),
-                              dashboardFullOf, hbmChainsR, hbmModelSpec)
-import Hgg.Plot.Spec (ColData (..))
-import Hgg.Plot.Frame (BoundPlot, (|>>))
-import Hgg.Plot.Backend.Rasterific (savePNGBound)
-
-import Common (summarize, printSummary, timeSamplingMs)
-
--- | posteriordb の @nes1972.json@ 形状
--- ({"N":1330,"partyid7":[...],"real_ideo":[...],"race_adj":[...],
---   "educ1":[...],"gender":[...],"income":[...],"age_discrete":[...]})。
-data NesRaw = NesRaw
-  { partyid7Raw    :: [Double]
-  , realIdeoRaw    :: [Double]
-  , raceAdjRaw     :: [Double]
-  , educ1Raw       :: [Double]
-  , genderRaw      :: [Double]
-  , incomeRaw      :: [Double]
-  , ageDiscreteRaw :: [Int]
-  }
-
-instance FromJSON NesRaw where
-  parseJSON = withObject "NesRaw" $ \v ->
-    NesRaw <$> v .: "partyid7" <*> v .: "real_ideo" <*> v .: "race_adj"
-           <*> v .: "educ1" <*> v .: "gender" <*> v .: "income"
-           <*> v .: "age_discrete"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/17-nes/data/nes1972.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/17-nes/figures"
-
-readData :: IO NesRaw
-readData = either fail pure =<< eitherDecodeFileStrict dataPath
-
--- | 9変数線形回帰 (Stan 原典と同一構造)。 データは df 経由で束縛
--- ('dataNamedX'/'dataNamedObs')・O(1) 索引のため 'Data.Vector' に変換して
--- から 'plateI_' で反復する。
-nesModel :: ModelP ()
-nesModel = do
-  b1 <- sample "beta1" (Normal 0 1000)
-  b2 <- sample "beta2" (Normal 0 1000)
-  b3 <- sample "beta3" (Normal 0 1000)
-  b4 <- sample "beta4" (Normal 0 1000)
-  b5 <- sample "beta5" (Normal 0 1000)
-  b6 <- sample "beta6" (Normal 0 1000)
-  b7 <- sample "beta7" (Normal 0 1000)
-  b8 <- sample "beta8" (Normal 0 1000)
-  b9 <- sample "beta9" (Normal 0 1000)
-  sigma <- sample "sigma" (HalfCauchy 25)
-  ideoV  <- V.fromList <$> dataNamedX "real_ideo" []
-  raceV  <- V.fromList <$> dataNamedX "race_adj"  []
-  a3044V <- V.fromList <$> dataNamedX "age30_44"  []
-  a4564V <- V.fromList <$> dataNamedX "age45_64"  []
-  a65upV <- V.fromList <$> dataNamedX "age65up"   []
-  educV  <- V.fromList <$> dataNamedX "educ1"     []
-  gendV  <- V.fromList <$> dataNamedX "gender"    []
-  incV   <- V.fromList <$> dataNamedX "income"    []
-  ysV    <- V.fromList <$> dataNamedObs "partyid7" []
-  plateI_ "obs" (V.length ysV) $ \i ->
-    let mu = b1 + b2 * (ideoV V.! i) + b3 * (raceV V.! i)
-           + b4 * (a3044V V.! i) + b5 * (a4564V V.! i) + b6 * (a65upV V.! i)
-           + b7 * (educV V.! i) + b8 * (gendV V.! i) + b9 * (incV V.! i)
-    in observe "partyid7" (Normal mu sigma) [ysV V.! i]
-
-main :: IO ()
-main = do
-  d <- readData
-  let age3044 = [ if a == (2 :: Int) then 1 else 0 | a <- ageDiscreteRaw d ] :: [Double]
-      age4564 = [ if a == (3 :: Int) then 1 else 0 | a <- ageDiscreteRaw d ] :: [Double]
-      age65up = [ if a == (4 :: Int) then 1 else 0 | a <- ageDiscreteRaw d ] :: [Double]
-      df = [ ("real_ideo", NumData (V.fromList (realIdeoRaw d)))
-           , ("race_adj",  NumData (V.fromList (raceAdjRaw d)))
-           , ("age30_44",  NumData (V.fromList age3044))
-           , ("age45_64",  NumData (V.fromList age4564))
-           , ("age65up",   NumData (V.fromList age65up))
-           , ("educ1",     NumData (V.fromList (educ1Raw d)))
-           , ("gender",    NumData (V.fromList (genderRaw d)))
-           , ("income",    NumData (V.fromList (incomeRaw d)))
-           , ("partyid7",  NumData (V.fromList (partyid7Raw d)))
-           ] :: [(T.Text, ColData)]
-      -- PyMC 側 (model.py) と同じ設定を定数で揃える。
-      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                        , hbmWarmup = 1000, hbmSeed = Just 1 }
-      m = df |-> hbm cfg nesModel
-
-  -- 勾配経路 = compileGradUV が実際に選ぶ経路 (束縛済モデルで判定)。
-  -- Phase 91 A4: 9変数線形回帰は Gaussian LM 閉形式ブロックに吸収される。
-  -- ★生の nesModel を synthVecIR に渡すと data 空で Nothing と誤表示するため
-  --   'hbmModelSpec m' (df 束縛済) を 'gradPathLabel' に渡す。
-  putStrLn $ "勾配経路 = " ++ gradPathLabel (hbmModelSpec m)
-
-  (_, samplingMs) <- timeSamplingMs (hbmChainsR m)
-  printf "sampling wall = %.1f ms (draws only, no dashboard/startup)\n" samplingMs
-
-  savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-    (noDf |>> dashboardFullOf m "partyid7" :: BoundPlot)
-
-  printSummary $ summarize
-    ["beta1", "beta2", "beta3", "beta4", "beta5", "beta6", "beta7", "beta8", "beta9", "sigma"]
-    (hbmChainsR m)
diff --git a/bench/posteriordb/18-loss-curves/Model.hs b/bench/posteriordb/18-loss-curves/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/18-loss-curves/Model.hs
+++ /dev/null
@@ -1,127 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | loss_curves-losscurve_sislob (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89: posteriordb 横断ベンチマーク。保険数理の損失三角形
--- (loss reserving)。n_cohort=10 (契約年度)・n_time=10 (経過年)・
--- n_data=55 (= 10+9+...+1・下三角が未観測の古典的三角形構造)。
--- Weibull 型成長曲線 (`growthmodel_id=1`、データで確認済み) で
--- 損失の発展パターンをモデル化する。
---
--- Stan 原典 (posteriordb `models/stan/losscurve_sislob.stan`):
---   gf[t] = 1 - exp(-(t/theta)^omega)                      // growth_factor_weibull
---   lm[i] = LR[cohort_id[i]] * premium[cohort_id[i]] * gf[t_idx[i]]
---   loss[i] ~ normal(lm[i], loss_sd*premium[cohort_id[i]])
---   mu_LR ~ normal(0,0.5); sd_LR ~ lognormal(0,0.5); LR ~ lognormal(mu_LR,sd_LR)
---   loss_sd ~ lognormal(0,0.7); omega/theta ~ lognormal(0,0.5)
---
--- 全 prior が LogNormal/Normal (unconstrained or PositiveT) のため、
--- 01-glm-poisson/10-rats 系列で確立した「Uniform境界外初期値の罠」は
--- 該当しない (LogNormalはPositiveT変換を持つ・安全)。
---
--- reference_posterior_name = null (posteriordb に公式 reference posterior 無し)。
---
--- ビルド: cabal build --project-file=cabal.project.plot posteriordb-loss-curves
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import Text.Printf (printf)
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observe,
-                                    dataNamedX, dataNamedObs, plateI, plateForM_, (.#))
-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 の @loss_curves.json@ 形状
--- ({"n_data":55,"n_time":10,"n_cohort":10,"cohort_id":[...],"t_idx":[...],
---   "t_value":[...],"premium":[...],"loss":[...]}・1-based 添字)。
-data LossData = LossData
-  { nCohort  :: Int
-  , nTime    :: Int
-  , cohortId :: [Int]
-  , tIdx     :: [Int]
-  , tValue   :: [Double]
-  , premium  :: [Double]
-  , loss     :: [Double]
-  }
-
-instance FromJSON LossData where
-  parseJSON = withObject "LossData" $ \v ->
-    LossData <$> v .: "n_cohort" <*> v .: "n_time" <*> v .: "cohort_id"
-             <*> v .: "t_idx" <*> v .: "t_value" <*> v .: "premium" <*> v .: "loss"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/18-loss-curves/data/loss_curves.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/18-loss-curves/figures"
-
-readData :: IO LossData
-readData = either fail pure =<< eitherDecodeFileStrict dataPath
-
--- | Weibull成長曲線による損失三角形モデル (Stan 原典と同一構造・
--- growthmodel_id=1 固定)。 premium/t_value はコホート/経過年ごとの
--- 固定定数として df 経由で束縛 ('dataNamedX')・cohort_id/t_idx は
--- 微分対象ではない構造的添字なので素の @[Int]@ をクロージャで直接渡す
--- (05-mh/16-lda と同じ流儀)。
-lossModel :: Int -> [Int] -> [Int] -> ModelP ()
-lossModel nT cids tidxs = do
-  omega  <- sample "omega"   (LogNormal 0 0.5)
-  theta  <- sample "theta"   (LogNormal 0 0.5)
-  muLR   <- sample "mu_LR"   (Normal 0 0.5)
-  sdLR   <- sample "sd_LR"   (LogNormal 0 0.5)
-  lrs    <- plateI "cohort" 10 $ \i -> sample ("LR" .# i) (LogNormal muLR sdLR)
-  lossSd <- sample "loss_sd" (LogNormal 0 0.7)
-  tValues  <- dataNamedX   "t_value" []
-  premiums <- dataNamedX   "premium" []
-  losses   <- dataNamedObs "loss"    []
-  let tValueV  = V.fromList tValues
-      premiumV = V.fromList premiums
-      lrV      = V.fromList lrs
-      gfV = V.generate nT $ \i ->
-              let tv = tValueV V.! i
-              in 1 - exp (negate ((tv / theta) ** omega))
-  plateForM_ "obs" (zip3 cids tidxs losses) $ \(cid, tidx, lossVal) ->
-    let lm = (lrV V.! (cid - 1)) * (premiumV V.! (cid - 1)) * (gfV V.! (tidx - 1))
-        sd = lossSd * (premiumV V.! (cid - 1))
-    in observe "loss" (Normal lm sd) [lossVal]
-
-main :: IO ()
-main = do
-  d <- readData
-  let df = [ ("t_value", NumData (V.fromList (tValue d)))
-           , ("premium", NumData (V.fromList (premium d)))
-           , ("loss",    NumData (V.fromList (loss d)))
-           ] :: [(T.Text, ColData)]
-      -- PyMC 側 (model.py) と同じ設定を定数で揃える。
-      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                        , hbmWarmup = 1000, hbmSeed = Just 1 }
-      model :: ModelP ()
-      model = lossModel (nTime d) (cohortId d) (tIdx d)
-      m = df |-> hbm cfg model
-
-  -- 勾配経路 = 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
-
-  savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-    (noDf |>> dashboardFullOf m "loss" :: BoundPlot)
-
-  printSummary $ summarize
-    (["omega", "theta", "mu_LR", "sd_LR", "loss_sd"] ++
-     [ "LR_" <> T.pack (show i) | i <- [0 .. nCohort d - 1] ])
-    (hbmChainsR m)
diff --git a/bench/posteriordb/19-surgical/Model.hs b/bench/posteriordb/19-surgical/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/19-surgical/Model.hs
+++ /dev/null
@@ -1,93 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | surgical_data-surgical_model (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89: posteriordb 横断ベンチマーク。BUGS 古典例「12病院の心臓手術
--- 死亡率」(N=12病院・階層二項ロジット・共変量なしの最も単純な階層形)。
---
--- Stan 原典 (posteriordb `models/stan/surgical_model.stan`):
---   mu ~ normal(0,1000); sigmasq ~ inv_gamma(0.001,0.001); sigma=sqrt(sigmasq);
---   b[i] ~ normal(mu, sigma);  r[i] ~ binomial_logit(n[i], b[i]);
---
--- hanalyze の `Binomial n p` は確率パラメータ直接指定 (logit link 無し) の
--- ため、05-mh と同じく `p = invlogit(b)` を手計算してから渡す。
--- `n[i]` (病院ごとの手術数) は微分対象ではない構造的定数なので closure で
--- 直接渡す (05-mh の @T@ と同じ流儀)。
---
--- reference_posterior_name = null (posteriordb に公式 reference posterior 無し)。
---
--- ビルド: cabal build --project-file=cabal.project.plot posteriordb-surgical
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import Text.Printf (printf)
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observe,
-                                    dataNamedObs, plateI, 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 の @surgical_data.json@ 形状 ({"N":12,"n":[...],"r":[...]})。
-data SurgicalData = SurgicalData { nOps :: [Int], rDeaths :: [Int] }
-
-instance FromJSON SurgicalData where
-  parseJSON = withObject "SurgicalData" $ \v ->
-    SurgicalData <$> v .: "n" <*> v .: "r"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/19-surgical/data/surgical_data.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/19-surgical/figures"
-
-readData :: IO SurgicalData
-readData = either fail pure =<< eitherDecodeFileStrict dataPath
-
--- | 病院ごとの階層二項ロジット (Stan 原典と同一構造)。
-surgicalModel :: [Int] -> ModelP ()
-surgicalModel ns = do
-  mu      <- sample "mu"      (Normal 0 1000)
-  sigmasq <- sample "sigmasq" (InverseGamma 0.001 0.001)
-  sigma   <- deterministic "sigma" (sqrt sigmasq)
-  bs      <- plateI "hosp" (length ns) $ \i -> sample ("b" .# i) (Normal mu sigma)
-  _       <- deterministic "pop_mean" (1 / (1 + exp (negate mu)))
-  rs      <- dataNamedObs "r" []
-  plateForM_ "obs" (zip3 ns bs rs) $ \(n, b, rVal) ->
-    let p = 1 / (1 + exp (negate b))
-    in observe "r" (Binomial n p) [rVal]
-
-main :: IO ()
-main = do
-  d <- readData
-  let df = [ ("r", NumData (V.fromList (map fromIntegral (rDeaths d)))) ] :: [(T.Text, ColData)]
-      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                        , hbmWarmup = 1000, hbmSeed = Just 1 }
-      model :: ModelP ()
-      model = surgicalModel (nOps d)
-      m = df |-> hbm cfg model
-
-  -- 勾配経路 = 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
-
-  savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-    (noDf |>> dashboardFullOf m "r" :: BoundPlot)
-
-  let chainsAug = map (augmentChainWithDeterministic model) (hbmChainsR m)
-      bNames = [ "b_" <> T.pack (show i) | i <- [0 .. length (nOps d) - 1] ]
-  printSummary $ summarize (["mu", "sigma", "pop_mean"] ++ bNames) chainsAug
diff --git a/bench/posteriordb/20-bones/Model.hs b/bench/posteriordb/20-bones/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/20-bones/Model.hs
+++ /dev/null
@@ -1,111 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | bones_data-bones_model (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89: posteriordb 横断ベンチマーク。骨年齢の graded response IRT
--- モデル (BUGS 古典例)。nChild=13人の子供・nInd=34項目 (骨のX線指標)。
--- 各項目の困難度カットポイント (gamma) と識別力 (delta) は**固定データ**
--- (未サンプル)・各子供の能力 theta のみが latent。
---
--- Stan 原典 (posteriordb `models/stan/bones_model.stan`):
---   theta[i] ~ normal(0, 36);
---   for each i,j:
---     Q[i,j,k] = inv_logit(delta[j]*(theta[i]-gamma[j,k]))  for k=1..(ncat[j]-1)
---     p[i,j,1] = 1-Q[i,j,1]; p[i,j,k] = Q[i,j,k-1]-Q[i,j,k]; p[i,j,ncat[j]] = Q[i,j,ncat[j]-1]
---     if grade[i,j] != -1: target += log(p[i,j,grade[i,j]])   // 欠測はスキップ
---
--- 尤度は `observe` を使わず `potential` で直書きする (14-hmm-example/
--- 16-lda と同系統)。 difficulty/discrimination はデータなので
--- Haskell 側では単なる @[Double]@ として closure で渡す。
---
--- reference_posterior_name = null (posteriordb に公式 reference posterior 無し)。
---
--- ビルド: cabal build --project-file=cabal.project.plot posteriordb-bones
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import Text.Printf (printf)
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observeMV,
-                                    plateI, (.#))
-import Hanalyze.Model.HBM (gradPathLabel)
-import Hanalyze.Plot (hbmModelSpec)
-import Hanalyze.Plot (HBMConfig (..), defaultHBM, hbm, (|->),
-                              dashboardOf, hbmChainsR)
-import Hgg.Plot.Spec (ColData (..))
-import Hgg.Plot.Frame (BoundPlot, (|>>))
-import Hgg.Plot.Backend.Rasterific (savePNGBound)
-
-import Common (summarize, printSummary, timeSamplingMs)
-
--- | posteriordb の @bones_data.json@ 形状 ({"ncat":[...34...],
--- "nChild":13,"nInd":34,"grade":[[13x34]],"delta":[...34...],
--- "gamma":[[34x4]]}・grade の @-1@ は欠測)。
-data BonesData = BonesData
-  { bnCat   :: [Int]
-  , bnGrade :: [[Int]]
-  , bnDelta :: [Double]
-  , bnGamma :: [[Double]]
-  }
-
-instance FromJSON BonesData where
-  parseJSON = withObject "BonesData" $ \v ->
-    BonesData <$> v .: "ncat" <*> v .: "grade" <*> v .: "delta" <*> v .: "gamma"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/20-bones/data/bones_data.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/20-bones/figures"
-
-readData :: IO BonesData
-readData = either fail pure =<< eitherDecodeFileStrict dataPath
-
--- | graded response IRT モデル (Stan 原典と同一構造)。
---
--- Phase 101 A3: 尤度を @logCatProb + potential@ 直書きから構造化 primitive
--- 'GradedResponseIrt' + 'observeMV' へ移行 (密度は同値・'obsLogSum' が同じ
--- Q/p 構成を呼ぶ)。 θ_i のみが latent なため、勾配コンパイラが解析勾配
--- ('gradedIrtAnalyticVG'・dQ/dθ = δ·Q(1−Q) の隣接差・AD tape ゼロ) を選べる。
--- grade 行列は行優先 flatten して 1 観測で渡す (−1 = 欠測は skip)。
-bonesModel :: [Int] -> [Double] -> [[Double]] -> [[Int]] -> ModelP ()
-bonesModel ncats deltas gammas grades = do
-  thetas <- plateI "child" (length grades) $ \i -> sample ("theta" .# i) (Normal 0 36)
-  observeMV "grades" (GradedResponseIrt thetas ncats deltas gammas)
-            [map fromIntegral (concat grades)]
-
-main :: IO ()
-main = do
-  d <- readData
-  let nChild = length (bnGrade d)
-      -- ダッシュボード用の名目 obs 列 (14-hmm-example/16-lda と同様、
-      -- 実際の尤度は potential で直書きするため観測ノードは存在しない・
-      -- PPCパネルは空)。項目1 (indicator 0) の全児童分のgradeを使う。
-      grade1 = [ fromIntegral (row !! 0) | row <- bnGrade d ] :: [Double]
-      df = [ ("grade1", NumData (V.fromList grade1)) ] :: [(T.Text, ColData)]
-      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                        , hbmWarmup = 1000, hbmSeed = Just 1 }
-      model :: ModelP ()
-      model = bonesModel (bnCat d) (bnDelta d) (bnGamma d) (bnGrade d)
-      m = df |-> hbm cfg model
-
-  -- 勾配経路 = 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
-
-  -- nChild=13の小規模モデルのため dashboardFullOf でも問題ないが、
-  -- potential のみで尤度を構成 (PPCパネルは空) するため 05-mh/16-lda と
-  -- 同様 dashboardOf (健全性2x2パネルのみ) を使う。
-  savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-    (noDf |>> dashboardOf m "grade1" :: BoundPlot)
-
-  printSummary $ summarize
-    [ "theta_" <> T.pack (show i) | i <- [0 .. nChild - 1] ]
-    (hbmChainsR m)
diff --git a/bench/posteriordb/21-radon/Model.hs b/bench/posteriordb/21-radon/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/21-radon/Model.hs
+++ /dev/null
@@ -1,144 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | radon_mn-radon_hierarchical_intercept_noncentered (posteriordb) —
--- hanalyze (ModelP) 実装。
---
--- Phase 89: posteriordb 横断ベンチマーク。Gelman ラドン多水準回帰の
--- 古典例 (mc-stan.org radon case study)。ミネソタ州 J=85郡・N=919家屋の
--- 屋内ラドン濃度回帰 (郡ごとの varying intercept + 固定傾き2本、
--- non-centered パラメタ化)。
---
--- Stan 原典 (posteriordb `models/stan/radon_hierarchical_intercept_noncentered.stan`):
---   parameters { vector[J] alpha_raw; vector[2] beta; real mu_alpha;
---                real<lower=0> sigma_alpha; real<lower=0> sigma_y; }
---   transformed parameters { alpha = mu_alpha + sigma_alpha * alpha_raw; }
---   model {
---     sigma_alpha ~ normal(0,1); sigma_y ~ normal(0,1);
---     mu_alpha ~ normal(0,10); beta ~ normal(0,10); alpha_raw ~ normal(0,1);
---     for (n in 1:N) {
---       muj[n] = alpha[county_idx[n]] + log_uppm[n]*beta[1];
---       mu[n] = muj[n] + floor_measure[n]*beta[2];
---       log_radon[n] ~ normal(mu[n], sigma_y);
---     }
---   }
---
--- `sigma_alpha`/`sigma_y` の `<lower=0>` + `Normal(0,1)` prior は
--- half-normal と数学的に等価なので `HalfNormal 1` で移植 (09-eight-schools
--- 等と同じ流儀)。`alpha[j]` は `deterministic` (mu_alpha+sigma_alpha*
--- alpha_raw[j]) で登録し `Data.Vector` 経由で O(1) 索引する
--- (`county_idx` は1-based構造添字なのでclosureで直接渡す・05-mh/16-lda
--- と同じ流儀)。単一階層 (alphaのみ) なので10-ratsの「二重階層」DSL
--- ギャップは該当しない — eight-schools/seedsと同型の構造でvecIR成功が
--- 見込める規模 (J=85・N=919)。
---
--- reference_posterior_name = null (posteriordb に公式 reference posterior 無し)。
---
--- ビルド: cabal build --project-file=cabal.project.plot posteriordb-radon
-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, plateI, plateForM_,
-                                    (.#), deterministic, augmentChainWithDeterministic)
-import Hanalyze.Model.HBM (gradPathLabel)
-import Hanalyze.Plot (hbmModelSpec)
-import Hanalyze.Plot (HBMConfig (..), defaultHBM, hbm, (|->),
-                              dashboardOf, hbmChainsR)
-import Hgg.Plot.Spec (ColData (..))
-import Hgg.Plot.Frame (BoundPlot, (|>>))
-import Hgg.Plot.Backend.Rasterific (savePNGBound)
-
-import Common (summarize, printSummary, timeSamplingMs)
-
--- | posteriordb の @radon_mn.json@ 形状 ({"N":919,"J":85,
--- "floor_measure":[...],"log_radon":[...],"log_uppm":[...],
--- "county_idx":[...]}・county_idx は 1-based)。
-data RadonData = RadonData
-  { rdJ            :: Int
-  , rdCountyIdx    :: [Int]
-  , rdFloorMeasure :: [Double]
-  , rdLogRadon     :: [Double]
-  , rdLogUppm      :: [Double]
-  }
-
-instance FromJSON RadonData where
-  parseJSON = withObject "RadonData" $ \v ->
-    RadonData <$> v .: "J" <*> v .: "county_idx" <*> v .: "floor_measure"
-              <*> v .: "log_radon" <*> v .: "log_uppm"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/21-radon/data/radon_mn.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/21-radon/figures"
-
-readData :: IO RadonData
-readData = either fail pure =<< eitherDecodeFileStrict dataPath
-
--- | 郡ごとの varying intercept (non-centered) + 固定傾き2本の階層回帰
--- (Stan 原典と同一構造)。 @countyIdx@ は微分対象ではない構造的添字
--- (1-based) なので closure で直接渡す。
-radonModel :: Int -> [Int] -> ModelP ()
-radonModel nCounty countyIdx = do
-  muAlpha    <- sample "mu_alpha"    (Normal 0 10)
-  sigmaAlpha <- sample "sigma_alpha" (HalfNormal 1)
-  sigmaY     <- sample "sigma_y"     (HalfNormal 1)
-  beta1      <- sample "beta1" (Normal 0 10)
-  beta2      <- sample "beta2" (Normal 0 10)
-  alphaRaws  <- plateI "county" nCounty $ \j -> sample ("alpha_raw" .# j) (Normal 0 1)
-  alphas     <- mapM (\(j, ar) -> deterministic ("alpha" .# j) (muAlpha + sigmaAlpha * ar))
-                      (zip [0 :: Int ..] alphaRaws)
-  let alphaV = V.fromList alphas
-  logUppm      <- dataNamedX   "log_uppm"      []
-  floorMeasure <- dataNamedX   "floor_measure" []
-  ys           <- dataNamedObs "log_radon"     []
-  plateForM_ "obs" (zip4' countyIdx logUppm floorMeasure ys) $ \(cid, uppm, floorM, yVal) ->
-    let mu = (alphaV V.! (cid - 1)) + uppm * beta1 + floorM * beta2
-    in observe "log_radon" (Normal mu sigmaY) [yVal]
-  where
-    zip4' (a : as) (b : bs) (c : cs) (d : ds) = (a, b, c, d) : zip4' as bs cs ds
-    zip4' _ _ _ _ = []
-
-main :: IO ()
-main = do
-  d <- readData
-  -- Phase 102 A1: `prof` 引数で図出力 skip (15-dugongs と同じ理由。
-  -- サンプリング設定は本番のまま)。
-  args <- getArgs
-  let profRun = elem "prof" args
-  let df = [ ("log_uppm",      NumData (V.fromList (rdLogUppm d)))
-           , ("floor_measure", NumData (V.fromList (rdFloorMeasure d)))
-           , ("log_radon",     NumData (V.fromList (rdLogRadon d)))
-           ] :: [(T.Text, ColData)]
-      -- PyMC 側 (model.py) と同じ設定を定数で揃える。
-      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                        , hbmWarmup = 1000, hbmSeed = Just 1 }
-      model :: ModelP ()
-      model = radonModel (rdJ d) (rdCountyIdx d)
-      m = df |-> hbm cfg model
-
-  -- 勾配経路 = 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
-
-  -- J=85郡分のalpha latentを含むため dashboardFullOf ではなく dashboardOf
-  -- (健全性2x2パネルのみ・05-mh/10-ratsと同じ判断)。
-  unless profRun $
-    savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-      (noDf |>> dashboardOf m "log_radon" :: BoundPlot)
-
-  let chainsAug = map (augmentChainWithDeterministic model) (hbmChainsR m)
-  printSummary $ summarize
-    ["mu_alpha", "sigma_alpha", "sigma_y", "beta1", "beta2"]
-    chainsAug
diff --git a/bench/posteriordb/22-arma/Model.hs b/bench/posteriordb/22-arma/Model.hs
deleted file mode 100644
--- a/bench/posteriordb/22-arma/Model.hs
+++ /dev/null
@@ -1,95 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | arma-arma11 (posteriordb) — hanalyze (ModelP) 実装。
---
--- Phase 89: posteriordb 横断ベンチマーク。ARMA(1,1) 時系列 (T=200)。
--- ★新ファミリ: AR成分とMA成分を併せ持つ時系列 — 12-ark (純AR)・03-garch11
--- (再帰的分散) とは異なる構造。
---
--- Stan 原典 (posteriordb `models/stan/arma11.stan`):
---   mu ~ normal(0,10); phi ~ normal(0,2); theta ~ normal(0,2);
---   sigma ~ cauchy(0,2.5);
---   nu[1] = mu + phi*mu; err[1] = y[1]-nu[1];       // err[0]=0 とみなす
---   for (t in 2:T) { nu[t] = mu+phi*y[t-1]+theta*err[t-1]; err[t]=y[t]-nu[t]; }
---   err ~ normal(0, sigma);
---
--- err[t] が err[t-1] に依存する逐次再帰 (14-hmm-example の forward
--- algorithmと同系統)。Phase 101 A2: 尤度を `mapAccumL + potential` 直書きから
--- 構造化 primitive 'ArmaNormal' + 'observeMV' へ移行 (密度は同値・'obsLogSum'
--- が同じ err 再帰を呼ぶ)。役割 (μ/φ/θ/σ) が型で見えるため、勾配コンパイラが
--- 逆向き随伴の閉形式 ('armaAnalyticVG'・AD tape ゼロ) を選べる。
---
--- reference_posterior_name = "arma-arma11" (posteriordb に公式 reference
--- あり・hanalyze vs PyMC vs 公式reference の3者比較可能)。
---
--- ビルド: cabal build --project-file=cabal.project.plot posteriordb-arma
-module Main (main) where
-
-import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import Text.Printf (printf)
-
-import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observeMV,
-                                    dataNamedX)
-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 の @arma.json@ 形状 ({"T":200,"y":[...]})。
-data ArmaData = ArmaData { arT :: Int, arY :: [Double] }
-
-instance FromJSON ArmaData where
-  parseJSON = withObject "ArmaData" $ \v ->
-    ArmaData <$> v .: "T" <*> v .: "y"
-
-noDf :: [(T.Text, ColData)]
-noDf = []
-
-dataPath :: FilePath
-dataPath = "bench/posteriordb/22-arma/data/arma.json"
-
-figuresDir :: FilePath
-figuresDir = "bench/posteriordb/22-arma/figures"
-
-readData :: IO ArmaData
-readData = either fail pure =<< eitherDecodeFileStrict dataPath
-
--- | ARMA(1,1) (Stan 原典と同一構造)。
---
--- Phase 101 A2: 尤度を 'ArmaNormal' + 'observeMV' で渡す (err 再帰は
--- 'obsLogSum' 側の同値実装)。 dataNamedX "y" は dashboard の実データ参照用に
--- 残す。
-armaModel :: [Double] -> ModelP ()
-armaModel ysRaw = do
-  mu    <- sample "mu"    (Normal 0 10)
-  phi   <- sample "phi"   (Normal 0 2)
-  theta <- sample "theta" (Normal 0 2)
-  sigma <- sample "sigma" (HalfCauchy 2.5)
-  _ys <- dataNamedX "y" []
-  observeMV "y_seq" (ArmaNormal mu phi theta sigma) [ysRaw]
-
-main :: IO ()
-main = do
-  d <- readData
-  let df = [ ("y", NumData (V.fromList (arY d))) ] :: [(T.Text, ColData)]
-      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
-                        , hbmWarmup = 1000, hbmSeed = Just 1 }
-      m = df |-> hbm cfg (armaModel (arY d))
-
-  -- 勾配経路 = 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
-
-  savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
-    (noDf |>> dashboardFullOf m "y" :: BoundPlot)
-
-  printSummary $ summarize ["mu", "phi", "theta", "sigma"] (hbmChainsR m)
diff --git a/bench/posteriordb/Common.hs b/bench/posteriordb/Common.hs
deleted file mode 100644
--- a/bench/posteriordb/Common.hs
+++ /dev/null
@@ -1,88 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Phase 89 posteriordb 横断ベンチマーク: 全モデルで使い回す Haskell 側
--- 共有ユーティリティ (Python 側 @_common.py@ と対)。
---
--- 'summarize' は @arviz.summary@ の簡易な代替 (mean / sd / 94% HDI / ESS /
--- R-hat / MCSE を 1 表にまとめる)。
---
--- ★Phase 92 B4: ESS 列を arviz 互換の rank-normalized 多 chain **ess_bulk**
--- ('Hanalyze.Stat.MCMC.essBulk') に切替え、mean/sd/HDI も全 chain
--- プールで計算する (= @az.summary@ と同じ土俵)。旧版は chain 0 のみ +
--- Geyer IMSE ('ess'・tau 下限 1 クランプで n 頭打ち) で、PyMC 側の
--- ess_bulk と直接比較できない指標非対称の原因だった (hmm で 766.7 vs
--- 実際は 3143 = 4.1 倍の過小表示・詳細 = phase-92 md B4)。
-module Common
-  ( ParamSummary (..)
-  , summarize
-  , printSummary
-  , timeSamplingMs
-  ) where
-
-import Control.DeepSeq (NFData, force)
-import Control.Exception (evaluate)
-import Data.Time.Clock (getCurrentTime, diffUTCTime)
-import qualified Data.Text as T
-import Text.Printf (printf)
-
-import Hanalyze.MCMC.Core (Chain, chainVals)
-import Hanalyze.Stat.MCMC (essBulk, rhat, hdi)
-
-data ParamSummary = ParamSummary
-  { psName :: T.Text
-  , psMean :: Double
-  , psSd   :: Double
-  , psHdiLo, psHdiHi :: Double  -- ^ 94% HDI (全 chain プールの post-warmup draw)。
-  , psEss  :: Double            -- ^ 'essBulk' (arviz 互換 rank-normalized・全 chain)。
-  , psRhat :: Maybe Double      -- ^ 全 chain の split-R-hat (chain ≥2 が必要)。
-  , psMcseMean :: Double        -- ^ 事後平均の モンテカルロ標準誤差 = sd/√ess。
-  }
-
--- | パラメータごとの要約統計 ('az.summary' 相当・全 chain プール)。
-summarize :: [T.Text] -> [Chain] -> [ParamSummary]
-summarize pars chains = map summarize1 pars
-  where
-    summarize1 p =
-      let allVals = map (chainVals p) chains
-          pooled  = concat allVals
-          n       = fromIntegral (length pooled) :: Double
-          mean_   = sum pooled / n
-          sd_     = sqrt (sum [ (x - mean_) ^ (2 :: Int) | x <- pooled ] / (n - 1))
-          (lo, hi) = hdi 0.94 pooled
-          essV    = essBulk allVals
-      in ParamSummary
-           { psName = p, psMean = mean_, psSd = sd_
-           , psHdiLo = lo, psHdiHi = hi, psEss = essV
-           , psRhat = rhat allVals
-           , psMcseMean = sd_ / sqrt essV
-           }
-
--- | 表として整形して標準出力へ。
-printSummary :: [ParamSummary] -> IO ()
-printSummary ps = do
-  printf "%-10s %9s %9s %17s %9s %8s %9s\n"
-    ("param" :: String) ("mean" :: String) ("sd" :: String)
-    ("hdi_3%..hdi_97%" :: String) ("ess_bulk" :: String) ("r_hat" :: String)
-    ("mcse_mean" :: String)
-  mapM_ printRow ps
-  where
-    printRow p = printf "%-10s %9.4f %9.4f [%6.3f, %6.3f] %9.1f %8s %9.5f\n"
-      (T.unpack (psName p)) (psMean p) (psSd p) (psHdiLo p) (psHdiHi p)
-      (psEss p) (maybe "NA" (printf "%.4f") (psRhat p) :: String) (psMcseMean p)
-
--- | サンプリング**のみ**の壁時計 (ms) を計測する。PyMC 側マトリクス
--- (@run_pymc_matrix.py@) が @t0 = time.perf_counter(); pm.sample(...)@ で
--- サンプリングだけを計測するのに対応させるための共通部品 (2026-07-11
--- 追加・09-eight-schools/07-gp-regr で「GHC起動+コンパイル試行+
--- dashboardFullOf の PNG 生成を含むプロセス全体」を計測してしまい PyMC 側
--- と比較不能だった反省から)。
---
--- @action@ は @df |-> hbm cfg model@ で得た @HBMModel@ の @hbmChainsR@ 等、
--- 遅延評価で未確定のサンプリング結果を渡す。'force' (deepseq) で完全評価
--- してから時刻差を取るので、遅延サンクの一部だけ強制されて計測が不正確に
--- なることはない。戻り値は @(結果, 経過ms)@。
-timeSamplingMs :: NFData a => a -> IO (a, Double)
-timeSamplingMs result = do
-  t0 <- getCurrentTime
-  r  <- evaluate (force result)
-  t1 <- getCurrentTime
-  pure (r, realToFrac (diffUTCTime t1 t0) * 1000)
diff --git a/data/dirty/01_clean.csv b/data/dirty/01_clean.csv
deleted file mode 100644
--- a/data/dirty/01_clean.csv
+++ /dev/null
@@ -1,4 +0,0 @@
-x,y
-1.0,2.0
-2.0,4.1
-3.0,5.9
diff --git a/data/dirty/02_no_header.csv b/data/dirty/02_no_header.csv
deleted file mode 100644
--- a/data/dirty/02_no_header.csv
+++ /dev/null
@@ -1,4 +0,0 @@
-1.0,2.0
-2.0,4.1
-3.0,5.9
-4.0,8.0
diff --git a/data/dirty/03_preamble.csv b/data/dirty/03_preamble.csv
deleted file mode 100644
--- a/data/dirty/03_preamble.csv
+++ /dev/null
@@ -1,7 +0,0 @@
-# Source: Lab A, generated 2026-05-03
-# Note: x = dose (mg), y = response (mV)
-# ---
-x,y
-1.0,2.0
-2.0,4.1
-3.0,5.9
diff --git a/data/dirty/04_ragged.csv b/data/dirty/04_ragged.csv
deleted file mode 100644
--- a/data/dirty/04_ragged.csv
+++ /dev/null
@@ -1,5 +0,0 @@
-x,y,z
-1,2,3
-4,5
-6,7,8,9
-10,11,12
diff --git a/data/dirty/05_dup_header.csv b/data/dirty/05_dup_header.csv
deleted file mode 100644
--- a/data/dirty/05_dup_header.csv
+++ /dev/null
@@ -1,4 +0,0 @@
-x,y,x
-1,2,10
-3,4,30
-5,6,50
diff --git a/data/dirty/06_blank_unnamed.csv b/data/dirty/06_blank_unnamed.csv
deleted file mode 100644
--- a/data/dirty/06_blank_unnamed.csv
+++ /dev/null
@@ -1,4 +0,0 @@
-x,,y,
-1,foo,2,a
-2,bar,4,b
-3,baz,6,c
diff --git a/data/dirty/07_mixed_na.csv b/data/dirty/07_mixed_na.csv
deleted file mode 100644
--- a/data/dirty/07_mixed_na.csv
+++ /dev/null
@@ -1,8 +0,0 @@
-id,score,group
-1,85,A
-2,NA,B
-3,,A
-4,null,C
-5,n/a,B
-6,92,A
-7,-,C
diff --git a/data/dirty/08_thousands_currency.csv b/data/dirty/08_thousands_currency.csv
deleted file mode 100644
--- a/data/dirty/08_thousands_currency.csv
+++ /dev/null
@@ -1,5 +0,0 @@
-item,price,qty
-A,"1,234.56",10
-B,"$2,500.00",5
-C,3000,7
-D,"4 567.8",2
diff --git a/data/dirty/09_quotes_commas.csv b/data/dirty/09_quotes_commas.csv
deleted file mode 100644
--- a/data/dirty/09_quotes_commas.csv
+++ /dev/null
@@ -1,5 +0,0 @@
-name,note,value
-"Smith, John","Likes ""tea""",1.5
-"O'Brien","Multi
-line note",2.5
-Plain,no quote,3.5
diff --git a/data/dirty/10_bom.csv b/data/dirty/10_bom.csv
deleted file mode 100644
--- a/data/dirty/10_bom.csv
+++ /dev/null
@@ -1,3 +0,0 @@
-﻿x,y
-1,2
-3,4
diff --git a/data/dirty/11_semicolon_eu.csv b/data/dirty/11_semicolon_eu.csv
deleted file mode 100644
--- a/data/dirty/11_semicolon_eu.csv
+++ /dev/null
@@ -1,3 +0,0 @@
-x;y;z
-1,5;2,5;3,0
-4,5;5,5;6,0
diff --git a/data/dirty/13_crlf.csv b/data/dirty/13_crlf.csv
deleted file mode 100644
--- a/data/dirty/13_crlf.csv
+++ /dev/null
@@ -1,3 +0,0 @@
-x	y
-1	2
-3	4
diff --git a/data/dirty/14_wrong_ext.csv b/data/dirty/14_wrong_ext.csv
deleted file mode 100644
--- a/data/dirty/14_wrong_ext.csv
+++ /dev/null
@@ -1,3 +0,0 @@
-x	y
-1	2
-3	4
diff --git a/data/dirty/15_trailing_blank.csv b/data/dirty/15_trailing_blank.csv
deleted file mode 100644
--- a/data/dirty/15_trailing_blank.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-x,y
-1,2
-3,4
-
-5,6
-
diff --git a/data/dirty/16_dates_units.csv b/data/dirty/16_dates_units.csv
deleted file mode 100644
--- a/data/dirty/16_dates_units.csv
+++ /dev/null
@@ -1,4 +0,0 @@
-date,length_cm,weight
-2026-01-01,12.3,5kg
-2026-01-02,11.5cm,5.2kg
-2026-01-03,10.0,4.8kg
diff --git a/data/dirty/17_empty.csv b/data/dirty/17_empty.csv
deleted file mode 100644
--- a/data/dirty/17_empty.csv
+++ /dev/null
diff --git a/data/dirty/18_header_only.csv b/data/dirty/18_header_only.csv
deleted file mode 100644
--- a/data/dirty/18_header_only.csv
+++ /dev/null
@@ -1,1 +0,0 @@
-x,y,z
diff --git a/data/dirty/19_whitespace.csv b/data/dirty/19_whitespace.csv
deleted file mode 100644
--- a/data/dirty/19_whitespace.csv
+++ /dev/null
@@ -1,4 +0,0 @@
- x , y , group
- 1 , 2 , A
- 3 , 4 ,  B
- 5 , 6 , A
diff --git a/data/distributions/exponential.csv b/data/distributions/exponential.csv
deleted file mode 100644
--- a/data/distributions/exponential.csv
+++ /dev/null
@@ -1,401 +0,0 @@
-time
-1.2531
-3.0481
-1.6896
-3.1037
-2.6295
-0.3767
-2.1709
-1.0966
-1.1378
-1.4458
-2.1357
-0.7638
-6.0561
-4.6137
-0.487
-1.5549
-4.1711
-0.3071
-1.6361
-14.5988
-0.0857
-3.1958
-0.7462
-4.0512
-0.8616
-3.678
-0.1393
-2.5891
-0.8475
-2.7695
-1.9786
-0.2023
-3.5973
-1.851
-2.3716
-0.7149
-2.9036
-0.8448
-0.7028
-13.6499
-1.4813
-4.0393
-2.9728
-0.7714
-9.3583
-0.726
-0.404
-0.0238
-1.724
-4.0473
-5.295
-1.9191
-0.6273
-4.5572
-2.3209
-0.2534
-3.9737
-0.5138
-0.567
-4.0326
-0.0142
-3.8919
-1.2678
-9.5413
-0.8615
-1.6415
-0.6503
-0.9302
-3.776
-1.7749
-0.7521
-0.3017
-4.8909
-4.5959
-0.5689
-1.0567
-1.9141
-0.0749
-2.3135
-3.9347
-2.5677
-4.948
-1.1834
-1.0216
-0.9965
-0.4996
-0.7407
-0.33
-0.8359
-2.397
-1.3165
-1.3486
-0.5811
-2.4379
-5.8166
-0.2154
-0.0928
-1.4068
-4.3653
-1.388
-1.042
-1.2762
-0.0451
-0.0264
-0.1367
-4.0502
-0.2998
-1.1299
-2.0135
-0.7627
-0.5417
-0.0932
-0.5218
-8.186
-5.3903
-2.6774
-6.4464
-5.6113
-0.4732
-1.36
-0.9286
-1.3821
-1.7569
-0.7082
-4.9917
-1.2451
-0.9689
-2.5641
-2.343
-1.3416
-3.1872
-0.4262
-1.2458
-1.8795
-0.9105
-0.3618
-0.7682
-5.4327
-0.7171
-0.6295
-0.3334
-5.6979
-4.9019
-1.6672
-1.5821
-0.9924
-2.347
-0.5976
-0.6004
-4.2498
-0.6909
-0.7091
-3.6178
-0.0962
-1.2963
-0.4893
-0.6549
-3.58
-4.1282
-0.494
-2.6278
-0.2411
-0.519
-7.0565
-0.4286
-2.604
-5.5017
-0.6784
-1.101
-5.1261
-1.574
-2.0426
-1.3879
-1.1352
-2.001
-2.7321
-4.5479
-1.1106
-0.6493
-2.9533
-1.3513
-6.2454
-0.2948
-2.8167
-1.5026
-1.9195
-3.7931
-0.1426
-0.3075
-1.1853
-0.18
-0.6004
-1.738
-2.2676
-1.7529
-0.6562
-2.6094
-5.105
-1.9734
-1.3781
-0.2064
-3.4369
-0.4355
-0.0375
-0.094
-5.3494
-1.531
-2.5295
-0.3372
-2.2338
-1.1845
-9.6643
-3.2122
-1.1461
-2.3821
-0.9473
-1.5361
-1.0495
-1.413
-0.516
-3.2652
-0.2097
-0.5476
-2.812
-10.1098
-1.783
-2.9135
-2.1202
-0.3497
-0.262
-0.1005
-13.0558
-0.8392
-0.3274
-0.6371
-4.5277
-1.2705
-2.8696
-1.4184
-5.6303
-0.0061
-0.6803
-4.7497
-0.164
-0.2168
-1.3088
-0.7109
-1.9751
-0.0515
-4.9325
-4.6956
-4.0271
-0.397
-5.1851
-1.1319
-1.6649
-0.0729
-2.8813
-2.6865
-2.3103
-0.4442
-0.7113
-0.6148
-2.291
-2.6042
-5.1889
-3.1919
-0.4971
-1.0733
-3.7234
-3.6112
-1.5269
-1.8003
-1.2475
-0.072
-3.1439
-2.3533
-2.656
-4.242
-3.6952
-0.7536
-0.3814
-0.7223
-6.4213
-0.3584
-2.2307
-4.7891
-2.7869
-2.0671
-1.3332
-0.7796
-2.6929
-0.0187
-6.9422
-1.8107
-1.5873
-0.5805
-2.7738
-1.5442
-0.4366
-3.9352
-8.8529
-0.3718
-0.0352
-4.0695
-0.388
-1.9764
-0.9231
-0.878
-1.0815
-2.7032
-0.4147
-7.6517
-5.483
-0.2052
-1.6264
-4.0991
-0.1995
-0.3742
-2.208
-6.3073
-1.5487
-3.5677
-1.1107
-0.3928
-1.8577
-7.0479
-0.7619
-3.5112
-3.0769
-3.3517
-2.547
-0.2479
-6.7249
-0.9587
-2.8066
-2.4408
-1.7735
-1.1932
-5.5944
-2.5475
-3.9721
-3.2243
-0.2451
-1.2856
-0.9217
-0.4409
-0.4592
-0.0213
-0.492
-2.0482
-1.2157
-1.4485
-0.1827
-1.3767
-1.8915
-3.4316
-2.2859
-3.0372
-0.2202
-0.5007
-5.6743
-0.017
-1.2719
-0.5309
-0.0008
-0.0527
-4.6025
-0.8406
-2.6446
-0.406
-0.1727
-5.7681
-0.0072
-3.0336
-0.3333
-0.4528
-2.0724
-0.3506
-0.3363
-3.4734
-1.0467
-0.4308
-0.7201
-0.1799
-7.1364
-0.7117
-0.1077
-1.1468
-1.1486
-3.3401
-0.0241
-0.252
-1.4176
-2.3485
-1.4249
-4.8102
-2.9167
-3.0391
-1.2832
diff --git a/data/distributions/normal.csv b/data/distributions/normal.csv
deleted file mode 100644
--- a/data/distributions/normal.csv
+++ /dev/null
@@ -1,501 +0,0 @@
-x
-6.8681
-5.5384
-4.3047
-5.8163
-7.5815
-1.5145
-6.7025
-3.2167
-2.0548
-6.2995
-4.5793
-6.6434
-4.6938
-8.5807
-4.0853
-4.8337
-7.2011
-2.3599
-4.1005
-4.0352
-6.6048
-4.1574
-5.4853
-4.2511
-3.7425
-5.7097
-7.9963
-3.5581
-4.5086
-2.2338
-4.815
-4.1358
-5.1902
-8.3641
-4.662
-8.3036
-4.5938
-3.5429
-4.1828
-5.0691
-4.8281
-6.6598
-5.5191
-4.8598
-9.1893
-2.0725
-3.7945
-1.5444
-4.8266
-5.3107
-3.9065
-3.2963
-2.9429
-1.1582
-7.4093
-4.9135
-6.0198
-5.5875
-3.0017
-6.1419
-3.5721
-3.6108
-1.5278
-10.0654
-5.4992
-7.8703
-5.964
-0.5833
-5.3077
-6.0108
-2.679
-7.2671
-5.6236
-4.7205
-3.6456
-4.8839
-2.4461
-5.6507
-2.4964
-5.6378
-1.8743
-7.8455
-5.9928
-3.3484
-7.3658
-4.3413
-5.5284
-5.4783
-1.0783
-2.4346
-4.2637
-5.2912
-3.5644
-5.0092
-1.5445
-4.5313
-7.9514
-6.6431
-7.0764
-5.039
-5.4492
-5.575
-5.5428
-7.5222
-7.7742
-5.6922
-2.7781
-5.5905
-4.5725
-3.8824
-4.5568
-4.9719
-0.869
-6.4107
-8.7296
-6.7633
-4.0772
-7.4821
-5.0229
-3.5078
-2.7189
-7.0889
-4.2485
-2.3148
-4.4487
-5.0356
-0.5751
-5.1112
-6.0492
-7.9762
-5.7312
-6.5457
-4.6529
-6.9767
-6.841
-4.6119
-6.6802
-7.9991
-1.0061
-2.4491
-5.4683
-4.8325
-6.9828
-5.9672
-4.5626
-5.8889
-4.8575
-3.5331
-7.6019
-8.3764
-4.1377
-3.9553
-7.7212
-7.4707
-5.8655
-5.2292
-7.6612
-6.0521
-3.0511
-8.4959
-6.0668
-5.9522
-4.5414
-7.6931
-5.8722
-4.8635
-6.076
-5.6596
-6.4089
-4.1533
-5.1899
-6.9142
-6.965
-4.9056
-3.4776
-4.6199
-5.5302
-5.2127
-4.9464
-1.1353
-4.38
-3.0041
-2.5511
-4.153
-11.3784
-4.5752
-4.2609
-7.5722
-4.1265
-6.7587
-4.4957
-2.4297
-4.2365
-1.436
-4.6907
-4.3235
-2.0401
-3.1832
-2.7225
-3.2036
-5.5777
-6.99
-5.6531
-0.5779
-4.0243
-5.0091
-5.2825
-8.5509
-1.6857
-6.0239
-3.18
-3.8029
-5.0897
-6.8352
-5.1901
-1.2274
-8.5994
-7.1808
-6.9314
-5.7952
-6.7928
-6.937
-4.2715
-3.3771
-3.836
-4.9629
-4.8591
-2.8311
-3.5521
-4.7024
-3.9931
-3.6307
-6.4323
-3.8194
-6.5335
-2.416
-5.7099
-7.1769
-4.3039
-5.1088
-7.7789
-5.0631
-6.0781
-4.9588
-3.2471
-5.664
-7.5348
-3.7282
-6.3695
-6.7152
-6.9202
-2.4908
-1.6474
-1.1179
-1.4536
-3.3636
-3.9286
-5.7434
-7.8894
--0.4453
-4.984
-5.3619
-5.0492
-5.2012
-8.2124
-3.0276
-5.428
-5.1792
-2.0471
-6.1464
-6.2318
-9.1639
-3.6544
-4.5343
-6.4549
-3.8004
-5.6018
-4.0802
-9.0888
-4.197
-3.2105
-3.7338
-5.5013
-9.1133
-5.9037
-3.4705
-6.5367
-6.1572
-2.7856
-4.6933
-6.9348
-6.0419
-5.0387
-3.0955
-7.7409
-5.4719
-6.0427
-7.1446
-4.7231
-10.74
-4.1711
-5.1227
-3.953
-5.6359
-5.7737
-4.4062
-2.9469
-2.9915
-4.2556
-4.7799
-7.4133
-7.6532
-5.1709
-5.7701
-4.617
-4.57
-7.5647
-5.5106
-8.6925
-4.6592
-4.569
-6.1909
-3.9395
-4.0674
-2.7103
-8.4879
-4.3097
-3.984
-1.8649
-7.8635
-4.2884
-5.1625
-3.8492
-5.2085
-2.8307
-6.6295
-4.9035
-3.4929
-6.4684
-5.8596
-6.0044
-8.3329
--1.5537
-4.4766
-4.0859
-3.3905
-6.5376
-4.0237
-6.6636
-2.486
-6.564
-6.112
-3.6576
-3.6208
-7.2385
-4.4635
-4.3128
-7.0789
-0.3787
-4.993
-9.251
-3.9339
-5.6354
-4.7821
-5.1996
-3.5753
-5.3495
-3.629
-7.002
-3.5474
-6.1209
-3.9676
-7.9695
-3.7784
-4.0099
-7.9531
-2.3192
-4.8305
-3.1089
-2.6363
-3.9461
-5.4263
-6.9673
-4.1792
-9.0487
-5.1339
-1.3547
-6.8163
-5.213
-6.8401
-3.7876
-3.3698
-6.6347
-5.0157
-3.3756
-4.1943
-5.6661
-5.7979
-7.4295
-5.9987
-4.0453
-7.0646
-6.4219
-3.7479
-2.9001
-3.9066
-3.8168
-5.218
-6.3727
-4.5737
-4.0243
-5.7788
-4.6065
--0.1347
-6.4032
-4.035
-2.8153
-3.6384
-6.0406
-7.4135
-3.9631
-6.1081
-5.717
-3.8685
-5.096
-6.0186
-1.8538
-5.8117
-3.6013
-2.1267
-4.0205
-6.4043
-6.4189
-2.7874
-1.1486
-5.5634
-8.3111
-4.2319
-7.8811
-1.1732
-4.8892
-7.0566
-4.5993
-1.4124
-2.5147
-3.317
-6.8769
-2.3097
-5.1937
-3.1151
-4.9321
-4.7697
-2.056
-4.9418
-1.1461
-6.9497
-5.956
-3.802
-3.6218
-4.6796
-5.9622
-6.584
-4.3238
-2.7535
-6.3321
-4.9505
-7.5164
-7.1867
-6.9264
-4.9466
-3.7663
-4.9716
-7.2054
-5.3486
-5.3332
-6.5939
-5.1735
-5.7172
--0.694
-3.8298
-3.4636
-6.1636
-5.4279
-9.057
-6.8515
-8.2028
-6.0486
-2.4054
-8.7436
-1.9185
-8.2795
-1.1708
-4.9229
-4.4176
-5.8718
-7.1558
-2.9964
-5.6172
-2.3582
-5.3918
diff --git a/data/distributions/poisson.csv b/data/distributions/poisson.csv
deleted file mode 100644
--- a/data/distributions/poisson.csv
+++ /dev/null
@@ -1,301 +0,0 @@
-count
-4
-2
-5
-3
-2
-3
-1
-2
-3
-0
-3
-3
-4
-4
-3
-4
-2
-5
-4
-5
-3
-3
-3
-5
-2
-8
-2
-10
-5
-1
-4
-2
-5
-1
-3
-5
-5
-2
-2
-2
-4
-4
-1
-5
-1
-6
-4
-3
-7
-4
-9
-2
-3
-3
-5
-7
-2
-4
-3
-3
-5
-2
-6
-6
-9
-3
-3
-2
-4
-3
-5
-4
-6
-8
-4
-4
-3
-4
-2
-2
-5
-5
-2
-4
-1
-3
-2
-2
-4
-2
-6
-2
-7
-6
-3
-1
-1
-6
-4
-9
-5
-2
-5
-6
-5
-2
-3
-8
-3
-4
-5
-4
-4
-8
-3
-5
-6
-8
-4
-2
-7
-3
-3
-5
-2
-6
-5
-2
-5
-4
-3
-3
-4
-5
-5
-8
-4
-3
-5
-6
-1
-5
-5
-8
-3
-7
-5
-2
-3
-5
-4
-4
-5
-4
-3
-4
-7
-4
-5
-4
-4
-6
-4
-7
-8
-3
-7
-3
-8
-2
-2
-4
-1
-3
-5
-4
-2
-6
-3
-3
-2
-3
-2
-3
-2
-7
-4
-2
-9
-5
-4
-1
-5
-4
-3
-2
-3
-3
-2
-6
-3
-5
-7
-7
-4
-4
-4
-7
-1
-5
-6
-4
-7
-4
-3
-7
-3
-4
-3
-5
-6
-0
-1
-3
-5
-2
-4
-8
-2
-0
-4
-3
-4
-4
-7
-3
-6
-4
-4
-6
-5
-4
-1
-3
-4
-3
-3
-6
-2
-3
-4
-3
-7
-5
-4
-3
-8
-6
-4
-8
-5
-2
-6
-5
-5
-3
-4
-5
-5
-3
-4
-2
-5
-3
-4
-8
-4
-1
-4
-2
-6
-2
-1
-6
-3
-4
-4
-5
-6
-7
-5
-2
-2
-4
-9
-8
-4
-5
-0
-6
diff --git a/data/io/melted_sample.csv b/data/io/melted_sample.csv
deleted file mode 100644
--- a/data/io/melted_sample.csv
+++ /dev/null
@@ -1,28 +0,0 @@
-name,x1,x2,t,y
-a,1,0,1.0,1.0
-a,1,0,3.0,3.0
-a,1,0,5.0,5.0
-a,1,0,7.0,7.0
-a,1,0,9.0,9.0
-b,2,0,2.0,4.0
-b,2,0,4.0,8.0
-b,2,0,5.0,10.0
-b,2,0,7.0,14.0
-b,2,0,10.0,20.0
-c,3,0,1.0,0.1
-c,3,0,2.0,0.2
-c,3,0,4.0,0.4
-c,3,0,6.0,0.6
-c,3,0,8.0,0.8
-c,3,0,10.0,1.0
-d,4,0,2.0,1.0
-d,4,0,3.0,1.5
-d,4,0,4.0,2.0
-d,4,0,5.0,2.5
-d,4,0,8.0,4.0
-d,4,0,10.0,5.0
-e,5,0,1.0,3.0
-e,5,0,3.0,9.0
-e,5,0,4.0,12.0
-e,5,0,6.0,18.0
-e,5,0,10.0,30.0
diff --git a/data/io/potential_long.csv b/data/io/potential_long.csv
deleted file mode 100644
--- a/data/io/potential_long.csv
+++ /dev/null
@@ -1,2101 +0,0 @@
-name,energy,dose,z,y
-c01_E100.0_D6.0,100.0,6.0,0.2362,3.2332
-c01_E100.0_D6.0,100.0,6.0,2.5677,2.4696
-c01_E100.0_D6.0,100.0,6.0,4.1821,2.4739
-c01_E100.0_D6.0,100.0,6.0,6.6191,1.8719
-c01_E100.0_D6.0,100.0,6.0,8.2551,1.6444
-c01_E100.0_D6.0,100.0,6.0,9.8336,1.1441
-c01_E100.0_D6.0,100.0,6.0,12.6972,0.5568
-c01_E100.0_D6.0,100.0,6.0,13.8137,0.2422
-c01_E100.0_D6.0,100.0,6.0,16.0078,-0.3247
-c01_E100.0_D6.0,100.0,6.0,18.1786,-1.0442
-c01_E100.0_D6.0,100.0,6.0,19.9817,-1.6603
-c01_E100.0_D6.0,100.0,6.0,22.1926,-2.5690
-c01_E100.0_D6.0,100.0,6.0,24.7990,-3.2088
-c01_E100.0_D6.0,100.0,6.0,26.5233,-3.8435
-c01_E100.0_D6.0,100.0,6.0,28.2354,-4.2280
-c01_E100.0_D6.0,100.0,6.0,30.3559,-4.9320
-c01_E100.0_D6.0,100.0,6.0,32.7812,-5.3824
-c01_E100.0_D6.0,100.0,6.0,34.0409,-5.7055
-c01_E100.0_D6.0,100.0,6.0,36.1434,-5.8673
-c01_E100.0_D6.0,100.0,6.0,37.8006,-6.0234
-c01_E100.0_D6.0,100.0,6.0,39.9901,-6.0959
-c01_E100.0_D6.0,100.0,6.0,42.1818,-5.9871
-c01_E100.0_D6.0,100.0,6.0,44.1335,-5.5734
-c01_E100.0_D6.0,100.0,6.0,46.5621,-4.9858
-c01_E100.0_D6.0,100.0,6.0,48.5120,-4.7809
-c01_E100.0_D6.0,100.0,6.0,51.0975,-4.0614
-c01_E100.0_D6.0,100.0,6.0,52.9465,-3.6515
-c01_E100.0_D6.0,100.0,6.0,54.4530,-3.3438
-c01_E100.0_D6.0,100.0,6.0,56.8764,-2.7125
-c01_E100.0_D6.0,100.0,6.0,58.3410,-2.3572
-c01_E100.0_D6.0,100.0,6.0,60.9469,-1.7201
-c01_E100.0_D6.0,100.0,6.0,63.1805,-1.1624
-c01_E100.0_D6.0,100.0,6.0,65.2521,-0.9704
-c01_E100.0_D6.0,100.0,6.0,66.3768,-0.7183
-c01_E100.0_D6.0,100.0,6.0,69.0318,-0.5986
-c01_E100.0_D6.0,100.0,6.0,71.2782,-0.2402
-c01_E100.0_D6.0,100.0,6.0,72.4995,-0.3005
-c01_E100.0_D6.0,100.0,6.0,74.1500,0.0091
-c01_E100.0_D6.0,100.0,6.0,76.6022,0.1262
-c01_E100.0_D6.0,100.0,6.0,78.9285,0.0707
-c01_E100.0_D6.0,100.0,6.0,80.5331,0.3087
-c01_E100.0_D6.0,100.0,6.0,83.2731,0.0625
-c01_E100.0_D6.0,100.0,6.0,84.5351,0.0835
-c01_E100.0_D6.0,100.0,6.0,86.3187,0.2192
-c01_E100.0_D6.0,100.0,6.0,88.9866,0.2575
-c01_E100.0_D6.0,100.0,6.0,91.2443,0.2515
-c01_E100.0_D6.0,100.0,6.0,93.5068,0.1399
-c01_E100.0_D6.0,100.0,6.0,95.2976,-0.0626
-c01_E100.0_D6.0,100.0,6.0,96.5358,0.1126
-c01_E100.0_D6.0,100.0,6.0,98.6413,0.0945
-c01_E100.0_D6.0,100.0,6.0,101.5878,0.2002
-c01_E100.0_D6.0,100.0,6.0,103.4871,0.0295
-c01_E100.0_D6.0,100.0,6.0,105.4748,0.2003
-c01_E100.0_D6.0,100.0,6.0,107.6334,0.2320
-c01_E100.0_D6.0,100.0,6.0,108.8109,0.0768
-c01_E100.0_D6.0,100.0,6.0,111.1480,0.1376
-c01_E100.0_D6.0,100.0,6.0,113.1213,0.0630
-c01_E100.0_D6.0,100.0,6.0,115.1393,0.0628
-c01_E100.0_D6.0,100.0,6.0,117.4196,-0.0971
-c01_E100.0_D6.0,100.0,6.0,119.5172,-0.0554
-c01_E100.0_D6.0,100.0,6.0,121.8047,-0.0389
-c01_E100.0_D6.0,100.0,6.0,122.9153,-0.0847
-c01_E100.0_D6.0,100.0,6.0,124.8605,0.1587
-c01_E100.0_D6.0,100.0,6.0,126.7925,-0.0151
-c01_E100.0_D6.0,100.0,6.0,129.5865,0.0845
-c01_E100.0_D6.0,100.0,6.0,131.3978,0.0457
-c01_E100.0_D6.0,100.0,6.0,132.8098,-0.1178
-c01_E100.0_D6.0,100.0,6.0,134.9398,-0.0012
-c01_E100.0_D6.0,100.0,6.0,137.9421,0.0691
-c01_E100.0_D6.0,100.0,6.0,139.3022,0.0660
-c01_E100.0_D6.0,100.0,6.0,141.6466,-0.1280
-c01_E100.0_D6.0,100.0,6.0,143.1581,0.0750
-c01_E100.0_D6.0,100.0,6.0,144.9177,-0.0841
-c01_E100.0_D6.0,100.0,6.0,147.7170,-0.0220
-c01_E100.0_D6.0,100.0,6.0,149.8036,-0.1189
-c01_E100.0_D6.0,100.0,6.0,151.3132,0.1532
-c01_E100.0_D6.0,100.0,6.0,153.4756,-0.0920
-c01_E100.0_D6.0,100.0,6.0,155.3160,-0.0430
-c01_E100.0_D6.0,100.0,6.0,157.4858,-0.1153
-c01_E100.0_D6.0,100.0,6.0,159.1645,0.1336
-c01_E100.0_D6.0,100.0,6.0,161.2475,0.0378
-c01_E100.0_D6.0,100.0,6.0,163.8929,0.0289
-c01_E100.0_D6.0,100.0,6.0,165.3373,-0.0914
-c01_E100.0_D6.0,100.0,6.0,167.6146,-0.0342
-c01_E100.0_D6.0,100.0,6.0,169.1670,0.0895
-c01_E100.0_D6.0,100.0,6.0,171.4118,-0.0144
-c01_E100.0_D6.0,100.0,6.0,173.2250,0.1440
-c01_E100.0_D6.0,100.0,6.0,175.5948,-0.0543
-c01_E100.0_D6.0,100.0,6.0,177.9121,0.0181
-c01_E100.0_D6.0,100.0,6.0,179.7448,0.1826
-c01_E100.0_D6.0,100.0,6.0,181.7071,0.1057
-c01_E100.0_D6.0,100.0,6.0,183.2490,-0.0348
-c01_E100.0_D6.0,100.0,6.0,185.5893,0.1287
-c01_E100.0_D6.0,100.0,6.0,188.2351,0.0258
-c01_E100.0_D6.0,100.0,6.0,189.6375,0.1567
-c01_E100.0_D6.0,100.0,6.0,191.8687,0.0046
-c01_E100.0_D6.0,100.0,6.0,194.1360,-0.0308
-c01_E100.0_D6.0,100.0,6.0,196.1513,0.0054
-c01_E100.0_D6.0,100.0,6.0,197.8745,0.0397
-c01_E100.0_D6.0,100.0,6.0,200.0000,-0.1509
-c02_E100.0_D6.4,100.0,6.4,0.0922,3.1656
-c02_E100.0_D6.4,100.0,6.4,1.6882,2.8124
-c02_E100.0_D6.4,100.0,6.4,3.7152,2.6379
-c02_E100.0_D6.4,100.0,6.4,5.9466,2.0391
-c02_E100.0_D6.4,100.0,6.4,7.5934,1.6584
-c02_E100.0_D6.4,100.0,6.4,10.0455,1.3110
-c02_E100.0_D6.4,100.0,6.4,11.6530,0.8008
-c02_E100.0_D6.4,100.0,6.4,14.6687,-0.0544
-c02_E100.0_D6.4,100.0,6.4,15.7891,-0.3022
-c02_E100.0_D6.4,100.0,6.4,18.3140,-1.1788
-c02_E100.0_D6.4,100.0,6.4,19.7738,-1.5948
-c02_E100.0_D6.4,100.0,6.4,22.6842,-2.8531
-c02_E100.0_D6.4,100.0,6.4,24.4607,-3.1371
-c02_E100.0_D6.4,100.0,6.4,26.8355,-3.9580
-c02_E100.0_D6.4,100.0,6.4,28.2338,-4.3743
-c02_E100.0_D6.4,100.0,6.4,30.2215,-5.1489
-c02_E100.0_D6.4,100.0,6.4,32.0454,-5.4064
-c02_E100.0_D6.4,100.0,6.4,34.5005,-5.9572
-c02_E100.0_D6.4,100.0,6.4,36.7201,-5.9435
-c02_E100.0_D6.4,100.0,6.4,38.9012,-5.9689
-c02_E100.0_D6.4,100.0,6.4,39.9827,-6.1715
-c02_E100.0_D6.4,100.0,6.4,42.3046,-6.0285
-c02_E100.0_D6.4,100.0,6.4,44.6599,-5.5503
-c02_E100.0_D6.4,100.0,6.4,46.7587,-5.2681
-c02_E100.0_D6.4,100.0,6.4,48.1010,-4.9720
-c02_E100.0_D6.4,100.0,6.4,50.8190,-4.2938
-c02_E100.0_D6.4,100.0,6.4,52.1826,-3.9420
-c02_E100.0_D6.4,100.0,6.4,54.6130,-3.2010
-c02_E100.0_D6.4,100.0,6.4,56.5435,-2.9402
-c02_E100.0_D6.4,100.0,6.4,58.5977,-2.2627
-c02_E100.0_D6.4,100.0,6.4,61.0275,-1.8167
-c02_E100.0_D6.4,100.0,6.4,63.0558,-1.2133
-c02_E100.0_D6.4,100.0,6.4,64.4734,-1.0825
-c02_E100.0_D6.4,100.0,6.4,66.2115,-0.7579
-c02_E100.0_D6.4,100.0,6.4,69.2213,-0.3131
-c02_E100.0_D6.4,100.0,6.4,70.4332,-0.2918
-c02_E100.0_D6.4,100.0,6.4,72.9758,0.0099
-c02_E100.0_D6.4,100.0,6.4,74.9734,0.0862
-c02_E100.0_D6.4,100.0,6.4,77.2075,0.0443
-c02_E100.0_D6.4,100.0,6.4,78.4745,-0.1005
-c02_E100.0_D6.4,100.0,6.4,81.1710,0.1684
-c02_E100.0_D6.4,100.0,6.4,83.2488,0.3598
-c02_E100.0_D6.4,100.0,6.4,85.4193,0.2170
-c02_E100.0_D6.4,100.0,6.4,86.8163,0.2279
-c02_E100.0_D6.4,100.0,6.4,88.5263,0.1794
-c02_E100.0_D6.4,100.0,6.4,91.4610,0.1459
-c02_E100.0_D6.4,100.0,6.4,93.3041,0.2629
-c02_E100.0_D6.4,100.0,6.4,94.3436,0.1586
-c02_E100.0_D6.4,100.0,6.4,96.4672,0.2545
-c02_E100.0_D6.4,100.0,6.4,98.5136,0.3815
-c02_E100.0_D6.4,100.0,6.4,100.6631,0.1232
-c02_E100.0_D6.4,100.0,6.4,102.5198,0.1795
-c02_E100.0_D6.4,100.0,6.4,104.5271,0.0204
-c02_E100.0_D6.4,100.0,6.4,107.0555,0.1267
-c02_E100.0_D6.4,100.0,6.4,109.3584,0.0799
-c02_E100.0_D6.4,100.0,6.4,110.5487,0.1776
-c02_E100.0_D6.4,100.0,6.4,113.2900,0.2040
-c02_E100.0_D6.4,100.0,6.4,114.5813,0.2288
-c02_E100.0_D6.4,100.0,6.4,116.5812,0.2318
-c02_E100.0_D6.4,100.0,6.4,119.1658,-0.0103
-c02_E100.0_D6.4,100.0,6.4,121.2012,0.2062
-c02_E100.0_D6.4,100.0,6.4,123.3039,-0.0038
-c02_E100.0_D6.4,100.0,6.4,125.0227,0.0804
-c02_E100.0_D6.4,100.0,6.4,127.2171,0.1789
-c02_E100.0_D6.4,100.0,6.4,129.5001,0.2333
-c02_E100.0_D6.4,100.0,6.4,131.1613,0.0750
-c02_E100.0_D6.4,100.0,6.4,132.9972,0.0708
-c02_E100.0_D6.4,100.0,6.4,135.7364,0.0946
-c02_E100.0_D6.4,100.0,6.4,137.9583,0.0032
-c02_E100.0_D6.4,100.0,6.4,139.1007,-0.0773
-c02_E100.0_D6.4,100.0,6.4,141.4178,-0.1232
-c02_E100.0_D6.4,100.0,6.4,143.6814,0.0621
-c02_E100.0_D6.4,100.0,6.4,145.1448,-0.0966
-c02_E100.0_D6.4,100.0,6.4,147.5781,0.1830
-c02_E100.0_D6.4,100.0,6.4,148.9210,-0.0793
-c02_E100.0_D6.4,100.0,6.4,151.0469,0.0625
-c02_E100.0_D6.4,100.0,6.4,153.0481,0.1765
-c02_E100.0_D6.4,100.0,6.4,155.5544,0.0403
-c02_E100.0_D6.4,100.0,6.4,157.4597,-0.1033
-c02_E100.0_D6.4,100.0,6.4,159.5406,0.0386
-c02_E100.0_D6.4,100.0,6.4,161.6245,0.0947
-c02_E100.0_D6.4,100.0,6.4,163.7755,0.1466
-c02_E100.0_D6.4,100.0,6.4,165.4378,0.1108
-c02_E100.0_D6.4,100.0,6.4,168.0168,0.0871
-c02_E100.0_D6.4,100.0,6.4,169.6112,0.0044
-c02_E100.0_D6.4,100.0,6.4,171.9711,-0.0302
-c02_E100.0_D6.4,100.0,6.4,173.5460,-0.0910
-c02_E100.0_D6.4,100.0,6.4,176.0502,-0.0580
-c02_E100.0_D6.4,100.0,6.4,177.7432,0.0192
-c02_E100.0_D6.4,100.0,6.4,179.9143,0.1703
-c02_E100.0_D6.4,100.0,6.4,181.7520,-0.1688
-c02_E100.0_D6.4,100.0,6.4,184.1177,0.0240
-c02_E100.0_D6.4,100.0,6.4,185.5743,0.0425
-c02_E100.0_D6.4,100.0,6.4,188.0607,0.0714
-c02_E100.0_D6.4,100.0,6.4,189.6922,-0.0196
-c02_E100.0_D6.4,100.0,6.4,191.7735,-0.1319
-c02_E100.0_D6.4,100.0,6.4,194.4518,-0.0156
-c02_E100.0_D6.4,100.0,6.4,195.4072,-0.0849
-c02_E100.0_D6.4,100.0,6.4,197.7348,0.0039
-c02_E100.0_D6.4,100.0,6.4,199.6339,-0.0322
-c03_E100.0_D6.8,100.0,6.8,0.3534,3.0560
-c03_E100.0_D6.8,100.0,6.8,2.2567,2.7776
-c03_E100.0_D6.8,100.0,6.8,3.6175,2.4346
-c03_E100.0_D6.8,100.0,6.8,6.1503,2.0884
-c03_E100.0_D6.8,100.0,6.8,8.2906,1.6570
-c03_E100.0_D6.8,100.0,6.8,10.5925,1.0061
-c03_E100.0_D6.8,100.0,6.8,12.2183,0.6212
-c03_E100.0_D6.8,100.0,6.8,13.6499,0.4751
-c03_E100.0_D6.8,100.0,6.8,16.5395,-0.6227
-c03_E100.0_D6.8,100.0,6.8,18.1993,-1.3670
-c03_E100.0_D6.8,100.0,6.8,20.7473,-2.0349
-c03_E100.0_D6.8,100.0,6.8,22.3611,-2.6007
-c03_E100.0_D6.8,100.0,6.8,23.9022,-3.1323
-c03_E100.0_D6.8,100.0,6.8,26.8619,-4.1999
-c03_E100.0_D6.8,100.0,6.8,28.0660,-4.6353
-c03_E100.0_D6.8,100.0,6.8,30.2705,-5.2252
-c03_E100.0_D6.8,100.0,6.8,32.6454,-5.6399
-c03_E100.0_D6.8,100.0,6.8,34.9203,-6.1189
-c03_E100.0_D6.8,100.0,6.8,36.1350,-6.1898
-c03_E100.0_D6.8,100.0,6.8,38.1656,-6.2591
-c03_E100.0_D6.8,100.0,6.8,40.2277,-6.1430
-c03_E100.0_D6.8,100.0,6.8,41.9051,-5.9959
-c03_E100.0_D6.8,100.0,6.8,44.3723,-5.7268
-c03_E100.0_D6.8,100.0,6.8,46.4292,-5.2052
-c03_E100.0_D6.8,100.0,6.8,48.7139,-4.9064
-c03_E100.0_D6.8,100.0,6.8,50.7259,-4.3208
-c03_E100.0_D6.8,100.0,6.8,52.8217,-3.8758
-c03_E100.0_D6.8,100.0,6.8,54.8174,-3.2482
-c03_E100.0_D6.8,100.0,6.8,56.8378,-2.5490
-c03_E100.0_D6.8,100.0,6.8,59.1023,-2.2068
-c03_E100.0_D6.8,100.0,6.8,60.1739,-1.9995
-c03_E100.0_D6.8,100.0,6.8,62.9973,-1.3928
-c03_E100.0_D6.8,100.0,6.8,65.0112,-0.7749
-c03_E100.0_D6.8,100.0,6.8,66.3582,-0.7155
-c03_E100.0_D6.8,100.0,6.8,69.1185,-0.5101
-c03_E100.0_D6.8,100.0,6.8,70.7073,-0.2964
-c03_E100.0_D6.8,100.0,6.8,72.8734,-0.0613
-c03_E100.0_D6.8,100.0,6.8,75.1828,0.0464
-c03_E100.0_D6.8,100.0,6.8,76.3847,-0.0232
-c03_E100.0_D6.8,100.0,6.8,78.6257,0.2943
-c03_E100.0_D6.8,100.0,6.8,80.3089,0.0209
-c03_E100.0_D6.8,100.0,6.8,82.7202,0.1948
-c03_E100.0_D6.8,100.0,6.8,84.3734,0.2083
-c03_E100.0_D6.8,100.0,6.8,86.3526,-0.0060
-c03_E100.0_D6.8,100.0,6.8,88.7370,-0.0422
-c03_E100.0_D6.8,100.0,6.8,90.8867,0.0595
-c03_E100.0_D6.8,100.0,6.8,92.4901,0.2800
-c03_E100.0_D6.8,100.0,6.8,94.4635,-0.0142
-c03_E100.0_D6.8,100.0,6.8,97.4999,0.0664
-c03_E100.0_D6.8,100.0,6.8,98.7448,0.3023
-c03_E100.0_D6.8,100.0,6.8,100.8900,0.0552
-c03_E100.0_D6.8,100.0,6.8,102.5971,0.2663
-c03_E100.0_D6.8,100.0,6.8,105.3932,-0.0415
-c03_E100.0_D6.8,100.0,6.8,106.9310,0.0776
-c03_E100.0_D6.8,100.0,6.8,109.6061,-0.0584
-c03_E100.0_D6.8,100.0,6.8,111.6238,-0.0784
-c03_E100.0_D6.8,100.0,6.8,113.3269,0.0580
-c03_E100.0_D6.8,100.0,6.8,114.5725,0.1221
-c03_E100.0_D6.8,100.0,6.8,117.4801,0.1893
-c03_E100.0_D6.8,100.0,6.8,119.4458,-0.0341
-c03_E100.0_D6.8,100.0,6.8,121.2428,0.0344
-c03_E100.0_D6.8,100.0,6.8,123.2118,0.2070
-c03_E100.0_D6.8,100.0,6.8,124.9647,0.0441
-c03_E100.0_D6.8,100.0,6.8,127.7189,0.2624
-c03_E100.0_D6.8,100.0,6.8,128.7125,-0.0917
-c03_E100.0_D6.8,100.0,6.8,131.1875,0.2275
-c03_E100.0_D6.8,100.0,6.8,133.1255,-0.0315
-c03_E100.0_D6.8,100.0,6.8,135.7756,-0.0478
-c03_E100.0_D6.8,100.0,6.8,137.0726,0.1836
-c03_E100.0_D6.8,100.0,6.8,139.9553,0.0367
-c03_E100.0_D6.8,100.0,6.8,141.7772,-0.0547
-c03_E100.0_D6.8,100.0,6.8,142.8878,-0.0128
-c03_E100.0_D6.8,100.0,6.8,145.9816,0.0936
-c03_E100.0_D6.8,100.0,6.8,147.9929,-0.0108
-c03_E100.0_D6.8,100.0,6.8,149.3812,-0.0140
-c03_E100.0_D6.8,100.0,6.8,151.6420,0.0879
-c03_E100.0_D6.8,100.0,6.8,153.3727,0.1634
-c03_E100.0_D6.8,100.0,6.8,156.0075,-0.0413
-c03_E100.0_D6.8,100.0,6.8,157.1127,0.1447
-c03_E100.0_D6.8,100.0,6.8,159.0032,-0.0689
-c03_E100.0_D6.8,100.0,6.8,162.0992,-0.0990
-c03_E100.0_D6.8,100.0,6.8,163.3456,-0.1073
-c03_E100.0_D6.8,100.0,6.8,165.3908,-0.0548
-c03_E100.0_D6.8,100.0,6.8,167.5738,0.1181
-c03_E100.0_D6.8,100.0,6.8,169.5226,-0.0884
-c03_E100.0_D6.8,100.0,6.8,171.1633,0.0280
-c03_E100.0_D6.8,100.0,6.8,173.6951,0.0642
-c03_E100.0_D6.8,100.0,6.8,175.6847,0.0638
-c03_E100.0_D6.8,100.0,6.8,177.4867,0.1537
-c03_E100.0_D6.8,100.0,6.8,179.2801,-0.0306
-c03_E100.0_D6.8,100.0,6.8,182.4101,0.0609
-c03_E100.0_D6.8,100.0,6.8,184.0779,-0.1247
-c03_E100.0_D6.8,100.0,6.8,185.7973,0.1371
-c03_E100.0_D6.8,100.0,6.8,188.1364,0.2053
-c03_E100.0_D6.8,100.0,6.8,189.4370,0.0714
-c03_E100.0_D6.8,100.0,6.8,192.4030,0.1987
-c03_E100.0_D6.8,100.0,6.8,194.3969,0.0452
-c03_E100.0_D6.8,100.0,6.8,196.3415,0.0495
-c03_E100.0_D6.8,100.0,6.8,197.5386,0.0138
-c03_E100.0_D6.8,100.0,6.8,199.8367,-0.2349
-c04_E100.0_D7.2,100.0,7.2,0.0000,3.1210
-c04_E100.0_D7.2,100.0,7.2,2.0366,2.7365
-c04_E100.0_D7.2,100.0,7.2,3.5219,2.4722
-c04_E100.0_D7.2,100.0,7.2,6.3111,1.9170
-c04_E100.0_D7.2,100.0,7.2,7.9606,1.5561
-c04_E100.0_D7.2,100.0,7.2,10.5420,1.0656
-c04_E100.0_D7.2,100.0,7.2,12.5778,0.4217
-c04_E100.0_D7.2,100.0,7.2,13.8288,0.2598
-c04_E100.0_D7.2,100.0,7.2,16.0555,-0.5058
-c04_E100.0_D7.2,100.0,7.2,17.9967,-1.2688
-c04_E100.0_D7.2,100.0,7.2,20.4329,-2.1417
-c04_E100.0_D7.2,100.0,7.2,21.9085,-2.4739
-c04_E100.0_D7.2,100.0,7.2,24.2476,-3.2494
-c04_E100.0_D7.2,100.0,7.2,26.6929,-4.3569
-c04_E100.0_D7.2,100.0,7.2,28.7413,-4.8648
-c04_E100.0_D7.2,100.0,7.2,29.7902,-4.9100
-c04_E100.0_D7.2,100.0,7.2,32.3615,-5.7591
-c04_E100.0_D7.2,100.0,7.2,34.1203,-5.9379
-c04_E100.0_D7.2,100.0,7.2,36.1499,-6.2444
-c04_E100.0_D7.2,100.0,7.2,38.0750,-6.2132
-c04_E100.0_D7.2,100.0,7.2,40.8215,-6.1973
-c04_E100.0_D7.2,100.0,7.2,42.4478,-6.1107
-c04_E100.0_D7.2,100.0,7.2,45.0075,-5.7157
-c04_E100.0_D7.2,100.0,7.2,47.0021,-5.3797
-c04_E100.0_D7.2,100.0,7.2,48.8998,-4.9656
-c04_E100.0_D7.2,100.0,7.2,50.0622,-4.4883
-c04_E100.0_D7.2,100.0,7.2,52.2926,-4.0425
-c04_E100.0_D7.2,100.0,7.2,54.6218,-3.5161
-c04_E100.0_D7.2,100.0,7.2,56.4809,-2.7234
-c04_E100.0_D7.2,100.0,7.2,58.6428,-2.2604
-c04_E100.0_D7.2,100.0,7.2,60.5366,-1.9409
-c04_E100.0_D7.2,100.0,7.2,62.6247,-1.4143
-c04_E100.0_D7.2,100.0,7.2,64.2818,-1.1871
-c04_E100.0_D7.2,100.0,7.2,66.1118,-0.9686
-c04_E100.0_D7.2,100.0,7.2,68.6686,-0.4825
-c04_E100.0_D7.2,100.0,7.2,71.2222,-0.4395
-c04_E100.0_D7.2,100.0,7.2,73.0322,-0.0635
-c04_E100.0_D7.2,100.0,7.2,74.7607,-0.2328
-c04_E100.0_D7.2,100.0,7.2,76.7730,0.0419
-c04_E100.0_D7.2,100.0,7.2,79.2526,0.0973
-c04_E100.0_D7.2,100.0,7.2,80.4707,0.1683
-c04_E100.0_D7.2,100.0,7.2,82.2234,0.1764
-c04_E100.0_D7.2,100.0,7.2,84.8758,0.2790
-c04_E100.0_D7.2,100.0,7.2,86.8805,0.3144
-c04_E100.0_D7.2,100.0,7.2,89.0318,0.1067
-c04_E100.0_D7.2,100.0,7.2,90.9481,0.1178
-c04_E100.0_D7.2,100.0,7.2,93.0559,0.0300
-c04_E100.0_D7.2,100.0,7.2,95.3851,-0.0217
-c04_E100.0_D7.2,100.0,7.2,97.2149,0.1433
-c04_E100.0_D7.2,100.0,7.2,98.6863,0.1613
-c04_E100.0_D7.2,100.0,7.2,101.5154,0.2272
-c04_E100.0_D7.2,100.0,7.2,102.6186,-0.0650
-c04_E100.0_D7.2,100.0,7.2,105.6404,0.2897
-c04_E100.0_D7.2,100.0,7.2,106.8122,0.1970
-c04_E100.0_D7.2,100.0,7.2,109.5707,0.0469
-c04_E100.0_D7.2,100.0,7.2,110.9667,0.1131
-c04_E100.0_D7.2,100.0,7.2,112.6621,0.0101
-c04_E100.0_D7.2,100.0,7.2,115.0860,0.0473
-c04_E100.0_D7.2,100.0,7.2,117.4438,0.1567
-c04_E100.0_D7.2,100.0,7.2,119.5991,0.0868
-c04_E100.0_D7.2,100.0,7.2,121.3490,-0.0435
-c04_E100.0_D7.2,100.0,7.2,122.7281,0.1031
-c04_E100.0_D7.2,100.0,7.2,125.0337,0.0235
-c04_E100.0_D7.2,100.0,7.2,127.5965,0.1301
-c04_E100.0_D7.2,100.0,7.2,129.6277,0.0511
-c04_E100.0_D7.2,100.0,7.2,131.3311,-0.0717
-c04_E100.0_D7.2,100.0,7.2,133.4264,0.0436
-c04_E100.0_D7.2,100.0,7.2,135.2503,0.0868
-c04_E100.0_D7.2,100.0,7.2,136.7921,-0.1330
-c04_E100.0_D7.2,100.0,7.2,139.3003,-0.0689
-c04_E100.0_D7.2,100.0,7.2,140.9201,0.1385
-c04_E100.0_D7.2,100.0,7.2,143.0944,-0.0040
-c04_E100.0_D7.2,100.0,7.2,145.1028,-0.0750
-c04_E100.0_D7.2,100.0,7.2,147.6848,-0.0727
-c04_E100.0_D7.2,100.0,7.2,149.3032,0.1967
-c04_E100.0_D7.2,100.0,7.2,151.8263,0.0968
-c04_E100.0_D7.2,100.0,7.2,153.4505,0.0670
-c04_E100.0_D7.2,100.0,7.2,155.9824,-0.0636
-c04_E100.0_D7.2,100.0,7.2,158.0644,-0.0974
-c04_E100.0_D7.2,100.0,7.2,160.0265,0.1024
-c04_E100.0_D7.2,100.0,7.2,161.9268,0.1240
-c04_E100.0_D7.2,100.0,7.2,163.2116,0.1273
-c04_E100.0_D7.2,100.0,7.2,165.4465,-0.1353
-c04_E100.0_D7.2,100.0,7.2,167.5512,-0.0552
-c04_E100.0_D7.2,100.0,7.2,169.5494,-0.0764
-c04_E100.0_D7.2,100.0,7.2,171.3821,0.0078
-c04_E100.0_D7.2,100.0,7.2,173.4138,0.0322
-c04_E100.0_D7.2,100.0,7.2,175.6476,0.0371
-c04_E100.0_D7.2,100.0,7.2,177.2745,0.0928
-c04_E100.0_D7.2,100.0,7.2,180.4007,0.0449
-c04_E100.0_D7.2,100.0,7.2,182.2713,0.2037
-c04_E100.0_D7.2,100.0,7.2,184.1879,0.0031
-c04_E100.0_D7.2,100.0,7.2,185.3144,-0.0111
-c04_E100.0_D7.2,100.0,7.2,188.1133,-0.0861
-c04_E100.0_D7.2,100.0,7.2,190.2174,0.1111
-c04_E100.0_D7.2,100.0,7.2,191.6680,-0.0791
-c04_E100.0_D7.2,100.0,7.2,194.4330,0.0615
-c04_E100.0_D7.2,100.0,7.2,196.4251,-0.0585
-c04_E100.0_D7.2,100.0,7.2,197.6926,0.0813
-c04_E100.0_D7.2,100.0,7.2,199.5201,0.0765
-c05_E100.0_D7.6,100.0,7.6,0.5282,3.0191
-c05_E100.0_D7.6,100.0,7.6,2.5544,2.6941
-c05_E100.0_D7.6,100.0,7.6,4.2444,2.4933
-c05_E100.0_D7.6,100.0,7.6,6.5881,1.9268
-c05_E100.0_D7.6,100.0,7.6,8.0301,1.3990
-c05_E100.0_D7.6,100.0,7.6,10.5869,0.9562
-c05_E100.0_D7.6,100.0,7.6,12.2116,0.4648
-c05_E100.0_D7.6,100.0,7.6,13.8945,0.1068
-c05_E100.0_D7.6,100.0,7.6,16.2909,-0.8396
-c05_E100.0_D7.6,100.0,7.6,18.7236,-1.3773
-c05_E100.0_D7.6,100.0,7.6,19.9771,-1.8236
-c05_E100.0_D7.6,100.0,7.6,21.7156,-2.6475
-c05_E100.0_D7.6,100.0,7.6,24.4024,-3.5312
-c05_E100.0_D7.6,100.0,7.6,26.6755,-4.1000
-c05_E100.0_D7.6,100.0,7.6,27.9498,-4.7352
-c05_E100.0_D7.6,100.0,7.6,30.2580,-5.2819
-c05_E100.0_D7.6,100.0,7.6,32.8617,-5.9020
-c05_E100.0_D7.6,100.0,7.6,34.4572,-5.9773
-c05_E100.0_D7.6,100.0,7.6,36.3521,-6.2403
-c05_E100.0_D7.6,100.0,7.6,37.9540,-6.4013
-c05_E100.0_D7.6,100.0,7.6,40.7573,-6.5768
-c05_E100.0_D7.6,100.0,7.6,42.8785,-6.2556
-c05_E100.0_D7.6,100.0,7.6,44.2690,-5.9118
-c05_E100.0_D7.6,100.0,7.6,46.3147,-5.4955
-c05_E100.0_D7.6,100.0,7.6,48.6475,-4.9896
-c05_E100.0_D7.6,100.0,7.6,49.9727,-4.7209
-c05_E100.0_D7.6,100.0,7.6,53.1153,-3.6765
-c05_E100.0_D7.6,100.0,7.6,55.0983,-3.4777
-c05_E100.0_D7.6,100.0,7.6,56.2446,-2.8653
-c05_E100.0_D7.6,100.0,7.6,58.2764,-2.1692
-c05_E100.0_D7.6,100.0,7.6,60.3870,-1.9432
-c05_E100.0_D7.6,100.0,7.6,62.6188,-1.4445
-c05_E100.0_D7.6,100.0,7.6,65.0629,-1.0008
-c05_E100.0_D7.6,100.0,7.6,66.6550,-0.7033
-c05_E100.0_D7.6,100.0,7.6,69.2275,-0.4958
-c05_E100.0_D7.6,100.0,7.6,70.5261,-0.3141
-c05_E100.0_D7.6,100.0,7.6,73.0327,-0.1644
-c05_E100.0_D7.6,100.0,7.6,75.2636,-0.0823
-c05_E100.0_D7.6,100.0,7.6,76.7814,-0.0188
-c05_E100.0_D7.6,100.0,7.6,78.7373,-0.1174
-c05_E100.0_D7.6,100.0,7.6,80.2707,-0.0413
-c05_E100.0_D7.6,100.0,7.6,83.0630,-0.0840
-c05_E100.0_D7.6,100.0,7.6,85.2551,0.0618
-c05_E100.0_D7.6,100.0,7.6,86.8136,0.0740
-c05_E100.0_D7.6,100.0,7.6,88.3250,0.0575
-c05_E100.0_D7.6,100.0,7.6,91.2511,0.2711
-c05_E100.0_D7.6,100.0,7.6,92.8553,0.2289
-c05_E100.0_D7.6,100.0,7.6,95.2582,0.1762
-c05_E100.0_D7.6,100.0,7.6,96.5564,0.2583
-c05_E100.0_D7.6,100.0,7.6,99.1842,0.3062
-c05_E100.0_D7.6,100.0,7.6,101.4116,0.0530
-c05_E100.0_D7.6,100.0,7.6,103.0751,0.0687
-c05_E100.0_D7.6,100.0,7.6,105.5690,0.1640
-c05_E100.0_D7.6,100.0,7.6,106.8667,0.1754
-c05_E100.0_D7.6,100.0,7.6,109.6537,0.2057
-c05_E100.0_D7.6,100.0,7.6,111.2269,0.0605
-c05_E100.0_D7.6,100.0,7.6,112.8969,0.2083
-c05_E100.0_D7.6,100.0,7.6,114.6337,0.0772
-c05_E100.0_D7.6,100.0,7.6,117.0422,0.0003
-c05_E100.0_D7.6,100.0,7.6,118.9486,0.0902
-c05_E100.0_D7.6,100.0,7.6,121.3735,-0.1376
-c05_E100.0_D7.6,100.0,7.6,122.7580,-0.0205
-c05_E100.0_D7.6,100.0,7.6,124.6702,0.0147
-c05_E100.0_D7.6,100.0,7.6,127.1642,0.0358
-c05_E100.0_D7.6,100.0,7.6,129.8811,0.0297
-c05_E100.0_D7.6,100.0,7.6,131.6716,0.1045
-c05_E100.0_D7.6,100.0,7.6,132.7475,0.0431
-c05_E100.0_D7.6,100.0,7.6,135.9431,0.0460
-c05_E100.0_D7.6,100.0,7.6,137.3991,-0.0289
-c05_E100.0_D7.6,100.0,7.6,139.2191,0.1610
-c05_E100.0_D7.6,100.0,7.6,140.9375,0.1172
-c05_E100.0_D7.6,100.0,7.6,143.2362,-0.0264
-c05_E100.0_D7.6,100.0,7.6,145.1451,0.2309
-c05_E100.0_D7.6,100.0,7.6,147.3923,-0.1019
-c05_E100.0_D7.6,100.0,7.6,149.5103,-0.0067
-c05_E100.0_D7.6,100.0,7.6,151.4113,0.1750
-c05_E100.0_D7.6,100.0,7.6,153.5003,0.0390
-c05_E100.0_D7.6,100.0,7.6,156.0086,-0.0288
-c05_E100.0_D7.6,100.0,7.6,157.7825,0.0989
-c05_E100.0_D7.6,100.0,7.6,159.7037,0.1351
-c05_E100.0_D7.6,100.0,7.6,161.5030,-0.1006
-c05_E100.0_D7.6,100.0,7.6,163.1642,0.0131
-c05_E100.0_D7.6,100.0,7.6,165.7744,-0.2141
-c05_E100.0_D7.6,100.0,7.6,168.2737,-0.0311
-c05_E100.0_D7.6,100.0,7.6,170.0739,-0.0857
-c05_E100.0_D7.6,100.0,7.6,171.9381,0.0530
-c05_E100.0_D7.6,100.0,7.6,173.1878,0.1259
-c05_E100.0_D7.6,100.0,7.6,175.1662,-0.0360
-c05_E100.0_D7.6,100.0,7.6,177.7991,-0.0231
-c05_E100.0_D7.6,100.0,7.6,179.9597,-0.0731
-c05_E100.0_D7.6,100.0,7.6,182.2477,-0.0406
-c05_E100.0_D7.6,100.0,7.6,183.7232,-0.2070
-c05_E100.0_D7.6,100.0,7.6,186.1712,0.1043
-c05_E100.0_D7.6,100.0,7.6,187.8106,0.1485
-c05_E100.0_D7.6,100.0,7.6,190.0266,-0.0471
-c05_E100.0_D7.6,100.0,7.6,192.2730,-0.1719
-c05_E100.0_D7.6,100.0,7.6,193.8690,0.1097
-c05_E100.0_D7.6,100.0,7.6,196.2713,-0.0026
-c05_E100.0_D7.6,100.0,7.6,198.4933,-0.0144
-c05_E100.0_D7.6,100.0,7.6,200.0000,-0.1006
-c06_E100.0_D8.0,100.0,8.0,0.0000,2.9488
-c06_E100.0_D8.0,100.0,8.0,1.8099,2.9165
-c06_E100.0_D8.0,100.0,8.0,4.2544,2.2018
-c06_E100.0_D8.0,100.0,8.0,6.0304,1.9702
-c06_E100.0_D8.0,100.0,8.0,7.7823,1.6996
-c06_E100.0_D8.0,100.0,8.0,9.8621,1.0776
-c06_E100.0_D8.0,100.0,8.0,12.1292,0.4397
-c06_E100.0_D8.0,100.0,8.0,14.1767,-0.0533
-c06_E100.0_D8.0,100.0,8.0,16.2581,-0.8120
-c06_E100.0_D8.0,100.0,8.0,18.4145,-1.4362
-c06_E100.0_D8.0,100.0,8.0,20.2086,-2.0983
-c06_E100.0_D8.0,100.0,8.0,22.3894,-2.9743
-c06_E100.0_D8.0,100.0,8.0,24.5419,-3.6576
-c06_E100.0_D8.0,100.0,8.0,26.2507,-4.2106
-c06_E100.0_D8.0,100.0,8.0,28.5521,-4.7720
-c06_E100.0_D8.0,100.0,8.0,30.6726,-5.3431
-c06_E100.0_D8.0,100.0,8.0,32.6166,-6.1191
-c06_E100.0_D8.0,100.0,8.0,33.9578,-6.1657
-c06_E100.0_D8.0,100.0,8.0,36.7002,-6.5390
-c06_E100.0_D8.0,100.0,8.0,38.5952,-6.4619
-c06_E100.0_D8.0,100.0,8.0,40.9984,-6.4866
-c06_E100.0_D8.0,100.0,8.0,42.4336,-6.5247
-c06_E100.0_D8.0,100.0,8.0,44.1866,-6.0729
-c06_E100.0_D8.0,100.0,8.0,46.1613,-5.6507
-c06_E100.0_D8.0,100.0,8.0,48.8584,-5.0239
-c06_E100.0_D8.0,100.0,8.0,50.6071,-4.6540
-c06_E100.0_D8.0,100.0,8.0,52.1902,-4.2461
-c06_E100.0_D8.0,100.0,8.0,54.5616,-3.5807
-c06_E100.0_D8.0,100.0,8.0,56.6536,-2.8176
-c06_E100.0_D8.0,100.0,8.0,58.6150,-2.2993
-c06_E100.0_D8.0,100.0,8.0,60.0459,-2.0089
-c06_E100.0_D8.0,100.0,8.0,63.1541,-1.3992
-c06_E100.0_D8.0,100.0,8.0,65.1274,-0.9754
-c06_E100.0_D8.0,100.0,8.0,67.1270,-0.6824
-c06_E100.0_D8.0,100.0,8.0,68.8759,-0.6079
-c06_E100.0_D8.0,100.0,8.0,70.1799,-0.5678
-c06_E100.0_D8.0,100.0,8.0,73.0747,-0.2222
-c06_E100.0_D8.0,100.0,8.0,75.3111,0.0374
-c06_E100.0_D8.0,100.0,8.0,76.8396,0.0832
-c06_E100.0_D8.0,100.0,8.0,78.3355,0.1171
-c06_E100.0_D8.0,100.0,8.0,81.2795,0.1538
-c06_E100.0_D8.0,100.0,8.0,83.1941,0.2270
-c06_E100.0_D8.0,100.0,8.0,84.8135,0.1341
-c06_E100.0_D8.0,100.0,8.0,86.6995,0.0448
-c06_E100.0_D8.0,100.0,8.0,88.3238,0.0494
-c06_E100.0_D8.0,100.0,8.0,91.3748,0.1865
-c06_E100.0_D8.0,100.0,8.0,93.1564,0.2342
-c06_E100.0_D8.0,100.0,8.0,95.1018,0.1860
-c06_E100.0_D8.0,100.0,8.0,96.6334,0.1100
-c06_E100.0_D8.0,100.0,8.0,99.0201,-0.0420
-c06_E100.0_D8.0,100.0,8.0,101.2464,0.1309
-c06_E100.0_D8.0,100.0,8.0,102.7282,-0.0316
-c06_E100.0_D8.0,100.0,8.0,104.5652,0.0736
-c06_E100.0_D8.0,100.0,8.0,107.2543,0.0532
-c06_E100.0_D8.0,100.0,8.0,108.8494,0.1389
-c06_E100.0_D8.0,100.0,8.0,111.4756,0.1504
-c06_E100.0_D8.0,100.0,8.0,113.0303,0.2383
-c06_E100.0_D8.0,100.0,8.0,115.5732,0.1274
-c06_E100.0_D8.0,100.0,8.0,116.9824,0.1222
-c06_E100.0_D8.0,100.0,8.0,119.6411,0.2134
-c06_E100.0_D8.0,100.0,8.0,121.2633,0.0352
-c06_E100.0_D8.0,100.0,8.0,123.1405,0.0533
-c06_E100.0_D8.0,100.0,8.0,124.9123,0.1224
-c06_E100.0_D8.0,100.0,8.0,126.9371,0.1477
-c06_E100.0_D8.0,100.0,8.0,129.5083,0.0488
-c06_E100.0_D8.0,100.0,8.0,130.9911,0.1669
-c06_E100.0_D8.0,100.0,8.0,133.8958,0.0019
-c06_E100.0_D8.0,100.0,8.0,135.5418,-0.1035
-c06_E100.0_D8.0,100.0,8.0,137.5487,-0.1791
-c06_E100.0_D8.0,100.0,8.0,139.4567,-0.0637
-c06_E100.0_D8.0,100.0,8.0,141.3594,0.0376
-c06_E100.0_D8.0,100.0,8.0,143.3586,0.0433
-c06_E100.0_D8.0,100.0,8.0,145.7542,0.0461
-c06_E100.0_D8.0,100.0,8.0,147.9356,-0.1826
-c06_E100.0_D8.0,100.0,8.0,149.7167,-0.0142
-c06_E100.0_D8.0,100.0,8.0,152.0438,0.0245
-c06_E100.0_D8.0,100.0,8.0,153.1583,0.0432
-c06_E100.0_D8.0,100.0,8.0,155.0899,-0.0641
-c06_E100.0_D8.0,100.0,8.0,157.4482,-0.0052
-c06_E100.0_D8.0,100.0,8.0,160.0210,-0.0114
-c06_E100.0_D8.0,100.0,8.0,161.8478,-0.0072
-c06_E100.0_D8.0,100.0,8.0,163.1236,0.1318
-c06_E100.0_D8.0,100.0,8.0,165.1798,-0.0208
-c06_E100.0_D8.0,100.0,8.0,168.1159,0.0767
-c06_E100.0_D8.0,100.0,8.0,169.7212,0.2022
-c06_E100.0_D8.0,100.0,8.0,171.4958,0.0064
-c06_E100.0_D8.0,100.0,8.0,173.2776,0.0796
-c06_E100.0_D8.0,100.0,8.0,176.1913,0.1021
-c06_E100.0_D8.0,100.0,8.0,178.1840,-0.1045
-c06_E100.0_D8.0,100.0,8.0,180.0754,0.0326
-c06_E100.0_D8.0,100.0,8.0,182.1131,-0.0840
-c06_E100.0_D8.0,100.0,8.0,183.2615,0.1460
-c06_E100.0_D8.0,100.0,8.0,185.3687,-0.0152
-c06_E100.0_D8.0,100.0,8.0,188.2350,-0.0488
-c06_E100.0_D8.0,100.0,8.0,189.5427,-0.0678
-c06_E100.0_D8.0,100.0,8.0,191.9836,-0.0856
-c06_E100.0_D8.0,100.0,8.0,193.5640,-0.0119
-c06_E100.0_D8.0,100.0,8.0,196.4955,-0.0464
-c06_E100.0_D8.0,100.0,8.0,198.1883,-0.1151
-c06_E100.0_D8.0,100.0,8.0,200.0000,0.0399
-c07_E100.0_D8.4,100.0,8.4,0.0000,3.1311
-c07_E100.0_D8.4,100.0,8.4,2.1184,2.9177
-c07_E100.0_D8.4,100.0,8.4,3.9621,2.6583
-c07_E100.0_D8.4,100.0,8.4,6.6343,1.8648
-c07_E100.0_D8.4,100.0,8.4,8.0007,1.6529
-c07_E100.0_D8.4,100.0,8.4,10.2990,0.9394
-c07_E100.0_D8.4,100.0,8.4,12.3128,0.4925
-c07_E100.0_D8.4,100.0,8.4,14.2658,0.0025
-c07_E100.0_D8.4,100.0,8.4,15.6662,-0.6199
-c07_E100.0_D8.4,100.0,8.4,17.7569,-1.1666
-c07_E100.0_D8.4,100.0,8.4,19.7583,-1.9523
-c07_E100.0_D8.4,100.0,8.4,22.3981,-3.0687
-c07_E100.0_D8.4,100.0,8.4,23.8910,-3.4585
-c07_E100.0_D8.4,100.0,8.4,26.4489,-4.4399
-c07_E100.0_D8.4,100.0,8.4,28.0359,-4.7790
-c07_E100.0_D8.4,100.0,8.4,30.0328,-5.3083
-c07_E100.0_D8.4,100.0,8.4,31.8664,-6.0435
-c07_E100.0_D8.4,100.0,8.4,34.1108,-6.4883
-c07_E100.0_D8.4,100.0,8.4,36.5833,-6.5050
-c07_E100.0_D8.4,100.0,8.4,38.7969,-6.6429
-c07_E100.0_D8.4,100.0,8.4,40.9592,-6.5633
-c07_E100.0_D8.4,100.0,8.4,43.0009,-6.3440
-c07_E100.0_D8.4,100.0,8.4,44.5321,-6.1960
-c07_E100.0_D8.4,100.0,8.4,46.9621,-5.5573
-c07_E100.0_D8.4,100.0,8.4,48.2857,-5.2474
-c07_E100.0_D8.4,100.0,8.4,50.7139,-4.6376
-c07_E100.0_D8.4,100.0,8.4,53.0406,-3.9575
-c07_E100.0_D8.4,100.0,8.4,53.9941,-3.7356
-c07_E100.0_D8.4,100.0,8.4,56.3981,-2.9854
-c07_E100.0_D8.4,100.0,8.4,58.2598,-2.4835
-c07_E100.0_D8.4,100.0,8.4,61.2077,-1.6926
-c07_E100.0_D8.4,100.0,8.4,62.6809,-1.5993
-c07_E100.0_D8.4,100.0,8.4,64.7610,-1.1501
-c07_E100.0_D8.4,100.0,8.4,66.9902,-0.8081
-c07_E100.0_D8.4,100.0,8.4,68.8185,-0.7151
-c07_E100.0_D8.4,100.0,8.4,71.2047,-0.2880
-c07_E100.0_D8.4,100.0,8.4,72.6673,-0.3340
-c07_E100.0_D8.4,100.0,8.4,75.0126,-0.1306
-c07_E100.0_D8.4,100.0,8.4,76.8083,0.1396
-c07_E100.0_D8.4,100.0,8.4,78.1925,0.1250
-c07_E100.0_D8.4,100.0,8.4,80.8546,0.1415
-c07_E100.0_D8.4,100.0,8.4,83.1004,0.2204
-c07_E100.0_D8.4,100.0,8.4,84.3194,0.2731
-c07_E100.0_D8.4,100.0,8.4,86.9616,0.1990
-c07_E100.0_D8.4,100.0,8.4,89.3709,0.1644
-c07_E100.0_D8.4,100.0,8.4,91.1887,0.1369
-c07_E100.0_D8.4,100.0,8.4,93.5064,0.1308
-c07_E100.0_D8.4,100.0,8.4,95.3547,0.2003
-c07_E100.0_D8.4,100.0,8.4,97.1535,0.1694
-c07_E100.0_D8.4,100.0,8.4,98.6874,0.0937
-c07_E100.0_D8.4,100.0,8.4,100.6495,0.1735
-c07_E100.0_D8.4,100.0,8.4,102.5874,-0.0119
-c07_E100.0_D8.4,100.0,8.4,104.8776,0.2417
-c07_E100.0_D8.4,100.0,8.4,107.4603,0.0858
-c07_E100.0_D8.4,100.0,8.4,109.6715,-0.0072
-c07_E100.0_D8.4,100.0,8.4,111.5262,0.2199
-c07_E100.0_D8.4,100.0,8.4,113.4454,-0.0289
-c07_E100.0_D8.4,100.0,8.4,115.4708,0.1437
-c07_E100.0_D8.4,100.0,8.4,116.7567,0.1818
-c07_E100.0_D8.4,100.0,8.4,119.7597,0.0889
-c07_E100.0_D8.4,100.0,8.4,121.1928,-0.0686
-c07_E100.0_D8.4,100.0,8.4,122.9414,0.0545
-c07_E100.0_D8.4,100.0,8.4,125.5586,0.1023
-c07_E100.0_D8.4,100.0,8.4,127.7092,0.0663
-c07_E100.0_D8.4,100.0,8.4,129.2580,0.0760
-c07_E100.0_D8.4,100.0,8.4,131.5665,0.1921
-c07_E100.0_D8.4,100.0,8.4,132.8514,-0.0403
-c07_E100.0_D8.4,100.0,8.4,135.8039,0.1100
-c07_E100.0_D8.4,100.0,8.4,136.9805,0.0838
-c07_E100.0_D8.4,100.0,8.4,139.0835,0.0429
-c07_E100.0_D8.4,100.0,8.4,141.0562,0.2206
-c07_E100.0_D8.4,100.0,8.4,142.9375,0.0893
-c07_E100.0_D8.4,100.0,8.4,145.8346,0.1052
-c07_E100.0_D8.4,100.0,8.4,147.4472,-0.0172
-c07_E100.0_D8.4,100.0,8.4,149.7416,0.0349
-c07_E100.0_D8.4,100.0,8.4,151.3389,0.1548
-c07_E100.0_D8.4,100.0,8.4,153.6176,-0.0709
-c07_E100.0_D8.4,100.0,8.4,155.9413,0.0548
-c07_E100.0_D8.4,100.0,8.4,157.0720,-0.0231
-c07_E100.0_D8.4,100.0,8.4,160.1591,-0.1161
-c07_E100.0_D8.4,100.0,8.4,162.1689,0.0295
-c07_E100.0_D8.4,100.0,8.4,163.4009,-0.0417
-c07_E100.0_D8.4,100.0,8.4,166.2058,0.0741
-c07_E100.0_D8.4,100.0,8.4,168.1100,-0.0083
-c07_E100.0_D8.4,100.0,8.4,170.1093,0.0317
-c07_E100.0_D8.4,100.0,8.4,171.8574,-0.0230
-c07_E100.0_D8.4,100.0,8.4,173.3458,-0.0152
-c07_E100.0_D8.4,100.0,8.4,175.8542,0.0105
-c07_E100.0_D8.4,100.0,8.4,177.1855,-0.0773
-c07_E100.0_D8.4,100.0,8.4,180.0509,0.2005
-c07_E100.0_D8.4,100.0,8.4,181.8715,0.0285
-c07_E100.0_D8.4,100.0,8.4,184.2935,-0.1854
-c07_E100.0_D8.4,100.0,8.4,185.5655,0.0005
-c07_E100.0_D8.4,100.0,8.4,188.2149,-0.0475
-c07_E100.0_D8.4,100.0,8.4,190.1635,-0.0474
-c07_E100.0_D8.4,100.0,8.4,192.3058,0.2319
-c07_E100.0_D8.4,100.0,8.4,194.4280,0.0650
-c07_E100.0_D8.4,100.0,8.4,195.8627,0.0393
-c07_E100.0_D8.4,100.0,8.4,198.0911,0.1255
-c07_E100.0_D8.4,100.0,8.4,200.0000,0.1321
-c08_E100.0_D8.8,100.0,8.8,0.0000,3.3598
-c08_E100.0_D8.8,100.0,8.8,2.5931,2.6924
-c08_E100.0_D8.8,100.0,8.8,3.4345,2.3107
-c08_E100.0_D8.8,100.0,8.8,5.6517,1.7616
-c08_E100.0_D8.8,100.0,8.8,8.6546,1.4256
-c08_E100.0_D8.8,100.0,8.8,10.0226,1.0573
-c08_E100.0_D8.8,100.0,8.8,11.7950,0.6397
-c08_E100.0_D8.8,100.0,8.8,14.0987,-0.0277
-c08_E100.0_D8.8,100.0,8.8,16.7216,-1.0205
-c08_E100.0_D8.8,100.0,8.8,18.6639,-1.3549
-c08_E100.0_D8.8,100.0,8.8,20.3133,-2.2431
-c08_E100.0_D8.8,100.0,8.8,22.4967,-3.0993
-c08_E100.0_D8.8,100.0,8.8,24.3457,-3.7655
-c08_E100.0_D8.8,100.0,8.8,25.9720,-4.2778
-c08_E100.0_D8.8,100.0,8.8,27.9901,-5.0335
-c08_E100.0_D8.8,100.0,8.8,29.9810,-5.7285
-c08_E100.0_D8.8,100.0,8.8,31.8956,-6.1552
-c08_E100.0_D8.8,100.0,8.8,34.7097,-6.4089
-c08_E100.0_D8.8,100.0,8.8,35.9150,-6.6364
-c08_E100.0_D8.8,100.0,8.8,37.9043,-6.7165
-c08_E100.0_D8.8,100.0,8.8,40.7766,-6.6299
-c08_E100.0_D8.8,100.0,8.8,41.9353,-6.5546
-c08_E100.0_D8.8,100.0,8.8,43.9209,-6.2871
-c08_E100.0_D8.8,100.0,8.8,47.0665,-5.5389
-c08_E100.0_D8.8,100.0,8.8,48.3270,-5.3817
-c08_E100.0_D8.8,100.0,8.8,49.9680,-4.9348
-c08_E100.0_D8.8,100.0,8.8,52.1507,-4.3784
-c08_E100.0_D8.8,100.0,8.8,54.2874,-3.6920
-c08_E100.0_D8.8,100.0,8.8,56.7166,-2.9732
-c08_E100.0_D8.8,100.0,8.8,58.5672,-2.2932
-c08_E100.0_D8.8,100.0,8.8,61.1119,-1.7699
-c08_E100.0_D8.8,100.0,8.8,62.0804,-1.4965
-c08_E100.0_D8.8,100.0,8.8,64.7571,-1.2555
-c08_E100.0_D8.8,100.0,8.8,67.0782,-0.7776
-c08_E100.0_D8.8,100.0,8.8,68.9919,-0.3579
-c08_E100.0_D8.8,100.0,8.8,70.8651,-0.3937
-c08_E100.0_D8.8,100.0,8.8,72.2572,-0.2601
-c08_E100.0_D8.8,100.0,8.8,74.3278,0.1154
-c08_E100.0_D8.8,100.0,8.8,76.8906,-0.0892
-c08_E100.0_D8.8,100.0,8.8,78.3966,-0.1351
-c08_E100.0_D8.8,100.0,8.8,81.0489,0.0113
-c08_E100.0_D8.8,100.0,8.8,82.8037,0.1893
-c08_E100.0_D8.8,100.0,8.8,84.7714,0.1051
-c08_E100.0_D8.8,100.0,8.8,87.3686,0.2742
-c08_E100.0_D8.8,100.0,8.8,88.4112,0.1418
-c08_E100.0_D8.8,100.0,8.8,90.3736,0.0437
-c08_E100.0_D8.8,100.0,8.8,93.4664,0.2153
-c08_E100.0_D8.8,100.0,8.8,94.8386,0.2561
-c08_E100.0_D8.8,100.0,8.8,97.3083,0.2348
-c08_E100.0_D8.8,100.0,8.8,98.8179,0.1351
-c08_E100.0_D8.8,100.0,8.8,100.6931,0.0160
-c08_E100.0_D8.8,100.0,8.8,102.8192,0.1405
-c08_E100.0_D8.8,100.0,8.8,105.5751,0.2824
-c08_E100.0_D8.8,100.0,8.8,107.5720,0.0212
-c08_E100.0_D8.8,100.0,8.8,109.6495,0.1379
-c08_E100.0_D8.8,100.0,8.8,110.6390,0.0641
-c08_E100.0_D8.8,100.0,8.8,113.0552,-0.0419
-c08_E100.0_D8.8,100.0,8.8,115.3313,0.2127
-c08_E100.0_D8.8,100.0,8.8,117.4948,-0.1671
-c08_E100.0_D8.8,100.0,8.8,119.7588,0.0738
-c08_E100.0_D8.8,100.0,8.8,121.4988,0.0422
-c08_E100.0_D8.8,100.0,8.8,123.7743,0.0775
-c08_E100.0_D8.8,100.0,8.8,124.8556,-0.0347
-c08_E100.0_D8.8,100.0,8.8,127.4473,0.2526
-c08_E100.0_D8.8,100.0,8.8,129.8706,0.1348
-c08_E100.0_D8.8,100.0,8.8,131.7033,0.0524
-c08_E100.0_D8.8,100.0,8.8,132.8531,0.0271
-c08_E100.0_D8.8,100.0,8.8,135.1791,0.0320
-c08_E100.0_D8.8,100.0,8.8,136.9533,-0.0099
-c08_E100.0_D8.8,100.0,8.8,139.6237,0.1787
-c08_E100.0_D8.8,100.0,8.8,141.6592,0.0297
-c08_E100.0_D8.8,100.0,8.8,143.8781,-0.1024
-c08_E100.0_D8.8,100.0,8.8,145.6556,0.0710
-c08_E100.0_D8.8,100.0,8.8,147.0057,-0.0096
-c08_E100.0_D8.8,100.0,8.8,149.4159,-0.1023
-c08_E100.0_D8.8,100.0,8.8,151.1790,-0.2227
-c08_E100.0_D8.8,100.0,8.8,153.9016,0.1511
-c08_E100.0_D8.8,100.0,8.8,155.6930,0.0550
-c08_E100.0_D8.8,100.0,8.8,157.7106,0.0690
-c08_E100.0_D8.8,100.0,8.8,159.4988,0.1498
-c08_E100.0_D8.8,100.0,8.8,161.0351,0.1922
-c08_E100.0_D8.8,100.0,8.8,163.9543,-0.0436
-c08_E100.0_D8.8,100.0,8.8,165.7026,0.2576
-c08_E100.0_D8.8,100.0,8.8,167.2456,-0.0566
-c08_E100.0_D8.8,100.0,8.8,169.3036,0.0755
-c08_E100.0_D8.8,100.0,8.8,171.3174,0.0755
-c08_E100.0_D8.8,100.0,8.8,173.7052,-0.0881
-c08_E100.0_D8.8,100.0,8.8,176.0844,-0.1011
-c08_E100.0_D8.8,100.0,8.8,177.3866,-0.0717
-c08_E100.0_D8.8,100.0,8.8,180.0159,0.1004
-c08_E100.0_D8.8,100.0,8.8,182.0587,0.0351
-c08_E100.0_D8.8,100.0,8.8,183.5576,0.0688
-c08_E100.0_D8.8,100.0,8.8,185.7764,0.0872
-c08_E100.0_D8.8,100.0,8.8,188.1288,-0.0353
-c08_E100.0_D8.8,100.0,8.8,189.8572,0.0604
-c08_E100.0_D8.8,100.0,8.8,192.4810,-0.1817
-c08_E100.0_D8.8,100.0,8.8,194.2438,0.0723
-c08_E100.0_D8.8,100.0,8.8,195.6169,0.0851
-c08_E100.0_D8.8,100.0,8.8,197.8546,-0.0001
-c08_E100.0_D8.8,100.0,8.8,199.4745,0.0486
-c09_E100.0_D9.2,100.0,9.2,0.2216,3.0493
-c09_E100.0_D9.2,100.0,9.2,1.4867,2.8419
-c09_E100.0_D9.2,100.0,9.2,3.6924,2.4259
-c09_E100.0_D9.2,100.0,9.2,5.6347,2.2512
-c09_E100.0_D9.2,100.0,9.2,8.2594,1.5746
-c09_E100.0_D9.2,100.0,9.2,9.9680,1.0666
-c09_E100.0_D9.2,100.0,9.2,12.0038,0.5307
-c09_E100.0_D9.2,100.0,9.2,14.0889,-0.2514
-c09_E100.0_D9.2,100.0,9.2,15.7275,-0.6987
-c09_E100.0_D9.2,100.0,9.2,18.2716,-1.3711
-c09_E100.0_D9.2,100.0,9.2,19.8820,-2.0352
-c09_E100.0_D9.2,100.0,9.2,22.2691,-2.9495
-c09_E100.0_D9.2,100.0,9.2,24.0668,-3.7048
-c09_E100.0_D9.2,100.0,9.2,26.4460,-4.6439
-c09_E100.0_D9.2,100.0,9.2,28.3927,-4.9571
-c09_E100.0_D9.2,100.0,9.2,30.8225,-5.7871
-c09_E100.0_D9.2,100.0,9.2,32.0544,-5.9083
-c09_E100.0_D9.2,100.0,9.2,34.2432,-6.4877
-c09_E100.0_D9.2,100.0,9.2,36.7720,-6.7385
-c09_E100.0_D9.2,100.0,9.2,38.2233,-6.9005
-c09_E100.0_D9.2,100.0,9.2,40.8901,-6.7579
-c09_E100.0_D9.2,100.0,9.2,42.3452,-6.5824
-c09_E100.0_D9.2,100.0,9.2,45.0430,-6.1542
-c09_E100.0_D9.2,100.0,9.2,47.0491,-5.6911
-c09_E100.0_D9.2,100.0,9.2,48.5524,-5.3019
-c09_E100.0_D9.2,100.0,9.2,50.5631,-4.7430
-c09_E100.0_D9.2,100.0,9.2,52.9648,-4.2066
-c09_E100.0_D9.2,100.0,9.2,54.9162,-3.4052
-c09_E100.0_D9.2,100.0,9.2,56.4130,-3.1333
-c09_E100.0_D9.2,100.0,9.2,59.0898,-2.5848
-c09_E100.0_D9.2,100.0,9.2,60.7316,-1.9683
-c09_E100.0_D9.2,100.0,9.2,62.8578,-1.6676
-c09_E100.0_D9.2,100.0,9.2,64.8799,-1.0750
-c09_E100.0_D9.2,100.0,9.2,66.8290,-0.8905
-c09_E100.0_D9.2,100.0,9.2,68.3591,-0.7042
-c09_E100.0_D9.2,100.0,9.2,70.7431,-0.2719
-c09_E100.0_D9.2,100.0,9.2,73.1938,-0.1267
-c09_E100.0_D9.2,100.0,9.2,74.4881,-0.1439
-c09_E100.0_D9.2,100.0,9.2,76.1762,0.0285
-c09_E100.0_D9.2,100.0,9.2,78.7074,0.1799
-c09_E100.0_D9.2,100.0,9.2,80.8819,-0.0305
-c09_E100.0_D9.2,100.0,9.2,82.6566,0.3043
-c09_E100.0_D9.2,100.0,9.2,84.8903,0.1115
-c09_E100.0_D9.2,100.0,9.2,86.4340,0.1777
-c09_E100.0_D9.2,100.0,9.2,88.5440,0.0869
-c09_E100.0_D9.2,100.0,9.2,90.7388,0.1514
-c09_E100.0_D9.2,100.0,9.2,93.0665,0.2731
-c09_E100.0_D9.2,100.0,9.2,94.8379,0.0669
-c09_E100.0_D9.2,100.0,9.2,97.3236,0.1209
-c09_E100.0_D9.2,100.0,9.2,99.4834,0.1793
-c09_E100.0_D9.2,100.0,9.2,100.6732,0.1163
-c09_E100.0_D9.2,100.0,9.2,103.0595,0.0459
-c09_E100.0_D9.2,100.0,9.2,104.9152,0.2775
-c09_E100.0_D9.2,100.0,9.2,107.3725,0.0482
-c09_E100.0_D9.2,100.0,9.2,109.4895,0.1511
-c09_E100.0_D9.2,100.0,9.2,110.8624,0.1797
-c09_E100.0_D9.2,100.0,9.2,113.3611,0.0562
-c09_E100.0_D9.2,100.0,9.2,114.8912,0.0968
-c09_E100.0_D9.2,100.0,9.2,116.7570,0.2117
-c09_E100.0_D9.2,100.0,9.2,119.5135,0.1584
-c09_E100.0_D9.2,100.0,9.2,121.2299,-0.2083
-c09_E100.0_D9.2,100.0,9.2,123.2836,0.1681
-c09_E100.0_D9.2,100.0,9.2,124.8282,-0.1070
-c09_E100.0_D9.2,100.0,9.2,127.7420,0.1109
-c09_E100.0_D9.2,100.0,9.2,128.7834,0.2448
-c09_E100.0_D9.2,100.0,9.2,130.8140,0.1466
-c09_E100.0_D9.2,100.0,9.2,133.4898,0.1813
-c09_E100.0_D9.2,100.0,9.2,135.5622,0.0726
-c09_E100.0_D9.2,100.0,9.2,136.7881,-0.0291
-c09_E100.0_D9.2,100.0,9.2,139.1157,0.0807
-c09_E100.0_D9.2,100.0,9.2,141.1933,0.1668
-c09_E100.0_D9.2,100.0,9.2,143.5755,0.2511
-c09_E100.0_D9.2,100.0,9.2,145.2671,0.1000
-c09_E100.0_D9.2,100.0,9.2,147.0181,-0.0096
-c09_E100.0_D9.2,100.0,9.2,149.3267,0.0316
-c09_E100.0_D9.2,100.0,9.2,151.6387,-0.0284
-c09_E100.0_D9.2,100.0,9.2,153.1824,0.0775
-c09_E100.0_D9.2,100.0,9.2,155.1512,-0.1882
-c09_E100.0_D9.2,100.0,9.2,157.3714,-0.0767
-c09_E100.0_D9.2,100.0,9.2,159.1483,0.0199
-c09_E100.0_D9.2,100.0,9.2,161.8259,-0.0681
-c09_E100.0_D9.2,100.0,9.2,163.4145,-0.0524
-c09_E100.0_D9.2,100.0,9.2,165.9486,-0.0791
-c09_E100.0_D9.2,100.0,9.2,168.0449,0.0508
-c09_E100.0_D9.2,100.0,9.2,169.8052,-0.0121
-c09_E100.0_D9.2,100.0,9.2,171.9209,-0.0367
-c09_E100.0_D9.2,100.0,9.2,173.2751,-0.0359
-c09_E100.0_D9.2,100.0,9.2,175.8817,-0.0001
-c09_E100.0_D9.2,100.0,9.2,177.6778,-0.0569
-c09_E100.0_D9.2,100.0,9.2,180.3446,-0.1241
-c09_E100.0_D9.2,100.0,9.2,181.3275,-0.0669
-c09_E100.0_D9.2,100.0,9.2,183.6745,-0.1075
-c09_E100.0_D9.2,100.0,9.2,185.5194,-0.0397
-c09_E100.0_D9.2,100.0,9.2,188.0282,0.1392
-c09_E100.0_D9.2,100.0,9.2,190.2484,0.0778
-c09_E100.0_D9.2,100.0,9.2,191.5119,0.0708
-c09_E100.0_D9.2,100.0,9.2,193.7853,-0.0613
-c09_E100.0_D9.2,100.0,9.2,196.0607,0.0962
-c09_E100.0_D9.2,100.0,9.2,198.1490,0.0144
-c09_E100.0_D9.2,100.0,9.2,200.0000,0.1094
-c10_E100.0_D9.6,100.0,9.6,0.0000,3.1394
-c10_E100.0_D9.6,100.0,9.6,2.5631,2.5447
-c10_E100.0_D9.6,100.0,9.6,4.5957,2.3487
-c10_E100.0_D9.6,100.0,9.6,6.5377,1.7950
-c10_E100.0_D9.6,100.0,9.6,8.6516,1.3985
-c10_E100.0_D9.6,100.0,9.6,10.1275,0.8663
-c10_E100.0_D9.6,100.0,9.6,12.6075,0.3722
-c10_E100.0_D9.6,100.0,9.6,14.6432,-0.2177
-c10_E100.0_D9.6,100.0,9.6,16.4524,-0.9731
-c10_E100.0_D9.6,100.0,9.6,18.5664,-1.7936
-c10_E100.0_D9.6,100.0,9.6,20.3970,-2.4360
-c10_E100.0_D9.6,100.0,9.6,21.9040,-2.7567
-c10_E100.0_D9.6,100.0,9.6,23.8683,-3.4986
-c10_E100.0_D9.6,100.0,9.6,26.7707,-4.7630
-c10_E100.0_D9.6,100.0,9.6,28.7042,-5.3228
-c10_E100.0_D9.6,100.0,9.6,30.8359,-5.7215
-c10_E100.0_D9.6,100.0,9.6,32.6381,-6.1019
-c10_E100.0_D9.6,100.0,9.6,33.8825,-6.5136
-c10_E100.0_D9.6,100.0,9.6,35.8211,-6.8146
-c10_E100.0_D9.6,100.0,9.6,38.9646,-6.7823
-c10_E100.0_D9.6,100.0,9.6,40.2193,-6.9747
-c10_E100.0_D9.6,100.0,9.6,42.1509,-6.8139
-c10_E100.0_D9.6,100.0,9.6,43.9039,-6.5491
-c10_E100.0_D9.6,100.0,9.6,46.5224,-5.6920
-c10_E100.0_D9.6,100.0,9.6,48.6380,-5.4290
-c10_E100.0_D9.6,100.0,9.6,50.7771,-4.6786
-c10_E100.0_D9.6,100.0,9.6,51.9537,-4.6550
-c10_E100.0_D9.6,100.0,9.6,54.7118,-3.6654
-c10_E100.0_D9.6,100.0,9.6,57.1503,-2.8954
-c10_E100.0_D9.6,100.0,9.6,57.9833,-2.6260
-c10_E100.0_D9.6,100.0,9.6,60.7705,-1.8392
-c10_E100.0_D9.6,100.0,9.6,62.2144,-1.7361
-c10_E100.0_D9.6,100.0,9.6,65.1112,-1.1005
-c10_E100.0_D9.6,100.0,9.6,66.7702,-0.8366
-c10_E100.0_D9.6,100.0,9.6,68.3090,-0.4875
-c10_E100.0_D9.6,100.0,9.6,70.9818,-0.4428
-c10_E100.0_D9.6,100.0,9.6,72.2185,-0.2383
-c10_E100.0_D9.6,100.0,9.6,74.9116,0.0670
-c10_E100.0_D9.6,100.0,9.6,76.4845,-0.1103
-c10_E100.0_D9.6,100.0,9.6,78.3254,-0.0260
-c10_E100.0_D9.6,100.0,9.6,80.3412,0.2321
-c10_E100.0_D9.6,100.0,9.6,82.3199,0.1743
-c10_E100.0_D9.6,100.0,9.6,84.2641,0.1140
-c10_E100.0_D9.6,100.0,9.6,86.9231,0.2852
-c10_E100.0_D9.6,100.0,9.6,88.5578,0.1695
-c10_E100.0_D9.6,100.0,9.6,90.9500,0.0870
-c10_E100.0_D9.6,100.0,9.6,93.3733,0.1900
-c10_E100.0_D9.6,100.0,9.6,95.2636,-0.0691
-c10_E100.0_D9.6,100.0,9.6,96.3783,0.1648
-c10_E100.0_D9.6,100.0,9.6,98.6795,0.1659
-c10_E100.0_D9.6,100.0,9.6,100.7819,0.1327
-c10_E100.0_D9.6,100.0,9.6,103.3435,0.0532
-c10_E100.0_D9.6,100.0,9.6,105.3848,0.1732
-c10_E100.0_D9.6,100.0,9.6,106.6892,0.0668
-c10_E100.0_D9.6,100.0,9.6,109.6615,0.1544
-c10_E100.0_D9.6,100.0,9.6,111.4423,0.0258
-c10_E100.0_D9.6,100.0,9.6,113.4172,0.1645
-c10_E100.0_D9.6,100.0,9.6,114.7795,0.1813
-c10_E100.0_D9.6,100.0,9.6,117.6969,0.0607
-c10_E100.0_D9.6,100.0,9.6,119.7088,0.2738
-c10_E100.0_D9.6,100.0,9.6,121.1217,-0.0235
-c10_E100.0_D9.6,100.0,9.6,122.6759,0.0556
-c10_E100.0_D9.6,100.0,9.6,125.0990,0.1479
-c10_E100.0_D9.6,100.0,9.6,126.9147,0.0280
-c10_E100.0_D9.6,100.0,9.6,129.7948,0.0773
-c10_E100.0_D9.6,100.0,9.6,131.5369,-0.0682
-c10_E100.0_D9.6,100.0,9.6,132.9347,-0.0453
-c10_E100.0_D9.6,100.0,9.6,135.7488,0.0416
-c10_E100.0_D9.6,100.0,9.6,137.1393,-0.0325
-c10_E100.0_D9.6,100.0,9.6,139.7080,0.0482
-c10_E100.0_D9.6,100.0,9.6,141.5339,0.0792
-c10_E100.0_D9.6,100.0,9.6,142.8334,0.1848
-c10_E100.0_D9.6,100.0,9.6,145.7138,-0.0511
-c10_E100.0_D9.6,100.0,9.6,147.9601,-0.1270
-c10_E100.0_D9.6,100.0,9.6,148.9250,-0.0155
-c10_E100.0_D9.6,100.0,9.6,151.9426,0.0532
-c10_E100.0_D9.6,100.0,9.6,153.0315,0.0192
-c10_E100.0_D9.6,100.0,9.6,155.0607,0.0189
-c10_E100.0_D9.6,100.0,9.6,157.6639,-0.2828
-c10_E100.0_D9.6,100.0,9.6,159.2719,-0.0235
-c10_E100.0_D9.6,100.0,9.6,161.9535,-0.0821
-c10_E100.0_D9.6,100.0,9.6,163.6651,-0.1250
-c10_E100.0_D9.6,100.0,9.6,165.7858,0.0881
-c10_E100.0_D9.6,100.0,9.6,167.0773,0.0252
-c10_E100.0_D9.6,100.0,9.6,169.9411,0.0999
-c10_E100.0_D9.6,100.0,9.6,171.1419,0.0817
-c10_E100.0_D9.6,100.0,9.6,173.9653,0.1092
-c10_E100.0_D9.6,100.0,9.6,175.3205,0.0186
-c10_E100.0_D9.6,100.0,9.6,177.1966,0.0690
-c10_E100.0_D9.6,100.0,9.6,180.3163,-0.0143
-c10_E100.0_D9.6,100.0,9.6,181.3467,-0.0587
-c10_E100.0_D9.6,100.0,9.6,184.2772,0.1190
-c10_E100.0_D9.6,100.0,9.6,185.3328,0.1460
-c10_E100.0_D9.6,100.0,9.6,187.8516,0.0724
-c10_E100.0_D9.6,100.0,9.6,189.7856,-0.0609
-c10_E100.0_D9.6,100.0,9.6,192.2038,-0.0608
-c10_E100.0_D9.6,100.0,9.6,194.3833,0.0072
-c10_E100.0_D9.6,100.0,9.6,195.4841,0.0447
-c10_E100.0_D9.6,100.0,9.6,198.3618,-0.0240
-c10_E100.0_D9.6,100.0,9.6,199.9200,-0.0370
-c11_E100.0_D10.0,100.0,10.0,0.0000,3.1263
-c11_E100.0_D10.0,100.0,10.0,1.4592,2.8490
-c11_E100.0_D10.0,100.0,10.0,3.7301,2.5015
-c11_E100.0_D10.0,100.0,10.0,6.1201,2.0199
-c11_E100.0_D10.0,100.0,10.0,8.3465,1.4733
-c11_E100.0_D10.0,100.0,10.0,10.0487,0.8145
-c11_E100.0_D10.0,100.0,10.0,12.6415,0.3933
-c11_E100.0_D10.0,100.0,10.0,14.1614,-0.2210
-c11_E100.0_D10.0,100.0,10.0,16.1514,-0.9064
-c11_E100.0_D10.0,100.0,10.0,18.0908,-1.3941
-c11_E100.0_D10.0,100.0,10.0,19.9421,-2.1524
-c11_E100.0_D10.0,100.0,10.0,21.7407,-2.7562
-c11_E100.0_D10.0,100.0,10.0,24.7118,-3.8610
-c11_E100.0_D10.0,100.0,10.0,26.5741,-4.7269
-c11_E100.0_D10.0,100.0,10.0,28.0275,-5.0957
-c11_E100.0_D10.0,100.0,10.0,30.5819,-5.7932
-c11_E100.0_D10.0,100.0,10.0,32.4594,-6.3186
-c11_E100.0_D10.0,100.0,10.0,34.4169,-6.5340
-c11_E100.0_D10.0,100.0,10.0,36.7413,-6.9743
-c11_E100.0_D10.0,100.0,10.0,38.8580,-7.0246
-c11_E100.0_D10.0,100.0,10.0,40.7769,-6.9045
-c11_E100.0_D10.0,100.0,10.0,42.8687,-6.7685
-c11_E100.0_D10.0,100.0,10.0,44.9443,-6.3411
-c11_E100.0_D10.0,100.0,10.0,46.2722,-6.2670
-c11_E100.0_D10.0,100.0,10.0,48.8573,-5.3427
-c11_E100.0_D10.0,100.0,10.0,50.6820,-4.7602
-c11_E100.0_D10.0,100.0,10.0,52.7223,-4.1309
-c11_E100.0_D10.0,100.0,10.0,55.0642,-3.4830
-c11_E100.0_D10.0,100.0,10.0,56.4649,-3.2326
-c11_E100.0_D10.0,100.0,10.0,58.2193,-2.5072
-c11_E100.0_D10.0,100.0,10.0,60.7021,-1.9482
-c11_E100.0_D10.0,100.0,10.0,63.1138,-1.3451
-c11_E100.0_D10.0,100.0,10.0,65.1305,-1.1333
-c11_E100.0_D10.0,100.0,10.0,66.5150,-0.7143
-c11_E100.0_D10.0,100.0,10.0,68.2230,-0.7357
-c11_E100.0_D10.0,100.0,10.0,71.1621,-0.4338
-c11_E100.0_D10.0,100.0,10.0,72.8451,-0.1065
-c11_E100.0_D10.0,100.0,10.0,74.3960,-0.1653
-c11_E100.0_D10.0,100.0,10.0,76.5857,-0.0604
-c11_E100.0_D10.0,100.0,10.0,78.2145,-0.0875
-c11_E100.0_D10.0,100.0,10.0,80.9780,-0.0687
-c11_E100.0_D10.0,100.0,10.0,82.6150,0.0943
-c11_E100.0_D10.0,100.0,10.0,84.3841,0.0181
-c11_E100.0_D10.0,100.0,10.0,87.3421,0.0925
-c11_E100.0_D10.0,100.0,10.0,89.3745,0.1577
-c11_E100.0_D10.0,100.0,10.0,91.2032,0.1815
-c11_E100.0_D10.0,100.0,10.0,93.4316,0.1048
-c11_E100.0_D10.0,100.0,10.0,95.1871,0.1842
-c11_E100.0_D10.0,100.0,10.0,96.9965,0.1200
-c11_E100.0_D10.0,100.0,10.0,98.7161,0.1712
-c11_E100.0_D10.0,100.0,10.0,100.5539,0.2699
-c11_E100.0_D10.0,100.0,10.0,102.8795,0.0919
-c11_E100.0_D10.0,100.0,10.0,104.8042,0.2309
-c11_E100.0_D10.0,100.0,10.0,106.9847,0.1309
-c11_E100.0_D10.0,100.0,10.0,109.4703,0.0486
-c11_E100.0_D10.0,100.0,10.0,110.7325,0.3596
-c11_E100.0_D10.0,100.0,10.0,113.2040,0.0335
-c11_E100.0_D10.0,100.0,10.0,114.7937,0.2615
-c11_E100.0_D10.0,100.0,10.0,116.9032,-0.1104
-c11_E100.0_D10.0,100.0,10.0,119.5556,-0.0307
-c11_E100.0_D10.0,100.0,10.0,121.4993,-0.0314
-c11_E100.0_D10.0,100.0,10.0,123.1137,0.0336
-c11_E100.0_D10.0,100.0,10.0,125.8307,0.2285
-c11_E100.0_D10.0,100.0,10.0,127.3665,0.1039
-c11_E100.0_D10.0,100.0,10.0,129.8194,0.0473
-c11_E100.0_D10.0,100.0,10.0,130.8379,0.1333
-c11_E100.0_D10.0,100.0,10.0,133.7457,0.0703
-c11_E100.0_D10.0,100.0,10.0,135.8967,0.0624
-c11_E100.0_D10.0,100.0,10.0,137.6527,-0.0412
-c11_E100.0_D10.0,100.0,10.0,139.6837,0.0255
-c11_E100.0_D10.0,100.0,10.0,140.9810,0.1037
-c11_E100.0_D10.0,100.0,10.0,143.8748,0.0567
-c11_E100.0_D10.0,100.0,10.0,145.7519,0.1185
-c11_E100.0_D10.0,100.0,10.0,147.3062,0.0530
-c11_E100.0_D10.0,100.0,10.0,149.0693,-0.0804
-c11_E100.0_D10.0,100.0,10.0,151.6314,-0.0322
-c11_E100.0_D10.0,100.0,10.0,153.3620,0.0259
-c11_E100.0_D10.0,100.0,10.0,155.7430,0.1539
-c11_E100.0_D10.0,100.0,10.0,157.2358,0.0764
-c11_E100.0_D10.0,100.0,10.0,159.4103,0.0504
-c11_E100.0_D10.0,100.0,10.0,161.0153,0.0962
-c11_E100.0_D10.0,100.0,10.0,164.2337,-0.2273
-c11_E100.0_D10.0,100.0,10.0,165.1007,0.1348
-c11_E100.0_D10.0,100.0,10.0,167.7335,-0.0768
-c11_E100.0_D10.0,100.0,10.0,169.3774,-0.0500
-c11_E100.0_D10.0,100.0,10.0,171.5701,-0.0046
-c11_E100.0_D10.0,100.0,10.0,173.7603,0.1751
-c11_E100.0_D10.0,100.0,10.0,175.1712,0.0052
-c11_E100.0_D10.0,100.0,10.0,177.6123,0.1061
-c11_E100.0_D10.0,100.0,10.0,179.6862,0.0323
-c11_E100.0_D10.0,100.0,10.0,181.2716,-0.1039
-c11_E100.0_D10.0,100.0,10.0,184.0168,-0.0518
-c11_E100.0_D10.0,100.0,10.0,185.6319,-0.0095
-c11_E100.0_D10.0,100.0,10.0,188.4334,-0.0099
-c11_E100.0_D10.0,100.0,10.0,190.3149,-0.0178
-c11_E100.0_D10.0,100.0,10.0,192.1314,0.2002
-c11_E100.0_D10.0,100.0,10.0,194.4978,-0.1617
-c11_E100.0_D10.0,100.0,10.0,196.0439,0.0697
-c11_E100.0_D10.0,100.0,10.0,198.5642,0.0033
-c11_E100.0_D10.0,100.0,10.0,200.0000,0.2104
-c12_E100.0_D10.4,100.0,10.4,0.0648,3.1530
-c12_E100.0_D10.4,100.0,10.4,1.8038,2.7171
-c12_E100.0_D10.4,100.0,10.4,3.4592,2.4105
-c12_E100.0_D10.4,100.0,10.4,5.7535,2.1312
-c12_E100.0_D10.4,100.0,10.4,7.5272,1.5932
-c12_E100.0_D10.4,100.0,10.4,10.2204,1.0348
-c12_E100.0_D10.4,100.0,10.4,11.9215,0.5369
-c12_E100.0_D10.4,100.0,10.4,13.8187,0.0170
-c12_E100.0_D10.4,100.0,10.4,16.6576,-0.8964
-c12_E100.0_D10.4,100.0,10.4,17.7871,-1.3093
-c12_E100.0_D10.4,100.0,10.4,19.7827,-2.1338
-c12_E100.0_D10.4,100.0,10.4,22.3879,-3.1732
-c12_E100.0_D10.4,100.0,10.4,23.9855,-3.8822
-c12_E100.0_D10.4,100.0,10.4,25.7164,-4.4041
-c12_E100.0_D10.4,100.0,10.4,28.2259,-5.3333
-c12_E100.0_D10.4,100.0,10.4,30.7316,-5.9084
-c12_E100.0_D10.4,100.0,10.4,32.1564,-6.4681
-c12_E100.0_D10.4,100.0,10.4,34.5632,-6.8340
-c12_E100.0_D10.4,100.0,10.4,36.5029,-7.2167
-c12_E100.0_D10.4,100.0,10.4,38.0058,-6.9277
-c12_E100.0_D10.4,100.0,10.4,40.0539,-7.1419
-c12_E100.0_D10.4,100.0,10.4,42.5462,-6.7512
-c12_E100.0_D10.4,100.0,10.4,44.4338,-6.5667
-c12_E100.0_D10.4,100.0,10.4,46.9767,-6.0371
-c12_E100.0_D10.4,100.0,10.4,48.2330,-5.5371
-c12_E100.0_D10.4,100.0,10.4,50.3222,-5.0924
-c12_E100.0_D10.4,100.0,10.4,52.7063,-4.2170
-c12_E100.0_D10.4,100.0,10.4,54.7516,-3.5644
-c12_E100.0_D10.4,100.0,10.4,56.1871,-3.2304
-c12_E100.0_D10.4,100.0,10.4,58.2407,-2.6778
-c12_E100.0_D10.4,100.0,10.4,60.1726,-2.3043
-c12_E100.0_D10.4,100.0,10.4,62.4369,-1.7070
-c12_E100.0_D10.4,100.0,10.4,65.2348,-1.1531
-c12_E100.0_D10.4,100.0,10.4,66.2793,-1.0588
-c12_E100.0_D10.4,100.0,10.4,69.1442,-0.5189
-c12_E100.0_D10.4,100.0,10.4,70.8563,-0.5857
-c12_E100.0_D10.4,100.0,10.4,73.0264,-0.2214
-c12_E100.0_D10.4,100.0,10.4,75.2658,-0.1084
-c12_E100.0_D10.4,100.0,10.4,76.4689,-0.1248
-c12_E100.0_D10.4,100.0,10.4,79.2944,0.0584
-c12_E100.0_D10.4,100.0,10.4,80.5470,0.0650
-c12_E100.0_D10.4,100.0,10.4,82.6074,0.2114
-c12_E100.0_D10.4,100.0,10.4,85.2160,0.1969
-c12_E100.0_D10.4,100.0,10.4,86.9104,0.2053
-c12_E100.0_D10.4,100.0,10.4,89.0381,0.3750
-c12_E100.0_D10.4,100.0,10.4,91.2707,0.2062
-c12_E100.0_D10.4,100.0,10.4,93.1504,0.2000
-c12_E100.0_D10.4,100.0,10.4,94.3918,0.3249
-c12_E100.0_D10.4,100.0,10.4,97.2518,0.1897
-c12_E100.0_D10.4,100.0,10.4,99.3907,0.1189
-c12_E100.0_D10.4,100.0,10.4,101.5948,0.0948
-c12_E100.0_D10.4,100.0,10.4,103.5956,0.2329
-c12_E100.0_D10.4,100.0,10.4,105.0669,0.1175
-c12_E100.0_D10.4,100.0,10.4,106.7723,0.1711
-c12_E100.0_D10.4,100.0,10.4,108.5182,-0.0027
-c12_E100.0_D10.4,100.0,10.4,111.3266,0.0997
-c12_E100.0_D10.4,100.0,10.4,113.1222,0.1430
-c12_E100.0_D10.4,100.0,10.4,115.3297,0.0878
-c12_E100.0_D10.4,100.0,10.4,117.1840,0.1944
-c12_E100.0_D10.4,100.0,10.4,119.7745,0.0633
-c12_E100.0_D10.4,100.0,10.4,121.3046,0.0504
-c12_E100.0_D10.4,100.0,10.4,122.9808,0.0785
-c12_E100.0_D10.4,100.0,10.4,125.0867,-0.0078
-c12_E100.0_D10.4,100.0,10.4,127.0998,0.0524
-c12_E100.0_D10.4,100.0,10.4,129.2270,0.0824
-c12_E100.0_D10.4,100.0,10.4,130.8581,0.2092
-c12_E100.0_D10.4,100.0,10.4,133.4094,0.0658
-c12_E100.0_D10.4,100.0,10.4,135.2246,0.0382
-c12_E100.0_D10.4,100.0,10.4,137.7014,0.2067
-c12_E100.0_D10.4,100.0,10.4,139.3734,0.0150
-c12_E100.0_D10.4,100.0,10.4,140.9087,0.0687
-c12_E100.0_D10.4,100.0,10.4,143.6579,0.1000
-c12_E100.0_D10.4,100.0,10.4,144.9927,0.0989
-c12_E100.0_D10.4,100.0,10.4,146.9281,-0.1799
-c12_E100.0_D10.4,100.0,10.4,148.8930,0.1084
-c12_E100.0_D10.4,100.0,10.4,151.8947,0.0471
-c12_E100.0_D10.4,100.0,10.4,153.9472,0.0689
-c12_E100.0_D10.4,100.0,10.4,155.6390,0.1867
-c12_E100.0_D10.4,100.0,10.4,157.4076,0.0046
-c12_E100.0_D10.4,100.0,10.4,159.1088,-0.0673
-c12_E100.0_D10.4,100.0,10.4,161.1002,-0.1619
-c12_E100.0_D10.4,100.0,10.4,163.3231,-0.0682
-c12_E100.0_D10.4,100.0,10.4,165.3269,0.0740
-c12_E100.0_D10.4,100.0,10.4,167.2320,0.1198
-c12_E100.0_D10.4,100.0,10.4,170.0644,0.1026
-c12_E100.0_D10.4,100.0,10.4,171.8906,-0.2021
-c12_E100.0_D10.4,100.0,10.4,174.0094,0.0394
-c12_E100.0_D10.4,100.0,10.4,176.0126,0.0817
-c12_E100.0_D10.4,100.0,10.4,177.5871,0.0912
-c12_E100.0_D10.4,100.0,10.4,179.4830,0.0095
-c12_E100.0_D10.4,100.0,10.4,181.8481,-0.0222
-c12_E100.0_D10.4,100.0,10.4,183.9904,-0.0281
-c12_E100.0_D10.4,100.0,10.4,185.8082,0.0064
-c12_E100.0_D10.4,100.0,10.4,188.2986,-0.2417
-c12_E100.0_D10.4,100.0,10.4,190.2206,0.0535
-c12_E100.0_D10.4,100.0,10.4,192.0691,0.0228
-c12_E100.0_D10.4,100.0,10.4,194.1674,-0.0383
-c12_E100.0_D10.4,100.0,10.4,195.3945,0.0458
-c12_E100.0_D10.4,100.0,10.4,198.3088,-0.0587
-c12_E100.0_D10.4,100.0,10.4,200.0000,0.1166
-c13_E100.0_D10.8,100.0,10.8,0.5679,3.0546
-c13_E100.0_D10.8,100.0,10.8,2.5410,2.7250
-c13_E100.0_D10.8,100.0,10.8,4.4144,2.3530
-c13_E100.0_D10.8,100.0,10.8,6.2240,2.0403
-c13_E100.0_D10.8,100.0,10.8,7.5753,1.6459
-c13_E100.0_D10.8,100.0,10.8,9.5085,1.1615
-c13_E100.0_D10.8,100.0,10.8,11.6586,0.5614
-c13_E100.0_D10.8,100.0,10.8,14.6686,-0.3680
-c13_E100.0_D10.8,100.0,10.8,16.0776,-0.7570
-c13_E100.0_D10.8,100.0,10.8,18.1249,-1.4782
-c13_E100.0_D10.8,100.0,10.8,20.7623,-2.6298
-c13_E100.0_D10.8,100.0,10.8,21.6529,-2.8363
-c13_E100.0_D10.8,100.0,10.8,24.2768,-3.9857
-c13_E100.0_D10.8,100.0,10.8,26.3173,-4.8199
-c13_E100.0_D10.8,100.0,10.8,27.7667,-5.0317
-c13_E100.0_D10.8,100.0,10.8,29.6975,-5.8510
-c13_E100.0_D10.8,100.0,10.8,32.1155,-6.4133
-c13_E100.0_D10.8,100.0,10.8,33.8289,-6.8017
-c13_E100.0_D10.8,100.0,10.8,36.8681,-7.2041
-c13_E100.0_D10.8,100.0,10.8,38.4123,-7.2272
-c13_E100.0_D10.8,100.0,10.8,39.9946,-7.2036
-c13_E100.0_D10.8,100.0,10.8,42.0527,-6.7938
-c13_E100.0_D10.8,100.0,10.8,44.1988,-6.6389
-c13_E100.0_D10.8,100.0,10.8,46.0851,-6.1755
-c13_E100.0_D10.8,100.0,10.8,48.9141,-5.3154
-c13_E100.0_D10.8,100.0,10.8,50.1830,-5.1232
-c13_E100.0_D10.8,100.0,10.8,52.8764,-4.3952
-c13_E100.0_D10.8,100.0,10.8,54.8468,-3.8156
-c13_E100.0_D10.8,100.0,10.8,56.4239,-3.2516
-c13_E100.0_D10.8,100.0,10.8,58.4926,-2.6483
-c13_E100.0_D10.8,100.0,10.8,60.6424,-2.0604
-c13_E100.0_D10.8,100.0,10.8,62.5330,-1.7682
-c13_E100.0_D10.8,100.0,10.8,65.2268,-1.1175
-c13_E100.0_D10.8,100.0,10.8,66.7202,-0.8338
-c13_E100.0_D10.8,100.0,10.8,68.2718,-0.7012
-c13_E100.0_D10.8,100.0,10.8,70.9889,-0.5748
-c13_E100.0_D10.8,100.0,10.8,72.3778,-0.3735
-c13_E100.0_D10.8,100.0,10.8,74.7836,-0.0840
-c13_E100.0_D10.8,100.0,10.8,76.4076,-0.1219
-c13_E100.0_D10.8,100.0,10.8,78.7699,-0.0129
-c13_E100.0_D10.8,100.0,10.8,81.2624,0.2337
-c13_E100.0_D10.8,100.0,10.8,82.7067,0.2260
-c13_E100.0_D10.8,100.0,10.8,84.9532,0.0506
-c13_E100.0_D10.8,100.0,10.8,86.5944,0.2251
-c13_E100.0_D10.8,100.0,10.8,88.4789,0.1706
-c13_E100.0_D10.8,100.0,10.8,91.3758,0.2428
-c13_E100.0_D10.8,100.0,10.8,93.4487,0.1207
-c13_E100.0_D10.8,100.0,10.8,94.3599,0.2566
-c13_E100.0_D10.8,100.0,10.8,97.1644,0.0920
-c13_E100.0_D10.8,100.0,10.8,98.5323,0.0896
-c13_E100.0_D10.8,100.0,10.8,101.5974,0.2841
-c13_E100.0_D10.8,100.0,10.8,102.9087,0.0704
-c13_E100.0_D10.8,100.0,10.8,105.1677,0.0867
-c13_E100.0_D10.8,100.0,10.8,107.5816,0.0561
-c13_E100.0_D10.8,100.0,10.8,108.8591,0.2140
-c13_E100.0_D10.8,100.0,10.8,111.7153,0.2608
-c13_E100.0_D10.8,100.0,10.8,112.9198,0.1181
-c13_E100.0_D10.8,100.0,10.8,115.6795,0.1989
-c13_E100.0_D10.8,100.0,10.8,116.9222,0.0348
-c13_E100.0_D10.8,100.0,10.8,119.6759,0.0532
-c13_E100.0_D10.8,100.0,10.8,120.8306,-0.0288
-c13_E100.0_D10.8,100.0,10.8,123.2084,0.2057
-c13_E100.0_D10.8,100.0,10.8,125.3129,0.0421
-c13_E100.0_D10.8,100.0,10.8,127.5341,0.0358
-c13_E100.0_D10.8,100.0,10.8,129.4685,0.1881
-c13_E100.0_D10.8,100.0,10.8,130.8359,-0.0703
-c13_E100.0_D10.8,100.0,10.8,132.7448,-0.0976
-c13_E100.0_D10.8,100.0,10.8,135.7699,-0.0633
-c13_E100.0_D10.8,100.0,10.8,137.0130,-0.0046
-c13_E100.0_D10.8,100.0,10.8,139.2754,0.0106
-c13_E100.0_D10.8,100.0,10.8,140.9415,-0.0153
-c13_E100.0_D10.8,100.0,10.8,143.0772,0.0718
-c13_E100.0_D10.8,100.0,10.8,145.6864,0.1109
-c13_E100.0_D10.8,100.0,10.8,147.4386,0.0161
-c13_E100.0_D10.8,100.0,10.8,149.9356,-0.0421
-c13_E100.0_D10.8,100.0,10.8,151.8180,-0.1023
-c13_E100.0_D10.8,100.0,10.8,153.8216,0.0292
-c13_E100.0_D10.8,100.0,10.8,155.3346,-0.0725
-c13_E100.0_D10.8,100.0,10.8,157.6671,0.2298
-c13_E100.0_D10.8,100.0,10.8,159.1198,0.0926
-c13_E100.0_D10.8,100.0,10.8,162.1208,0.0855
-c13_E100.0_D10.8,100.0,10.8,164.1836,0.0029
-c13_E100.0_D10.8,100.0,10.8,165.8742,-0.0180
-c13_E100.0_D10.8,100.0,10.8,167.3939,-0.0437
-c13_E100.0_D10.8,100.0,10.8,169.7188,0.1250
-c13_E100.0_D10.8,100.0,10.8,172.0103,0.0912
-c13_E100.0_D10.8,100.0,10.8,174.0440,0.0062
-c13_E100.0_D10.8,100.0,10.8,175.4144,0.0787
-c13_E100.0_D10.8,100.0,10.8,177.4748,-0.0346
-c13_E100.0_D10.8,100.0,10.8,179.5529,0.0174
-c13_E100.0_D10.8,100.0,10.8,181.5966,-0.0357
-c13_E100.0_D10.8,100.0,10.8,183.5916,-0.1084
-c13_E100.0_D10.8,100.0,10.8,186.3682,-0.0660
-c13_E100.0_D10.8,100.0,10.8,187.9320,-0.0066
-c13_E100.0_D10.8,100.0,10.8,190.3988,0.1070
-c13_E100.0_D10.8,100.0,10.8,191.8813,-0.1279
-c13_E100.0_D10.8,100.0,10.8,193.6202,-0.0207
-c13_E100.0_D10.8,100.0,10.8,196.1844,-0.0042
-c13_E100.0_D10.8,100.0,10.8,197.6893,-0.0188
-c13_E100.0_D10.8,100.0,10.8,199.4846,0.0337
-c14_E100.0_D11.2,100.0,11.2,0.1076,3.0596
-c14_E100.0_D11.2,100.0,11.2,2.2545,2.7072
-c14_E100.0_D11.2,100.0,11.2,3.9171,2.5853
-c14_E100.0_D11.2,100.0,11.2,6.2333,1.8209
-c14_E100.0_D11.2,100.0,11.2,7.7081,1.7172
-c14_E100.0_D11.2,100.0,11.2,10.2190,1.0687
-c14_E100.0_D11.2,100.0,11.2,11.9011,0.3667
-c14_E100.0_D11.2,100.0,11.2,14.5873,-0.4358
-c14_E100.0_D11.2,100.0,11.2,15.8032,-0.7765
-c14_E100.0_D11.2,100.0,11.2,18.4691,-1.8629
-c14_E100.0_D11.2,100.0,11.2,20.3319,-2.3132
-c14_E100.0_D11.2,100.0,11.2,22.0959,-3.1016
-c14_E100.0_D11.2,100.0,11.2,23.9226,-3.9497
-c14_E100.0_D11.2,100.0,11.2,25.8516,-4.5954
-c14_E100.0_D11.2,100.0,11.2,28.5472,-5.4286
-c14_E100.0_D11.2,100.0,11.2,29.8373,-5.9804
-c14_E100.0_D11.2,100.0,11.2,32.2794,-6.7003
-c14_E100.0_D11.2,100.0,11.2,34.0730,-6.9296
-c14_E100.0_D11.2,100.0,11.2,36.6051,-7.3487
-c14_E100.0_D11.2,100.0,11.2,38.0793,-7.2562
-c14_E100.0_D11.2,100.0,11.2,40.7029,-7.3933
-c14_E100.0_D11.2,100.0,11.2,43.0046,-6.9311
-c14_E100.0_D11.2,100.0,11.2,43.9982,-6.7162
-c14_E100.0_D11.2,100.0,11.2,46.1563,-6.2633
-c14_E100.0_D11.2,100.0,11.2,48.9332,-5.6895
-c14_E100.0_D11.2,100.0,11.2,50.5943,-4.9930
-c14_E100.0_D11.2,100.0,11.2,52.5893,-4.3721
-c14_E100.0_D11.2,100.0,11.2,54.1274,-4.0137
-c14_E100.0_D11.2,100.0,11.2,56.6674,-3.1372
-c14_E100.0_D11.2,100.0,11.2,58.2051,-2.7893
-c14_E100.0_D11.2,100.0,11.2,60.9147,-1.9973
-c14_E100.0_D11.2,100.0,11.2,62.0641,-1.8462
-c14_E100.0_D11.2,100.0,11.2,64.1969,-1.3842
-c14_E100.0_D11.2,100.0,11.2,66.3890,-0.9742
-c14_E100.0_D11.2,100.0,11.2,68.7408,-0.6200
-c14_E100.0_D11.2,100.0,11.2,70.4818,-0.3817
-c14_E100.0_D11.2,100.0,11.2,72.6959,-0.0140
-c14_E100.0_D11.2,100.0,11.2,74.6162,-0.1095
-c14_E100.0_D11.2,100.0,11.2,77.3073,-0.0957
-c14_E100.0_D11.2,100.0,11.2,78.9409,-0.0019
-c14_E100.0_D11.2,100.0,11.2,80.2832,0.3176
-c14_E100.0_D11.2,100.0,11.2,82.4594,0.2276
-c14_E100.0_D11.2,100.0,11.2,84.5653,0.2362
-c14_E100.0_D11.2,100.0,11.2,87.4498,0.4692
-c14_E100.0_D11.2,100.0,11.2,88.6316,0.1462
-c14_E100.0_D11.2,100.0,11.2,90.5017,0.1517
-c14_E100.0_D11.2,100.0,11.2,92.3436,-0.0673
-c14_E100.0_D11.2,100.0,11.2,95.4554,0.1804
-c14_E100.0_D11.2,100.0,11.2,97.0058,0.1595
-c14_E100.0_D11.2,100.0,11.2,98.7909,0.1987
-c14_E100.0_D11.2,100.0,11.2,100.5387,-0.1173
-c14_E100.0_D11.2,100.0,11.2,102.9678,0.3341
-c14_E100.0_D11.2,100.0,11.2,104.8762,0.0431
-c14_E100.0_D11.2,100.0,11.2,107.4268,0.1198
-c14_E100.0_D11.2,100.0,11.2,108.9777,0.1418
-c14_E100.0_D11.2,100.0,11.2,110.9747,-0.0914
-c14_E100.0_D11.2,100.0,11.2,113.0982,0.0743
-c14_E100.0_D11.2,100.0,11.2,115.2740,0.1480
-c14_E100.0_D11.2,100.0,11.2,117.3395,0.1110
-c14_E100.0_D11.2,100.0,11.2,119.3400,-0.0369
-c14_E100.0_D11.2,100.0,11.2,121.2837,-0.0206
-c14_E100.0_D11.2,100.0,11.2,123.1250,0.1008
-c14_E100.0_D11.2,100.0,11.2,124.6892,0.1084
-c14_E100.0_D11.2,100.0,11.2,127.7939,-0.0623
-c14_E100.0_D11.2,100.0,11.2,129.0999,-0.0544
-c14_E100.0_D11.2,100.0,11.2,131.4490,0.1341
-c14_E100.0_D11.2,100.0,11.2,133.6006,0.1633
-c14_E100.0_D11.2,100.0,11.2,135.1697,0.1611
-c14_E100.0_D11.2,100.0,11.2,137.7509,0.0167
-c14_E100.0_D11.2,100.0,11.2,138.9030,-0.0117
-c14_E100.0_D11.2,100.0,11.2,141.1013,-0.0258
-c14_E100.0_D11.2,100.0,11.2,143.4818,-0.0900
-c14_E100.0_D11.2,100.0,11.2,145.2161,0.1012
-c14_E100.0_D11.2,100.0,11.2,147.3149,0.0757
-c14_E100.0_D11.2,100.0,11.2,148.9767,0.1671
-c14_E100.0_D11.2,100.0,11.2,150.9282,-0.1098
-c14_E100.0_D11.2,100.0,11.2,153.6370,-0.0472
-c14_E100.0_D11.2,100.0,11.2,155.5926,0.1012
-c14_E100.0_D11.2,100.0,11.2,157.1057,-0.0498
-c14_E100.0_D11.2,100.0,11.2,160.1423,-0.0314
-c14_E100.0_D11.2,100.0,11.2,161.9591,0.0036
-c14_E100.0_D11.2,100.0,11.2,163.3574,0.3885
-c14_E100.0_D11.2,100.0,11.2,166.2387,0.0490
-c14_E100.0_D11.2,100.0,11.2,168.2695,-0.0886
-c14_E100.0_D11.2,100.0,11.2,169.4499,0.1069
-c14_E100.0_D11.2,100.0,11.2,171.5825,0.0725
-c14_E100.0_D11.2,100.0,11.2,174.0072,-0.1607
-c14_E100.0_D11.2,100.0,11.2,176.2940,0.0006
-c14_E100.0_D11.2,100.0,11.2,177.6868,-0.0179
-c14_E100.0_D11.2,100.0,11.2,179.5218,0.0969
-c14_E100.0_D11.2,100.0,11.2,181.8810,-0.0443
-c14_E100.0_D11.2,100.0,11.2,184.0819,-0.0363
-c14_E100.0_D11.2,100.0,11.2,185.7133,0.0696
-c14_E100.0_D11.2,100.0,11.2,188.0786,0.0464
-c14_E100.0_D11.2,100.0,11.2,189.4693,-0.0577
-c14_E100.0_D11.2,100.0,11.2,192.4482,-0.0046
-c14_E100.0_D11.2,100.0,11.2,194.0853,0.0674
-c14_E100.0_D11.2,100.0,11.2,195.8080,-0.1204
-c14_E100.0_D11.2,100.0,11.2,197.9931,-0.1143
-c14_E100.0_D11.2,100.0,11.2,200.0000,-0.1734
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,0.0000,3.1628
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,2.1219,2.7738
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,3.7068,2.3706
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,6.3812,1.6362
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,8.1827,1.3867
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,9.7846,1.1153
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,12.2270,0.2533
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,14.5753,-0.4502
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,16.2077,-0.9486
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,18.2481,-1.8002
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,20.4268,-2.6049
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,22.8073,-3.5861
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,23.9534,-4.1385
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,25.6778,-4.5473
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,28.5888,-5.6644
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,30.8833,-6.2010
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,32.6053,-6.5537
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,34.7077,-7.1138
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,36.8818,-7.4058
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,38.0887,-7.3509
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,40.2138,-7.1676
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,42.0015,-7.1176
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,43.9113,-6.7880
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,46.6767,-6.1069
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,47.8993,-5.8983
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,50.9611,-4.9014
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,52.1855,-4.7192
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,55.0467,-3.8551
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,56.8122,-3.1430
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,58.0644,-2.9138
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,60.8468,-1.9248
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,62.2694,-1.8229
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,64.6127,-1.3160
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,66.2229,-1.0569
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,68.6328,-0.4573
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,71.0430,-0.3025
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,73.0136,-0.1597
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,74.3219,-0.0811
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,76.1903,0.0322
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,78.5977,0.1022
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,81.0296,-0.1024
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,82.4457,0.1154
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,84.4989,0.0609
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,87.0979,0.1001
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,89.4948,0.0284
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,90.3659,0.1735
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,92.8068,0.1397
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,94.5798,0.1850
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,97.2875,0.1183
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,99.3311,-0.0763
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,101.4357,0.0502
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,102.5896,0.2096
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,104.7994,0.0639
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,106.7403,0.2438
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,108.5424,0.1272
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,110.5200,0.0025
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,113.0324,0.0709
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,115.2531,0.1289
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,117.2841,0.1873
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,119.7757,0.0184
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,120.9972,0.0836
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,123.1921,0.0804
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,125.1328,-0.0028
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,127.1936,0.1236
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,128.8263,0.1793
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,131.4917,0.0604
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,132.9010,-0.0640
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,134.8255,0.0229
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,137.3940,0.1090
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,139.7379,-0.1545
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,141.7588,-0.1061
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,142.8440,0.0308
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,145.1955,-0.0231
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,147.3021,0.2463
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,149.6612,0.1468
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,151.1160,-0.1082
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,153.7120,-0.0516
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,155.9597,-0.1899
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,157.2065,-0.0039
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,159.8257,-0.1829
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,161.0709,0.0446
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,163.6393,0.0485
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,165.2910,0.1259
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,168.2390,0.0842
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,169.7606,-0.0670
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,171.5105,0.0854
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,173.6486,-0.0517
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,175.4622,-0.0406
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,177.5781,-0.0173
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,179.4928,-0.0059
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,182.0280,0.1417
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,183.2356,-0.0843
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,185.6719,0.0533
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,187.7599,0.0108
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,189.4004,0.0085
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,192.4342,-0.0605
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,194.5439,-0.0516
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,196.5565,-0.0789
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,197.3818,0.0638
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,200.0000,0.2020
-c16_E100.0_D12.0,100.0,12.0,0.5726,2.9980
-c16_E100.0_D12.0,100.0,12.0,1.9448,2.7585
-c16_E100.0_D12.0,100.0,12.0,4.4525,2.3095
-c16_E100.0_D12.0,100.0,12.0,5.5207,2.1459
-c16_E100.0_D12.0,100.0,12.0,7.6733,1.6393
-c16_E100.0_D12.0,100.0,12.0,10.7036,0.7950
-c16_E100.0_D12.0,100.0,12.0,12.6764,0.2007
-c16_E100.0_D12.0,100.0,12.0,14.1047,-0.2695
-c16_E100.0_D12.0,100.0,12.0,16.5474,-1.1053
-c16_E100.0_D12.0,100.0,12.0,18.4108,-1.8069
-c16_E100.0_D12.0,100.0,12.0,20.5590,-2.3954
-c16_E100.0_D12.0,100.0,12.0,22.2638,-3.2848
-c16_E100.0_D12.0,100.0,12.0,23.9042,-4.0065
-c16_E100.0_D12.0,100.0,12.0,26.0297,-4.7462
-c16_E100.0_D12.0,100.0,12.0,27.8212,-5.3462
-c16_E100.0_D12.0,100.0,12.0,30.5074,-6.2552
-c16_E100.0_D12.0,100.0,12.0,31.9027,-6.5764
-c16_E100.0_D12.0,100.0,12.0,34.0969,-6.9999
-c16_E100.0_D12.0,100.0,12.0,36.8839,-7.5221
-c16_E100.0_D12.0,100.0,12.0,37.8465,-7.4158
-c16_E100.0_D12.0,100.0,12.0,39.8133,-7.4567
-c16_E100.0_D12.0,100.0,12.0,42.5638,-7.0332
-c16_E100.0_D12.0,100.0,12.0,44.0264,-6.8449
-c16_E100.0_D12.0,100.0,12.0,45.9999,-6.1188
-c16_E100.0_D12.0,100.0,12.0,48.6627,-5.6223
-c16_E100.0_D12.0,100.0,12.0,49.9172,-5.3782
-c16_E100.0_D12.0,100.0,12.0,52.2208,-4.7427
-c16_E100.0_D12.0,100.0,12.0,55.0523,-3.7857
-c16_E100.0_D12.0,100.0,12.0,56.2440,-3.4333
-c16_E100.0_D12.0,100.0,12.0,58.7602,-2.6146
-c16_E100.0_D12.0,100.0,12.0,60.7160,-2.1406
-c16_E100.0_D12.0,100.0,12.0,62.1044,-1.9034
-c16_E100.0_D12.0,100.0,12.0,64.3838,-1.3947
-c16_E100.0_D12.0,100.0,12.0,67.1201,-0.9766
-c16_E100.0_D12.0,100.0,12.0,68.5894,-0.5240
-c16_E100.0_D12.0,100.0,12.0,70.1074,-0.6362
-c16_E100.0_D12.0,100.0,12.0,72.8410,-0.2788
-c16_E100.0_D12.0,100.0,12.0,75.1990,-0.0951
-c16_E100.0_D12.0,100.0,12.0,77.2876,0.0273
-c16_E100.0_D12.0,100.0,12.0,78.3196,0.2066
-c16_E100.0_D12.0,100.0,12.0,80.4287,0.1853
-c16_E100.0_D12.0,100.0,12.0,83.1721,0.0806
-c16_E100.0_D12.0,100.0,12.0,85.0427,0.1879
-c16_E100.0_D12.0,100.0,12.0,87.2646,0.1546
-c16_E100.0_D12.0,100.0,12.0,89.0340,0.2369
-c16_E100.0_D12.0,100.0,12.0,91.0081,0.1041
-c16_E100.0_D12.0,100.0,12.0,92.7302,0.0790
-c16_E100.0_D12.0,100.0,12.0,95.4242,0.2810
-c16_E100.0_D12.0,100.0,12.0,97.5353,0.0511
-c16_E100.0_D12.0,100.0,12.0,98.8554,0.2100
-c16_E100.0_D12.0,100.0,12.0,100.8331,0.1727
-c16_E100.0_D12.0,100.0,12.0,103.5907,0.1254
-c16_E100.0_D12.0,100.0,12.0,105.0871,0.0129
-c16_E100.0_D12.0,100.0,12.0,107.2370,0.2002
-c16_E100.0_D12.0,100.0,12.0,108.5871,0.1675
-c16_E100.0_D12.0,100.0,12.0,110.7262,0.0508
-c16_E100.0_D12.0,100.0,12.0,113.4372,-0.0212
-c16_E100.0_D12.0,100.0,12.0,115.6951,0.0822
-c16_E100.0_D12.0,100.0,12.0,116.7217,-0.0013
-c16_E100.0_D12.0,100.0,12.0,119.7935,0.1024
-c16_E100.0_D12.0,100.0,12.0,120.8937,-0.0262
-c16_E100.0_D12.0,100.0,12.0,122.6621,0.1203
-c16_E100.0_D12.0,100.0,12.0,125.3137,0.0156
-c16_E100.0_D12.0,100.0,12.0,127.4343,0.1124
-c16_E100.0_D12.0,100.0,12.0,128.8936,0.0730
-c16_E100.0_D12.0,100.0,12.0,131.8554,0.2145
-c16_E100.0_D12.0,100.0,12.0,132.8271,-0.0011
-c16_E100.0_D12.0,100.0,12.0,135.3535,-0.0787
-c16_E100.0_D12.0,100.0,12.0,136.7709,-0.0346
-c16_E100.0_D12.0,100.0,12.0,139.4228,0.0716
-c16_E100.0_D12.0,100.0,12.0,140.9671,-0.1179
-c16_E100.0_D12.0,100.0,12.0,143.1476,-0.1645
-c16_E100.0_D12.0,100.0,12.0,145.4478,0.0362
-c16_E100.0_D12.0,100.0,12.0,147.3413,-0.0273
-c16_E100.0_D12.0,100.0,12.0,149.7854,0.1335
-c16_E100.0_D12.0,100.0,12.0,151.2246,0.0334
-c16_E100.0_D12.0,100.0,12.0,153.1073,0.1205
-c16_E100.0_D12.0,100.0,12.0,155.9329,-0.0844
-c16_E100.0_D12.0,100.0,12.0,157.5171,-0.0334
-c16_E100.0_D12.0,100.0,12.0,159.8551,-0.1852
-c16_E100.0_D12.0,100.0,12.0,161.9580,-0.0429
-c16_E100.0_D12.0,100.0,12.0,163.9662,0.0607
-c16_E100.0_D12.0,100.0,12.0,166.2409,-0.0064
-c16_E100.0_D12.0,100.0,12.0,167.7218,-0.0212
-c16_E100.0_D12.0,100.0,12.0,169.5661,-0.0000
-c16_E100.0_D12.0,100.0,12.0,171.8248,-0.1326
-c16_E100.0_D12.0,100.0,12.0,173.5833,-0.1520
-c16_E100.0_D12.0,100.0,12.0,176.2384,0.1728
-c16_E100.0_D12.0,100.0,12.0,177.5571,-0.0649
-c16_E100.0_D12.0,100.0,12.0,179.9578,0.1068
-c16_E100.0_D12.0,100.0,12.0,181.7648,-0.1203
-c16_E100.0_D12.0,100.0,12.0,184.0660,0.0619
-c16_E100.0_D12.0,100.0,12.0,185.5566,-0.1155
-c16_E100.0_D12.0,100.0,12.0,188.1369,0.0285
-c16_E100.0_D12.0,100.0,12.0,190.0416,-0.0795
-c16_E100.0_D12.0,100.0,12.0,192.1226,0.0330
-c16_E100.0_D12.0,100.0,12.0,193.9070,-0.0525
-c16_E100.0_D12.0,100.0,12.0,196.4036,-0.0286
-c16_E100.0_D12.0,100.0,12.0,197.7410,0.1730
-c16_E100.0_D12.0,100.0,12.0,200.0000,-0.0337
-c17_E100.0_D12.4,100.0,12.4,0.1086,3.1788
-c17_E100.0_D12.4,100.0,12.4,1.5882,2.9568
-c17_E100.0_D12.4,100.0,12.4,4.2092,2.3057
-c17_E100.0_D12.4,100.0,12.4,5.8922,2.1186
-c17_E100.0_D12.4,100.0,12.4,8.5409,1.4863
-c17_E100.0_D12.4,100.0,12.4,9.6526,1.0584
-c17_E100.0_D12.4,100.0,12.4,12.6073,0.2742
-c17_E100.0_D12.4,100.0,12.4,13.9983,-0.4034
-c17_E100.0_D12.4,100.0,12.4,16.2450,-1.1677
-c17_E100.0_D12.4,100.0,12.4,18.4325,-1.9129
-c17_E100.0_D12.4,100.0,12.4,19.6277,-2.3684
-c17_E100.0_D12.4,100.0,12.4,22.6196,-3.3707
-c17_E100.0_D12.4,100.0,12.4,24.2661,-4.0156
-c17_E100.0_D12.4,100.0,12.4,25.8969,-4.8385
-c17_E100.0_D12.4,100.0,12.4,28.7156,-5.8386
-c17_E100.0_D12.4,100.0,12.4,30.5438,-6.3907
-c17_E100.0_D12.4,100.0,12.4,32.6336,-6.8070
-c17_E100.0_D12.4,100.0,12.4,33.7376,-7.0116
-c17_E100.0_D12.4,100.0,12.4,36.6009,-7.4997
-c17_E100.0_D12.4,100.0,12.4,38.3071,-7.4436
-c17_E100.0_D12.4,100.0,12.4,40.3787,-7.3830
-c17_E100.0_D12.4,100.0,12.4,42.6378,-7.2077
-c17_E100.0_D12.4,100.0,12.4,44.2691,-6.7800
-c17_E100.0_D12.4,100.0,12.4,46.4658,-6.4130
-c17_E100.0_D12.4,100.0,12.4,48.0736,-5.9713
-c17_E100.0_D12.4,100.0,12.4,50.6770,-4.9714
-c17_E100.0_D12.4,100.0,12.4,52.8589,-4.5090
-c17_E100.0_D12.4,100.0,12.4,53.9434,-4.2996
-c17_E100.0_D12.4,100.0,12.4,56.9809,-3.1980
-c17_E100.0_D12.4,100.0,12.4,58.5003,-2.7445
-c17_E100.0_D12.4,100.0,12.4,60.3440,-2.3695
-c17_E100.0_D12.4,100.0,12.4,62.1648,-1.8215
-c17_E100.0_D12.4,100.0,12.4,64.4044,-1.1942
-c17_E100.0_D12.4,100.0,12.4,66.6051,-1.0433
-c17_E100.0_D12.4,100.0,12.4,68.1371,-0.5532
-c17_E100.0_D12.4,100.0,12.4,70.5169,-0.3445
-c17_E100.0_D12.4,100.0,12.4,73.2055,-0.3069
-c17_E100.0_D12.4,100.0,12.4,74.5513,-0.1866
-c17_E100.0_D12.4,100.0,12.4,76.6747,-0.2685
-c17_E100.0_D12.4,100.0,12.4,78.4304,0.0139
-c17_E100.0_D12.4,100.0,12.4,80.9705,-0.0771
-c17_E100.0_D12.4,100.0,12.4,83.0607,0.0716
-c17_E100.0_D12.4,100.0,12.4,85.2388,-0.0506
-c17_E100.0_D12.4,100.0,12.4,86.8597,0.3980
-c17_E100.0_D12.4,100.0,12.4,88.9537,0.1289
-c17_E100.0_D12.4,100.0,12.4,91.1134,0.2128
-c17_E100.0_D12.4,100.0,12.4,92.3319,0.1254
-c17_E100.0_D12.4,100.0,12.4,94.9388,0.2890
-c17_E100.0_D12.4,100.0,12.4,96.9957,0.0049
-c17_E100.0_D12.4,100.0,12.4,98.8223,0.0266
-c17_E100.0_D12.4,100.0,12.4,101.3719,0.0189
-c17_E100.0_D12.4,100.0,12.4,102.5488,0.0418
-c17_E100.0_D12.4,100.0,12.4,105.2794,-0.0300
-c17_E100.0_D12.4,100.0,12.4,106.9725,0.0106
-c17_E100.0_D12.4,100.0,12.4,109.1768,-0.1022
-c17_E100.0_D12.4,100.0,12.4,111.1014,0.0813
-c17_E100.0_D12.4,100.0,12.4,113.4054,-0.0406
-c17_E100.0_D12.4,100.0,12.4,114.7258,0.0319
-c17_E100.0_D12.4,100.0,12.4,117.2102,0.1674
-c17_E100.0_D12.4,100.0,12.4,119.4539,0.1129
-c17_E100.0_D12.4,100.0,12.4,120.9203,0.1964
-c17_E100.0_D12.4,100.0,12.4,123.4172,0.0601
-c17_E100.0_D12.4,100.0,12.4,125.4974,0.0606
-c17_E100.0_D12.4,100.0,12.4,127.8284,0.1352
-c17_E100.0_D12.4,100.0,12.4,129.6801,-0.0952
-c17_E100.0_D12.4,100.0,12.4,131.0602,0.0170
-c17_E100.0_D12.4,100.0,12.4,133.6438,0.1159
-c17_E100.0_D12.4,100.0,12.4,135.1056,-0.0722
-c17_E100.0_D12.4,100.0,12.4,137.2289,0.1937
-c17_E100.0_D12.4,100.0,12.4,139.7566,0.0082
-c17_E100.0_D12.4,100.0,12.4,140.8381,-0.1171
-c17_E100.0_D12.4,100.0,12.4,143.4319,-0.0301
-c17_E100.0_D12.4,100.0,12.4,145.6709,0.0020
-c17_E100.0_D12.4,100.0,12.4,147.4136,0.0296
-c17_E100.0_D12.4,100.0,12.4,149.6279,0.0419
-c17_E100.0_D12.4,100.0,12.4,151.8061,-0.0992
-c17_E100.0_D12.4,100.0,12.4,153.6811,-0.0217
-c17_E100.0_D12.4,100.0,12.4,155.9211,0.0533
-c17_E100.0_D12.4,100.0,12.4,157.1957,0.0873
-c17_E100.0_D12.4,100.0,12.4,159.7419,0.1743
-c17_E100.0_D12.4,100.0,12.4,161.7203,0.0858
-c17_E100.0_D12.4,100.0,12.4,163.5588,0.0560
-c17_E100.0_D12.4,100.0,12.4,166.2601,-0.0025
-c17_E100.0_D12.4,100.0,12.4,167.9240,0.0675
-c17_E100.0_D12.4,100.0,12.4,169.8021,-0.1030
-c17_E100.0_D12.4,100.0,12.4,171.3074,-0.1048
-c17_E100.0_D12.4,100.0,12.4,173.2233,-0.0240
-c17_E100.0_D12.4,100.0,12.4,175.6624,-0.2290
-c17_E100.0_D12.4,100.0,12.4,178.1241,0.1240
-c17_E100.0_D12.4,100.0,12.4,180.1480,0.0962
-c17_E100.0_D12.4,100.0,12.4,182.3439,0.0886
-c17_E100.0_D12.4,100.0,12.4,183.7763,-0.0218
-c17_E100.0_D12.4,100.0,12.4,185.7041,-0.0946
-c17_E100.0_D12.4,100.0,12.4,187.9791,-0.0447
-c17_E100.0_D12.4,100.0,12.4,189.4334,-0.1681
-c17_E100.0_D12.4,100.0,12.4,191.4050,0.0441
-c17_E100.0_D12.4,100.0,12.4,194.3868,0.1087
-c17_E100.0_D12.4,100.0,12.4,195.6202,0.0357
-c17_E100.0_D12.4,100.0,12.4,197.8453,0.0440
-c17_E100.0_D12.4,100.0,12.4,200.0000,0.0601
-c18_E100.0_D12.8,100.0,12.8,0.0000,3.0650
-c18_E100.0_D12.8,100.0,12.8,1.4424,2.7114
-c18_E100.0_D12.8,100.0,12.8,4.0002,2.3441
-c18_E100.0_D12.8,100.0,12.8,6.6641,1.8212
-c18_E100.0_D12.8,100.0,12.8,8.0589,1.4240
-c18_E100.0_D12.8,100.0,12.8,10.4407,0.9523
-c18_E100.0_D12.8,100.0,12.8,11.5605,0.4173
-c18_E100.0_D12.8,100.0,12.8,13.5441,-0.1992
-c18_E100.0_D12.8,100.0,12.8,15.7565,-0.9628
-c18_E100.0_D12.8,100.0,12.8,18.6492,-2.1970
-c18_E100.0_D12.8,100.0,12.8,20.0887,-2.5601
-c18_E100.0_D12.8,100.0,12.8,22.0354,-3.4171
-c18_E100.0_D12.8,100.0,12.8,24.5025,-4.2420
-c18_E100.0_D12.8,100.0,12.8,26.6484,-5.2082
-c18_E100.0_D12.8,100.0,12.8,28.1671,-5.5331
-c18_E100.0_D12.8,100.0,12.8,29.8864,-6.1826
-c18_E100.0_D12.8,100.0,12.8,32.7435,-7.0308
-c18_E100.0_D12.8,100.0,12.8,34.3401,-7.3423
-c18_E100.0_D12.8,100.0,12.8,36.9310,-7.4675
-c18_E100.0_D12.8,100.0,12.8,37.8879,-7.4644
-c18_E100.0_D12.8,100.0,12.8,39.9385,-7.4634
-c18_E100.0_D12.8,100.0,12.8,42.2250,-7.4867
-c18_E100.0_D12.8,100.0,12.8,44.2997,-6.9420
-c18_E100.0_D12.8,100.0,12.8,46.1776,-6.5786
-c18_E100.0_D12.8,100.0,12.8,48.6796,-5.6450
-c18_E100.0_D12.8,100.0,12.8,51.0134,-5.0852
-c18_E100.0_D12.8,100.0,12.8,52.6170,-4.7100
-c18_E100.0_D12.8,100.0,12.8,54.4880,-3.8535
-c18_E100.0_D12.8,100.0,12.8,56.5908,-3.2907
-c18_E100.0_D12.8,100.0,12.8,59.0511,-2.6621
-c18_E100.0_D12.8,100.0,12.8,60.8771,-2.2456
-c18_E100.0_D12.8,100.0,12.8,62.1027,-1.8792
-c18_E100.0_D12.8,100.0,12.8,64.4180,-1.3812
-c18_E100.0_D12.8,100.0,12.8,66.3875,-1.0050
-c18_E100.0_D12.8,100.0,12.8,68.4497,-0.7874
-c18_E100.0_D12.8,100.0,12.8,70.2092,-0.5227
-c18_E100.0_D12.8,100.0,12.8,72.4652,-0.2462
-c18_E100.0_D12.8,100.0,12.8,74.9177,-0.0716
-c18_E100.0_D12.8,100.0,12.8,76.6772,-0.0550
-c18_E100.0_D12.8,100.0,12.8,78.4862,0.0567
-c18_E100.0_D12.8,100.0,12.8,80.2472,0.0848
-c18_E100.0_D12.8,100.0,12.8,82.2906,0.2408
-c18_E100.0_D12.8,100.0,12.8,84.6881,0.0179
-c18_E100.0_D12.8,100.0,12.8,86.2838,0.2172
-c18_E100.0_D12.8,100.0,12.8,89.4457,0.1277
-c18_E100.0_D12.8,100.0,12.8,91.2177,0.0755
-c18_E100.0_D12.8,100.0,12.8,93.2790,-0.0634
-c18_E100.0_D12.8,100.0,12.8,95.1625,0.1275
-c18_E100.0_D12.8,100.0,12.8,96.7985,-0.0063
-c18_E100.0_D12.8,100.0,12.8,98.7884,0.1420
-c18_E100.0_D12.8,100.0,12.8,100.5694,0.0888
-c18_E100.0_D12.8,100.0,12.8,102.9485,-0.1324
-c18_E100.0_D12.8,100.0,12.8,104.7154,0.2046
-c18_E100.0_D12.8,100.0,12.8,106.4927,0.1709
-c18_E100.0_D12.8,100.0,12.8,108.9288,0.2606
-c18_E100.0_D12.8,100.0,12.8,110.8833,-0.0128
-c18_E100.0_D12.8,100.0,12.8,112.9366,-0.0708
-c18_E100.0_D12.8,100.0,12.8,114.9296,0.3280
-c18_E100.0_D12.8,100.0,12.8,116.9300,0.2342
-c18_E100.0_D12.8,100.0,12.8,118.6139,0.1133
-c18_E100.0_D12.8,100.0,12.8,121.1475,0.0530
-c18_E100.0_D12.8,100.0,12.8,122.9558,0.2126
-c18_E100.0_D12.8,100.0,12.8,125.4409,0.0028
-c18_E100.0_D12.8,100.0,12.8,127.3835,-0.0425
-c18_E100.0_D12.8,100.0,12.8,129.4160,-0.0476
-c18_E100.0_D12.8,100.0,12.8,131.3579,0.0739
-c18_E100.0_D12.8,100.0,12.8,133.2616,0.2386
-c18_E100.0_D12.8,100.0,12.8,134.9246,0.0320
-c18_E100.0_D12.8,100.0,12.8,137.8077,0.0513
-c18_E100.0_D12.8,100.0,12.8,139.8138,0.1272
-c18_E100.0_D12.8,100.0,12.8,141.6921,0.1388
-c18_E100.0_D12.8,100.0,12.8,143.6857,0.1173
-c18_E100.0_D12.8,100.0,12.8,145.5791,0.0690
-c18_E100.0_D12.8,100.0,12.8,147.4753,-0.0625
-c18_E100.0_D12.8,100.0,12.8,149.4811,-0.0206
-c18_E100.0_D12.8,100.0,12.8,151.6386,-0.0308
-c18_E100.0_D12.8,100.0,12.8,153.6305,-0.1982
-c18_E100.0_D12.8,100.0,12.8,156.0490,0.0314
-c18_E100.0_D12.8,100.0,12.8,157.2449,0.1578
-c18_E100.0_D12.8,100.0,12.8,160.1365,-0.0402
-c18_E100.0_D12.8,100.0,12.8,161.1894,0.0562
-c18_E100.0_D12.8,100.0,12.8,164.1942,0.0257
-c18_E100.0_D12.8,100.0,12.8,165.6258,0.1493
-c18_E100.0_D12.8,100.0,12.8,167.6385,0.0171
-c18_E100.0_D12.8,100.0,12.8,169.9364,0.0554
-c18_E100.0_D12.8,100.0,12.8,171.1225,0.2133
-c18_E100.0_D12.8,100.0,12.8,174.0714,0.1118
-c18_E100.0_D12.8,100.0,12.8,175.2862,0.1831
-c18_E100.0_D12.8,100.0,12.8,178.0099,0.0126
-c18_E100.0_D12.8,100.0,12.8,179.9948,0.2848
-c18_E100.0_D12.8,100.0,12.8,182.0590,-0.0447
-c18_E100.0_D12.8,100.0,12.8,183.9518,0.1340
-c18_E100.0_D12.8,100.0,12.8,186.1670,-0.1072
-c18_E100.0_D12.8,100.0,12.8,187.3475,0.0621
-c18_E100.0_D12.8,100.0,12.8,189.4359,0.0896
-c18_E100.0_D12.8,100.0,12.8,191.3357,-0.0314
-c18_E100.0_D12.8,100.0,12.8,193.4516,0.0526
-c18_E100.0_D12.8,100.0,12.8,196.0703,-0.2019
-c18_E100.0_D12.8,100.0,12.8,197.4938,-0.0862
-c18_E100.0_D12.8,100.0,12.8,199.8718,0.0997
-c19_E100.0_D13.2,100.0,13.2,0.0000,3.1382
-c19_E100.0_D13.2,100.0,13.2,2.3370,2.7780
-c19_E100.0_D13.2,100.0,13.2,4.3997,2.3696
-c19_E100.0_D13.2,100.0,13.2,6.0691,1.9900
-c19_E100.0_D13.2,100.0,13.2,8.1674,1.3987
-c19_E100.0_D13.2,100.0,13.2,10.4290,1.0778
-c19_E100.0_D13.2,100.0,13.2,12.6066,0.0277
-c19_E100.0_D13.2,100.0,13.2,13.5678,-0.2475
-c19_E100.0_D13.2,100.0,13.2,16.6493,-1.4596
-c19_E100.0_D13.2,100.0,13.2,18.5878,-1.9496
-c19_E100.0_D13.2,100.0,13.2,20.7676,-2.7239
-c19_E100.0_D13.2,100.0,13.2,22.7896,-3.5772
-c19_E100.0_D13.2,100.0,13.2,24.5837,-4.3677
-c19_E100.0_D13.2,100.0,13.2,26.4102,-5.0336
-c19_E100.0_D13.2,100.0,13.2,28.3996,-5.9507
-c19_E100.0_D13.2,100.0,13.2,30.0359,-6.2131
-c19_E100.0_D13.2,100.0,13.2,32.3133,-6.8930
-c19_E100.0_D13.2,100.0,13.2,33.9602,-7.1984
-c19_E100.0_D13.2,100.0,13.2,36.5244,-7.4192
-c19_E100.0_D13.2,100.0,13.2,38.9393,-7.6048
-c19_E100.0_D13.2,100.0,13.2,40.7200,-7.6589
-c19_E100.0_D13.2,100.0,13.2,42.6080,-7.2704
-c19_E100.0_D13.2,100.0,13.2,44.6196,-7.0426
-c19_E100.0_D13.2,100.0,13.2,46.0019,-6.7610
-c19_E100.0_D13.2,100.0,13.2,48.4920,-5.9832
-c19_E100.0_D13.2,100.0,13.2,50.5640,-5.1747
-c19_E100.0_D13.2,100.0,13.2,52.6737,-4.7329
-c19_E100.0_D13.2,100.0,13.2,54.1607,-4.1912
-c19_E100.0_D13.2,100.0,13.2,56.7053,-3.3917
-c19_E100.0_D13.2,100.0,13.2,59.0871,-2.7499
-c19_E100.0_D13.2,100.0,13.2,60.0355,-2.6228
-c19_E100.0_D13.2,100.0,13.2,63.0778,-1.7507
-c19_E100.0_D13.2,100.0,13.2,64.6759,-1.2808
-c19_E100.0_D13.2,100.0,13.2,67.0772,-0.8133
-c19_E100.0_D13.2,100.0,13.2,68.4999,-0.7213
-c19_E100.0_D13.2,100.0,13.2,70.6717,-0.5109
-c19_E100.0_D13.2,100.0,13.2,72.2173,-0.4471
-c19_E100.0_D13.2,100.0,13.2,74.3761,-0.1605
-c19_E100.0_D13.2,100.0,13.2,77.0531,0.0888
-c19_E100.0_D13.2,100.0,13.2,78.8898,-0.1050
-c19_E100.0_D13.2,100.0,13.2,80.5543,0.1235
-c19_E100.0_D13.2,100.0,13.2,83.2147,0.0012
-c19_E100.0_D13.2,100.0,13.2,84.4915,0.1757
-c19_E100.0_D13.2,100.0,13.2,86.5645,0.0991
-c19_E100.0_D13.2,100.0,13.2,88.5454,0.1000
-c19_E100.0_D13.2,100.0,13.2,91.1077,0.0734
-c19_E100.0_D13.2,100.0,13.2,92.5806,0.0960
-c19_E100.0_D13.2,100.0,13.2,94.3473,0.1427
-c19_E100.0_D13.2,100.0,13.2,96.9580,0.2044
-c19_E100.0_D13.2,100.0,13.2,98.4953,0.1455
-c19_E100.0_D13.2,100.0,13.2,101.2959,0.1311
-c19_E100.0_D13.2,100.0,13.2,102.6990,0.0336
-c19_E100.0_D13.2,100.0,13.2,104.5348,0.0670
-c19_E100.0_D13.2,100.0,13.2,107.0636,0.0046
-c19_E100.0_D13.2,100.0,13.2,109.5979,0.1341
-c19_E100.0_D13.2,100.0,13.2,110.8405,0.1183
-c19_E100.0_D13.2,100.0,13.2,113.6181,0.1386
-c19_E100.0_D13.2,100.0,13.2,114.9618,0.2995
-c19_E100.0_D13.2,100.0,13.2,116.6689,-0.0860
-c19_E100.0_D13.2,100.0,13.2,119.5280,0.0768
-c19_E100.0_D13.2,100.0,13.2,121.3525,0.1518
-c19_E100.0_D13.2,100.0,13.2,122.7944,0.0501
-c19_E100.0_D13.2,100.0,13.2,125.4539,0.0888
-c19_E100.0_D13.2,100.0,13.2,127.3897,0.0135
-c19_E100.0_D13.2,100.0,13.2,129.4638,0.1377
-c19_E100.0_D13.2,100.0,13.2,131.6865,0.1318
-c19_E100.0_D13.2,100.0,13.2,133.1680,0.1399
-c19_E100.0_D13.2,100.0,13.2,135.1770,-0.0748
-c19_E100.0_D13.2,100.0,13.2,136.9241,0.0935
-c19_E100.0_D13.2,100.0,13.2,139.9177,-0.0516
-c19_E100.0_D13.2,100.0,13.2,141.8585,0.1351
-c19_E100.0_D13.2,100.0,13.2,143.0117,-0.0339
-c19_E100.0_D13.2,100.0,13.2,145.9052,0.0882
-c19_E100.0_D13.2,100.0,13.2,146.9233,0.1149
-c19_E100.0_D13.2,100.0,13.2,149.7560,-0.1826
-c19_E100.0_D13.2,100.0,13.2,151.4372,-0.0662
-c19_E100.0_D13.2,100.0,13.2,152.9766,0.1606
-c19_E100.0_D13.2,100.0,13.2,155.9463,0.0016
-c19_E100.0_D13.2,100.0,13.2,157.3699,-0.1685
-c19_E100.0_D13.2,100.0,13.2,159.1045,-0.2761
-c19_E100.0_D13.2,100.0,13.2,161.7450,-0.0453
-c19_E100.0_D13.2,100.0,13.2,163.0504,0.1773
-c19_E100.0_D13.2,100.0,13.2,165.8223,-0.1276
-c19_E100.0_D13.2,100.0,13.2,168.2164,0.1280
-c19_E100.0_D13.2,100.0,13.2,170.1106,-0.0645
-c19_E100.0_D13.2,100.0,13.2,171.2140,0.1499
-c19_E100.0_D13.2,100.0,13.2,174.2972,0.0988
-c19_E100.0_D13.2,100.0,13.2,175.9141,-0.0537
-c19_E100.0_D13.2,100.0,13.2,177.6444,-0.1217
-c19_E100.0_D13.2,100.0,13.2,180.2897,0.0143
-c19_E100.0_D13.2,100.0,13.2,182.3135,0.0526
-c19_E100.0_D13.2,100.0,13.2,183.6947,-0.0706
-c19_E100.0_D13.2,100.0,13.2,186.1946,-0.0527
-c19_E100.0_D13.2,100.0,13.2,188.0453,0.2444
-c19_E100.0_D13.2,100.0,13.2,190.0514,-0.0356
-c19_E100.0_D13.2,100.0,13.2,192.2107,0.0105
-c19_E100.0_D13.2,100.0,13.2,193.9560,-0.0564
-c19_E100.0_D13.2,100.0,13.2,195.9807,0.1025
-c19_E100.0_D13.2,100.0,13.2,198.0897,-0.0852
-c19_E100.0_D13.2,100.0,13.2,200.0000,0.1009
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,0.0000,3.0573
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,1.7344,2.6971
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,3.5738,2.3123
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,6.5005,1.7121
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,8.3908,1.3868
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,10.0096,0.8655
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,11.6285,0.4439
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,14.5217,-0.4975
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,15.9111,-0.8872
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,18.7221,-1.9665
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,20.1255,-2.5525
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,21.6485,-3.2375
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,24.5142,-4.4449
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,25.9379,-4.9651
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,28.8598,-5.9106
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,30.7162,-6.6588
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,32.8376,-7.1681
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,34.7202,-7.2566
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,36.1418,-7.4085
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,38.0267,-7.5368
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,40.7578,-7.5738
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,42.2076,-7.3876
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,44.6271,-7.0655
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,46.0267,-6.7603
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,48.0259,-6.0943
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,50.1631,-5.5262
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,52.7538,-4.5934
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,54.8274,-3.9824
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,56.7898,-3.3555
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,59.1701,-2.6783
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,60.0874,-2.3754
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,63.0814,-1.5946
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,64.6552,-1.4323
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,66.7544,-0.9592
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,68.8450,-0.7432
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,71.2696,-0.4727
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,72.2820,-0.2856
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,74.6048,-0.0925
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,76.4071,-0.0149
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,79.3796,-0.1027
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,80.3562,0.1611
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,82.4488,0.0730
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,84.8019,0.2394
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,86.5272,0.1610
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,88.7245,0.0926
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,90.7782,0.1501
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,93.1393,0.0254
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,95.3270,0.1228
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,96.3863,0.0099
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,99.5433,0.0289
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,101.3650,0.2656
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,103.3080,0.3255
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,105.0318,0.1836
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,106.7934,0.0864
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,109.2627,0.0043
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,111.1432,0.0544
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,113.1195,0.0351
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,115.5326,0.1021
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,116.8560,0.0899
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,119.1670,0.1112
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,121.4807,0.1386
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,122.6693,-0.0215
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,124.8665,0.0384
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,127.4249,-0.2363
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,129.1451,0.1052
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,131.4954,-0.0040
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,133.9169,0.0318
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,135.4259,-0.0341
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,137.8652,0.1317
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,139.4280,-0.0154
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,141.0741,-0.1012
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,143.0290,0.0258
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,145.6012,0.0296
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,147.4230,0.0595
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,149.6375,0.0837
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,151.3520,0.1031
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,153.7321,-0.0270
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,155.3241,0.0853
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,157.3733,-0.2568
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,159.0928,-0.1206
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,161.8889,-0.0912
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,163.8660,-0.0682
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,165.6991,0.2168
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,168.0865,0.0477
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,170.0624,-0.0526
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,171.1886,0.2139
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,173.9918,0.1569
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,175.3548,-0.0898
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,177.2843,0.1456
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,179.2968,-0.0886
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,181.7947,0.0247
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,184.3003,0.1251
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,186.0521,0.2089
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,187.8942,-0.1885
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,190.4910,0.0412
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,192.0525,-0.0397
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,193.4221,-0.0934
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,195.9837,-0.0889
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,197.4260,0.0811
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,199.8211,0.0791
-c21_E100.0_D14.0,100.0,14.0,0.3470,3.1113
-c21_E100.0_D14.0,100.0,14.0,2.4854,2.8150
-c21_E100.0_D14.0,100.0,14.0,3.9420,2.4698
-c21_E100.0_D14.0,100.0,14.0,6.4398,1.7284
-c21_E100.0_D14.0,100.0,14.0,8.2296,1.5018
-c21_E100.0_D14.0,100.0,14.0,10.3828,0.8239
-c21_E100.0_D14.0,100.0,14.0,12.2403,0.2806
-c21_E100.0_D14.0,100.0,14.0,13.9855,-0.3838
-c21_E100.0_D14.0,100.0,14.0,16.1464,-1.0756
-c21_E100.0_D14.0,100.0,14.0,17.8266,-1.8614
-c21_E100.0_D14.0,100.0,14.0,19.8271,-2.5956
-c21_E100.0_D14.0,100.0,14.0,22.7177,-3.6423
-c21_E100.0_D14.0,100.0,14.0,23.9953,-4.2510
-c21_E100.0_D14.0,100.0,14.0,26.3899,-5.1081
-c21_E100.0_D14.0,100.0,14.0,28.8862,-6.1470
-c21_E100.0_D14.0,100.0,14.0,30.5374,-6.6373
-c21_E100.0_D14.0,100.0,14.0,32.8251,-7.1047
-c21_E100.0_D14.0,100.0,14.0,34.0423,-7.3110
-c21_E100.0_D14.0,100.0,14.0,36.6579,-7.5972
-c21_E100.0_D14.0,100.0,14.0,38.2523,-7.7649
-c21_E100.0_D14.0,100.0,14.0,40.5284,-7.8366
-c21_E100.0_D14.0,100.0,14.0,42.3443,-7.4120
-c21_E100.0_D14.0,100.0,14.0,44.6444,-6.9009
-c21_E100.0_D14.0,100.0,14.0,46.5429,-6.6446
-c21_E100.0_D14.0,100.0,14.0,48.9888,-5.9967
-c21_E100.0_D14.0,100.0,14.0,50.9427,-5.3021
-c21_E100.0_D14.0,100.0,14.0,52.6134,-4.6923
-c21_E100.0_D14.0,100.0,14.0,55.0635,-3.8165
-c21_E100.0_D14.0,100.0,14.0,56.3136,-3.3770
-c21_E100.0_D14.0,100.0,14.0,58.0792,-3.1744
-c21_E100.0_D14.0,100.0,14.0,60.1461,-2.2010
-c21_E100.0_D14.0,100.0,14.0,62.0918,-1.8024
-c21_E100.0_D14.0,100.0,14.0,64.3957,-1.5617
-c21_E100.0_D14.0,100.0,14.0,66.6898,-0.8730
-c21_E100.0_D14.0,100.0,14.0,69.2110,-0.5681
-c21_E100.0_D14.0,100.0,14.0,70.7991,-0.3678
-c21_E100.0_D14.0,100.0,14.0,72.1579,-0.3436
-c21_E100.0_D14.0,100.0,14.0,74.2704,-0.1478
-c21_E100.0_D14.0,100.0,14.0,76.9951,-0.0513
-c21_E100.0_D14.0,100.0,14.0,78.8856,-0.0558
-c21_E100.0_D14.0,100.0,14.0,80.6908,-0.0558
-c21_E100.0_D14.0,100.0,14.0,83.4264,-0.0420
-c21_E100.0_D14.0,100.0,14.0,84.6617,0.0836
-c21_E100.0_D14.0,100.0,14.0,86.5112,0.1452
-c21_E100.0_D14.0,100.0,14.0,88.8692,0.1120
-c21_E100.0_D14.0,100.0,14.0,91.1075,0.1408
-c21_E100.0_D14.0,100.0,14.0,92.5330,0.1298
-c21_E100.0_D14.0,100.0,14.0,94.5602,0.1976
-c21_E100.0_D14.0,100.0,14.0,96.6589,0.1599
-c21_E100.0_D14.0,100.0,14.0,99.1065,0.1627
-c21_E100.0_D14.0,100.0,14.0,101.3221,0.1113
-c21_E100.0_D14.0,100.0,14.0,103.2707,0.1523
-c21_E100.0_D14.0,100.0,14.0,105.3256,0.1001
-c21_E100.0_D14.0,100.0,14.0,107.3089,0.0610
-c21_E100.0_D14.0,100.0,14.0,109.3402,-0.0187
-c21_E100.0_D14.0,100.0,14.0,110.8222,0.0565
-c21_E100.0_D14.0,100.0,14.0,113.1371,0.2365
-c21_E100.0_D14.0,100.0,14.0,115.4736,0.1439
-c21_E100.0_D14.0,100.0,14.0,117.6603,-0.1125
-c21_E100.0_D14.0,100.0,14.0,119.3141,0.0450
-c21_E100.0_D14.0,100.0,14.0,121.7735,-0.0093
-c21_E100.0_D14.0,100.0,14.0,123.5539,0.1704
-c21_E100.0_D14.0,100.0,14.0,125.5083,-0.0450
-c21_E100.0_D14.0,100.0,14.0,127.4996,-0.1249
-c21_E100.0_D14.0,100.0,14.0,128.9408,-0.0725
-c21_E100.0_D14.0,100.0,14.0,131.0439,-0.0074
-c21_E100.0_D14.0,100.0,14.0,132.8173,0.0379
-c21_E100.0_D14.0,100.0,14.0,135.2601,-0.0275
-c21_E100.0_D14.0,100.0,14.0,137.0811,-0.1162
-c21_E100.0_D14.0,100.0,14.0,139.8066,-0.0051
-c21_E100.0_D14.0,100.0,14.0,141.4864,0.0236
-c21_E100.0_D14.0,100.0,14.0,143.5431,0.1452
-c21_E100.0_D14.0,100.0,14.0,145.0067,0.0494
-c21_E100.0_D14.0,100.0,14.0,146.9478,-0.0355
-c21_E100.0_D14.0,100.0,14.0,149.1377,0.1914
-c21_E100.0_D14.0,100.0,14.0,151.0163,-0.0513
-c21_E100.0_D14.0,100.0,14.0,154.0761,0.0774
-c21_E100.0_D14.0,100.0,14.0,155.5771,0.1044
-c21_E100.0_D14.0,100.0,14.0,157.7323,-0.1254
-c21_E100.0_D14.0,100.0,14.0,159.0463,-0.0117
-c21_E100.0_D14.0,100.0,14.0,161.4174,0.0444
-c21_E100.0_D14.0,100.0,14.0,163.3858,0.0136
-c21_E100.0_D14.0,100.0,14.0,166.0709,0.0962
-c21_E100.0_D14.0,100.0,14.0,168.0583,0.1129
-c21_E100.0_D14.0,100.0,14.0,170.1022,0.0915
-c21_E100.0_D14.0,100.0,14.0,171.4344,0.1070
-c21_E100.0_D14.0,100.0,14.0,173.4199,0.0668
-c21_E100.0_D14.0,100.0,14.0,175.8370,0.0263
-c21_E100.0_D14.0,100.0,14.0,178.0254,0.1135
-c21_E100.0_D14.0,100.0,14.0,179.8100,0.0616
-c21_E100.0_D14.0,100.0,14.0,181.9152,0.0006
-c21_E100.0_D14.0,100.0,14.0,183.3871,-0.0492
-c21_E100.0_D14.0,100.0,14.0,186.3568,-0.0550
-c21_E100.0_D14.0,100.0,14.0,187.7559,0.1009
-c21_E100.0_D14.0,100.0,14.0,189.7714,0.0378
-c21_E100.0_D14.0,100.0,14.0,191.5274,-0.2101
-c21_E100.0_D14.0,100.0,14.0,194.4460,-0.1482
-c21_E100.0_D14.0,100.0,14.0,195.6791,-0.0046
-c21_E100.0_D14.0,100.0,14.0,197.4134,-0.0567
-c21_E100.0_D14.0,100.0,14.0,199.4161,0.0142
diff --git a/data/io/potential_long_jagged.csv b/data/io/potential_long_jagged.csv
deleted file mode 100644
--- a/data/io/potential_long_jagged.csv
+++ /dev/null
@@ -1,1710 +0,0 @@
-name,energy,dose,z,y
-c01_E100.0_D6.0,100.0,6.0,0.0000,3.0386
-c01_E100.0_D6.0,100.0,6.0,2.3163,2.9069
-c01_E100.0_D6.0,100.0,6.0,4.6672,2.3650
-c01_E100.0_D6.0,100.0,6.0,6.6790,1.9684
-c01_E100.0_D6.0,100.0,6.0,7.6206,1.9457
-c01_E100.0_D6.0,100.0,6.0,11.3970,0.8275
-c01_E100.0_D6.0,100.0,6.0,11.5377,0.8962
-c01_E100.0_D6.0,100.0,6.0,19.0672,-1.5304
-c01_E100.0_D6.0,100.0,6.0,19.1905,-1.6508
-c01_E100.0_D6.0,100.0,6.0,20.6933,-1.9465
-c01_E100.0_D6.0,100.0,6.0,22.1883,-2.5133
-c01_E100.0_D6.0,100.0,6.0,27.3402,-3.9741
-c01_E100.0_D6.0,100.0,6.0,28.0114,-4.2872
-c01_E100.0_D6.0,100.0,6.0,29.3690,-4.6514
-c01_E100.0_D6.0,100.0,6.0,29.7754,-4.7029
-c01_E100.0_D6.0,100.0,6.0,31.8086,-5.2809
-c01_E100.0_D6.0,100.0,6.0,39.1809,-6.0844
-c01_E100.0_D6.0,100.0,6.0,40.3232,-5.9410
-c01_E100.0_D6.0,100.0,6.0,40.4608,-6.0019
-c01_E100.0_D6.0,100.0,6.0,40.7976,-5.9860
-c01_E100.0_D6.0,100.0,6.0,45.7983,-5.3682
-c01_E100.0_D6.0,100.0,6.0,46.3748,-5.2730
-c01_E100.0_D6.0,100.0,6.0,53.1603,-3.6348
-c01_E100.0_D6.0,100.0,6.0,53.7814,-3.3212
-c01_E100.0_D6.0,100.0,6.0,55.0527,-3.0180
-c01_E100.0_D6.0,100.0,6.0,58.4335,-2.2359
-c01_E100.0_D6.0,100.0,6.0,59.3322,-2.1430
-c01_E100.0_D6.0,100.0,6.0,60.6439,-1.6402
-c01_E100.0_D6.0,100.0,6.0,61.5996,-1.6136
-c01_E100.0_D6.0,100.0,6.0,65.7056,-0.7573
-c01_E100.0_D6.0,100.0,6.0,71.3068,-0.3127
-c01_E100.0_D6.0,100.0,6.0,71.9123,-0.1020
-c01_E100.0_D6.0,100.0,6.0,72.4626,-0.2554
-c01_E100.0_D6.0,100.0,6.0,74.3181,-0.0784
-c01_E100.0_D6.0,100.0,6.0,76.0848,0.2438
-c01_E100.0_D6.0,100.0,6.0,78.2496,0.1979
-c01_E100.0_D6.0,100.0,6.0,80.5331,0.1904
-c01_E100.0_D6.0,100.0,6.0,80.6883,0.1277
-c01_E100.0_D6.0,100.0,6.0,81.7912,0.1717
-c01_E100.0_D6.0,100.0,6.0,87.2711,0.1826
-c01_E100.0_D6.0,100.0,6.0,90.6213,0.2565
-c01_E100.0_D6.0,100.0,6.0,92.7028,0.0112
-c01_E100.0_D6.0,100.0,6.0,95.4858,0.0914
-c01_E100.0_D6.0,100.0,6.0,96.1883,0.0195
-c01_E100.0_D6.0,100.0,6.0,97.3616,0.1200
-c01_E100.0_D6.0,100.0,6.0,100.4861,0.1724
-c01_E100.0_D6.0,100.0,6.0,106.0258,0.1743
-c01_E100.0_D6.0,100.0,6.0,107.0719,0.2766
-c01_E100.0_D6.0,100.0,6.0,107.1071,0.1301
-c01_E100.0_D6.0,100.0,6.0,108.4521,0.0594
-c01_E100.0_D6.0,100.0,6.0,111.5461,0.0770
-c01_E100.0_D6.0,100.0,6.0,116.6512,0.0670
-c01_E100.0_D6.0,100.0,6.0,120.4586,0.1086
-c01_E100.0_D6.0,100.0,6.0,122.0410,0.0105
-c01_E100.0_D6.0,100.0,6.0,122.6275,0.2232
-c01_E100.0_D6.0,100.0,6.0,125.0385,0.1179
-c01_E100.0_D6.0,100.0,6.0,130.9424,0.0025
-c01_E100.0_D6.0,100.0,6.0,132.4513,0.1669
-c01_E100.0_D6.0,100.0,6.0,133.0874,0.0647
-c01_E100.0_D6.0,100.0,6.0,136.7667,0.0065
-c01_E100.0_D6.0,100.0,6.0,138.3216,0.1047
-c01_E100.0_D6.0,100.0,6.0,140.6066,0.1460
-c01_E100.0_D6.0,100.0,6.0,140.8377,0.0303
-c01_E100.0_D6.0,100.0,6.0,146.0054,0.0927
-c01_E100.0_D6.0,100.0,6.0,146.1063,-0.2626
-c01_E100.0_D6.0,100.0,6.0,152.8991,0.1764
-c01_E100.0_D6.0,100.0,6.0,154.0685,0.0400
-c01_E100.0_D6.0,100.0,6.0,155.0307,0.1488
-c01_E100.0_D6.0,100.0,6.0,160.9483,-0.1129
-c01_E100.0_D6.0,100.0,6.0,164.2455,0.0591
-c01_E100.0_D6.0,100.0,6.0,168.2051,-0.0164
-c01_E100.0_D6.0,100.0,6.0,168.2625,-0.1583
-c01_E100.0_D6.0,100.0,6.0,168.9853,0.0356
-c01_E100.0_D6.0,100.0,6.0,171.0899,0.0073
-c01_E100.0_D6.0,100.0,6.0,175.7250,0.0860
-c01_E100.0_D6.0,100.0,6.0,176.3093,-0.0037
-c01_E100.0_D6.0,100.0,6.0,176.8245,0.0300
-c01_E100.0_D6.0,100.0,6.0,180.7714,0.0163
-c01_E100.0_D6.0,100.0,6.0,183.5248,-0.1568
-c01_E100.0_D6.0,100.0,6.0,188.9713,0.1459
-c01_E100.0_D6.0,100.0,6.0,189.4223,-0.0005
-c01_E100.0_D6.0,100.0,6.0,191.2886,-0.0619
-c01_E100.0_D6.0,100.0,6.0,196.7753,-0.0855
-c01_E100.0_D6.0,100.0,6.0,200.0000,0.0351
-c02_E100.0_D6.4,100.0,6.4,0.0000,3.2350
-c02_E100.0_D6.4,100.0,6.4,5.7644,1.9298
-c02_E100.0_D6.4,100.0,6.4,7.5689,1.6852
-c02_E100.0_D6.4,100.0,6.4,11.1816,1.0081
-c02_E100.0_D6.4,100.0,6.4,11.4024,0.8827
-c02_E100.0_D6.4,100.0,6.4,15.4842,-0.2030
-c02_E100.0_D6.4,100.0,6.4,22.4539,-2.3560
-c02_E100.0_D6.4,100.0,6.4,23.0348,-2.8987
-c02_E100.0_D6.4,100.0,6.4,25.2131,-3.4885
-c02_E100.0_D6.4,100.0,6.4,26.8702,-4.1113
-c02_E100.0_D6.4,100.0,6.4,29.4998,-4.9675
-c02_E100.0_D6.4,100.0,6.4,30.1652,-5.1178
-c02_E100.0_D6.4,100.0,6.4,31.6207,-5.2793
-c02_E100.0_D6.4,100.0,6.4,36.4268,-5.9872
-c02_E100.0_D6.4,100.0,6.4,40.0971,-6.2521
-c02_E100.0_D6.4,100.0,6.4,41.1331,-6.3181
-c02_E100.0_D6.4,100.0,6.4,42.8332,-5.9275
-c02_E100.0_D6.4,100.0,6.4,43.6603,-5.9063
-c02_E100.0_D6.4,100.0,6.4,45.2752,-5.4567
-c02_E100.0_D6.4,100.0,6.4,51.2064,-3.9922
-c02_E100.0_D6.4,100.0,6.4,53.7085,-3.4698
-c02_E100.0_D6.4,100.0,6.4,55.0358,-3.1101
-c02_E100.0_D6.4,100.0,6.4,58.7753,-2.3231
-c02_E100.0_D6.4,100.0,6.4,60.6860,-1.7773
-c02_E100.0_D6.4,100.0,6.4,63.2488,-1.2284
-c02_E100.0_D6.4,100.0,6.4,64.8220,-0.9346
-c02_E100.0_D6.4,100.0,6.4,69.7037,-0.4477
-c02_E100.0_D6.4,100.0,6.4,70.2457,-0.3745
-c02_E100.0_D6.4,100.0,6.4,70.3342,-0.4292
-c02_E100.0_D6.4,100.0,6.4,70.8084,-0.4264
-c02_E100.0_D6.4,100.0,6.4,76.1482,-0.1071
-c02_E100.0_D6.4,100.0,6.4,77.8323,0.0124
-c02_E100.0_D6.4,100.0,6.4,80.4512,0.1033
-c02_E100.0_D6.4,100.0,6.4,80.5486,0.1691
-c02_E100.0_D6.4,100.0,6.4,80.5767,-0.0483
-c02_E100.0_D6.4,100.0,6.4,85.6998,0.0316
-c02_E100.0_D6.4,100.0,6.4,86.4441,0.0957
-c02_E100.0_D6.4,100.0,6.4,91.8420,0.2477
-c02_E100.0_D6.4,100.0,6.4,92.0484,0.1824
-c02_E100.0_D6.4,100.0,6.4,94.0253,0.2678
-c02_E100.0_D6.4,100.0,6.4,95.0635,0.1477
-c02_E100.0_D6.4,100.0,6.4,97.0448,-0.0941
-c02_E100.0_D6.4,100.0,6.4,100.7326,-0.0546
-c02_E100.0_D6.4,100.0,6.4,100.7665,0.1613
-c02_E100.0_D6.4,100.0,6.4,100.9426,-0.0237
-c02_E100.0_D6.4,100.0,6.4,102.0965,0.1388
-c02_E100.0_D6.4,100.0,6.4,103.1834,-0.0130
-c02_E100.0_D6.4,100.0,6.4,106.5198,0.3850
-c02_E100.0_D6.4,100.0,6.4,108.8360,0.0698
-c02_E100.0_D6.4,100.0,6.4,111.5732,0.1920
-c02_E100.0_D6.4,100.0,6.4,117.4798,0.0736
-c02_E100.0_D6.4,100.0,6.4,118.9893,0.0145
-c02_E100.0_D6.4,100.0,6.4,126.0101,-0.0156
-c02_E100.0_D6.4,100.0,6.4,127.5532,0.0639
-c02_E100.0_D6.4,100.0,6.4,131.4319,-0.0576
-c02_E100.0_D6.4,100.0,6.4,133.4386,0.0053
-c02_E100.0_D6.4,100.0,6.4,133.4953,0.0339
-c02_E100.0_D6.4,100.0,6.4,133.8885,0.0489
-c02_E100.0_D6.4,100.0,6.4,134.9707,-0.0049
-c02_E100.0_D6.4,100.0,6.4,138.9830,-0.0051
-c02_E100.0_D6.4,100.0,6.4,145.7628,-0.0198
-c02_E100.0_D6.4,100.0,6.4,149.2418,0.0237
-c02_E100.0_D6.4,100.0,6.4,149.5181,0.0558
-c02_E100.0_D6.4,100.0,6.4,149.6826,0.0087
-c02_E100.0_D6.4,100.0,6.4,150.6095,0.1041
-c02_E100.0_D6.4,100.0,6.4,153.7849,0.0090
-c02_E100.0_D6.4,100.0,6.4,155.9657,-0.0006
-c02_E100.0_D6.4,100.0,6.4,161.2949,-0.0117
-c02_E100.0_D6.4,100.0,6.4,164.0572,-0.0160
-c02_E100.0_D6.4,100.0,6.4,164.2857,-0.0394
-c02_E100.0_D6.4,100.0,6.4,169.3439,0.0872
-c02_E100.0_D6.4,100.0,6.4,172.7436,0.1107
-c02_E100.0_D6.4,100.0,6.4,173.4974,0.0043
-c02_E100.0_D6.4,100.0,6.4,174.5896,-0.0974
-c02_E100.0_D6.4,100.0,6.4,178.6606,-0.1728
-c02_E100.0_D6.4,100.0,6.4,180.3718,-0.0377
-c02_E100.0_D6.4,100.0,6.4,181.1592,-0.0559
-c02_E100.0_D6.4,100.0,6.4,183.3154,0.0453
-c02_E100.0_D6.4,100.0,6.4,184.7530,0.0209
-c02_E100.0_D6.4,100.0,6.4,190.6402,0.0011
-c02_E100.0_D6.4,100.0,6.4,191.8152,0.0423
-c02_E100.0_D6.4,100.0,6.4,196.9623,0.0573
-c02_E100.0_D6.4,100.0,6.4,198.7293,-0.0993
-c02_E100.0_D6.4,100.0,6.4,200.0000,0.0703
-c02_E100.0_D6.4,100.0,6.4,200.0000,-0.0561
-c03_E100.0_D6.8,100.0,6.8,0.0000,3.0182
-c03_E100.0_D6.8,100.0,6.8,0.8206,3.1651
-c03_E100.0_D6.8,100.0,6.8,5.3705,2.0443
-c03_E100.0_D6.8,100.0,6.8,9.3360,1.4783
-c03_E100.0_D6.8,100.0,6.8,15.1693,-0.3027
-c03_E100.0_D6.8,100.0,6.8,17.2013,-1.0470
-c03_E100.0_D6.8,100.0,6.8,17.3181,-0.9931
-c03_E100.0_D6.8,100.0,6.8,17.3915,-1.0607
-c03_E100.0_D6.8,100.0,6.8,20.3683,-2.0905
-c03_E100.0_D6.8,100.0,6.8,21.4238,-2.4834
-c03_E100.0_D6.8,100.0,6.8,24.5577,-3.4497
-c03_E100.0_D6.8,100.0,6.8,25.2851,-3.5753
-c03_E100.0_D6.8,100.0,6.8,25.5608,-3.6101
-c03_E100.0_D6.8,100.0,6.8,28.7392,-4.7153
-c03_E100.0_D6.8,100.0,6.8,31.3721,-5.3358
-c03_E100.0_D6.8,100.0,6.8,36.4917,-6.2266
-c03_E100.0_D6.8,100.0,6.8,36.5814,-6.0730
-c03_E100.0_D6.8,100.0,6.8,37.4554,-6.1651
-c03_E100.0_D6.8,100.0,6.8,38.5032,-6.4253
-c03_E100.0_D6.8,100.0,6.8,42.7435,-6.0930
-c03_E100.0_D6.8,100.0,6.8,44.7542,-5.5337
-c03_E100.0_D6.8,100.0,6.8,49.6390,-4.6243
-c03_E100.0_D6.8,100.0,6.8,49.6893,-4.4775
-c03_E100.0_D6.8,100.0,6.8,51.5659,-4.0979
-c03_E100.0_D6.8,100.0,6.8,52.4610,-3.7784
-c03_E100.0_D6.8,100.0,6.8,55.9116,-2.9005
-c03_E100.0_D6.8,100.0,6.8,60.0436,-1.9758
-c03_E100.0_D6.8,100.0,6.8,61.3142,-1.6662
-c03_E100.0_D6.8,100.0,6.8,64.6153,-0.9896
-c03_E100.0_D6.8,100.0,6.8,66.9635,-0.6519
-c03_E100.0_D6.8,100.0,6.8,68.1466,-0.5611
-c03_E100.0_D6.8,100.0,6.8,68.6301,-0.4036
-c03_E100.0_D6.8,100.0,6.8,71.8626,-0.1674
-c03_E100.0_D6.8,100.0,6.8,75.4308,-0.0263
-c03_E100.0_D6.8,100.0,6.8,76.8651,-0.0325
-c03_E100.0_D6.8,100.0,6.8,80.1241,0.1592
-c03_E100.0_D6.8,100.0,6.8,82.1856,0.2755
-c03_E100.0_D6.8,100.0,6.8,83.0214,0.1392
-c03_E100.0_D6.8,100.0,6.8,89.2890,0.2214
-c03_E100.0_D6.8,100.0,6.8,90.4503,0.1773
-c03_E100.0_D6.8,100.0,6.8,92.4805,0.1899
-c03_E100.0_D6.8,100.0,6.8,95.8611,0.2714
-c03_E100.0_D6.8,100.0,6.8,99.4321,-0.0063
-c03_E100.0_D6.8,100.0,6.8,102.0937,-0.0436
-c03_E100.0_D6.8,100.0,6.8,105.7693,0.0469
-c03_E100.0_D6.8,100.0,6.8,106.3438,0.0872
-c03_E100.0_D6.8,100.0,6.8,107.7469,-0.0556
-c03_E100.0_D6.8,100.0,6.8,108.9821,0.0570
-c03_E100.0_D6.8,100.0,6.8,111.2274,0.1330
-c03_E100.0_D6.8,100.0,6.8,117.3086,0.0271
-c03_E100.0_D6.8,100.0,6.8,120.7725,0.1054
-c03_E100.0_D6.8,100.0,6.8,121.1870,0.0940
-c03_E100.0_D6.8,100.0,6.8,122.6047,0.1252
-c03_E100.0_D6.8,100.0,6.8,126.1362,0.0438
-c03_E100.0_D6.8,100.0,6.8,129.5488,0.3903
-c03_E100.0_D6.8,100.0,6.8,132.1542,-0.0274
-c03_E100.0_D6.8,100.0,6.8,132.8818,0.0680
-c03_E100.0_D6.8,100.0,6.8,134.4830,0.0008
-c03_E100.0_D6.8,100.0,6.8,139.1016,0.0779
-c03_E100.0_D6.8,100.0,6.8,139.7010,0.0309
-c03_E100.0_D6.8,100.0,6.8,145.3683,0.0044
-c03_E100.0_D6.8,100.0,6.8,148.5610,-0.0182
-c03_E100.0_D6.8,100.0,6.8,152.1686,-0.0877
-c03_E100.0_D6.8,100.0,6.8,155.0532,-0.1273
-c03_E100.0_D6.8,100.0,6.8,156.6017,0.0441
-c03_E100.0_D6.8,100.0,6.8,157.1300,0.1092
-c03_E100.0_D6.8,100.0,6.8,159.0531,0.1518
-c03_E100.0_D6.8,100.0,6.8,165.5813,-0.0417
-c03_E100.0_D6.8,100.0,6.8,167.8652,-0.0219
-c03_E100.0_D6.8,100.0,6.8,171.4954,0.0062
-c03_E100.0_D6.8,100.0,6.8,172.6496,0.1299
-c03_E100.0_D6.8,100.0,6.8,173.3562,0.1206
-c03_E100.0_D6.8,100.0,6.8,173.3567,-0.0118
-c03_E100.0_D6.8,100.0,6.8,177.4264,-0.1285
-c03_E100.0_D6.8,100.0,6.8,185.1037,-0.0300
-c03_E100.0_D6.8,100.0,6.8,185.2277,-0.0446
-c03_E100.0_D6.8,100.0,6.8,185.3008,-0.0819
-c03_E100.0_D6.8,100.0,6.8,185.9704,-0.0354
-c03_E100.0_D6.8,100.0,6.8,188.2102,-0.0824
-c03_E100.0_D6.8,100.0,6.8,188.4282,-0.0883
-c03_E100.0_D6.8,100.0,6.8,191.7423,0.0018
-c03_E100.0_D6.8,100.0,6.8,194.9691,0.0435
-c03_E100.0_D6.8,100.0,6.8,197.3621,-0.1138
-c03_E100.0_D6.8,100.0,6.8,198.6174,-0.0504
-c04_E100.0_D7.2,100.0,7.2,0.0000,3.1249
-c04_E100.0_D7.2,100.0,7.2,2.5386,2.7808
-c04_E100.0_D7.2,100.0,7.2,4.9565,2.3769
-c04_E100.0_D7.2,100.0,7.2,6.8599,1.9811
-c04_E100.0_D7.2,100.0,7.2,7.9467,1.6649
-c04_E100.0_D7.2,100.0,7.2,9.7811,1.1879
-c04_E100.0_D7.2,100.0,7.2,10.5000,1.0165
-c04_E100.0_D7.2,100.0,7.2,14.0230,0.0140
-c04_E100.0_D7.2,100.0,7.2,14.3749,0.0107
-c04_E100.0_D7.2,100.0,7.2,18.0498,-1.0971
-c04_E100.0_D7.2,100.0,7.2,18.2065,-1.1379
-c04_E100.0_D7.2,100.0,7.2,23.9302,-3.2693
-c04_E100.0_D7.2,100.0,7.2,24.0274,-3.2704
-c04_E100.0_D7.2,100.0,7.2,24.2393,-3.2453
-c04_E100.0_D7.2,100.0,7.2,26.1402,-4.0625
-c04_E100.0_D7.2,100.0,7.2,30.7956,-5.1563
-c04_E100.0_D7.2,100.0,7.2,35.1033,-6.0734
-c04_E100.0_D7.2,100.0,7.2,35.2964,-6.1789
-c04_E100.0_D7.2,100.0,7.2,36.9125,-6.1010
-c04_E100.0_D7.2,100.0,7.2,46.4361,-5.2304
-c04_E100.0_D7.2,100.0,7.2,47.8396,-5.1784
-c04_E100.0_D7.2,100.0,7.2,49.0211,-4.7852
-c04_E100.0_D7.2,100.0,7.2,53.2136,-3.6660
-c04_E100.0_D7.2,100.0,7.2,53.4530,-3.7261
-c04_E100.0_D7.2,100.0,7.2,54.7323,-3.1993
-c04_E100.0_D7.2,100.0,7.2,55.8177,-3.1493
-c04_E100.0_D7.2,100.0,7.2,63.8828,-1.0374
-c04_E100.0_D7.2,100.0,7.2,64.3770,-1.1641
-c04_E100.0_D7.2,100.0,7.2,64.7472,-1.0291
-c04_E100.0_D7.2,100.0,7.2,70.5286,-0.1723
-c04_E100.0_D7.2,100.0,7.2,72.7903,0.0420
-c04_E100.0_D7.2,100.0,7.2,75.6218,-0.0500
-c04_E100.0_D7.2,100.0,7.2,76.8024,-0.0085
-c04_E100.0_D7.2,100.0,7.2,76.9419,-0.0922
-c04_E100.0_D7.2,100.0,7.2,78.9812,0.0612
-c04_E100.0_D7.2,100.0,7.2,80.5527,-0.0229
-c04_E100.0_D7.2,100.0,7.2,81.5261,0.1845
-c04_E100.0_D7.2,100.0,7.2,92.1302,0.1339
-c04_E100.0_D7.2,100.0,7.2,96.4755,0.2768
-c04_E100.0_D7.2,100.0,7.2,97.2627,0.0962
-c04_E100.0_D7.2,100.0,7.2,98.6646,0.1859
-c04_E100.0_D7.2,100.0,7.2,101.9068,0.0735
-c04_E100.0_D7.2,100.0,7.2,103.2655,0.0831
-c04_E100.0_D7.2,100.0,7.2,103.5018,0.0167
-c04_E100.0_D7.2,100.0,7.2,104.0572,0.2232
-c04_E100.0_D7.2,100.0,7.2,104.8823,0.0636
-c04_E100.0_D7.2,100.0,7.2,109.2751,0.0220
-c04_E100.0_D7.2,100.0,7.2,113.1306,0.1835
-c04_E100.0_D7.2,100.0,7.2,114.5497,0.0312
-c04_E100.0_D7.2,100.0,7.2,117.9294,0.1496
-c04_E100.0_D7.2,100.0,7.2,119.8314,-0.1151
-c04_E100.0_D7.2,100.0,7.2,121.0234,0.1245
-c04_E100.0_D7.2,100.0,7.2,121.6645,-0.0021
-c04_E100.0_D7.2,100.0,7.2,124.8725,0.0774
-c04_E100.0_D7.2,100.0,7.2,126.2984,-0.0766
-c04_E100.0_D7.2,100.0,7.2,129.0481,0.0202
-c04_E100.0_D7.2,100.0,7.2,130.8869,0.0293
-c04_E100.0_D7.2,100.0,7.2,131.7385,0.0643
-c04_E100.0_D7.2,100.0,7.2,133.2390,0.0215
-c04_E100.0_D7.2,100.0,7.2,138.0680,-0.2108
-c04_E100.0_D7.2,100.0,7.2,138.1097,-0.0033
-c04_E100.0_D7.2,100.0,7.2,138.6168,-0.0296
-c04_E100.0_D7.2,100.0,7.2,141.1069,0.0810
-c04_E100.0_D7.2,100.0,7.2,144.6044,-0.0088
-c04_E100.0_D7.2,100.0,7.2,145.5844,-0.0317
-c04_E100.0_D7.2,100.0,7.2,152.7159,-0.0215
-c04_E100.0_D7.2,100.0,7.2,153.2508,-0.2678
-c04_E100.0_D7.2,100.0,7.2,153.2944,0.1676
-c04_E100.0_D7.2,100.0,7.2,154.1617,0.2067
-c04_E100.0_D7.2,100.0,7.2,158.3148,0.0120
-c04_E100.0_D7.2,100.0,7.2,160.5252,-0.0705
-c04_E100.0_D7.2,100.0,7.2,161.8715,0.0461
-c04_E100.0_D7.2,100.0,7.2,163.1550,-0.0174
-c04_E100.0_D7.2,100.0,7.2,169.2277,0.0330
-c04_E100.0_D7.2,100.0,7.2,169.8570,-0.1090
-c04_E100.0_D7.2,100.0,7.2,171.1296,0.0709
-c04_E100.0_D7.2,100.0,7.2,172.4998,0.0710
-c04_E100.0_D7.2,100.0,7.2,173.4180,0.0099
-c04_E100.0_D7.2,100.0,7.2,177.2845,0.0080
-c04_E100.0_D7.2,100.0,7.2,178.9129,-0.0122
-c04_E100.0_D7.2,100.0,7.2,180.0809,-0.0153
-c04_E100.0_D7.2,100.0,7.2,184.5785,-0.0932
-c04_E100.0_D7.2,100.0,7.2,189.9117,0.0444
-c04_E100.0_D7.2,100.0,7.2,191.5494,0.0226
-c04_E100.0_D7.2,100.0,7.2,192.8802,0.0909
-c04_E100.0_D7.2,100.0,7.2,193.6093,0.0841
-c04_E100.0_D7.2,100.0,7.2,196.5254,0.0946
-c04_E100.0_D7.2,100.0,7.2,200.0000,0.0909
-c05_E100.0_D7.6,100.0,7.6,0.0000,3.0871
-c05_E100.0_D7.6,100.0,7.6,5.7632,1.9983
-c05_E100.0_D7.6,100.0,7.6,7.0091,1.9572
-c05_E100.0_D7.6,100.0,7.6,7.6191,1.5713
-c05_E100.0_D7.6,100.0,7.6,7.8722,1.7606
-c05_E100.0_D7.6,100.0,7.6,10.9835,1.0977
-c05_E100.0_D7.6,100.0,7.6,12.6297,0.3981
-c05_E100.0_D7.6,100.0,7.6,21.5064,-2.4903
-c05_E100.0_D7.6,100.0,7.6,24.1796,-3.4067
-c05_E100.0_D7.6,100.0,7.6,29.0093,-5.0316
-c05_E100.0_D7.6,100.0,7.6,30.9678,-5.4854
-c05_E100.0_D7.6,100.0,7.6,31.6383,-5.7320
-c05_E100.0_D7.6,100.0,7.6,34.0426,-6.2704
-c05_E100.0_D7.6,100.0,7.6,35.2227,-6.3830
-c05_E100.0_D7.6,100.0,7.6,39.5631,-6.5800
-c05_E100.0_D7.6,100.0,7.6,41.4235,-6.1628
-c05_E100.0_D7.6,100.0,7.6,42.6269,-6.0523
-c05_E100.0_D7.6,100.0,7.6,44.9398,-5.7933
-c05_E100.0_D7.6,100.0,7.6,45.3911,-5.8817
-c05_E100.0_D7.6,100.0,7.6,46.6746,-5.4361
-c05_E100.0_D7.6,100.0,7.6,48.1776,-5.0894
-c05_E100.0_D7.6,100.0,7.6,48.3525,-5.0805
-c05_E100.0_D7.6,100.0,7.6,54.8334,-3.3320
-c05_E100.0_D7.6,100.0,7.6,56.2743,-2.9450
-c05_E100.0_D7.6,100.0,7.6,57.6160,-2.4843
-c05_E100.0_D7.6,100.0,7.6,59.5555,-2.0892
-c05_E100.0_D7.6,100.0,7.6,66.9043,-0.5084
-c05_E100.0_D7.6,100.0,7.6,68.8785,-0.5064
-c05_E100.0_D7.6,100.0,7.6,69.1180,-0.5424
-c05_E100.0_D7.6,100.0,7.6,71.7433,-0.1893
-c05_E100.0_D7.6,100.0,7.6,72.1984,-0.1971
-c05_E100.0_D7.6,100.0,7.6,79.0897,0.1675
-c05_E100.0_D7.6,100.0,7.6,80.2548,0.1387
-c05_E100.0_D7.6,100.0,7.6,80.3896,0.0828
-c05_E100.0_D7.6,100.0,7.6,82.2814,0.0853
-c05_E100.0_D7.6,100.0,7.6,84.6608,0.2280
-c05_E100.0_D7.6,100.0,7.6,89.4876,0.2125
-c05_E100.0_D7.6,100.0,7.6,90.6347,-0.0079
-c05_E100.0_D7.6,100.0,7.6,94.7139,0.1948
-c05_E100.0_D7.6,100.0,7.6,98.3683,0.2406
-c05_E100.0_D7.6,100.0,7.6,100.4579,0.3076
-c05_E100.0_D7.6,100.0,7.6,104.4272,0.1671
-c05_E100.0_D7.6,100.0,7.6,106.9526,0.2590
-c05_E100.0_D7.6,100.0,7.6,108.7359,0.2089
-c05_E100.0_D7.6,100.0,7.6,110.6250,0.1455
-c05_E100.0_D7.6,100.0,7.6,113.3203,-0.1186
-c05_E100.0_D7.6,100.0,7.6,114.9519,0.0804
-c05_E100.0_D7.6,100.0,7.6,115.3102,0.2247
-c05_E100.0_D7.6,100.0,7.6,117.8126,0.1241
-c05_E100.0_D7.6,100.0,7.6,120.2988,-0.1200
-c05_E100.0_D7.6,100.0,7.6,123.2456,0.1173
-c05_E100.0_D7.6,100.0,7.6,125.9119,0.3374
-c05_E100.0_D7.6,100.0,7.6,128.0591,-0.0283
-c05_E100.0_D7.6,100.0,7.6,128.3544,-0.0659
-c05_E100.0_D7.6,100.0,7.6,130.4500,0.0362
-c05_E100.0_D7.6,100.0,7.6,131.0474,0.2853
-c05_E100.0_D7.6,100.0,7.6,136.8969,0.0653
-c05_E100.0_D7.6,100.0,7.6,139.1634,0.0315
-c05_E100.0_D7.6,100.0,7.6,139.4329,0.0229
-c05_E100.0_D7.6,100.0,7.6,147.9645,0.0584
-c05_E100.0_D7.6,100.0,7.6,148.2202,0.0903
-c05_E100.0_D7.6,100.0,7.6,150.8234,0.1760
-c05_E100.0_D7.6,100.0,7.6,151.8213,0.0871
-c05_E100.0_D7.6,100.0,7.6,154.8407,0.0297
-c05_E100.0_D7.6,100.0,7.6,157.7497,-0.0459
-c05_E100.0_D7.6,100.0,7.6,161.1422,0.1253
-c05_E100.0_D7.6,100.0,7.6,162.1882,0.0451
-c05_E100.0_D7.6,100.0,7.6,170.4934,0.2006
-c05_E100.0_D7.6,100.0,7.6,172.5968,-0.1068
-c05_E100.0_D7.6,100.0,7.6,173.4772,-0.0080
-c05_E100.0_D7.6,100.0,7.6,174.4725,-0.1598
-c05_E100.0_D7.6,100.0,7.6,180.5110,0.0873
-c05_E100.0_D7.6,100.0,7.6,180.8603,-0.0388
-c05_E100.0_D7.6,100.0,7.6,181.8998,0.1927
-c05_E100.0_D7.6,100.0,7.6,186.9020,0.0570
-c05_E100.0_D7.6,100.0,7.6,186.9555,-0.2379
-c05_E100.0_D7.6,100.0,7.6,192.8226,-0.0418
-c05_E100.0_D7.6,100.0,7.6,195.0984,-0.0018
-c05_E100.0_D7.6,100.0,7.6,195.1275,0.0802
-c05_E100.0_D7.6,100.0,7.6,195.6114,-0.0051
-c05_E100.0_D7.6,100.0,7.6,197.1820,0.0807
-c05_E100.0_D7.6,100.0,7.6,200.0000,0.0427
-c06_E100.0_D8.0,100.0,8.0,0.0348,3.2017
-c06_E100.0_D8.0,100.0,8.0,0.8301,3.1303
-c06_E100.0_D8.0,100.0,8.0,4.5086,2.2423
-c06_E100.0_D8.0,100.0,8.0,10.5332,1.1290
-c06_E100.0_D8.0,100.0,8.0,12.1771,0.5402
-c06_E100.0_D8.0,100.0,8.0,12.1953,0.4235
-c06_E100.0_D8.0,100.0,8.0,18.6657,-1.6195
-c06_E100.0_D8.0,100.0,8.0,19.9668,-1.8486
-c06_E100.0_D8.0,100.0,8.0,23.1555,-3.2541
-c06_E100.0_D8.0,100.0,8.0,24.0972,-3.3266
-c06_E100.0_D8.0,100.0,8.0,25.2153,-4.0323
-c06_E100.0_D8.0,100.0,8.0,26.1307,-4.1682
-c06_E100.0_D8.0,100.0,8.0,26.4977,-4.2171
-c06_E100.0_D8.0,100.0,8.0,28.6252,-4.8513
-c06_E100.0_D8.0,100.0,8.0,30.0215,-5.3057
-c06_E100.0_D8.0,100.0,8.0,31.3447,-5.6434
-c06_E100.0_D8.0,100.0,8.0,33.5550,-6.2162
-c06_E100.0_D8.0,100.0,8.0,38.3884,-6.4103
-c06_E100.0_D8.0,100.0,8.0,40.3363,-6.4580
-c06_E100.0_D8.0,100.0,8.0,43.0628,-6.1987
-c06_E100.0_D8.0,100.0,8.0,44.6631,-5.9571
-c06_E100.0_D8.0,100.0,8.0,51.5574,-4.3942
-c06_E100.0_D8.0,100.0,8.0,51.8022,-4.2707
-c06_E100.0_D8.0,100.0,8.0,53.3838,-3.8861
-c06_E100.0_D8.0,100.0,8.0,57.6241,-2.6409
-c06_E100.0_D8.0,100.0,8.0,61.4165,-1.8416
-c06_E100.0_D8.0,100.0,8.0,63.5829,-1.2023
-c06_E100.0_D8.0,100.0,8.0,65.5108,-0.9241
-c06_E100.0_D8.0,100.0,8.0,69.9866,-0.4647
-c06_E100.0_D8.0,100.0,8.0,71.4613,-0.1541
-c06_E100.0_D8.0,100.0,8.0,74.3744,-0.0272
-c06_E100.0_D8.0,100.0,8.0,74.8413,-0.0515
-c06_E100.0_D8.0,100.0,8.0,76.0728,-0.0528
-c06_E100.0_D8.0,100.0,8.0,76.2260,0.0599
-c06_E100.0_D8.0,100.0,8.0,79.3192,0.0988
-c06_E100.0_D8.0,100.0,8.0,80.2221,0.0798
-c06_E100.0_D8.0,100.0,8.0,81.0635,0.0724
-c06_E100.0_D8.0,100.0,8.0,91.2765,0.0489
-c06_E100.0_D8.0,100.0,8.0,91.4316,0.1834
-c06_E100.0_D8.0,100.0,8.0,91.4500,0.0506
-c06_E100.0_D8.0,100.0,8.0,100.9641,0.2027
-c06_E100.0_D8.0,100.0,8.0,101.0985,0.0118
-c06_E100.0_D8.0,100.0,8.0,103.1581,0.2355
-c06_E100.0_D8.0,100.0,8.0,104.5601,0.0976
-c06_E100.0_D8.0,100.0,8.0,105.8973,0.2851
-c06_E100.0_D8.0,100.0,8.0,112.0581,-0.1079
-c06_E100.0_D8.0,100.0,8.0,115.2802,0.3308
-c06_E100.0_D8.0,100.0,8.0,117.8616,0.1269
-c06_E100.0_D8.0,100.0,8.0,119.5107,0.1285
-c06_E100.0_D8.0,100.0,8.0,124.1825,0.0482
-c06_E100.0_D8.0,100.0,8.0,125.3081,-0.0070
-c06_E100.0_D8.0,100.0,8.0,128.1202,0.0221
-c06_E100.0_D8.0,100.0,8.0,128.5175,-0.0959
-c06_E100.0_D8.0,100.0,8.0,130.4875,-0.0346
-c06_E100.0_D8.0,100.0,8.0,133.0998,0.0560
-c06_E100.0_D8.0,100.0,8.0,138.6104,-0.0488
-c06_E100.0_D8.0,100.0,8.0,139.6277,0.1346
-c06_E100.0_D8.0,100.0,8.0,140.8671,0.0418
-c06_E100.0_D8.0,100.0,8.0,142.4322,0.0289
-c06_E100.0_D8.0,100.0,8.0,146.2712,-0.0180
-c06_E100.0_D8.0,100.0,8.0,147.3741,0.1861
-c06_E100.0_D8.0,100.0,8.0,148.3536,-0.2569
-c06_E100.0_D8.0,100.0,8.0,148.4054,-0.0581
-c06_E100.0_D8.0,100.0,8.0,152.4380,-0.0606
-c06_E100.0_D8.0,100.0,8.0,155.4510,0.0367
-c06_E100.0_D8.0,100.0,8.0,157.9245,0.1341
-c06_E100.0_D8.0,100.0,8.0,158.9812,0.0148
-c06_E100.0_D8.0,100.0,8.0,159.3469,0.0106
-c06_E100.0_D8.0,100.0,8.0,161.9682,0.0829
-c06_E100.0_D8.0,100.0,8.0,167.1990,0.0576
-c06_E100.0_D8.0,100.0,8.0,167.8881,0.0182
-c06_E100.0_D8.0,100.0,8.0,170.4484,-0.0036
-c06_E100.0_D8.0,100.0,8.0,180.7762,0.0689
-c06_E100.0_D8.0,100.0,8.0,180.9484,0.0625
-c06_E100.0_D8.0,100.0,8.0,181.8514,-0.0437
-c06_E100.0_D8.0,100.0,8.0,183.0377,0.1205
-c06_E100.0_D8.0,100.0,8.0,184.2891,-0.0727
-c06_E100.0_D8.0,100.0,8.0,186.0209,0.1815
-c06_E100.0_D8.0,100.0,8.0,186.2041,0.0031
-c06_E100.0_D8.0,100.0,8.0,193.5630,-0.1485
-c06_E100.0_D8.0,100.0,8.0,194.8697,0.0804
-c06_E100.0_D8.0,100.0,8.0,195.3092,-0.0670
-c06_E100.0_D8.0,100.0,8.0,196.6054,-0.1157
-c06_E100.0_D8.0,100.0,8.0,198.0505,-0.0872
-c07_E100.0_D8.4,100.0,8.4,0.0000,3.0422
-c07_E100.0_D8.4,100.0,8.4,6.6162,2.0181
-c07_E100.0_D8.4,100.0,8.4,9.7367,1.1274
-c07_E100.0_D8.4,100.0,8.4,10.2492,0.9859
-c07_E100.0_D8.4,100.0,8.4,13.2444,0.2070
-c07_E100.0_D8.4,100.0,8.4,13.7772,0.0363
-c07_E100.0_D8.4,100.0,8.4,14.2000,-0.0736
-c07_E100.0_D8.4,100.0,8.4,16.8585,-1.1560
-c07_E100.0_D8.4,100.0,8.4,17.3730,-1.1499
-c07_E100.0_D8.4,100.0,8.4,20.3114,-2.1419
-c07_E100.0_D8.4,100.0,8.4,25.3633,-3.9758
-c07_E100.0_D8.4,100.0,8.4,25.5000,-3.9038
-c07_E100.0_D8.4,100.0,8.4,25.9437,-4.1113
-c07_E100.0_D8.4,100.0,8.4,30.8280,-5.5673
-c07_E100.0_D8.4,100.0,8.4,33.3368,-6.2471
-c07_E100.0_D8.4,100.0,8.4,34.9190,-6.3796
-c07_E100.0_D8.4,100.0,8.4,35.1700,-6.5066
-c07_E100.0_D8.4,100.0,8.4,35.5546,-6.3937
-c07_E100.0_D8.4,100.0,8.4,45.5117,-6.0264
-c07_E100.0_D8.4,100.0,8.4,46.0522,-5.8179
-c07_E100.0_D8.4,100.0,8.4,46.1084,-5.8938
-c07_E100.0_D8.4,100.0,8.4,46.9670,-5.6885
-c07_E100.0_D8.4,100.0,8.4,47.9115,-5.5392
-c07_E100.0_D8.4,100.0,8.4,54.2125,-3.5851
-c07_E100.0_D8.4,100.0,8.4,55.4128,-3.2835
-c07_E100.0_D8.4,100.0,8.4,60.3137,-2.0111
-c07_E100.0_D8.4,100.0,8.4,62.3842,-1.5652
-c07_E100.0_D8.4,100.0,8.4,62.7180,-1.5711
-c07_E100.0_D8.4,100.0,8.4,67.1398,-0.8719
-c07_E100.0_D8.4,100.0,8.4,68.2314,-0.7377
-c07_E100.0_D8.4,100.0,8.4,71.6318,-0.3149
-c07_E100.0_D8.4,100.0,8.4,74.2085,0.1057
-c07_E100.0_D8.4,100.0,8.4,76.5746,-0.0686
-c07_E100.0_D8.4,100.0,8.4,84.5734,0.0977
-c07_E100.0_D8.4,100.0,8.4,86.6113,-0.0143
-c07_E100.0_D8.4,100.0,8.4,87.0244,0.0715
-c07_E100.0_D8.4,100.0,8.4,88.4653,0.0730
-c07_E100.0_D8.4,100.0,8.4,90.8375,0.1251
-c07_E100.0_D8.4,100.0,8.4,91.4098,-0.0103
-c07_E100.0_D8.4,100.0,8.4,93.7316,-0.0375
-c07_E100.0_D8.4,100.0,8.4,95.4164,0.0570
-c07_E100.0_D8.4,100.0,8.4,95.5427,0.1488
-c07_E100.0_D8.4,100.0,8.4,100.8771,0.1133
-c07_E100.0_D8.4,100.0,8.4,101.2758,0.2101
-c07_E100.0_D8.4,100.0,8.4,101.6950,0.2227
-c07_E100.0_D8.4,100.0,8.4,109.3524,0.0330
-c07_E100.0_D8.4,100.0,8.4,110.3486,0.0624
-c07_E100.0_D8.4,100.0,8.4,113.8933,0.1386
-c07_E100.0_D8.4,100.0,8.4,114.2331,0.1232
-c07_E100.0_D8.4,100.0,8.4,117.9280,-0.0008
-c07_E100.0_D8.4,100.0,8.4,120.6509,0.0980
-c07_E100.0_D8.4,100.0,8.4,126.2742,0.0716
-c07_E100.0_D8.4,100.0,8.4,127.7606,0.1735
-c07_E100.0_D8.4,100.0,8.4,128.4917,0.2191
-c07_E100.0_D8.4,100.0,8.4,129.3688,0.0850
-c07_E100.0_D8.4,100.0,8.4,130.7683,0.0728
-c07_E100.0_D8.4,100.0,8.4,137.1375,-0.0021
-c07_E100.0_D8.4,100.0,8.4,138.0550,0.0306
-c07_E100.0_D8.4,100.0,8.4,142.4248,0.2289
-c07_E100.0_D8.4,100.0,8.4,143.5319,0.0107
-c07_E100.0_D8.4,100.0,8.4,143.9282,0.0100
-c07_E100.0_D8.4,100.0,8.4,147.5340,0.1323
-c07_E100.0_D8.4,100.0,8.4,147.9639,0.0387
-c07_E100.0_D8.4,100.0,8.4,149.4210,0.1534
-c07_E100.0_D8.4,100.0,8.4,156.1762,0.0247
-c07_E100.0_D8.4,100.0,8.4,156.5635,0.0074
-c07_E100.0_D8.4,100.0,8.4,158.2685,-0.0638
-c07_E100.0_D8.4,100.0,8.4,158.9570,0.0525
-c07_E100.0_D8.4,100.0,8.4,160.2188,-0.0959
-c07_E100.0_D8.4,100.0,8.4,160.2188,-0.0308
-c07_E100.0_D8.4,100.0,8.4,162.2715,-0.0659
-c07_E100.0_D8.4,100.0,8.4,163.3854,-0.0006
-c07_E100.0_D8.4,100.0,8.4,165.7551,0.1172
-c07_E100.0_D8.4,100.0,8.4,169.4128,-0.0393
-c07_E100.0_D8.4,100.0,8.4,171.0354,0.0755
-c07_E100.0_D8.4,100.0,8.4,172.3531,0.0010
-c07_E100.0_D8.4,100.0,8.4,175.0204,-0.1482
-c07_E100.0_D8.4,100.0,8.4,178.4292,-0.0941
-c07_E100.0_D8.4,100.0,8.4,179.1061,-0.0685
-c07_E100.0_D8.4,100.0,8.4,180.0328,0.0038
-c07_E100.0_D8.4,100.0,8.4,185.9035,-0.0399
-c07_E100.0_D8.4,100.0,8.4,188.0613,0.1316
-c07_E100.0_D8.4,100.0,8.4,195.8915,-0.0245
-c07_E100.0_D8.4,100.0,8.4,197.4572,0.0021
-c07_E100.0_D8.4,100.0,8.4,198.3371,0.0288
-c08_E100.0_D8.8,100.0,8.8,1.6809,2.8614
-c08_E100.0_D8.8,100.0,8.8,5.5821,2.1690
-c08_E100.0_D8.8,100.0,8.8,6.9666,1.9384
-c08_E100.0_D8.8,100.0,8.8,12.2794,0.4988
-c08_E100.0_D8.8,100.0,8.8,15.2867,-0.3386
-c08_E100.0_D8.8,100.0,8.8,17.6203,-1.2204
-c08_E100.0_D8.8,100.0,8.8,18.5647,-1.6561
-c08_E100.0_D8.8,100.0,8.8,19.0256,-1.7673
-c08_E100.0_D8.8,100.0,8.8,19.1311,-1.7155
-c08_E100.0_D8.8,100.0,8.8,22.4375,-2.8850
-c08_E100.0_D8.8,100.0,8.8,22.7570,-2.9685
-c08_E100.0_D8.8,100.0,8.8,27.2492,-4.5169
-c08_E100.0_D8.8,100.0,8.8,32.0641,-6.1117
-c08_E100.0_D8.8,100.0,8.8,34.6153,-6.3662
-c08_E100.0_D8.8,100.0,8.8,37.1754,-6.6218
-c08_E100.0_D8.8,100.0,8.8,39.8552,-6.6951
-c08_E100.0_D8.8,100.0,8.8,40.2685,-6.7715
-c08_E100.0_D8.8,100.0,8.8,41.0899,-6.6407
-c08_E100.0_D8.8,100.0,8.8,43.0617,-6.3397
-c08_E100.0_D8.8,100.0,8.8,43.9301,-6.3141
-c08_E100.0_D8.8,100.0,8.8,47.6984,-5.3919
-c08_E100.0_D8.8,100.0,8.8,55.6293,-3.2881
-c08_E100.0_D8.8,100.0,8.8,57.7151,-2.6373
-c08_E100.0_D8.8,100.0,8.8,62.2012,-1.5774
-c08_E100.0_D8.8,100.0,8.8,63.1935,-1.5870
-c08_E100.0_D8.8,100.0,8.8,64.1626,-1.0192
-c08_E100.0_D8.8,100.0,8.8,66.7130,-0.7725
-c08_E100.0_D8.8,100.0,8.8,66.7548,-0.7824
-c08_E100.0_D8.8,100.0,8.8,75.6072,-0.0229
-c08_E100.0_D8.8,100.0,8.8,76.7445,0.0244
-c08_E100.0_D8.8,100.0,8.8,79.0801,0.0385
-c08_E100.0_D8.8,100.0,8.8,79.3378,0.0854
-c08_E100.0_D8.8,100.0,8.8,79.3934,0.1780
-c08_E100.0_D8.8,100.0,8.8,82.9978,0.0556
-c08_E100.0_D8.8,100.0,8.8,83.1602,0.1583
-c08_E100.0_D8.8,100.0,8.8,84.7691,-0.0483
-c08_E100.0_D8.8,100.0,8.8,84.7893,0.0955
-c08_E100.0_D8.8,100.0,8.8,89.0510,0.1515
-c08_E100.0_D8.8,100.0,8.8,90.0236,-0.0267
-c08_E100.0_D8.8,100.0,8.8,92.8789,0.1123
-c08_E100.0_D8.8,100.0,8.8,94.9921,0.0452
-c08_E100.0_D8.8,100.0,8.8,98.3321,0.0985
-c08_E100.0_D8.8,100.0,8.8,99.3210,-0.0062
-c08_E100.0_D8.8,100.0,8.8,103.3231,0.1801
-c08_E100.0_D8.8,100.0,8.8,106.7429,0.1063
-c08_E100.0_D8.8,100.0,8.8,106.9049,0.0400
-c08_E100.0_D8.8,100.0,8.8,107.5667,0.0889
-c08_E100.0_D8.8,100.0,8.8,111.8936,0.1998
-c08_E100.0_D8.8,100.0,8.8,113.2949,-0.0363
-c08_E100.0_D8.8,100.0,8.8,114.9121,-0.1398
-c08_E100.0_D8.8,100.0,8.8,115.7887,0.1007
-c08_E100.0_D8.8,100.0,8.8,115.8317,-0.0653
-c08_E100.0_D8.8,100.0,8.8,115.9226,-0.0167
-c08_E100.0_D8.8,100.0,8.8,119.8586,0.2109
-c08_E100.0_D8.8,100.0,8.8,121.8636,0.1684
-c08_E100.0_D8.8,100.0,8.8,124.5248,0.2571
-c08_E100.0_D8.8,100.0,8.8,125.7983,0.0692
-c08_E100.0_D8.8,100.0,8.8,129.1773,0.0209
-c08_E100.0_D8.8,100.0,8.8,132.1652,-0.0052
-c08_E100.0_D8.8,100.0,8.8,138.3628,-0.0026
-c08_E100.0_D8.8,100.0,8.8,138.6252,-0.0901
-c08_E100.0_D8.8,100.0,8.8,139.2385,0.1181
-c08_E100.0_D8.8,100.0,8.8,141.7958,0.1018
-c08_E100.0_D8.8,100.0,8.8,143.5594,0.0671
-c08_E100.0_D8.8,100.0,8.8,147.8625,0.1748
-c08_E100.0_D8.8,100.0,8.8,148.8334,0.1471
-c08_E100.0_D8.8,100.0,8.8,151.7648,0.0682
-c08_E100.0_D8.8,100.0,8.8,152.2680,0.1104
-c08_E100.0_D8.8,100.0,8.8,158.1853,0.0833
-c08_E100.0_D8.8,100.0,8.8,160.5266,-0.0583
-c08_E100.0_D8.8,100.0,8.8,161.3000,-0.0194
-c08_E100.0_D8.8,100.0,8.8,167.6387,-0.0791
-c08_E100.0_D8.8,100.0,8.8,170.3347,0.0327
-c08_E100.0_D8.8,100.0,8.8,172.3935,-0.0133
-c08_E100.0_D8.8,100.0,8.8,175.5319,0.0891
-c08_E100.0_D8.8,100.0,8.8,179.4981,-0.0318
-c08_E100.0_D8.8,100.0,8.8,180.1732,0.0419
-c08_E100.0_D8.8,100.0,8.8,182.8653,0.1035
-c08_E100.0_D8.8,100.0,8.8,185.9592,0.1194
-c08_E100.0_D8.8,100.0,8.8,186.1932,0.1015
-c08_E100.0_D8.8,100.0,8.8,192.5344,0.0337
-c08_E100.0_D8.8,100.0,8.8,193.0429,0.0674
-c08_E100.0_D8.8,100.0,8.8,195.5408,-0.0508
-c08_E100.0_D8.8,100.0,8.8,196.1163,-0.1300
-c08_E100.0_D8.8,100.0,8.8,200.0000,0.0251
-c08_E100.0_D8.8,100.0,8.8,200.0000,0.1898
-c09_E100.0_D9.2,100.0,9.2,0.0000,3.0107
-c09_E100.0_D9.2,100.0,9.2,5.9034,2.0278
-c09_E100.0_D9.2,100.0,9.2,7.5956,1.6060
-c09_E100.0_D9.2,100.0,9.2,8.5921,1.4583
-c09_E100.0_D9.2,100.0,9.2,14.6832,-0.3775
-c09_E100.0_D9.2,100.0,9.2,15.5259,-0.4615
-c09_E100.0_D9.2,100.0,9.2,18.2769,-1.3852
-c09_E100.0_D9.2,100.0,9.2,18.6710,-1.6875
-c09_E100.0_D9.2,100.0,9.2,21.6893,-2.6064
-c09_E100.0_D9.2,100.0,9.2,22.9559,-3.2102
-c09_E100.0_D9.2,100.0,9.2,27.1882,-4.8033
-c09_E100.0_D9.2,100.0,9.2,30.8038,-5.7607
-c09_E100.0_D9.2,100.0,9.2,32.2384,-6.0741
-c09_E100.0_D9.2,100.0,9.2,35.4696,-6.6989
-c09_E100.0_D9.2,100.0,9.2,38.8607,-6.8667
-c09_E100.0_D9.2,100.0,9.2,40.0553,-6.8634
-c09_E100.0_D9.2,100.0,9.2,41.5764,-6.6849
-c09_E100.0_D9.2,100.0,9.2,42.5393,-6.5448
-c09_E100.0_D9.2,100.0,9.2,45.8535,-5.8257
-c09_E100.0_D9.2,100.0,9.2,50.6126,-4.5515
-c09_E100.0_D9.2,100.0,9.2,51.5884,-4.5702
-c09_E100.0_D9.2,100.0,9.2,57.6288,-2.6183
-c09_E100.0_D9.2,100.0,9.2,57.6714,-2.7752
-c09_E100.0_D9.2,100.0,9.2,59.1552,-2.5289
-c09_E100.0_D9.2,100.0,9.2,63.4682,-1.4342
-c09_E100.0_D9.2,100.0,9.2,64.8285,-0.9832
-c09_E100.0_D9.2,100.0,9.2,66.9078,-0.7849
-c09_E100.0_D9.2,100.0,9.2,69.0484,-0.4690
-c09_E100.0_D9.2,100.0,9.2,70.0452,-0.4618
-c09_E100.0_D9.2,100.0,9.2,75.7526,0.0460
-c09_E100.0_D9.2,100.0,9.2,76.5403,0.0951
-c09_E100.0_D9.2,100.0,9.2,80.4724,0.0196
-c09_E100.0_D9.2,100.0,9.2,81.9189,-0.0082
-c09_E100.0_D9.2,100.0,9.2,83.6776,0.2422
-c09_E100.0_D9.2,100.0,9.2,84.7446,0.2645
-c09_E100.0_D9.2,100.0,9.2,84.9472,0.2229
-c09_E100.0_D9.2,100.0,9.2,85.5088,-0.0563
-c09_E100.0_D9.2,100.0,9.2,93.6336,0.4714
-c09_E100.0_D9.2,100.0,9.2,95.6311,0.0059
-c09_E100.0_D9.2,100.0,9.2,96.8926,0.0271
-c09_E100.0_D9.2,100.0,9.2,97.3019,0.1199
-c09_E100.0_D9.2,100.0,9.2,97.5717,0.0894
-c09_E100.0_D9.2,100.0,9.2,99.4975,-0.0001
-c09_E100.0_D9.2,100.0,9.2,103.8529,0.0759
-c09_E100.0_D9.2,100.0,9.2,105.6080,0.0820
-c09_E100.0_D9.2,100.0,9.2,105.9802,0.1828
-c09_E100.0_D9.2,100.0,9.2,109.4829,0.1269
-c09_E100.0_D9.2,100.0,9.2,109.7734,-0.1089
-c09_E100.0_D9.2,100.0,9.2,119.7572,0.0337
-c09_E100.0_D9.2,100.0,9.2,125.9936,-0.0439
-c09_E100.0_D9.2,100.0,9.2,127.3588,0.1219
-c09_E100.0_D9.2,100.0,9.2,129.8609,-0.0465
-c09_E100.0_D9.2,100.0,9.2,134.0686,-0.0291
-c09_E100.0_D9.2,100.0,9.2,134.3772,-0.0325
-c09_E100.0_D9.2,100.0,9.2,136.1619,0.1086
-c09_E100.0_D9.2,100.0,9.2,144.1427,0.1330
-c09_E100.0_D9.2,100.0,9.2,146.4925,0.1584
-c09_E100.0_D9.2,100.0,9.2,147.9878,-0.0761
-c09_E100.0_D9.2,100.0,9.2,151.6683,0.1556
-c09_E100.0_D9.2,100.0,9.2,152.8801,-0.0983
-c09_E100.0_D9.2,100.0,9.2,153.7796,0.0551
-c09_E100.0_D9.2,100.0,9.2,161.7274,-0.0800
-c09_E100.0_D9.2,100.0,9.2,167.1199,0.0310
-c09_E100.0_D9.2,100.0,9.2,168.5660,-0.1120
-c09_E100.0_D9.2,100.0,9.2,171.6659,0.0065
-c09_E100.0_D9.2,100.0,9.2,171.7316,-0.0148
-c09_E100.0_D9.2,100.0,9.2,171.9709,-0.0845
-c09_E100.0_D9.2,100.0,9.2,177.0214,0.0001
-c09_E100.0_D9.2,100.0,9.2,183.9653,-0.0022
-c09_E100.0_D9.2,100.0,9.2,185.6848,-0.0526
-c09_E100.0_D9.2,100.0,9.2,186.9348,0.1056
-c09_E100.0_D9.2,100.0,9.2,188.0958,-0.0181
-c09_E100.0_D9.2,100.0,9.2,189.2380,-0.0950
-c09_E100.0_D9.2,100.0,9.2,189.7869,-0.1302
-c09_E100.0_D9.2,100.0,9.2,190.6142,0.0451
-c10_E100.0_D9.6,100.0,9.6,0.0000,3.1924
-c10_E100.0_D9.6,100.0,9.6,0.0000,3.0690
-c10_E100.0_D9.6,100.0,9.6,5.6727,2.0383
-c10_E100.0_D9.6,100.0,9.6,6.0132,2.0137
-c10_E100.0_D9.6,100.0,9.6,8.4701,1.4831
-c10_E100.0_D9.6,100.0,9.6,8.9811,1.3819
-c10_E100.0_D9.6,100.0,9.6,9.6975,1.1466
-c10_E100.0_D9.6,100.0,9.6,14.7487,-0.3012
-c10_E100.0_D9.6,100.0,9.6,17.2711,-1.0789
-c10_E100.0_D9.6,100.0,9.6,18.3973,-1.6691
-c10_E100.0_D9.6,100.0,9.6,20.9669,-2.6199
-c10_E100.0_D9.6,100.0,9.6,21.8013,-2.7392
-c10_E100.0_D9.6,100.0,9.6,24.6611,-3.8535
-c10_E100.0_D9.6,100.0,9.6,25.6212,-4.2982
-c10_E100.0_D9.6,100.0,9.6,27.1947,-4.7612
-c10_E100.0_D9.6,100.0,9.6,32.9540,-6.2100
-c10_E100.0_D9.6,100.0,9.6,34.6705,-6.7566
-c10_E100.0_D9.6,100.0,9.6,37.5646,-6.8697
-c10_E100.0_D9.6,100.0,9.6,39.6727,-6.7877
-c10_E100.0_D9.6,100.0,9.6,39.7942,-6.9902
-c10_E100.0_D9.6,100.0,9.6,46.1339,-5.8961
-c10_E100.0_D9.6,100.0,9.6,46.4137,-5.7903
-c10_E100.0_D9.6,100.0,9.6,50.7598,-4.7435
-c10_E100.0_D9.6,100.0,9.6,56.4867,-3.1436
-c10_E100.0_D9.6,100.0,9.6,58.1238,-2.5497
-c10_E100.0_D9.6,100.0,9.6,58.4652,-2.4946
-c10_E100.0_D9.6,100.0,9.6,65.7338,-0.9215
-c10_E100.0_D9.6,100.0,9.6,65.8866,-1.0180
-c10_E100.0_D9.6,100.0,9.6,66.5093,-0.9516
-c10_E100.0_D9.6,100.0,9.6,68.8931,-0.4728
-c10_E100.0_D9.6,100.0,9.6,70.2700,-0.3893
-c10_E100.0_D9.6,100.0,9.6,76.5818,0.1160
-c10_E100.0_D9.6,100.0,9.6,76.5942,-0.0579
-c10_E100.0_D9.6,100.0,9.6,76.9757,-0.2251
-c10_E100.0_D9.6,100.0,9.6,80.5823,0.0008
-c10_E100.0_D9.6,100.0,9.6,85.9833,0.2022
-c10_E100.0_D9.6,100.0,9.6,86.7515,0.1582
-c10_E100.0_D9.6,100.0,9.6,95.8064,0.1859
-c10_E100.0_D9.6,100.0,9.6,98.0311,0.0226
-c10_E100.0_D9.6,100.0,9.6,98.3074,0.0899
-c10_E100.0_D9.6,100.0,9.6,99.5982,0.2711
-c10_E100.0_D9.6,100.0,9.6,101.0191,-0.0004
-c10_E100.0_D9.6,100.0,9.6,105.2245,-0.0099
-c10_E100.0_D9.6,100.0,9.6,111.4248,0.1197
-c10_E100.0_D9.6,100.0,9.6,113.0854,0.2101
-c10_E100.0_D9.6,100.0,9.6,116.3495,0.1371
-c10_E100.0_D9.6,100.0,9.6,116.5722,0.0791
-c10_E100.0_D9.6,100.0,9.6,118.0391,0.1287
-c10_E100.0_D9.6,100.0,9.6,122.5443,-0.0325
-c10_E100.0_D9.6,100.0,9.6,125.0340,0.2645
-c10_E100.0_D9.6,100.0,9.6,127.4289,0.2044
-c10_E100.0_D9.6,100.0,9.6,132.0583,0.1356
-c10_E100.0_D9.6,100.0,9.6,132.3853,0.1406
-c10_E100.0_D9.6,100.0,9.6,137.2093,0.0978
-c10_E100.0_D9.6,100.0,9.6,141.2294,0.0438
-c10_E100.0_D9.6,100.0,9.6,142.0646,-0.0586
-c10_E100.0_D9.6,100.0,9.6,143.3628,-0.1182
-c10_E100.0_D9.6,100.0,9.6,143.5899,0.0294
-c10_E100.0_D9.6,100.0,9.6,152.2604,-0.0124
-c10_E100.0_D9.6,100.0,9.6,155.2868,-0.0519
-c10_E100.0_D9.6,100.0,9.6,157.2760,0.0470
-c10_E100.0_D9.6,100.0,9.6,157.6753,0.0406
-c10_E100.0_D9.6,100.0,9.6,158.3546,0.0013
-c10_E100.0_D9.6,100.0,9.6,163.3731,0.0245
-c10_E100.0_D9.6,100.0,9.6,167.6115,-0.0865
-c10_E100.0_D9.6,100.0,9.6,170.1889,0.2122
-c10_E100.0_D9.6,100.0,9.6,172.0782,0.0716
-c10_E100.0_D9.6,100.0,9.6,172.9052,-0.1248
-c10_E100.0_D9.6,100.0,9.6,176.0036,0.1034
-c10_E100.0_D9.6,100.0,9.6,176.8402,0.0212
-c10_E100.0_D9.6,100.0,9.6,179.3037,0.0080
-c10_E100.0_D9.6,100.0,9.6,184.4423,-0.1404
-c10_E100.0_D9.6,100.0,9.6,186.1608,-0.0544
-c10_E100.0_D9.6,100.0,9.6,187.9089,0.0875
-c10_E100.0_D9.6,100.0,9.6,189.1634,-0.0428
-c10_E100.0_D9.6,100.0,9.6,198.3330,-0.2667
-c10_E100.0_D9.6,100.0,9.6,198.6971,0.1218
-c10_E100.0_D9.6,100.0,9.6,199.0178,-0.1371
-c10_E100.0_D9.6,100.0,9.6,200.0000,-0.1280
-c11_E100.0_D10.0,100.0,10.0,0.0000,3.0094
-c11_E100.0_D10.0,100.0,10.0,0.0000,2.9563
-c11_E100.0_D10.0,100.0,10.0,0.0000,3.1272
-c11_E100.0_D10.0,100.0,10.0,5.2081,2.2685
-c11_E100.0_D10.0,100.0,10.0,8.1194,1.5100
-c11_E100.0_D10.0,100.0,10.0,11.1067,0.7395
-c11_E100.0_D10.0,100.0,10.0,13.8487,-0.1517
-c11_E100.0_D10.0,100.0,10.0,16.8558,-0.9433
-c11_E100.0_D10.0,100.0,10.0,22.1974,-3.0373
-c11_E100.0_D10.0,100.0,10.0,22.2654,-3.0436
-c11_E100.0_D10.0,100.0,10.0,24.1275,-3.7891
-c11_E100.0_D10.0,100.0,10.0,29.3886,-5.6197
-c11_E100.0_D10.0,100.0,10.0,29.7556,-5.7686
-c11_E100.0_D10.0,100.0,10.0,32.4629,-6.4442
-c11_E100.0_D10.0,100.0,10.0,36.8049,-7.1078
-c11_E100.0_D10.0,100.0,10.0,41.7554,-6.9558
-c11_E100.0_D10.0,100.0,10.0,45.4482,-6.5255
-c11_E100.0_D10.0,100.0,10.0,46.0939,-6.0204
-c11_E100.0_D10.0,100.0,10.0,49.8293,-5.0729
-c11_E100.0_D10.0,100.0,10.0,50.7022,-4.6380
-c11_E100.0_D10.0,100.0,10.0,53.4259,-3.8979
-c11_E100.0_D10.0,100.0,10.0,55.7750,-3.3728
-c11_E100.0_D10.0,100.0,10.0,58.5411,-2.6602
-c11_E100.0_D10.0,100.0,10.0,60.7208,-2.0005
-c11_E100.0_D10.0,100.0,10.0,65.8885,-1.0794
-c11_E100.0_D10.0,100.0,10.0,66.7621,-0.9185
-c11_E100.0_D10.0,100.0,10.0,66.7872,-0.8642
-c11_E100.0_D10.0,100.0,10.0,67.0572,-0.8794
-c11_E100.0_D10.0,100.0,10.0,68.4698,-0.5357
-c11_E100.0_D10.0,100.0,10.0,74.8139,0.0758
-c11_E100.0_D10.0,100.0,10.0,77.5562,-0.0287
-c11_E100.0_D10.0,100.0,10.0,78.4413,0.0961
-c11_E100.0_D10.0,100.0,10.0,80.3342,0.0958
-c11_E100.0_D10.0,100.0,10.0,81.0238,0.0885
-c11_E100.0_D10.0,100.0,10.0,82.0470,0.0928
-c11_E100.0_D10.0,100.0,10.0,82.7275,0.0832
-c11_E100.0_D10.0,100.0,10.0,89.1193,0.2835
-c11_E100.0_D10.0,100.0,10.0,94.9314,0.1071
-c11_E100.0_D10.0,100.0,10.0,95.0804,0.2420
-c11_E100.0_D10.0,100.0,10.0,96.8505,0.1988
-c11_E100.0_D10.0,100.0,10.0,97.8227,0.2077
-c11_E100.0_D10.0,100.0,10.0,99.3065,0.0834
-c11_E100.0_D10.0,100.0,10.0,105.9122,0.1782
-c11_E100.0_D10.0,100.0,10.0,107.8262,0.1316
-c11_E100.0_D10.0,100.0,10.0,113.5907,0.0923
-c11_E100.0_D10.0,100.0,10.0,115.5296,0.1367
-c11_E100.0_D10.0,100.0,10.0,115.7354,0.0725
-c11_E100.0_D10.0,100.0,10.0,116.1312,0.1815
-c11_E100.0_D10.0,100.0,10.0,124.8559,-0.0178
-c11_E100.0_D10.0,100.0,10.0,129.5030,-0.1767
-c11_E100.0_D10.0,100.0,10.0,130.2235,0.1504
-c11_E100.0_D10.0,100.0,10.0,135.6411,0.0296
-c11_E100.0_D10.0,100.0,10.0,136.1404,0.2574
-c11_E100.0_D10.0,100.0,10.0,138.4593,0.0470
-c11_E100.0_D10.0,100.0,10.0,140.1836,-0.0089
-c11_E100.0_D10.0,100.0,10.0,143.6453,-0.0941
-c11_E100.0_D10.0,100.0,10.0,147.8074,-0.0710
-c11_E100.0_D10.0,100.0,10.0,148.1382,-0.1257
-c11_E100.0_D10.0,100.0,10.0,148.7701,0.0130
-c11_E100.0_D10.0,100.0,10.0,152.2871,0.0031
-c11_E100.0_D10.0,100.0,10.0,153.3534,-0.0710
-c11_E100.0_D10.0,100.0,10.0,157.5833,0.0108
-c11_E100.0_D10.0,100.0,10.0,158.7234,-0.1296
-c11_E100.0_D10.0,100.0,10.0,159.6560,-0.0908
-c11_E100.0_D10.0,100.0,10.0,161.7660,-0.1168
-c11_E100.0_D10.0,100.0,10.0,162.6524,0.0519
-c11_E100.0_D10.0,100.0,10.0,167.8324,0.1021
-c11_E100.0_D10.0,100.0,10.0,167.9982,0.0410
-c11_E100.0_D10.0,100.0,10.0,172.5522,-0.0723
-c11_E100.0_D10.0,100.0,10.0,173.3854,-0.0116
-c11_E100.0_D10.0,100.0,10.0,175.9175,0.0934
-c11_E100.0_D10.0,100.0,10.0,176.0320,-0.1724
-c11_E100.0_D10.0,100.0,10.0,176.8298,0.0592
-c11_E100.0_D10.0,100.0,10.0,178.4188,0.0891
-c11_E100.0_D10.0,100.0,10.0,180.5097,0.0073
-c11_E100.0_D10.0,100.0,10.0,183.2418,-0.1075
-c11_E100.0_D10.0,100.0,10.0,185.7450,0.0969
-c11_E100.0_D10.0,100.0,10.0,189.1804,0.1528
-c11_E100.0_D10.0,100.0,10.0,190.9102,-0.0266
-c11_E100.0_D10.0,100.0,10.0,191.2540,-0.0936
-c11_E100.0_D10.0,100.0,10.0,193.1326,-0.1234
-c11_E100.0_D10.0,100.0,10.0,199.3907,0.1480
-c12_E100.0_D10.4,100.0,10.4,0.3401,3.3017
-c12_E100.0_D10.4,100.0,10.4,1.2446,2.7689
-c12_E100.0_D10.4,100.0,10.4,3.1977,2.5802
-c12_E100.0_D10.4,100.0,10.4,3.6196,2.4546
-c12_E100.0_D10.4,100.0,10.4,7.2914,1.5462
-c12_E100.0_D10.4,100.0,10.4,10.5811,0.8665
-c12_E100.0_D10.4,100.0,10.4,13.4785,0.1177
-c12_E100.0_D10.4,100.0,10.4,13.9510,-0.3277
-c12_E100.0_D10.4,100.0,10.4,16.7846,-0.9250
-c12_E100.0_D10.4,100.0,10.4,18.3139,-1.6331
-c12_E100.0_D10.4,100.0,10.4,20.2550,-2.3912
-c12_E100.0_D10.4,100.0,10.4,23.0523,-3.3977
-c12_E100.0_D10.4,100.0,10.4,25.9475,-4.4641
-c12_E100.0_D10.4,100.0,10.4,27.0740,-4.8275
-c12_E100.0_D10.4,100.0,10.4,30.2854,-5.8766
-c12_E100.0_D10.4,100.0,10.4,35.8767,-6.9538
-c12_E100.0_D10.4,100.0,10.4,36.5581,-7.0273
-c12_E100.0_D10.4,100.0,10.4,37.2415,-7.2062
-c12_E100.0_D10.4,100.0,10.4,41.6640,-6.9978
-c12_E100.0_D10.4,100.0,10.4,44.2889,-6.4928
-c12_E100.0_D10.4,100.0,10.4,48.0973,-5.8448
-c12_E100.0_D10.4,100.0,10.4,51.4106,-4.9580
-c12_E100.0_D10.4,100.0,10.4,55.4439,-3.5216
-c12_E100.0_D10.4,100.0,10.4,55.5315,-3.5302
-c12_E100.0_D10.4,100.0,10.4,55.8617,-3.2821
-c12_E100.0_D10.4,100.0,10.4,57.5978,-2.8593
-c12_E100.0_D10.4,100.0,10.4,60.7862,-2.1350
-c12_E100.0_D10.4,100.0,10.4,66.0148,-1.2127
-c12_E100.0_D10.4,100.0,10.4,66.4611,-0.8145
-c12_E100.0_D10.4,100.0,10.4,71.5602,-0.2886
-c12_E100.0_D10.4,100.0,10.4,72.2406,-0.3273
-c12_E100.0_D10.4,100.0,10.4,76.4228,-0.1338
-c12_E100.0_D10.4,100.0,10.4,76.8678,-0.0946
-c12_E100.0_D10.4,100.0,10.4,77.0389,-0.1757
-c12_E100.0_D10.4,100.0,10.4,83.2413,0.1494
-c12_E100.0_D10.4,100.0,10.4,83.7547,0.2784
-c12_E100.0_D10.4,100.0,10.4,84.4706,0.1384
-c12_E100.0_D10.4,100.0,10.4,85.6203,0.2968
-c12_E100.0_D10.4,100.0,10.4,92.7381,0.1264
-c12_E100.0_D10.4,100.0,10.4,94.2458,0.0911
-c12_E100.0_D10.4,100.0,10.4,103.6892,0.1565
-c12_E100.0_D10.4,100.0,10.4,105.0412,0.2435
-c12_E100.0_D10.4,100.0,10.4,108.6994,0.1319
-c12_E100.0_D10.4,100.0,10.4,110.0599,0.1481
-c12_E100.0_D10.4,100.0,10.4,111.6804,0.1913
-c12_E100.0_D10.4,100.0,10.4,114.8801,0.1426
-c12_E100.0_D10.4,100.0,10.4,116.4669,0.1187
-c12_E100.0_D10.4,100.0,10.4,119.5029,-0.0648
-c12_E100.0_D10.4,100.0,10.4,120.4974,0.0967
-c12_E100.0_D10.4,100.0,10.4,132.0436,0.1320
-c12_E100.0_D10.4,100.0,10.4,132.3205,0.0388
-c12_E100.0_D10.4,100.0,10.4,133.0356,0.0850
-c12_E100.0_D10.4,100.0,10.4,133.5576,-0.0263
-c12_E100.0_D10.4,100.0,10.4,135.0725,0.1040
-c12_E100.0_D10.4,100.0,10.4,136.3903,0.0075
-c12_E100.0_D10.4,100.0,10.4,139.6248,0.0931
-c12_E100.0_D10.4,100.0,10.4,144.4158,0.1917
-c12_E100.0_D10.4,100.0,10.4,147.0102,-0.0815
-c12_E100.0_D10.4,100.0,10.4,148.1919,0.0964
-c12_E100.0_D10.4,100.0,10.4,149.2814,0.0929
-c12_E100.0_D10.4,100.0,10.4,150.7833,0.1647
-c12_E100.0_D10.4,100.0,10.4,151.7144,0.0291
-c12_E100.0_D10.4,100.0,10.4,154.8006,0.0530
-c12_E100.0_D10.4,100.0,10.4,159.6338,-0.1895
-c12_E100.0_D10.4,100.0,10.4,161.1702,-0.0856
-c12_E100.0_D10.4,100.0,10.4,161.7948,-0.0814
-c12_E100.0_D10.4,100.0,10.4,166.9891,0.1164
-c12_E100.0_D10.4,100.0,10.4,169.5581,-0.0492
-c12_E100.0_D10.4,100.0,10.4,170.3286,-0.0198
-c12_E100.0_D10.4,100.0,10.4,170.6330,0.0595
-c12_E100.0_D10.4,100.0,10.4,174.1077,0.0675
-c12_E100.0_D10.4,100.0,10.4,176.1041,-0.1821
-c12_E100.0_D10.4,100.0,10.4,177.5177,0.0837
-c12_E100.0_D10.4,100.0,10.4,179.7881,0.0608
-c12_E100.0_D10.4,100.0,10.4,180.0786,0.0071
-c12_E100.0_D10.4,100.0,10.4,184.9045,-0.0114
-c12_E100.0_D10.4,100.0,10.4,191.3210,0.0626
-c12_E100.0_D10.4,100.0,10.4,191.8099,-0.0567
-c12_E100.0_D10.4,100.0,10.4,192.3804,0.0553
-c12_E100.0_D10.4,100.0,10.4,195.9368,0.1268
-c12_E100.0_D10.4,100.0,10.4,198.7894,0.0483
-c13_E100.0_D10.8,100.0,10.8,7.0173,1.5721
-c13_E100.0_D10.8,100.0,10.8,8.2836,1.4897
-c13_E100.0_D10.8,100.0,10.8,9.1620,1.0834
-c13_E100.0_D10.8,100.0,10.8,13.1651,0.2624
-c13_E100.0_D10.8,100.0,10.8,13.6657,-0.0378
-c13_E100.0_D10.8,100.0,10.8,15.5017,-0.7551
-c13_E100.0_D10.8,100.0,10.8,16.9460,-1.1178
-c13_E100.0_D10.8,100.0,10.8,24.1806,-4.1358
-c13_E100.0_D10.8,100.0,10.8,27.0266,-5.0368
-c13_E100.0_D10.8,100.0,10.8,27.0863,-4.8920
-c13_E100.0_D10.8,100.0,10.8,30.1621,-5.9068
-c13_E100.0_D10.8,100.0,10.8,30.4727,-6.1676
-c13_E100.0_D10.8,100.0,10.8,34.9345,-6.8808
-c13_E100.0_D10.8,100.0,10.8,36.6788,-7.2688
-c13_E100.0_D10.8,100.0,10.8,37.9368,-7.2570
-c13_E100.0_D10.8,100.0,10.8,38.1011,-7.2065
-c13_E100.0_D10.8,100.0,10.8,42.7862,-6.9936
-c13_E100.0_D10.8,100.0,10.8,42.9445,-6.8480
-c13_E100.0_D10.8,100.0,10.8,43.4672,-6.8441
-c13_E100.0_D10.8,100.0,10.8,47.0456,-6.0117
-c13_E100.0_D10.8,100.0,10.8,47.5319,-5.9805
-c13_E100.0_D10.8,100.0,10.8,48.9010,-5.6178
-c13_E100.0_D10.8,100.0,10.8,52.3004,-4.4501
-c13_E100.0_D10.8,100.0,10.8,60.0827,-2.2535
-c13_E100.0_D10.8,100.0,10.8,61.9457,-1.7234
-c13_E100.0_D10.8,100.0,10.8,61.9851,-1.9217
-c13_E100.0_D10.8,100.0,10.8,63.3674,-1.4309
-c13_E100.0_D10.8,100.0,10.8,70.1737,-0.3196
-c13_E100.0_D10.8,100.0,10.8,72.6563,-0.2252
-c13_E100.0_D10.8,100.0,10.8,72.8131,-0.1581
-c13_E100.0_D10.8,100.0,10.8,74.9318,-0.1139
-c13_E100.0_D10.8,100.0,10.8,84.6794,0.1995
-c13_E100.0_D10.8,100.0,10.8,85.6639,0.2029
-c13_E100.0_D10.8,100.0,10.8,87.3223,0.1967
-c13_E100.0_D10.8,100.0,10.8,92.7876,0.1340
-c13_E100.0_D10.8,100.0,10.8,93.4046,0.2740
-c13_E100.0_D10.8,100.0,10.8,96.6512,-0.0259
-c13_E100.0_D10.8,100.0,10.8,101.1332,0.2837
-c13_E100.0_D10.8,100.0,10.8,101.6151,0.0792
-c13_E100.0_D10.8,100.0,10.8,103.0415,0.2271
-c13_E100.0_D10.8,100.0,10.8,105.2052,0.1474
-c13_E100.0_D10.8,100.0,10.8,105.4353,0.0795
-c13_E100.0_D10.8,100.0,10.8,107.1072,0.1676
-c13_E100.0_D10.8,100.0,10.8,112.0211,-0.1564
-c13_E100.0_D10.8,100.0,10.8,112.6401,0.2238
-c13_E100.0_D10.8,100.0,10.8,116.8676,0.1585
-c13_E100.0_D10.8,100.0,10.8,119.1687,-0.0326
-c13_E100.0_D10.8,100.0,10.8,119.5239,-0.0779
-c13_E100.0_D10.8,100.0,10.8,125.9330,0.0817
-c13_E100.0_D10.8,100.0,10.8,129.3754,-0.0216
-c13_E100.0_D10.8,100.0,10.8,130.4182,0.0185
-c13_E100.0_D10.8,100.0,10.8,132.7715,-0.0211
-c13_E100.0_D10.8,100.0,10.8,137.4359,-0.0070
-c13_E100.0_D10.8,100.0,10.8,138.2465,-0.0380
-c13_E100.0_D10.8,100.0,10.8,138.5280,-0.0112
-c13_E100.0_D10.8,100.0,10.8,139.6641,-0.0414
-c13_E100.0_D10.8,100.0,10.8,140.2805,0.1493
-c13_E100.0_D10.8,100.0,10.8,142.3211,0.2652
-c13_E100.0_D10.8,100.0,10.8,146.8296,0.1315
-c13_E100.0_D10.8,100.0,10.8,147.9573,0.0970
-c13_E100.0_D10.8,100.0,10.8,148.6888,0.0362
-c13_E100.0_D10.8,100.0,10.8,153.2435,-0.0985
-c13_E100.0_D10.8,100.0,10.8,157.2928,0.0041
-c13_E100.0_D10.8,100.0,10.8,158.1572,0.0993
-c13_E100.0_D10.8,100.0,10.8,162.4709,-0.1614
-c13_E100.0_D10.8,100.0,10.8,165.9053,0.2140
-c13_E100.0_D10.8,100.0,10.8,168.1707,0.0477
-c13_E100.0_D10.8,100.0,10.8,168.8598,0.0665
-c13_E100.0_D10.8,100.0,10.8,169.8709,0.0228
-c13_E100.0_D10.8,100.0,10.8,170.0669,0.0479
-c13_E100.0_D10.8,100.0,10.8,171.2845,0.1289
-c13_E100.0_D10.8,100.0,10.8,172.7876,-0.1173
-c13_E100.0_D10.8,100.0,10.8,177.0063,-0.1779
-c13_E100.0_D10.8,100.0,10.8,177.2720,-0.1461
-c13_E100.0_D10.8,100.0,10.8,179.8422,0.0769
-c13_E100.0_D10.8,100.0,10.8,182.2529,0.0439
-c13_E100.0_D10.8,100.0,10.8,190.8632,-0.0242
-c13_E100.0_D10.8,100.0,10.8,192.2553,-0.0102
-c13_E100.0_D10.8,100.0,10.8,192.6866,-0.0765
-c13_E100.0_D10.8,100.0,10.8,200.0000,0.0687
-c14_E100.0_D11.2,100.0,11.2,0.4050,3.0948
-c14_E100.0_D11.2,100.0,11.2,3.4384,2.5514
-c14_E100.0_D11.2,100.0,11.2,6.6196,1.6758
-c14_E100.0_D11.2,100.0,11.2,8.7074,1.1398
-c14_E100.0_D11.2,100.0,11.2,9.6133,1.2193
-c14_E100.0_D11.2,100.0,11.2,10.5191,0.6868
-c14_E100.0_D11.2,100.0,11.2,15.2846,-0.6127
-c14_E100.0_D11.2,100.0,11.2,15.8302,-0.7475
-c14_E100.0_D11.2,100.0,11.2,16.0605,-1.0605
-c14_E100.0_D11.2,100.0,11.2,21.4522,-2.8078
-c14_E100.0_D11.2,100.0,11.2,24.8227,-4.3085
-c14_E100.0_D11.2,100.0,11.2,26.2594,-4.8085
-c14_E100.0_D11.2,100.0,11.2,27.6458,-5.2224
-c14_E100.0_D11.2,100.0,11.2,31.1464,-6.2213
-c14_E100.0_D11.2,100.0,11.2,36.9858,-6.9513
-c14_E100.0_D11.2,100.0,11.2,40.1562,-7.0600
-c14_E100.0_D11.2,100.0,11.2,42.5391,-6.9893
-c14_E100.0_D11.2,100.0,11.2,46.2426,-6.3629
-c14_E100.0_D11.2,100.0,11.2,47.1227,-6.0989
-c14_E100.0_D11.2,100.0,11.2,51.1882,-4.9012
-c14_E100.0_D11.2,100.0,11.2,53.3060,-4.2138
-c14_E100.0_D11.2,100.0,11.2,55.2252,-3.7152
-c14_E100.0_D11.2,100.0,11.2,57.7172,-2.8221
-c14_E100.0_D11.2,100.0,11.2,59.3389,-2.3821
-c14_E100.0_D11.2,100.0,11.2,63.2939,-1.4062
-c14_E100.0_D11.2,100.0,11.2,64.1738,-1.4439
-c14_E100.0_D11.2,100.0,11.2,64.4925,-1.2553
-c14_E100.0_D11.2,100.0,11.2,65.7749,-0.9313
-c14_E100.0_D11.2,100.0,11.2,68.7041,-0.7514
-c14_E100.0_D11.2,100.0,11.2,76.4493,0.0134
-c14_E100.0_D11.2,100.0,11.2,76.6053,-0.0196
-c14_E100.0_D11.2,100.0,11.2,77.0233,-0.1037
-c14_E100.0_D11.2,100.0,11.2,79.5731,0.2249
-c14_E100.0_D11.2,100.0,11.2,80.8806,0.3699
-c14_E100.0_D11.2,100.0,11.2,81.4445,0.2964
-c14_E100.0_D11.2,100.0,11.2,88.3810,0.1404
-c14_E100.0_D11.2,100.0,11.2,88.7015,0.1994
-c14_E100.0_D11.2,100.0,11.2,91.3720,0.1318
-c14_E100.0_D11.2,100.0,11.2,94.7505,0.2339
-c14_E100.0_D11.2,100.0,11.2,97.6085,-0.0340
-c14_E100.0_D11.2,100.0,11.2,100.1217,0.0495
-c14_E100.0_D11.2,100.0,11.2,100.4805,-0.0178
-c14_E100.0_D11.2,100.0,11.2,107.2156,0.0298
-c14_E100.0_D11.2,100.0,11.2,110.7857,0.0799
-c14_E100.0_D11.2,100.0,11.2,113.6374,0.0551
-c14_E100.0_D11.2,100.0,11.2,114.7325,0.2183
-c14_E100.0_D11.2,100.0,11.2,115.7749,0.1106
-c14_E100.0_D11.2,100.0,11.2,116.4554,0.0177
-c14_E100.0_D11.2,100.0,11.2,121.9434,-0.0091
-c14_E100.0_D11.2,100.0,11.2,124.4317,-0.0104
-c14_E100.0_D11.2,100.0,11.2,124.9397,0.1746
-c14_E100.0_D11.2,100.0,11.2,126.3863,0.1356
-c14_E100.0_D11.2,100.0,11.2,136.5802,-0.0370
-c14_E100.0_D11.2,100.0,11.2,140.3869,0.1102
-c14_E100.0_D11.2,100.0,11.2,147.6968,-0.1304
-c14_E100.0_D11.2,100.0,11.2,150.6006,-0.0253
-c14_E100.0_D11.2,100.0,11.2,150.7409,0.1266
-c14_E100.0_D11.2,100.0,11.2,151.9023,-0.0249
-c14_E100.0_D11.2,100.0,11.2,152.3196,-0.0074
-c14_E100.0_D11.2,100.0,11.2,155.3529,-0.0183
-c14_E100.0_D11.2,100.0,11.2,155.7614,0.0696
-c14_E100.0_D11.2,100.0,11.2,156.4588,0.0101
-c14_E100.0_D11.2,100.0,11.2,157.6361,-0.0489
-c14_E100.0_D11.2,100.0,11.2,159.6950,0.1393
-c14_E100.0_D11.2,100.0,11.2,165.8371,-0.1021
-c14_E100.0_D11.2,100.0,11.2,165.9375,0.0446
-c14_E100.0_D11.2,100.0,11.2,168.5590,0.1438
-c14_E100.0_D11.2,100.0,11.2,174.4254,-0.0639
-c14_E100.0_D11.2,100.0,11.2,174.6815,0.1847
-c14_E100.0_D11.2,100.0,11.2,174.8246,0.0205
-c14_E100.0_D11.2,100.0,11.2,178.9429,0.1358
-c14_E100.0_D11.2,100.0,11.2,179.4641,-0.0417
-c14_E100.0_D11.2,100.0,11.2,184.5895,0.0310
-c14_E100.0_D11.2,100.0,11.2,185.1421,0.0326
-c14_E100.0_D11.2,100.0,11.2,190.0539,0.0807
-c14_E100.0_D11.2,100.0,11.2,191.8436,0.0571
-c14_E100.0_D11.2,100.0,11.2,192.9539,-0.0632
-c14_E100.0_D11.2,100.0,11.2,197.0854,-0.1309
-c14_E100.0_D11.2,100.0,11.2,197.1498,0.0342
-c14_E100.0_D11.2,100.0,11.2,198.9050,-0.0007
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,1.7173,2.7442
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,5.2985,1.9718
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,5.5724,1.9983
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,8.7859,1.3376
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,9.1051,1.0718
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,16.4078,-0.9806
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,18.1581,-1.8081
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,18.7350,-1.8077
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,20.2809,-2.4486
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,21.5675,-3.0735
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,25.7117,-4.6315
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,29.9000,-5.8210
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,31.3509,-6.5924
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,35.4994,-7.3341
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,39.5236,-7.3177
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,40.5937,-7.3469
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,43.9402,-6.8743
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,44.8767,-6.5430
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,45.8842,-6.3276
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,55.5889,-3.4926
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,56.7674,-3.1021
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,57.1905,-3.1379
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,58.7471,-2.7750
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,60.7006,-2.1317
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,68.2231,-0.7536
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,69.6469,-0.6005
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,71.4971,-0.3059
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,76.6446,-0.0640
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,86.7875,0.2651
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,88.9231,0.1344
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,90.5725,0.0735
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,92.8132,0.2017
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,93.4341,0.2562
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,96.9987,0.1159
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,103.2009,0.2504
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,103.3855,0.1428
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,105.0423,0.3167
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,106.6238,0.1958
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,107.5477,0.0006
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,109.1282,0.1539
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,110.8405,0.2058
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,111.4451,0.1235
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,113.4737,0.2773
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,116.9346,-0.0502
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,119.2884,0.1405
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,124.2620,-0.0129
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,125.0612,-0.0333
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,126.0231,0.1072
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,126.3917,0.1457
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,130.1333,-0.0764
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,131.5485,0.2408
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,135.0886,0.1821
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,137.1219,-0.0574
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,139.4498,0.0725
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,139.8243,0.1531
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,142.1211,-0.0149
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,142.8899,-0.0048
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,147.4672,0.1205
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,148.7479,0.2035
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,152.9208,-0.0247
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,155.1916,-0.0058
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,158.2222,-0.1137
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,162.2847,0.0651
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,167.5759,-0.1129
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,168.0103,0.0458
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,170.2454,0.1040
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,172.9281,0.0296
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,179.0922,-0.0891
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,180.6181,-0.1344
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,180.9369,0.1084
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,188.4458,-0.0152
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,191.1974,0.1006
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,191.7765,0.1666
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,192.9840,0.1093
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,195.5700,-0.0678
-c15_E100.0_D11.600000000000001,100.0,11.600000000000001,196.3713,-0.0299
-c16_E100.0_D12.0,100.0,12.0,0.0000,3.2076
-c16_E100.0_D12.0,100.0,12.0,2.4360,2.8081
-c16_E100.0_D12.0,100.0,12.0,4.0151,2.2051
-c16_E100.0_D12.0,100.0,12.0,9.1065,1.1985
-c16_E100.0_D12.0,100.0,12.0,10.1499,0.8413
-c16_E100.0_D12.0,100.0,12.0,12.7590,0.1628
-c16_E100.0_D12.0,100.0,12.0,12.8580,0.1144
-c16_E100.0_D12.0,100.0,12.0,13.0547,0.0565
-c16_E100.0_D12.0,100.0,12.0,13.7095,-0.1564
-c16_E100.0_D12.0,100.0,12.0,17.4977,-1.5023
-c16_E100.0_D12.0,100.0,12.0,18.9724,-1.8544
-c16_E100.0_D12.0,100.0,12.0,23.3744,-3.7111
-c16_E100.0_D12.0,100.0,12.0,30.6439,-6.4481
-c16_E100.0_D12.0,100.0,12.0,30.7721,-6.3493
-c16_E100.0_D12.0,100.0,12.0,33.9251,-7.1394
-c16_E100.0_D12.0,100.0,12.0,36.7508,-7.4883
-c16_E100.0_D12.0,100.0,12.0,38.4480,-7.4622
-c16_E100.0_D12.0,100.0,12.0,43.4351,-6.8840
-c16_E100.0_D12.0,100.0,12.0,46.6995,-6.1569
-c16_E100.0_D12.0,100.0,12.0,47.0311,-6.2455
-c16_E100.0_D12.0,100.0,12.0,49.2381,-5.4084
-c16_E100.0_D12.0,100.0,12.0,54.0310,-4.2157
-c16_E100.0_D12.0,100.0,12.0,54.4586,-4.0187
-c16_E100.0_D12.0,100.0,12.0,58.0251,-2.9035
-c16_E100.0_D12.0,100.0,12.0,59.1384,-2.4769
-c16_E100.0_D12.0,100.0,12.0,60.8445,-2.2031
-c16_E100.0_D12.0,100.0,12.0,65.4647,-1.0542
-c16_E100.0_D12.0,100.0,12.0,66.2633,-1.0955
-c16_E100.0_D12.0,100.0,12.0,67.1482,-0.8435
-c16_E100.0_D12.0,100.0,12.0,67.6560,-0.8786
-c16_E100.0_D12.0,100.0,12.0,69.0837,-0.6444
-c16_E100.0_D12.0,100.0,12.0,71.4581,-0.4193
-c16_E100.0_D12.0,100.0,12.0,72.1375,-0.2481
-c16_E100.0_D12.0,100.0,12.0,78.8134,-0.0702
-c16_E100.0_D12.0,100.0,12.0,81.1047,0.0928
-c16_E100.0_D12.0,100.0,12.0,89.3378,0.1873
-c16_E100.0_D12.0,100.0,12.0,89.5073,-0.0305
-c16_E100.0_D12.0,100.0,12.0,89.6894,0.2718
-c16_E100.0_D12.0,100.0,12.0,90.3740,0.1271
-c16_E100.0_D12.0,100.0,12.0,90.4633,0.0459
-c16_E100.0_D12.0,100.0,12.0,91.3969,0.2166
-c16_E100.0_D12.0,100.0,12.0,93.7325,0.2522
-c16_E100.0_D12.0,100.0,12.0,96.8822,0.2258
-c16_E100.0_D12.0,100.0,12.0,105.0735,0.0323
-c16_E100.0_D12.0,100.0,12.0,105.3060,0.1957
-c16_E100.0_D12.0,100.0,12.0,105.5597,0.1562
-c16_E100.0_D12.0,100.0,12.0,107.2499,0.0303
-c16_E100.0_D12.0,100.0,12.0,110.2087,0.1601
-c16_E100.0_D12.0,100.0,12.0,114.6485,-0.0125
-c16_E100.0_D12.0,100.0,12.0,116.4622,0.0605
-c16_E100.0_D12.0,100.0,12.0,116.9157,-0.0011
-c16_E100.0_D12.0,100.0,12.0,117.0582,-0.1421
-c16_E100.0_D12.0,100.0,12.0,118.1970,-0.0075
-c16_E100.0_D12.0,100.0,12.0,118.6364,0.0219
-c16_E100.0_D12.0,100.0,12.0,121.3220,0.0959
-c16_E100.0_D12.0,100.0,12.0,123.0483,0.1200
-c16_E100.0_D12.0,100.0,12.0,129.7863,0.0125
-c16_E100.0_D12.0,100.0,12.0,132.9360,-0.0723
-c16_E100.0_D12.0,100.0,12.0,133.2955,0.0638
-c16_E100.0_D12.0,100.0,12.0,140.1957,-0.0234
-c16_E100.0_D12.0,100.0,12.0,140.6909,-0.2730
-c16_E100.0_D12.0,100.0,12.0,142.0195,0.0651
-c16_E100.0_D12.0,100.0,12.0,142.3914,-0.0677
-c16_E100.0_D12.0,100.0,12.0,146.5252,0.1219
-c16_E100.0_D12.0,100.0,12.0,151.9700,0.1090
-c16_E100.0_D12.0,100.0,12.0,152.7952,0.1679
-c16_E100.0_D12.0,100.0,12.0,156.7564,-0.0317
-c16_E100.0_D12.0,100.0,12.0,158.6828,0.0279
-c16_E100.0_D12.0,100.0,12.0,160.8873,0.0414
-c16_E100.0_D12.0,100.0,12.0,161.2787,-0.0723
-c16_E100.0_D12.0,100.0,12.0,161.7560,0.1352
-c16_E100.0_D12.0,100.0,12.0,165.9001,-0.0143
-c16_E100.0_D12.0,100.0,12.0,167.9371,-0.0988
-c16_E100.0_D12.0,100.0,12.0,168.1808,0.0310
-c16_E100.0_D12.0,100.0,12.0,170.1462,-0.1953
-c16_E100.0_D12.0,100.0,12.0,177.7962,0.0189
-c16_E100.0_D12.0,100.0,12.0,178.9941,-0.2070
-c16_E100.0_D12.0,100.0,12.0,183.3204,0.0590
-c16_E100.0_D12.0,100.0,12.0,183.8250,0.0698
-c16_E100.0_D12.0,100.0,12.0,189.2773,0.0019
-c16_E100.0_D12.0,100.0,12.0,191.0301,0.0178
-c16_E100.0_D12.0,100.0,12.0,191.5261,0.2626
-c16_E100.0_D12.0,100.0,12.0,194.4044,0.0303
-c16_E100.0_D12.0,100.0,12.0,196.1439,-0.0657
-c16_E100.0_D12.0,100.0,12.0,197.4001,-0.0119
-c17_E100.0_D12.4,100.0,12.4,0.0000,3.0893
-c17_E100.0_D12.4,100.0,12.4,0.0000,3.2033
-c17_E100.0_D12.4,100.0,12.4,11.7785,0.4331
-c17_E100.0_D12.4,100.0,12.4,12.6588,-0.0098
-c17_E100.0_D12.4,100.0,12.4,16.2411,-1.0309
-c17_E100.0_D12.4,100.0,12.4,16.8279,-1.0623
-c17_E100.0_D12.4,100.0,12.4,20.2938,-2.4137
-c17_E100.0_D12.4,100.0,12.4,23.7930,-4.1116
-c17_E100.0_D12.4,100.0,12.4,24.3818,-4.2083
-c17_E100.0_D12.4,100.0,12.4,28.9028,-5.6863
-c17_E100.0_D12.4,100.0,12.4,29.6288,-6.1487
-c17_E100.0_D12.4,100.0,12.4,34.5965,-7.1885
-c17_E100.0_D12.4,100.0,12.4,42.3460,-7.3171
-c17_E100.0_D12.4,100.0,12.4,43.1238,-7.2138
-c17_E100.0_D12.4,100.0,12.4,45.8235,-6.6057
-c17_E100.0_D12.4,100.0,12.4,49.5315,-5.3138
-c17_E100.0_D12.4,100.0,12.4,50.6046,-5.2188
-c17_E100.0_D12.4,100.0,12.4,52.1387,-4.6999
-c17_E100.0_D12.4,100.0,12.4,55.3028,-3.6275
-c17_E100.0_D12.4,100.0,12.4,56.8258,-3.2229
-c17_E100.0_D12.4,100.0,12.4,60.9253,-2.2580
-c17_E100.0_D12.4,100.0,12.4,65.6417,-1.2102
-c17_E100.0_D12.4,100.0,12.4,71.3845,-0.3438
-c17_E100.0_D12.4,100.0,12.4,71.9370,-0.4075
-c17_E100.0_D12.4,100.0,12.4,73.2258,-0.2662
-c17_E100.0_D12.4,100.0,12.4,73.3364,-0.2672
-c17_E100.0_D12.4,100.0,12.4,75.3558,-0.1459
-c17_E100.0_D12.4,100.0,12.4,79.2789,0.0928
-c17_E100.0_D12.4,100.0,12.4,82.5941,0.1236
-c17_E100.0_D12.4,100.0,12.4,82.8959,0.0347
-c17_E100.0_D12.4,100.0,12.4,86.0246,0.1094
-c17_E100.0_D12.4,100.0,12.4,92.7220,-0.0053
-c17_E100.0_D12.4,100.0,12.4,95.6415,0.2142
-c17_E100.0_D12.4,100.0,12.4,96.4963,0.3601
-c17_E100.0_D12.4,100.0,12.4,105.1007,0.2543
-c17_E100.0_D12.4,100.0,12.4,105.8731,0.0285
-c17_E100.0_D12.4,100.0,12.4,105.9722,0.0151
-c17_E100.0_D12.4,100.0,12.4,108.0805,0.1185
-c17_E100.0_D12.4,100.0,12.4,113.0211,0.1312
-c17_E100.0_D12.4,100.0,12.4,113.4309,0.1692
-c17_E100.0_D12.4,100.0,12.4,116.0799,0.0089
-c17_E100.0_D12.4,100.0,12.4,117.0027,0.1853
-c17_E100.0_D12.4,100.0,12.4,119.2953,0.0174
-c17_E100.0_D12.4,100.0,12.4,120.8995,0.0998
-c17_E100.0_D12.4,100.0,12.4,125.1852,0.0832
-c17_E100.0_D12.4,100.0,12.4,126.3229,0.0385
-c17_E100.0_D12.4,100.0,12.4,133.5918,0.1106
-c17_E100.0_D12.4,100.0,12.4,136.6531,0.0223
-c17_E100.0_D12.4,100.0,12.4,139.1872,-0.0439
-c17_E100.0_D12.4,100.0,12.4,139.2785,-0.0293
-c17_E100.0_D12.4,100.0,12.4,139.8224,-0.0120
-c17_E100.0_D12.4,100.0,12.4,141.2066,0.0439
-c17_E100.0_D12.4,100.0,12.4,143.4243,0.0192
-c17_E100.0_D12.4,100.0,12.4,146.9705,0.2181
-c17_E100.0_D12.4,100.0,12.4,147.2403,0.0990
-c17_E100.0_D12.4,100.0,12.4,149.6187,-0.0520
-c17_E100.0_D12.4,100.0,12.4,150.8372,0.1053
-c17_E100.0_D12.4,100.0,12.4,154.6630,-0.0455
-c17_E100.0_D12.4,100.0,12.4,159.7525,-0.0705
-c17_E100.0_D12.4,100.0,12.4,164.5754,-0.0407
-c17_E100.0_D12.4,100.0,12.4,165.0014,-0.0505
-c17_E100.0_D12.4,100.0,12.4,166.8863,-0.1212
-c17_E100.0_D12.4,100.0,12.4,167.6397,0.0697
-c17_E100.0_D12.4,100.0,12.4,170.7881,0.0320
-c17_E100.0_D12.4,100.0,12.4,175.7654,0.0520
-c17_E100.0_D12.4,100.0,12.4,179.5964,0.1125
-c17_E100.0_D12.4,100.0,12.4,179.8920,0.0998
-c17_E100.0_D12.4,100.0,12.4,180.6588,-0.0898
-c17_E100.0_D12.4,100.0,12.4,188.8658,0.1079
-c17_E100.0_D12.4,100.0,12.4,190.7567,-0.0048
-c17_E100.0_D12.4,100.0,12.4,192.4714,0.1561
-c17_E100.0_D12.4,100.0,12.4,196.6018,0.0067
-c17_E100.0_D12.4,100.0,12.4,197.6507,-0.2625
-c17_E100.0_D12.4,100.0,12.4,198.2083,0.0177
-c17_E100.0_D12.4,100.0,12.4,198.4752,-0.2192
-c18_E100.0_D12.8,100.0,12.8,5.0510,2.1637
-c18_E100.0_D12.8,100.0,12.8,6.1477,1.9123
-c18_E100.0_D12.8,100.0,12.8,8.6247,1.3560
-c18_E100.0_D12.8,100.0,12.8,10.3489,0.7847
-c18_E100.0_D12.8,100.0,12.8,18.8346,-2.0824
-c18_E100.0_D12.8,100.0,12.8,20.2529,-2.5509
-c18_E100.0_D12.8,100.0,12.8,23.1283,-3.6539
-c18_E100.0_D12.8,100.0,12.8,25.2831,-4.4485
-c18_E100.0_D12.8,100.0,12.8,27.4989,-5.2322
-c18_E100.0_D12.8,100.0,12.8,29.5051,-5.9981
-c18_E100.0_D12.8,100.0,12.8,30.1746,-6.4790
-c18_E100.0_D12.8,100.0,12.8,30.9043,-6.2330
-c18_E100.0_D12.8,100.0,12.8,37.8349,-7.4728
-c18_E100.0_D12.8,100.0,12.8,41.4585,-7.4231
-c18_E100.0_D12.8,100.0,12.8,41.8205,-7.3094
-c18_E100.0_D12.8,100.0,12.8,42.5009,-7.1609
-c18_E100.0_D12.8,100.0,12.8,47.0721,-6.2673
-c18_E100.0_D12.8,100.0,12.8,51.0258,-5.0677
-c18_E100.0_D12.8,100.0,12.8,54.9529,-3.8140
-c18_E100.0_D12.8,100.0,12.8,57.3399,-3.2081
-c18_E100.0_D12.8,100.0,12.8,58.3814,-2.9200
-c18_E100.0_D12.8,100.0,12.8,61.0881,-2.3663
-c18_E100.0_D12.8,100.0,12.8,64.8516,-1.4635
-c18_E100.0_D12.8,100.0,12.8,66.1296,-1.0684
-c18_E100.0_D12.8,100.0,12.8,75.0445,-0.0982
-c18_E100.0_D12.8,100.0,12.8,76.4673,-0.1405
-c18_E100.0_D12.8,100.0,12.8,77.2728,-0.1583
-c18_E100.0_D12.8,100.0,12.8,77.9722,-0.1318
-c18_E100.0_D12.8,100.0,12.8,83.3877,-0.0930
-c18_E100.0_D12.8,100.0,12.8,87.2710,0.1687
-c18_E100.0_D12.8,100.0,12.8,88.8430,0.1002
-c18_E100.0_D12.8,100.0,12.8,90.6166,0.0816
-c18_E100.0_D12.8,100.0,12.8,92.1343,0.1889
-c18_E100.0_D12.8,100.0,12.8,94.5193,0.1259
-c18_E100.0_D12.8,100.0,12.8,94.5319,0.1425
-c18_E100.0_D12.8,100.0,12.8,94.7804,0.1079
-c18_E100.0_D12.8,100.0,12.8,99.7599,0.1297
-c18_E100.0_D12.8,100.0,12.8,102.1722,0.1679
-c18_E100.0_D12.8,100.0,12.8,104.3228,0.0331
-c18_E100.0_D12.8,100.0,12.8,109.7608,-0.0000
-c18_E100.0_D12.8,100.0,12.8,110.5367,0.0497
-c18_E100.0_D12.8,100.0,12.8,111.4380,0.0722
-c18_E100.0_D12.8,100.0,12.8,114.7712,0.0070
-c18_E100.0_D12.8,100.0,12.8,117.9547,-0.1077
-c18_E100.0_D12.8,100.0,12.8,119.5731,0.1010
-c18_E100.0_D12.8,100.0,12.8,119.6848,0.0438
-c18_E100.0_D12.8,100.0,12.8,120.4596,0.0345
-c18_E100.0_D12.8,100.0,12.8,121.6406,0.0740
-c18_E100.0_D12.8,100.0,12.8,129.7515,0.0948
-c18_E100.0_D12.8,100.0,12.8,130.3257,0.0986
-c18_E100.0_D12.8,100.0,12.8,137.6682,-0.0904
-c18_E100.0_D12.8,100.0,12.8,143.5065,0.0738
-c18_E100.0_D12.8,100.0,12.8,146.4308,0.1088
-c18_E100.0_D12.8,100.0,12.8,147.7632,-0.0436
-c18_E100.0_D12.8,100.0,12.8,149.5530,-0.0057
-c18_E100.0_D12.8,100.0,12.8,150.0244,0.0109
-c18_E100.0_D12.8,100.0,12.8,150.3781,-0.0220
-c18_E100.0_D12.8,100.0,12.8,156.8208,0.0515
-c18_E100.0_D12.8,100.0,12.8,157.2378,-0.1488
-c18_E100.0_D12.8,100.0,12.8,161.8913,-0.0465
-c18_E100.0_D12.8,100.0,12.8,162.5783,-0.0362
-c18_E100.0_D12.8,100.0,12.8,163.0090,-0.0199
-c18_E100.0_D12.8,100.0,12.8,163.8234,-0.0591
-c18_E100.0_D12.8,100.0,12.8,167.7531,-0.0507
-c18_E100.0_D12.8,100.0,12.8,170.1899,0.1673
-c18_E100.0_D12.8,100.0,12.8,171.4985,-0.0495
-c18_E100.0_D12.8,100.0,12.8,175.8538,-0.0180
-c18_E100.0_D12.8,100.0,12.8,177.0210,-0.1341
-c18_E100.0_D12.8,100.0,12.8,181.0447,0.0333
-c18_E100.0_D12.8,100.0,12.8,184.1588,-0.0747
-c18_E100.0_D12.8,100.0,12.8,185.3937,0.1928
-c18_E100.0_D12.8,100.0,12.8,187.8900,-0.0805
-c18_E100.0_D12.8,100.0,12.8,190.6153,0.0550
-c18_E100.0_D12.8,100.0,12.8,190.8862,-0.0190
-c18_E100.0_D12.8,100.0,12.8,193.6252,0.0843
-c18_E100.0_D12.8,100.0,12.8,194.9030,0.0169
-c18_E100.0_D12.8,100.0,12.8,196.0621,0.1332
-c18_E100.0_D12.8,100.0,12.8,200.0000,0.0703
-c19_E100.0_D13.2,100.0,13.2,0.2591,3.0635
-c19_E100.0_D13.2,100.0,13.2,3.2390,2.4880
-c19_E100.0_D13.2,100.0,13.2,4.2766,2.1955
-c19_E100.0_D13.2,100.0,13.2,5.0312,2.1877
-c19_E100.0_D13.2,100.0,13.2,5.3877,1.9864
-c19_E100.0_D13.2,100.0,13.2,10.7845,0.8490
-c19_E100.0_D13.2,100.0,13.2,13.2301,-0.0640
-c19_E100.0_D13.2,100.0,13.2,20.7590,-3.0259
-c19_E100.0_D13.2,100.0,13.2,21.6597,-3.3279
-c19_E100.0_D13.2,100.0,13.2,22.6585,-3.4842
-c19_E100.0_D13.2,100.0,13.2,27.0908,-5.2185
-c19_E100.0_D13.2,100.0,13.2,29.2859,-5.9605
-c19_E100.0_D13.2,100.0,13.2,30.4770,-6.4264
-c19_E100.0_D13.2,100.0,13.2,30.8604,-6.5016
-c19_E100.0_D13.2,100.0,13.2,31.3249,-6.8192
-c19_E100.0_D13.2,100.0,13.2,32.2613,-6.8054
-c19_E100.0_D13.2,100.0,13.2,34.2749,-7.2100
-c19_E100.0_D13.2,100.0,13.2,36.9547,-7.3301
-c19_E100.0_D13.2,100.0,13.2,40.2579,-7.4853
-c19_E100.0_D13.2,100.0,13.2,45.0190,-6.7651
-c19_E100.0_D13.2,100.0,13.2,45.2539,-6.7477
-c19_E100.0_D13.2,100.0,13.2,47.4652,-6.2363
-c19_E100.0_D13.2,100.0,13.2,53.2899,-4.4680
-c19_E100.0_D13.2,100.0,13.2,53.8049,-4.3558
-c19_E100.0_D13.2,100.0,13.2,57.7646,-3.0816
-c19_E100.0_D13.2,100.0,13.2,60.2136,-2.4075
-c19_E100.0_D13.2,100.0,13.2,65.1439,-1.3325
-c19_E100.0_D13.2,100.0,13.2,65.7956,-1.2887
-c19_E100.0_D13.2,100.0,13.2,69.2966,-0.4809
-c19_E100.0_D13.2,100.0,13.2,73.0974,-0.3028
-c19_E100.0_D13.2,100.0,13.2,74.3173,-0.0766
-c19_E100.0_D13.2,100.0,13.2,76.4835,-0.1024
-c19_E100.0_D13.2,100.0,13.2,78.1205,0.0507
-c19_E100.0_D13.2,100.0,13.2,84.8142,0.1194
-c19_E100.0_D13.2,100.0,13.2,85.9414,0.0384
-c19_E100.0_D13.2,100.0,13.2,92.8153,0.1138
-c19_E100.0_D13.2,100.0,13.2,96.5247,0.1396
-c19_E100.0_D13.2,100.0,13.2,97.1004,0.1785
-c19_E100.0_D13.2,100.0,13.2,97.2980,0.0629
-c19_E100.0_D13.2,100.0,13.2,100.1339,0.1915
-c19_E100.0_D13.2,100.0,13.2,106.2306,0.0414
-c19_E100.0_D13.2,100.0,13.2,106.8168,0.1204
-c19_E100.0_D13.2,100.0,13.2,108.7259,0.0116
-c19_E100.0_D13.2,100.0,13.2,112.7374,0.0162
-c19_E100.0_D13.2,100.0,13.2,116.4916,0.1269
-c19_E100.0_D13.2,100.0,13.2,117.5232,-0.0446
-c19_E100.0_D13.2,100.0,13.2,121.1821,-0.2340
-c19_E100.0_D13.2,100.0,13.2,121.2905,0.0995
-c19_E100.0_D13.2,100.0,13.2,126.6985,-0.0408
-c19_E100.0_D13.2,100.0,13.2,132.1456,0.0436
-c19_E100.0_D13.2,100.0,13.2,133.9311,0.1346
-c19_E100.0_D13.2,100.0,13.2,134.8439,-0.0027
-c19_E100.0_D13.2,100.0,13.2,135.9192,-0.1292
-c19_E100.0_D13.2,100.0,13.2,137.2979,0.1612
-c19_E100.0_D13.2,100.0,13.2,140.4336,0.0575
-c19_E100.0_D13.2,100.0,13.2,140.4802,-0.0166
-c19_E100.0_D13.2,100.0,13.2,141.2333,0.2006
-c19_E100.0_D13.2,100.0,13.2,146.7672,0.1867
-c19_E100.0_D13.2,100.0,13.2,151.1062,-0.0233
-c19_E100.0_D13.2,100.0,13.2,152.2118,0.0358
-c19_E100.0_D13.2,100.0,13.2,152.5756,-0.0481
-c19_E100.0_D13.2,100.0,13.2,161.4786,0.1350
-c19_E100.0_D13.2,100.0,13.2,162.0627,0.2934
-c19_E100.0_D13.2,100.0,13.2,164.3378,-0.1137
-c19_E100.0_D13.2,100.0,13.2,164.4894,-0.0864
-c19_E100.0_D13.2,100.0,13.2,172.0756,0.0020
-c19_E100.0_D13.2,100.0,13.2,172.3732,-0.0380
-c19_E100.0_D13.2,100.0,13.2,177.4199,-0.0309
-c19_E100.0_D13.2,100.0,13.2,177.7914,-0.1235
-c19_E100.0_D13.2,100.0,13.2,184.1664,-0.1389
-c19_E100.0_D13.2,100.0,13.2,184.4301,0.0050
-c19_E100.0_D13.2,100.0,13.2,184.9921,-0.0048
-c19_E100.0_D13.2,100.0,13.2,188.1774,-0.0823
-c19_E100.0_D13.2,100.0,13.2,189.7301,-0.0108
-c19_E100.0_D13.2,100.0,13.2,190.9312,0.0187
-c19_E100.0_D13.2,100.0,13.2,193.9842,-0.0905
-c19_E100.0_D13.2,100.0,13.2,194.8447,0.1287
-c19_E100.0_D13.2,100.0,13.2,195.5525,-0.0118
-c19_E100.0_D13.2,100.0,13.2,200.0000,-0.1810
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,0.3419,2.9528
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,0.9908,2.8906
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,3.5080,2.3496
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,7.7072,1.5824
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,7.9265,1.4563
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,11.7783,0.2697
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,13.6920,-0.2094
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,15.0800,-0.8208
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,18.0132,-1.7460
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,20.4699,-2.7827
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,21.4275,-3.2280
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,22.6244,-3.5309
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,24.2900,-4.1377
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,31.2759,-6.7818
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,31.9885,-6.8368
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,32.5367,-7.0361
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,36.3450,-7.4935
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,37.6106,-7.5899
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,38.5021,-7.5553
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,38.5915,-7.7241
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,41.8664,-7.4091
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,45.2638,-6.5420
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,46.5525,-6.5061
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,52.4306,-4.8156
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,56.9033,-3.2674
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,57.0608,-3.1791
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,58.2131,-2.7676
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,67.5259,-0.7315
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,72.3368,-0.4572
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,72.9405,-0.2422
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,75.5482,0.0764
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,77.7225,-0.1107
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,80.6870,0.1061
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,81.5076,0.0934
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,81.6048,0.0726
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,84.2589,0.3209
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,86.8763,0.3831
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,88.4223,0.1710
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,91.9869,0.1952
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,96.1387,0.1426
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,97.0924,0.2840
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,97.6939,0.1788
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,100.9089,0.1708
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,102.5514,0.1706
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,110.4801,0.1552
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,112.2854,0.1015
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,112.9951,0.1200
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,114.1207,0.1138
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,117.6756,-0.0241
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,121.4736,0.1451
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,126.0091,0.2061
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,126.9381,0.0800
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,129.2943,0.0497
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,135.7018,0.1912
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,136.7481,0.0080
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,140.4676,-0.0055
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,142.1000,-0.0997
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,142.8256,-0.0250
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,145.4483,0.0774
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,148.7123,0.0606
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,152.0488,0.0625
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,155.3705,0.0479
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,158.4309,-0.0070
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,161.7564,-0.0867
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,162.3751,-0.0042
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,163.1993,-0.0541
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,168.7806,0.0773
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,174.2847,0.0737
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,177.1970,-0.0893
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,177.6697,-0.0521
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,178.9074,-0.1693
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,180.4078,-0.0008
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,180.6029,0.1770
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,182.5519,-0.0509
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,189.7130,-0.0025
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,189.8250,0.1119
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,190.5332,0.0625
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,191.9424,-0.0075
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,192.8988,0.0315
-c20_E100.0_D13.600000000000001,100.0,13.600000000000001,200.0000,-0.0561
-c21_E100.0_D14.0,100.0,14.0,0.0000,3.1724
-c21_E100.0_D14.0,100.0,14.0,0.3182,3.1745
-c21_E100.0_D14.0,100.0,14.0,3.7760,2.4535
-c21_E100.0_D14.0,100.0,14.0,4.5738,2.2658
-c21_E100.0_D14.0,100.0,14.0,8.7938,1.2123
-c21_E100.0_D14.0,100.0,14.0,10.0976,0.7757
-c21_E100.0_D14.0,100.0,14.0,10.1356,0.9783
-c21_E100.0_D14.0,100.0,14.0,11.3688,0.5156
-c21_E100.0_D14.0,100.0,14.0,19.3174,-2.4262
-c21_E100.0_D14.0,100.0,14.0,19.9514,-2.8030
-c21_E100.0_D14.0,100.0,14.0,20.4735,-2.8039
-c21_E100.0_D14.0,100.0,14.0,20.5626,-2.5550
-c21_E100.0_D14.0,100.0,14.0,21.8644,-3.4930
-c21_E100.0_D14.0,100.0,14.0,24.7076,-4.6342
-c21_E100.0_D14.0,100.0,14.0,30.6763,-6.7785
-c21_E100.0_D14.0,100.0,14.0,33.9541,-7.2420
-c21_E100.0_D14.0,100.0,14.0,36.2924,-7.7558
-c21_E100.0_D14.0,100.0,14.0,39.1079,-7.7445
-c21_E100.0_D14.0,100.0,14.0,40.1243,-7.6951
-c21_E100.0_D14.0,100.0,14.0,42.8281,-7.5809
-c21_E100.0_D14.0,100.0,14.0,44.3291,-7.0670
-c21_E100.0_D14.0,100.0,14.0,51.4279,-5.0965
-c21_E100.0_D14.0,100.0,14.0,55.4199,-3.9369
-c21_E100.0_D14.0,100.0,14.0,55.5504,-3.6383
-c21_E100.0_D14.0,100.0,14.0,57.0454,-3.4212
-c21_E100.0_D14.0,100.0,14.0,59.2735,-2.7533
-c21_E100.0_D14.0,100.0,14.0,59.5433,-2.5339
-c21_E100.0_D14.0,100.0,14.0,62.5104,-1.8750
-c21_E100.0_D14.0,100.0,14.0,63.3907,-1.5274
-c21_E100.0_D14.0,100.0,14.0,64.8087,-1.4323
-c21_E100.0_D14.0,100.0,14.0,70.9197,-0.4610
-c21_E100.0_D14.0,100.0,14.0,70.9475,-0.3932
-c21_E100.0_D14.0,100.0,14.0,71.9687,-0.4127
-c21_E100.0_D14.0,100.0,14.0,75.1364,-0.1455
-c21_E100.0_D14.0,100.0,14.0,76.3444,-0.0985
-c21_E100.0_D14.0,100.0,14.0,82.0035,0.0402
-c21_E100.0_D14.0,100.0,14.0,82.4191,0.0237
-c21_E100.0_D14.0,100.0,14.0,86.8277,0.1597
-c21_E100.0_D14.0,100.0,14.0,91.9545,0.1628
-c21_E100.0_D14.0,100.0,14.0,96.4569,0.2937
-c21_E100.0_D14.0,100.0,14.0,96.7918,0.1551
-c21_E100.0_D14.0,100.0,14.0,98.0474,0.1531
-c21_E100.0_D14.0,100.0,14.0,103.3755,0.0542
-c21_E100.0_D14.0,100.0,14.0,103.4747,0.1404
-c21_E100.0_D14.0,100.0,14.0,106.6575,0.0839
-c21_E100.0_D14.0,100.0,14.0,106.7442,0.0272
-c21_E100.0_D14.0,100.0,14.0,108.2042,0.1180
-c21_E100.0_D14.0,100.0,14.0,109.0557,0.0774
-c21_E100.0_D14.0,100.0,14.0,116.6565,0.1514
-c21_E100.0_D14.0,100.0,14.0,118.5534,-0.1927
-c21_E100.0_D14.0,100.0,14.0,120.2231,0.1119
-c21_E100.0_D14.0,100.0,14.0,120.3606,0.1595
-c21_E100.0_D14.0,100.0,14.0,123.5420,0.2393
-c21_E100.0_D14.0,100.0,14.0,124.4859,0.0021
-c21_E100.0_D14.0,100.0,14.0,128.3533,-0.0250
-c21_E100.0_D14.0,100.0,14.0,130.8227,-0.0394
-c21_E100.0_D14.0,100.0,14.0,135.8647,0.1488
-c21_E100.0_D14.0,100.0,14.0,139.5138,-0.1193
-c21_E100.0_D14.0,100.0,14.0,139.8395,0.1153
-c21_E100.0_D14.0,100.0,14.0,142.4818,0.0719
-c21_E100.0_D14.0,100.0,14.0,144.7075,-0.0583
-c21_E100.0_D14.0,100.0,14.0,145.1402,0.0534
-c21_E100.0_D14.0,100.0,14.0,148.7883,0.0756
-c21_E100.0_D14.0,100.0,14.0,149.9824,0.0407
-c21_E100.0_D14.0,100.0,14.0,152.8546,0.0826
-c21_E100.0_D14.0,100.0,14.0,158.9110,-0.0495
-c21_E100.0_D14.0,100.0,14.0,163.1744,0.0996
-c21_E100.0_D14.0,100.0,14.0,164.4909,0.1103
-c21_E100.0_D14.0,100.0,14.0,165.2389,0.1779
-c21_E100.0_D14.0,100.0,14.0,170.2262,-0.0316
-c21_E100.0_D14.0,100.0,14.0,172.7110,0.0462
-c21_E100.0_D14.0,100.0,14.0,174.4115,0.1006
-c21_E100.0_D14.0,100.0,14.0,175.2286,-0.0920
-c21_E100.0_D14.0,100.0,14.0,176.2510,-0.0806
-c21_E100.0_D14.0,100.0,14.0,180.5489,-0.1394
-c21_E100.0_D14.0,100.0,14.0,184.0239,0.0611
-c21_E100.0_D14.0,100.0,14.0,185.5459,0.0311
-c21_E100.0_D14.0,100.0,14.0,190.3572,-0.0685
-c21_E100.0_D14.0,100.0,14.0,191.9961,0.0742
-c21_E100.0_D14.0,100.0,14.0,196.9127,-0.0898
-c21_E100.0_D14.0,100.0,14.0,200.0000,0.1141
diff --git a/data/io/potential_wide.csv b/data/io/potential_wide.csv
deleted file mode 100644
--- a/data/io/potential_wide.csv
+++ /dev/null
@@ -1,22 +0,0 @@
-dose,y_z001,y_z002,y_z003,y_z004,y_z005,y_z006,y_z007,y_z008,y_z009,y_z010,y_z011,y_z012,y_z013,y_z014,y_z015,y_z016,y_z017,y_z018,y_z019,y_z020,y_z021,y_z022,y_z023,y_z024,y_z025,y_z026,y_z027,y_z028,y_z029,y_z030,y_z031,y_z032,y_z033,y_z034,y_z035,y_z036,y_z037,y_z038,y_z039,y_z040,y_z041,y_z042,y_z043,y_z044,y_z045,y_z046,y_z047,y_z048,y_z049,y_z050,y_z051,y_z052,y_z053,y_z054,y_z055,y_z056,y_z057,y_z058,y_z059,y_z060,y_z061,y_z062,y_z063,y_z064,y_z065,y_z066,y_z067,y_z068,y_z069,y_z070,y_z071,y_z072,y_z073,y_z074,y_z075,y_z076,y_z077,y_z078,y_z079,y_z080,y_z081,y_z082,y_z083,y_z084,y_z085,y_z086,y_z087,y_z088,y_z089,y_z090,y_z091,y_z092,y_z093,y_z094,y_z095,y_z096,y_z097,y_z098,y_z099,y_z100
-6.0,3.0970,2.9275,2.5182,1.9747,1.6303,1.3944,0.8037,0.0438,-0.6412,-1.2222,-1.8430,-2.3980,-3.1521,-3.9623,-4.4257,-4.8440,-5.4712,-5.7513,-6.0028,-5.9464,-5.8763,-5.7633,-5.3654,-5.2259,-4.8587,-4.1144,-3.7279,-3.2046,-2.6203,-2.2479,-1.7300,-1.2005,-0.9935,-0.6806,-0.5049,-0.2390,-0.2460,-0.1919,-0.0853,0.1720,-0.0193,0.1127,0.3229,0.1865,0.3362,0.1529,0.0740,0.0841,-0.0130,0.3272,0.0994,-0.0915,0.0884,0.1927,0.0658,-0.1389,0.2360,-0.0325,0.0231,0.1161,0.0220,0.1011,0.1113,0.0173,-0.0502,0.1610,-0.1862,0.1183,0.1145,0.0613,0.0999,0.1092,0.0520,-0.0042,-0.0009,-0.0862,-0.1321,0.2041,0.0262,-0.0243,0.0248,0.1375,0.0189,0.0989,0.0759,0.0775,0.0290,-0.1229,0.1407,-0.0229,-0.0066,0.2416,0.0547,0.1561,0.2425,0.0511,-0.1016,-0.1102,-0.0283,0.0350
-6.4,3.2171,3.1378,2.3872,1.9298,1.5416,1.3067,0.6580,-0.0896,-0.4774,-1.2339,-1.8209,-2.5106,-3.1579,-3.9392,-4.2714,-4.9608,-5.4497,-5.7410,-6.0407,-6.1842,-5.9565,-5.8560,-5.7449,-5.4886,-4.6562,-4.2292,-3.9016,-3.2639,-2.6703,-2.3252,-1.6426,-1.1042,-0.9135,-0.8489,-0.4413,-0.3844,-0.1830,-0.0731,0.0613,0.1898,-0.0152,0.0165,0.2668,0.3262,0.1163,0.3928,0.3211,0.0365,0.2752,0.0764,-0.0130,0.1351,0.1044,0.0694,-0.1369,0.1385,0.1548,0.0738,0.0931,0.1813,0.0203,-0.0002,0.0367,0.0928,0.0303,0.1472,0.1274,-0.0830,0.1215,0.0922,0.0451,0.0741,0.1782,0.0912,0.1309,0.0192,0.0566,0.0438,0.1931,0.0702,-0.0000,0.0071,0.1539,0.0700,0.1841,0.0472,-0.0133,-0.0792,0.1061,-0.0673,0.0346,-0.0765,0.1332,0.1288,0.1814,-0.0357,-0.0342,-0.2149,-0.0553,0.0685
-6.8,3.2428,2.8094,2.6539,2.2147,1.7377,1.1038,0.6530,0.0064,-0.3656,-1.1133,-1.9421,-2.4711,-3.4115,-3.9996,-4.6833,-5.2062,-5.5350,-5.9527,-6.2236,-6.2680,-6.3279,-6.2479,-5.7252,-5.3098,-5.0251,-4.4389,-3.7883,-3.4286,-2.6172,-2.3309,-1.6541,-1.4468,-1.0819,-0.7366,-0.4032,-0.0818,-0.2890,0.0474,-0.1087,0.1941,0.0402,0.2149,0.0494,0.1991,0.1254,0.3357,0.2166,0.1704,0.1846,0.0899,0.0646,0.1735,-0.0059,0.0397,0.1510,0.0376,0.1633,0.0190,0.0176,0.0561,0.0879,-0.0452,0.1433,-0.0548,0.0650,0.0801,-0.1434,-0.0298,-0.0282,0.0336,0.1130,0.0570,-0.0403,0.0349,0.1315,-0.1779,0.1916,-0.0945,0.0306,0.0553,-0.1464,0.2142,0.0202,-0.0525,0.0484,-0.0484,0.0390,-0.0213,0.0686,0.0689,-0.0472,0.0707,0.0235,0.0785,0.0069,0.0309,0.1997,0.0192,0.0245,-0.1382
-7.2,3.1758,3.0446,2.2843,2.1169,1.8454,1.1673,0.6564,-0.0040,-0.6345,-1.2102,-1.9997,-2.6035,-3.3261,-4.1805,-4.7487,-5.2557,-5.5576,-6.0001,-6.3708,-6.2761,-6.3777,-6.2536,-5.9813,-5.5724,-4.7930,-4.4277,-3.8916,-3.5244,-2.8438,-2.3515,-1.9796,-1.3699,-0.9906,-0.8732,-0.5739,-0.3140,-0.2555,0.0552,-0.0375,0.1122,0.1649,0.1231,0.1654,0.2493,0.0339,0.2249,0.1472,0.2541,0.1194,0.1103,0.0061,-0.0232,0.2041,0.0364,0.1056,0.1765,0.0684,0.3276,-0.1212,0.1836,0.1533,0.0506,0.1253,0.0302,0.0134,0.0232,-0.0950,0.1128,0.1421,0.0952,0.0125,-0.1401,0.1462,0.1171,-0.0995,-0.0892,0.1109,-0.0691,0.0445,-0.0955,-0.0686,0.0594,0.1195,0.0776,0.1340,-0.0265,0.2741,0.0123,-0.0018,-0.0261,-0.1671,0.0527,-0.0654,-0.0157,-0.1122,0.0827,0.1884,-0.0065,-0.0660,-0.0773
-7.6,3.1111,2.7871,2.3286,2.0116,1.4363,1.1514,0.6964,-0.0325,-0.6213,-1.4376,-1.9393,-2.5610,-3.6096,-4.1321,-4.8727,-5.2810,-5.6978,-6.1221,-6.3941,-6.4763,-6.5728,-6.3233,-5.8439,-5.7102,-5.0152,-4.3850,-4.0041,-3.3688,-2.9025,-2.4784,-1.9464,-1.2623,-1.0031,-0.7837,-0.5755,-0.2557,-0.0600,-0.1063,-0.0030,0.2011,0.1184,0.0344,0.2283,0.0765,0.2310,0.1609,0.1910,0.1419,0.1929,0.1531,0.0690,0.0912,0.0938,0.1077,0.1475,0.1478,0.0218,0.1378,0.1373,-0.0234,0.0320,-0.0988,0.1170,0.0614,0.1011,0.0560,-0.0263,0.0857,-0.1258,0.0836,0.1259,0.1825,0.0029,-0.0825,0.0844,0.0744,-0.0213,-0.0210,-0.0185,0.0987,0.0400,-0.0541,-0.0082,-0.0663,-0.0886,-0.0711,-0.0993,-0.1500,0.2358,-0.0690,0.2105,-0.0856,0.1033,0.0091,-0.2826,0.1484,-0.0669,-0.1094,0.1287,0.0434
-8.0,3.2903,2.8822,2.4210,2.1650,1.5556,1.1333,0.7004,-0.0537,-0.6275,-1.3072,-2.0525,-2.7978,-3.7885,-4.1457,-4.9208,-5.3493,-5.8847,-6.3678,-6.5439,-6.4470,-6.6475,-6.3972,-6.1409,-5.3196,-5.2796,-4.5208,-4.1116,-3.6133,-2.8066,-2.3272,-1.7758,-1.4863,-1.0832,-0.7385,-0.5610,-0.3507,-0.2963,-0.2322,0.0391,-0.0338,0.0285,0.1498,0.0880,-0.0034,0.1653,-0.0548,0.0975,-0.0603,0.1946,0.2333,0.1112,0.0636,-0.0838,0.0019,0.0879,0.2047,0.0716,0.1474,0.1011,0.1869,0.1089,0.0431,0.1612,0.0027,0.0301,0.1682,0.1079,0.1321,-0.0665,0.0042,0.1892,-0.0091,-0.0326,-0.2236,-0.2056,-0.0044,0.1362,-0.1222,0.1463,-0.1238,-0.1496,0.0105,0.2665,-0.1051,-0.0700,0.0485,-0.0978,-0.0098,0.0137,0.0311,-0.0636,0.0658,0.0430,0.0584,-0.0220,0.0419,-0.0045,-0.0264,0.0398,0.0817
-8.4,3.2086,2.8474,2.3248,2.0251,1.7447,0.9188,0.6996,-0.3443,-0.6708,-1.4296,-2.0848,-2.9461,-3.7448,-4.2252,-5.0105,-5.3841,-5.8118,-6.2289,-6.5735,-6.7418,-6.6283,-6.5088,-6.1345,-5.7081,-5.2642,-4.7598,-4.0421,-3.5897,-3.0593,-2.4716,-1.9113,-1.6898,-1.2015,-0.8394,-0.2986,-0.5667,-0.2265,-0.0865,0.0891,-0.0361,0.1610,0.0224,0.2867,0.2128,0.0781,0.1575,0.1772,0.0829,0.0608,0.0974,0.0372,0.1649,-0.0706,0.1548,0.1539,0.0901,0.0764,0.1860,0.0583,0.1005,0.1478,0.1111,0.1338,-0.0618,0.0788,-0.0179,0.0628,0.2542,0.1745,0.0067,0.0054,-0.0364,-0.0327,0.0174,0.0761,0.1473,0.1516,-0.2042,-0.0727,-0.0273,0.2132,0.1023,0.0751,0.1493,-0.0065,-0.0470,-0.0007,-0.0736,-0.0371,0.0861,0.0577,0.0673,0.0964,-0.0976,-0.1769,-0.0835,-0.1686,0.0572,0.0358,0.0229
-8.8,3.2026,2.8260,2.4606,1.8999,1.4934,1.2959,0.5126,0.0193,-0.7530,-1.3152,-2.3102,-2.8318,-3.5744,-4.3344,-5.0532,-5.4960,-5.9525,-6.4282,-6.6719,-6.7284,-6.7641,-6.3962,-6.1436,-5.7964,-5.4806,-4.8504,-4.3589,-3.6292,-3.0409,-2.5863,-1.9432,-1.5343,-1.2354,-0.9464,-0.4830,-0.3736,-0.0249,-0.1390,-0.0486,0.2048,0.1078,-0.0069,0.1783,0.3822,0.1537,0.1340,0.1594,0.2863,-0.0080,0.0467,0.0982,0.1129,0.0441,0.1167,-0.0873,0.3266,-0.0935,0.1556,0.0389,-0.0544,-0.1483,-0.0126,0.1506,-0.0464,-0.0124,0.1297,-0.0314,0.0584,0.0139,-0.0509,-0.0662,-0.0487,0.1801,0.0062,-0.1188,0.0544,-0.1167,0.0980,-0.1519,0.0818,0.0187,-0.0563,-0.0952,0.1468,0.0521,0.0217,-0.0672,-0.0547,0.0605,0.0315,0.0252,0.0278,-0.1484,-0.0728,0.1059,0.0656,0.0637,0.0504,-0.0632,0.0506
-9.2,3.0357,2.8028,2.4375,1.9235,1.4092,1.0671,0.5832,-0.2367,-0.8539,-1.6054,-2.1992,-2.8407,-3.7153,-4.5916,-4.9576,-5.7194,-6.1914,-6.6906,-6.8850,-6.8221,-6.6666,-6.5549,-6.1554,-5.7809,-5.4150,-5.0002,-4.1580,-3.6115,-2.9858,-2.2643,-1.9210,-1.6531,-1.2829,-0.9290,-0.5974,-0.2696,-0.1787,-0.2061,-0.0254,0.0476,0.0572,0.0657,0.2409,0.1084,0.2476,0.1902,0.1251,0.2435,0.1622,0.1448,-0.0419,0.1885,0.2779,0.1021,0.1343,0.0748,0.1045,0.0579,0.1071,-0.0094,0.1982,-0.0574,0.0398,0.0102,0.0740,0.0925,0.0960,-0.0817,-0.0026,-0.0391,-0.0919,0.1058,0.0022,0.2200,-0.0212,0.0367,0.0139,0.0625,0.1410,0.0608,-0.0882,-0.1636,-0.0209,0.0227,-0.0495,0.1416,-0.0253,0.1708,-0.0969,-0.0407,-0.1083,0.0287,-0.0108,0.1527,0.0371,-0.1180,-0.0581,0.0112,0.1048,0.0751
-9.6,3.2901,2.7918,2.3206,1.9932,1.5606,1.0273,0.4667,-0.1904,-0.9284,-1.4317,-2.1809,-2.9008,-3.6982,-4.4921,-5.1145,-5.8882,-6.0946,-6.5300,-7.0111,-6.8159,-6.8938,-6.5703,-6.4368,-5.9668,-5.3596,-4.9452,-4.3715,-3.7406,-3.0771,-2.6244,-1.8546,-1.7220,-1.2891,-0.6389,-0.8028,-0.5005,-0.2786,-0.1357,0.1047,-0.0994,0.0363,0.0097,0.0714,0.1273,0.0426,0.2777,0.0681,0.2499,-0.0690,0.1889,0.0506,0.1002,0.0342,-0.0030,-0.0568,0.1232,0.0113,0.0774,0.2195,-0.0544,0.0921,0.0917,0.2633,0.0975,0.0925,0.0895,0.0733,0.1662,0.1862,-0.0836,-0.0834,0.0583,0.0115,0.1080,-0.0026,-0.1627,0.0251,0.0264,0.0470,-0.0457,-0.0370,0.0662,0.0091,0.0611,-0.0828,-0.1308,0.0859,0.0614,0.2648,-0.0323,-0.2039,-0.1085,-0.0698,0.0890,0.0018,0.0855,-0.1258,0.1835,0.0060,0.1419
-10.0,3.2806,2.8256,2.5282,2.0720,1.3477,0.9022,0.4788,-0.2005,-0.9533,-1.4762,-2.4323,-2.9159,-3.8480,-4.4780,-5.2018,-5.6772,-6.3527,-6.8255,-6.9758,-7.1537,-6.9705,-6.7773,-6.4950,-6.1459,-5.3946,-4.8508,-4.2664,-3.8242,-3.1440,-2.6454,-2.1064,-1.5508,-1.1811,-0.7958,-0.5592,-0.2676,-0.0938,0.0056,0.1387,-0.1006,0.2286,0.1291,0.0861,0.1240,0.2347,0.1502,-0.1645,0.0324,0.3256,0.0936,0.0837,0.0998,-0.0034,0.0689,0.1202,0.2041,0.0468,0.0834,-0.1985,0.0821,0.1561,-0.0304,0.1052,0.1098,0.1925,0.0621,0.1409,0.0892,0.0089,-0.1715,-0.0813,-0.1621,-0.0545,-0.1428,-0.0264,0.1649,-0.0604,-0.1886,-0.2393,0.0498,0.0197,-0.0444,0.1512,0.1862,0.0091,-0.1339,-0.0230,-0.2255,-0.0484,-0.1688,0.0028,0.0188,0.1324,-0.1052,-0.0215,0.1446,0.1678,-0.1434,-0.1552,0.0710
-10.4,3.3524,2.6739,2.3232,1.8618,1.4644,0.9823,0.3748,-0.1472,-0.9532,-1.5490,-2.3002,-3.0961,-3.9670,-4.5230,-5.2924,-5.9939,-6.4221,-6.8112,-7.0417,-7.0623,-7.2507,-6.9429,-6.5437,-6.2128,-5.5160,-5.0846,-4.3960,-3.5755,-3.3056,-2.7161,-2.1073,-1.5128,-1.0638,-0.7306,-0.4771,-0.4339,-0.3760,-0.2281,-0.0748,-0.1164,0.1352,0.1877,0.2583,0.1775,0.0689,0.0712,0.1040,-0.1831,0.1434,-0.0169,0.1283,0.0931,0.0821,0.0419,0.1588,0.0723,0.1754,0.1442,0.1319,0.0742,0.2288,0.0774,0.1513,-0.0009,0.1252,-0.0415,0.1002,-0.0792,0.0394,0.0299,-0.0184,-0.0246,0.0114,-0.0095,0.1812,0.1239,-0.0750,0.0141,0.0912,-0.0275,0.0561,-0.1181,0.1261,0.0093,0.0632,-0.0389,0.1059,0.1090,-0.1018,0.0139,-0.0766,-0.1408,-0.0388,0.0398,0.0212,0.0753,-0.0031,0.1413,0.0190,-0.1854
-10.8,3.1075,2.8719,2.3832,1.9329,1.7022,1.0093,0.2892,-0.3527,-0.8488,-1.7777,-2.5027,-3.2040,-3.8936,-4.6316,-5.2735,-5.9452,-6.3967,-6.8878,-7.0232,-7.1833,-7.2427,-6.9845,-6.6109,-6.1783,-5.6277,-5.0152,-4.3842,-3.8977,-3.3028,-2.5057,-2.0237,-1.5078,-1.2825,-0.7635,-0.5877,-0.3040,-0.1926,-0.1757,-0.1219,0.0768,0.0708,0.1839,0.2610,0.0252,0.1391,0.1452,0.0988,0.1746,-0.0839,0.1067,-0.0055,0.1398,0.3343,0.1940,-0.1226,0.1416,-0.0817,0.1957,0.1303,0.0762,0.1700,0.1527,0.0748,0.1043,0.1440,-0.1877,0.0163,-0.0163,0.0963,0.1804,0.1298,0.0710,0.0755,0.1335,0.0627,0.0033,-0.0232,0.0434,-0.1347,-0.0311,-0.2626,0.0396,-0.1456,-0.0594,-0.0617,0.1345,0.2312,-0.1012,0.0445,-0.1332,-0.1262,0.0891,-0.0222,0.1168,0.1192,-0.0830,0.0033,0.0750,0.1186,0.1013
-11.2,3.0608,2.8149,2.1380,1.9049,1.4924,0.9273,0.4243,-0.3576,-1.0064,-1.6951,-2.3659,-3.1344,-4.0475,-4.7214,-5.5721,-6.0875,-6.5163,-6.8701,-7.0594,-7.1191,-7.1586,-6.9738,-6.4684,-6.0589,-5.6956,-5.1043,-4.4893,-3.8757,-3.3872,-2.7029,-2.1184,-1.4138,-1.2186,-0.9449,-0.6050,-0.5671,-0.2886,-0.1848,-0.0298,0.1290,0.2054,0.0721,0.1629,0.1415,0.0559,0.3085,0.2105,0.0579,0.1915,0.0344,0.2049,0.2540,-0.0099,0.0601,0.1909,-0.0236,0.0048,0.0540,0.1110,0.1944,-0.0897,0.1469,-0.0217,0.1169,0.0258,0.0903,0.1072,0.1171,-0.0101,0.1259,0.0333,0.0685,0.0803,-0.0857,-0.0493,0.0449,0.0316,-0.1185,0.1431,0.1557,-0.0902,0.1295,0.2246,0.0572,-0.0998,-0.0309,0.0785,0.0418,-0.0130,-0.0709,-0.0075,0.0641,0.0675,-0.2057,-0.0100,0.0556,0.0784,-0.1104,-0.0405,0.1643
-11.600000000000001,3.2496,2.7639,2.4749,1.8608,1.4274,0.9050,0.3193,-0.1180,-0.8886,-1.5932,-2.4688,-3.1233,-3.9219,-4.7391,-5.5374,-5.9555,-6.5197,-6.9953,-7.1256,-7.3047,-7.3954,-7.0788,-6.7499,-6.3813,-5.9018,-5.0524,-4.5996,-3.9749,-3.3897,-2.5416,-2.1590,-1.7099,-1.3545,-1.0698,-0.5982,-0.5391,-0.1877,-0.0478,-0.1505,0.1487,0.1080,0.0314,0.1485,0.1908,0.2296,0.0490,0.3184,-0.0166,0.2330,0.2081,-0.0541,0.0171,0.1065,0.2048,0.0395,0.0214,0.0589,0.0985,-0.0138,-0.0241,0.0838,-0.0007,0.0191,0.1729,0.0447,0.2954,0.0434,-0.0075,0.0150,0.0792,-0.0803,-0.0217,0.0240,0.1452,0.0922,-0.1608,-0.0223,0.0788,0.1491,0.0716,0.1024,-0.1321,0.0969,-0.1806,-0.0119,-0.0461,0.0602,0.0099,-0.0657,-0.1347,0.0143,-0.0034,-0.0228,-0.0291,0.0599,0.0860,0.0978,-0.0606,-0.0419,0.0633
-12.0,2.9703,2.5163,2.4986,1.9303,1.3661,0.9965,0.5463,-0.4156,-0.9820,-1.6812,-2.5681,-3.3340,-3.9684,-4.9579,-5.3294,-6.0197,-6.7479,-7.2551,-7.1526,-7.4935,-7.3282,-7.1599,-6.7033,-6.1273,-5.7567,-5.0475,-4.5934,-3.8265,-3.3164,-2.4565,-2.0520,-1.8248,-1.2109,-0.9584,-0.6920,-0.3740,-0.4028,-0.1994,-0.1242,0.0472,0.0434,0.1513,-0.0366,0.1022,0.1106,0.2245,0.0301,0.0012,0.2847,0.1179,0.1718,0.1583,0.1384,0.2947,0.1180,0.1535,0.1354,0.1240,0.0564,0.3004,0.2045,0.0865,0.0613,-0.0514,0.0509,0.1880,0.1485,-0.0373,0.0420,0.0447,-0.0993,0.0720,0.1079,-0.0206,0.1968,0.0679,0.0626,-0.0736,-0.0056,0.1392,0.0390,0.0514,-0.0919,-0.0204,0.0816,0.0710,0.1298,0.0349,0.0262,0.1338,0.0454,0.2069,-0.0163,0.0698,0.0770,0.0813,0.0556,0.0250,-0.1172,-0.0464
-12.4,3.2366,2.8245,2.3073,1.9156,1.4131,0.9129,0.3135,-0.3367,-0.9857,-1.7659,-2.5679,-3.4764,-4.1982,-4.9061,-5.5073,-6.2658,-6.7324,-7.2897,-7.3364,-7.3568,-7.4887,-7.1353,-6.9065,-6.5068,-5.8674,-5.2254,-4.7508,-3.9406,-3.3451,-2.5292,-2.0966,-1.6665,-1.3094,-1.0670,-0.6872,-0.5294,-0.2227,-0.2518,0.0107,0.2501,0.1918,0.1081,-0.0392,0.2103,0.1268,0.1554,0.1437,0.2543,0.0760,0.2296,0.1544,0.2470,0.0132,0.1084,0.0364,0.1430,0.0615,0.0835,0.1022,0.0772,-0.0469,0.0684,0.0755,-0.0421,0.1236,0.0058,-0.1401,-0.1105,0.0008,0.1513,0.0609,-0.0682,0.0675,-0.0373,-0.0391,-0.0287,0.1257,-0.0409,0.0904,0.1221,0.0370,-0.1414,0.0012,0.2260,0.0948,0.0446,-0.0299,-0.1441,-0.0122,-0.1124,0.0756,-0.0220,-0.0284,-0.1605,0.0640,-0.0360,-0.0241,0.0829,-0.0767,-0.0960
-12.8,2.9974,2.7540,2.3417,1.9695,1.4744,0.9573,0.3835,-0.3855,-0.9905,-1.9606,-2.6932,-3.3879,-4.1370,-4.8544,-5.6405,-6.3132,-6.9439,-7.2028,-7.5628,-7.4357,-7.4054,-7.3608,-6.8890,-6.4558,-5.8794,-5.1568,-4.6217,-3.9472,-3.2913,-2.6114,-2.1691,-1.6268,-1.2123,-0.9942,-0.6129,-0.4379,-0.2501,-0.0511,0.2392,0.0515,0.1718,0.3720,0.2063,0.1608,0.1008,0.2162,0.1970,0.1450,0.1748,0.0893,0.0560,0.0901,0.1714,0.1372,0.1236,0.2283,0.1381,0.1225,0.0289,0.0576,0.1450,0.0349,0.1071,0.0235,0.0778,-0.0307,0.2258,0.1727,-0.0406,0.2003,-0.0421,0.1620,0.2118,0.1633,0.1267,0.0567,0.0970,0.0825,0.0388,0.1797,-0.2398,0.0163,0.0312,-0.1073,0.1018,0.0391,0.0784,0.2433,0.0072,-0.0924,0.0100,-0.0224,0.0117,0.0064,-0.1933,0.0523,-0.1268,-0.0929,-0.0473,-0.0328
-13.2,3.0646,2.7098,2.3235,1.9244,1.2589,0.8112,0.2398,-0.3411,-1.0895,-1.7072,-2.4263,-3.6251,-4.1910,-5.1680,-5.7236,-6.3643,-7.0062,-7.1634,-7.5377,-7.7382,-7.4228,-7.2141,-6.9324,-6.5527,-5.8096,-5.2937,-4.7049,-4.0935,-3.3664,-2.7288,-2.3212,-1.7930,-1.4117,-1.0431,-0.5738,-0.5204,-0.1779,-0.1188,0.1397,-0.0703,0.0754,0.0575,0.1363,0.0258,0.1342,0.3630,0.2150,0.1502,0.1333,0.1946,0.0962,-0.0895,0.1481,-0.0560,0.0436,-0.0263,0.1282,-0.0026,0.1562,0.0901,0.0885,0.0564,0.1553,-0.0425,0.1161,-0.1128,0.0582,0.1827,0.0045,0.0310,0.0453,0.0674,-0.0885,0.0930,-0.0392,-0.0028,0.0679,0.0056,0.0275,0.0387,0.0862,-0.2245,-0.0267,-0.0083,0.1579,0.1334,-0.2098,0.1109,-0.0549,-0.1426,0.1137,0.0460,0.0055,0.1681,0.1592,-0.1431,0.1051,0.0956,0.0020,0.0794
-13.600000000000001,3.1133,2.8718,2.4863,1.8895,1.5787,0.7693,0.2200,-0.4222,-1.0698,-1.8871,-2.6266,-3.4653,-4.1263,-4.9888,-5.8484,-6.5186,-6.9286,-7.2838,-7.5137,-7.6050,-7.7326,-7.3233,-7.0542,-6.6514,-6.1065,-5.4832,-4.7426,-4.1191,-3.3515,-2.8597,-2.2346,-1.6463,-1.3244,-0.9428,-0.9509,-0.3361,-0.3089,-0.3108,-0.0582,0.0084,0.0916,0.1143,-0.0008,0.1291,0.0272,0.2424,0.2406,0.3253,-0.0461,0.2240,0.1735,0.2583,0.0341,0.1091,-0.0249,0.0192,-0.1071,0.0051,0.0151,0.0532,0.0586,0.2484,-0.1664,0.1191,-0.1645,0.0431,0.1380,-0.0698,0.1643,-0.0038,0.0439,-0.0931,0.0285,-0.1366,0.0834,0.1214,0.0758,0.0415,0.2251,0.0228,0.0540,0.0726,0.0378,-0.1332,0.1916,-0.1251,0.0126,-0.0181,0.0774,0.0987,-0.1137,0.0021,0.0300,0.0261,0.0533,0.0291,-0.0389,-0.0562,-0.1297,-0.1218
-14.0,3.0610,2.5883,2.3679,2.1256,1.3904,0.8236,0.1310,-0.4376,-1.0711,-1.8475,-2.8669,-3.5687,-4.3421,-5.0184,-5.6755,-6.3163,-6.9886,-7.3576,-7.6889,-7.6813,-7.6518,-7.6362,-7.1786,-6.6240,-6.0339,-5.3852,-4.7187,-4.1884,-3.3750,-3.0202,-2.2904,-1.8313,-1.2876,-1.0203,-0.7649,-0.4166,-0.4261,-0.0706,-0.0111,-0.1922,0.1572,-0.0611,0.3403,0.2273,0.2567,0.3146,0.1862,0.0784,0.1242,0.2053,0.1193,0.2315,0.1618,0.0065,0.0793,0.1692,0.0151,0.1394,0.2751,0.0710,-0.1263,0.3203,-0.1294,-0.0864,0.0458,0.0531,-0.1556,0.0015,0.0698,0.0620,-0.0272,-0.0094,0.1446,0.0165,0.0417,0.0501,0.0314,-0.1315,-0.0734,-0.1435,0.1265,0.0311,-0.0135,0.0741,0.1445,0.0737,0.0194,0.0240,-0.1531,-0.1791,0.1829,-0.1427,0.1647,0.0871,0.1114,0.0655,-0.0431,-0.0649,0.0277,0.1383
diff --git a/data/io/wide_sample.csv b/data/io/wide_sample.csv
deleted file mode 100644
--- a/data/io/wide_sample.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-name,x1,x2,1,2,3,4,5,6,7,8,9,10
-a,1,0,1,,3,,5,,7,,9,
-b,2,0,,4,,8,10,,14,,,20
-c,3,0,0.1,0.2,,0.4,,0.6,,0.8,,1
-d,4,0,,1,1.5,2,2.5,,,4,,5
-e,5,0,3,,9,12,,18,,,,30
diff --git a/data/readme/sales.csv b/data/readme/sales.csv
deleted file mode 100644
--- a/data/readme/sales.csv
+++ /dev/null
@@ -1,21 +0,0 @@
-price,promo,sales
-9.99,0,142.3
-9.99,1,178.5
-14.99,0,118.2
-14.99,1,151.7
-19.99,0,95.4
-19.99,1,128.9
-24.99,0,76.1
-24.99,1,104.6
-29.99,0,58.8
-29.99,1,87.2
-12.49,0,131.4
-12.49,1,164.0
-17.49,0,107.1
-17.49,1,140.3
-22.49,0,85.6
-22.49,1,116.4
-27.49,0,67.5
-27.49,1,95.7
-9.99,0,138.1
-14.99,1,154.2
diff --git a/data/regression/test_lm.csv b/data/regression/test_lm.csv
deleted file mode 100644
--- a/data/regression/test_lm.csv
+++ /dev/null
@@ -1,51 +0,0 @@
-x,y
-6.3943,18.1739
-2.2321,6.7685
-7.3647,18.0047
-0.8694,0.3375
-4.2192,12.5833
-5.0536,13.8300
-0.2654,2.3500
-5.4494,16.6854
-2.2044,4.1982
-0.0650,-0.2904
-8.0582,20.7076
-1.5548,3.5909
-9.5721,24.5877
-0.9672,3.9842
-8.4749,20.0238
-7.2973,17.5926
-5.3623,15.8479
-5.5204,14.5551
-8.2940,19.5412
-5.7735,13.4116
-7.0457,19.6487
-2.8939,8.5410
-0.7979,3.0695
-2.7797,8.6375
-6.3568,15.9394
-2.0951,7.3206
-2.6698,9.6727
-6.0913,15.3881
-1.7114,5.1613
-3.7946,9.5981
-9.8952,24.5181
-6.8461,16.6406
-8.4285,22.2473
-0.3210,0.7350
-3.1545,8.7715
-9.4291,25.5990
-8.7637,22.0436
-3.9563,12.9021
-9.1455,22.7260
-2.4663,7.4665
-5.6137,14.8752
-8.9782,25.4275
-3.9940,11.9810
-5.0953,18.8409
-0.9091,3.9642
-6.2745,16.8971
-7.9208,20.3222
-3.8162,10.7958
-9.9612,21.9766
-8.6078,21.7931
diff --git a/data/regression/test_poisson.csv b/data/regression/test_poisson.csv
deleted file mode 100644
--- a/data/regression/test_poisson.csv
+++ /dev/null
@@ -1,101 +0,0 @@
-x,count
-1.9183,4
-2.6765,7
-1.6348,4
-2.0944,6
-1.8112,3
-1.1356,3
-1.7321,8
-1.0945,3
-1.9441,5
-1.9200,9
-2.6291,9
-2.6935,7
-2.3762,5
-2.5823,9
-2.6276,7
-0.4585,2
-1.5911,3
-2.6362,4
-0.2570,2
-2.2975,4
-1.2694,5
-1.9496,5
-0.6903,2
-0.6868,4
-0.6427,1
-1.7131,6
-1.4011,2
-0.2953,2
-0.7460,5
-1.3294,3
-2.5081,9
-0.7956,3
-2.9863,7
-0.1715,2
-0.4723,3
-2.0256,5
-1.2577,2
-0.6128,3
-0.9000,4
-2.9883,8
-2.1106,4
-0.8981,5
-0.8170,3
-0.7920,2
-0.2769,3
-1.9113,6
-2.7118,6
-2.3884,5
-2.6524,5
-2.4323,4
-2.4067,5
-2.5758,9
-2.9007,5
-0.3460,2
-0.7964,2
-0.9410,3
-0.7642,2
-1.6154,5
-0.9906,2
-0.9010,5
-2.8211,8
-1.9641,4
-2.3881,5
-2.2534,6
-0.2185,3
-1.4831,3
-2.3101,7
-1.3694,2
-1.7864,5
-1.6436,5
-2.2974,7
-0.4151,2
-0.1927,4
-2.7145,7
-2.5603,10
-1.2130,3
-0.0809,3
-0.4071,4
-2.2837,7
-1.0499,3
-2.8488,7
-2.3007,4
-2.8942,8
-1.8817,5
-2.3479,5
-2.4670,6
-2.8964,7
-2.4922,7
-0.5455,3
-2.1036,4
-1.4657,4
-0.2792,3
-1.4198,4
-0.8127,4
-2.2866,4
-1.5739,6
-0.8229,2
-0.3788,2
-1.7222,4
-1.4332,3
diff --git a/demo/Demo.hs b/demo/Demo.hs
deleted file mode 100644
--- a/demo/Demo.hs
+++ /dev/null
@@ -1,94 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-module Main where
-
-import qualified DataFrame.Internal.Column    as DX
-import qualified DataFrame.Internal.DataFrame  as DX
-import Hanalyze.Model.GLMM
-import Hanalyze.Model.Core (coeffList, rSquared1, fittedList)
-import Hanalyze.Model.LM   (multiPolyDesignMatrix, fitLMVec)
-
-import qualified Data.Vector           as V
-import qualified Data.Text             as T
-import qualified Numeric.LinearAlgebra as LA
-import Data.List   (zip4)
-import Text.Printf (printf)
-
--- ---------------------------------------------------------------------------
--- テストデータ: 3クラスの試験結果
---
--- 真のモデル: score = 64 + u_school + 2×hours + ε
---   u_A ≈ +20,  u_B ≈ 0,  u_C ≈ -20
---
--- クラスA(優秀): 1〜5時間、成績80台  ← 少ない時間で高得点
--- クラスB(平均): 3〜7時間、成績60台
--- クラスC(苦手): 6〜10時間、成績40台 ← 多くの時間で低得点
---
--- OLSで見ると: 「時間 ↑ → 成績 ↓」(Simpson's paradox)
--- GLMMで見ると: 「時間 +1h → +2点」(真の効果)
--- ---------------------------------------------------------------------------
-
-hoursVec :: V.Vector Double
-hoursVec = V.fromList [1,2,3,4,5, 3,4,5,6,7, 6,7,8,9,10]
-
-scoresVec :: V.Vector Double
-scoresVec = V.fromList
-  [ 80.2, 82.0, 84.1, 86.0, 88.2   -- class A
-  , 59.9, 62.1, 64.0, 66.2, 68.1   -- class B
-  , 40.1, 42.2, 44.0, 45.9, 47.8 ] -- class C
-
-schoolVec :: V.Vector String   -- annotated for readability; converted below
-schoolVec = V.fromList
-  ["A","A","A","A","A", "B","B","B","B","B", "C","C","C","C","C"]
-
-main :: IO ()
-main = do
-  let df = DX.insertColumn "hours"  (DX.fromList (V.toList hoursVec :: [Double]))
-         $ DX.insertColumn "score"  (DX.fromList (V.toList scoresVec :: [Double]))
-         $ DX.insertColumn "school" (DX.fromList
-             (["A","A","A","A","A","B","B","B","B","B","C","C","C","C","C"] :: [T.Text]))
-         $ DX.empty
-
-  -- ── OLS (school を無視した単純回帰) ──────────────────────────────────
-  let dm     = multiPolyDesignMatrix [(hoursVec, 1)]
-      y      = LA.fromList (V.toList scoresVec)
-      olsRes = fitLMVec dm y
-      (b0, b1) = case coeffList olsRes of { (a:b:_) -> (a,b); _ -> (0,0) }
-
-  putStrLn "╔══════════════════════════════════════════════════════════╗"
-  putStrLn "║  OLS  (school を無視した単純回帰)                       ║"
-  putStrLn "╚══════════════════════════════════════════════════════════╝"
-  printf "  β₀ (切片)   : %8.3f\n" b0
-  printf "  β₁ (hours)  : %8.3f   ← 負! 時間が増えると成績が下がる？\n" b1
-  printf "  R²           : %8.3f\n" (rSquared1 olsRes)
-  putStrLn "  ↑ Simpson's paradox: schoolベースライン差がhours効果を逆転させている"
-
-  -- ── GLMM (school ランダム切片) ────────────────────────────────────────
-  putStrLn ""
-  putStrLn "╔══════════════════════════════════════════════════════════╗"
-  putStrLn "║  GLMM (school ランダム切片モデル)                       ║"
-  putStrLn "╚══════════════════════════════════════════════════════════╝"
-  case fitLMEDataFrame [("hours", 1)] "school" "score" df of
-    Nothing -> putStrLn "Error: GLMM推定に失敗"
-    Just gr -> do
-      let (g0, g1) = case coeffList (glmmFixed gr) of { (a:b:_) -> (a,b); _ -> (0,0) }
-
-      putStrLn "  固定効果:"
-      printf "    β₀ (切片)   : %8.3f\n" g0
-      printf "    β₁ (hours)  : %8.3f   ← 正! 真の効果を回収\n" g1
-      putStrLn "  分散成分:"
-      printf "    σ²_u (school間) : %8.3f\n" (glmmRandVar gr)
-      printf "    σ²   (残差)     : %8.3f\n" (glmmResidVar gr)
-      printf "    ICC              : %8.3f  (分散の%.0f%%がschool間)\n"
-             (glmmICC gr) (glmmICC gr * 100)
-      putStrLn "  BLUPs (schoolごとのランダム切片 û_j):"
-      mapM_ (\(s, u) -> printf "    %s : %+8.3f\n" s u)
-            (zip (V.toList (glmmGroups gr)) (V.toList (glmmBLUPs gr)))
-      putStrLn ""
-      putStrLn "  観測値 vs 条件付きフィット値:"
-      putStrLn "  school  hours  actual  fitted  resid"
-      let fitted  = fittedList (glmmFixed gr)
-          sLabels = ["A","A","A","A","A","B","B","B","B","B","C","C","C","C","C"] :: [String]
-      mapM_ (\(s, h, ya, yf) ->
-               printf "    %-4s   %5.0f  %6.1f  %6.1f  %+5.2f\n"
-                      s h ya yf (ya - yf))
-            (zip4 sLabels (V.toList hoursVec) (V.toList scoresVec) fitted)
diff --git a/demo/IntegratedDemo.hs b/demo/IntegratedDemo.hs
deleted file mode 100644
--- a/demo/IntegratedDemo.hs
+++ /dev/null
@@ -1,177 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | 統合デモ (Phase K2): 1 つのリアルなシナリオで複数機能を組合せる。
---
--- シナリオ: 2 つの病院での治療効果比較 (階層モデル)
---   - 各病院 j で患者 i に効果 y_{ij} を観測
---   - 病院効果 μ_j ~ MvNormal([μ_pop, μ_pop], Σ) で相関を持つ
---     (= 同じ患者層を共有してるので相関がある)
---   - σ_y ~ InverseGamma(2, 3) (Phase I — 共役事前)
---   - 派生量: 治療効果差 Δ = μ_1 - μ_2  (Phase G1 — Deterministic)
---
--- 使う機能:
---   * Phase G6: mvNormalLatent (病院効果 μ_j のベクトル latent)
---   * Phase H4: lkjCorrCholesky で相関を学習
---   * Phase I:  InverseGamma で σ² 事前
---   * Phase G1: deterministic で Δ を保存
---   * Phase F1: posteriorSummaryFile で az.summary 風 HTML
---   * Phase F2: tracePlotHDIFile で 94% HDI トレース
---   * Phase F4: ppcPlotFile で観測との適合性チェック
---   * Phase G4: NUTS divergence 検出 (chainDivergences)
---   * Phase E:  energyPlotFile で BFMI 確認
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-import qualified System.Random.MWC.Distributions as MWC
-
-import Hanalyze.MCMC.Core (chainEnergy, chainDivergences)
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, deterministic,
-                  Distribution (..), augmentChainWithDeterministic,
-                  lkjCorrCholesky)
-import Hanalyze.Stat.MCMC (bfmi)
-import Hanalyze.Stat.PosteriorPredictive (posteriorPredictive)
-import Hanalyze.Viz.MCMC (printPosteriorSummary, posteriorSummaryFile,
-                 tracePlotHDIFile, energyPlotFile, ppcPlotFile)
-import Hanalyze.Viz.Core (defaultConfig, OutputFormat (..), PlotConfig (..))
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 1000
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.05
-        , nutsMaxDepth   = 7
-        }
-
--- 真値:
---   μ_pop = 1.0,  σ_pop = 0.3
---   病院効果 μ_1 = 1.2, μ_2 = 0.8 (差 Δ_true = 0.4)
---   σ_y = 0.5 (患者ごとの個体差)
-hospital1Obs, hospital2Obs :: [Double]
-hospital1Obs = [1.4, 0.9, 1.3, 1.2, 1.0, 1.5, 0.8, 1.25, 1.1, 1.18,
-                1.35, 1.05, 1.4, 1.15, 1.22]
-hospital2Obs = [0.7, 1.0, 0.85, 0.65, 0.9, 0.8, 1.05, 0.75, 0.92, 0.78,
-                0.6, 0.95, 0.82, 0.7, 0.88]
-
--- 共分散構造 (固定 sd=σ_pop=0.3、相関は LKJ 事前で学習)
-clinicalModel :: ModelP ()
-clinicalModel = do
-  -- 母集団パラメタ
-  muPop  <- sample "mu_pop"  (Normal 1 2)
-  sigPop <- sample "sig_pop" (HalfNormal 1)
-
-  -- 観測ノイズの分散事前 (Phase I: InverseGamma)
-  sig2y <- sample "sig2_y" (InverseGamma 2 0.3)
-  let sigY = sqrt sig2y
-
-  -- 病院効果の相関行列 (Phase H4: LKJ)
-  l <- lkjCorrCholesky "R" 2 1.0   -- η = 1: uniform 事前
-
-  -- 共分散 Σ = diag(σ_pop) × R × diag(σ_pop), R = L Lᵀ
-  -- L0 = (1, 0); L1 = (ρ, √(1-ρ²)). σ_pop で scale。
-  let l00 = (l !! 0) !! 0
-      l10 = (l !! 1) !! 0
-      l11 = (l !! 1) !! 1
-      sLL = sigPop * sigPop
-      cov = [ [sLL * l00 * l00, sLL * l00 * l10]
-            , [sLL * l00 * l10, sLL * (l10*l10 + l11*l11)] ]
-
-  -- 病院効果 μ_j を MvNormal latent (Phase G6 mvNormalLatent 相当)
-  -- ここでは 2D なので非中心化を直接書く。
-  raw0 <- sample "mu_h_raw0" (Normal 0 1)
-  raw1 <- sample "mu_h_raw1" (Normal 0 1)
-  let muH1 = muPop + sigPop * l00 * raw0
-      muH2 = muPop + sigPop * (l10 * raw0 + l11 * raw1)
-  _ <- deterministic "mu_h1" muH1
-  _ <- deterministic "mu_h2" muH2
-
-  -- 派生量: 治療効果差 (Phase G1)
-  _ <- deterministic "delta" (muH1 - muH2)
-
-  -- 観測 (Phase G5 spirit: パラメトリックモデル関数)
-  observe "y1" (Normal muH1 sigY) hospital1Obs
-  observe "y2" (Normal muH2 sigY) hospital2Obs
-  -- 共分散構造を活かして cov 自体は使っていない (簡易版)
-  -- (フル MvNormal observation だと両病院の個体間相関が要る)
-  let _ = cov  -- 抑制
-  return ()
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  統合デモ (Phase K2): 2 病院の治療効果比較 (階層モデル)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-  putStrLn "シナリオ:"
-  putStrLn "  μ_pop, σ_pop ~ Normal/HalfNormal      (母集団効果)"
-  putStrLn "  R ~ LKJ(η=1)                          (病院間相関)"
-  putStrLn "  σ²_y ~ InverseGamma(2, 0.3)           (観測ノイズ分散)"
-  putStrLn "  μ_h1, μ_h2 = MvN([μ_pop, μ_pop],"
-  putStrLn "                  diag(σ_pop) R diag(σ_pop))"
-  putStrLn "  Δ = μ_h1 - μ_h2                       (派生量, Deterministic)"
-  putStrLn ""
-  printf "  観測: 病院 1 (n=%d, mean=%.3f), 病院 2 (n=%d, mean=%.3f)\n"
-         (length hospital1Obs) (sum hospital1Obs / fromIntegral (length hospital1Obs))
-         (length hospital2Obs) (sum hospital2Obs / fromIntegral (length hospital2Obs))
-  putStrLn ""
-
-  gen <- createSystemRandom
-
-  let init0 = Map.fromList
-        [ ("mu_pop", 1.0), ("sig_pop", 0.3)
-        , ("sig2_y", 0.25)
-        , ("R_u1_0", 0.5)
-        , ("mu_h_raw0", 0.0), ("mu_h_raw1", 0.0)
-        ]
-
-  putStrLn "[1] NUTS (1000 iter, 500 burn-in) を実行中..."
-  rawCh <- nuts clinicalModel cfg init0 gen
-  let ch = augmentChainWithDeterministic clinicalModel rawCh
-
-  let names = [ "mu_pop", "sig_pop", "sig2_y"
-              , "R_pc1_0"        -- 病院間相関 ρ
-              , "mu_h1", "mu_h2", "delta" ]
-  printPosteriorSummary names [ch]
-  putStrLn ""
-
-  -- 診断: BFMI と divergences
-  let es     = chainEnergy rawCh
-      divs   = chainDivergences rawCh
-      bfmiV  = case bfmi es of
-        Just v  -> v
-        Nothing -> 0/0
-  printf "  BFMI = %.3f  (>0.3 で良好、>0.5 で理想)\n" bfmiV
-  printf "  Divergences: %d 件 / %d 反復\n"
-         (length divs) (nutsIterations cfg)
-  putStrLn ""
-
-  -- 出力: F1 / F2 / F4 / E のすべて
-  let pcfg t = (defaultConfig t) { plotWidth = 700, plotHeight = 280 }
-      hcfg t = (defaultConfig t) { plotWidth = 700, plotHeight = 90 }
-
-  posteriorSummaryFile "integrated-summary.html"
-    "Clinical hierarchical model — posterior summary" names [ch]
-  putStrLn "  → integrated-summary.html (F1: posterior summary)"
-
-  tracePlotHDIFile HTML "integrated-trace-hdi.html"
-    (hcfg "Clinical model — trace with 94% HDI") 0.94 names ch
-  putStrLn "  → integrated-trace-hdi.html (F2: HDI 帯付きトレース)"
-
-  energyPlotFile HTML "integrated-energy.html"
-    (pcfg "Clinical model — energy plot") rawCh
-  putStrLn "  → integrated-energy.html (E: energy plot + BFMI)"
-
-  -- 事後予測 (病院 1 のみ)
-  preds <- posteriorPredictive clinicalModel ch gen
-  let yReps = [Map.findWithDefault [] "y1" m | m <- preds]
-  ppcPlotFile HTML "integrated-ppc.html"
-    (pcfg "Clinical model — PP check (hospital 1)") hospital1Obs yReps 50
-  putStrLn "  → integrated-ppc.html (F4: posterior predictive check)"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ 階層モデル + 8 種の機能を 1 つのストーリーで統合"
-  putStrLn "    (LKJ + InvGamma + non-centered + Deterministic + 4 種の HTML)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/AR1Demo.hs b/demo/bayesian/AR1Demo.hs
deleted file mode 100644
--- a/demo/bayesian/AR1Demo.hs
+++ /dev/null
@@ -1,98 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | AR(1) 状態空間モデルのデモ (Phase J2)。
---
--- 真値: ϕ=0.7, σ_state=0.5, σ_obs=0.3
--- 真の x_t を AR(1) で生成、ノイズを加えて y_t を観測。
--- ϕ, σ_state, σ_obs を NUTS で同時推定 (x_t は latent ベクトル)。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Data.Text (Text)
-import qualified Data.Text as T
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-import qualified System.Random.MWC.Distributions as MWC
-
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, ar1Latent,
-                  Distribution (..), augmentChainWithDeterministic)
-import Hanalyze.Viz.MCMC (printPosteriorSummary, posteriorSummaryFile)
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 600
-        , nutsBurnIn     = 400
-        , nutsStepSize   = 0.05
-        , nutsMaxDepth   = 7
-        }
-
-genData :: Int -> Double -> Double -> Double -> IO ([Double], [Double])
-genData nT phi sigSt sigOb = do
-  gen <- createSystemRandom
-  let stat0 = sigSt / sqrt (1 - phi * phi)
-  z0 <- MWC.normal 0 stat0 gen
-  let go t prev acc
-        | t == nT = return (reverse acc)
-        | otherwise = do
-            eps <- MWC.normal 0 sigSt gen
-            let x = phi * prev + eps
-            go (t+1) x (x : acc)
-  xs <- go 1 z0 [z0]
-  ys <- mapM (\x -> do
-                 e <- MWC.normal 0 sigOb gen
-                 return (x + e)) xs
-  return (xs, ys)
-
-ar1Model :: Int -> [Double] -> ModelP ()
-ar1Model nT ys = do
-  phi   <- sample "phi"       (Uniform (-0.99) 0.99)
-  sigSt <- sample "sig_state" (HalfNormal 1)
-  sigOb <- sample "sig_obs"   (HalfNormal 1)
-  xs <- ar1Latent "x" nT phi sigSt
-  mapM_ (\(t, y) -> observe ("y_" <> tShow t) (Normal (xs !! t) sigOb) [y])
-        (zip [0 .. nT - 1] ys)
-  where
-    tShow :: Int -> Text
-    tShow = T.pack . show
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  AR(1) 状態空間モデル (Phase J2)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  let nT = 30
-  printf "真値: ϕ=0.7, σ_state=0.5, σ_obs=0.3, 系列長 N=%d\n" nT
-  (_, ysObs) <- genData nT 0.7 0.5 0.3
-  printf "観測 y の最初 5 点: %s\n"
-         (show (take 5 ysObs))
-  putStrLn ""
-
-  gen <- createSystemRandom
-  let init0 = Map.fromList $
-        [(("x_raw" <> T.pack (show t)) :: Text, 0.0 :: Double)
-         | t <- [0 .. nT - 1]]
-        ++ [("phi", 0.5), ("sig_state", 0.5), ("sig_obs", 0.3)]
-  ch0 <- nuts (ar1Model nT ysObs) cfg init0 gen
-  let ch = augmentChainWithDeterministic (ar1Model nT ysObs) ch0
-
-  putStrLn "[1] Posterior summary (主要パラメタのみ)"
-  printPosteriorSummary ["phi", "sig_state", "sig_obs"] [ch]
-  putStrLn ""
-
-  putStrLn "[2] 一部の latent state x_t (派生量)"
-  printPosteriorSummary
-    [ "x_" <> T.pack (show t) | t <- [0, 5, 10, 15, 20, 25, 29] ]
-    [ch]
-  putStrLn ""
-
-  posteriorSummaryFile "ar1-summary.html" "AR(1) posterior"
-    ["phi", "sig_state", "sig_obs"] [ch]
-  putStrLn "  → ar1-summary.html"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ AR(1) latent + 観測モデルで状態空間が動作"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/BenchMCMC.hs b/demo/bayesian/BenchMCMC.hs
deleted file mode 100644
--- a/demo/bayesian/BenchMCMC.hs
+++ /dev/null
@@ -1,215 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | MH / HMC / NUTS のパフォーマンス比較デモ
---
--- ケース 1 (易しい): 独立 2D 正規事後分布
---   μ₁ ~ N(0,5), μ₂ ~ N(0,5)
---   y₁ᵢ | μ₁ ~ N(μ₁,1),  y₂ᵢ | μ₂ ~ N(μ₂,1)
---   → 事後分布の等高線は円形。全手法で効率よく探索できる。
---
--- ケース 2 (難しい): 和制約による強反相関事後分布
---   α ~ N(0,5), β ~ N(0,5)
---   yᵢ | α,β ~ N(α+β, 1)
---   → 事後分布は α+β ≈ ȳ という細長い尾根 (ρ ≈ -0.998)。
---     MH は短軸 (SD≈0.2) にステップを合わせると長軸 (SD≈7) の探索が
---     ランダムウォーク化し ESS が激減する。
---     HMC/NUTS は勾配で尾根に沿って動けるため効率を維持できる。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import qualified Data.Text as T
-import Data.Time.Clock (getCurrentTime, diffUTCTime)
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.Model.HBM
-import Hanalyze.MCMC.Core (Chain (..), chainVals, acceptanceRate, posteriorMean)
-import Hanalyze.MCMC.MH   (metropolis, MCMCConfig (..))
-import Hanalyze.MCMC.HMC  (hmc,  HMCConfig (..),  defaultHMCConfig)
-import Hanalyze.MCMC.NUTS (nuts, NUTSConfig (..), defaultNUTSConfig)
-import Hanalyze.Stat.Distribution ()
-import Hanalyze.Stat.MCMC (ess)
-
--- ---------------------------------------------------------------------------
--- モデル定義
--- ---------------------------------------------------------------------------
-
--- | ケース 1: 独立 2 パラメータ
-easyModel :: [Double] -> [Double] -> ModelP ()
-easyModel ys1 ys2 = do
-  mu1 <- sample "mu1" (Normal 0 5)
-  mu2 <- sample "mu2" (Normal 0 5)
-  observe "y1" (Normal mu1 1) ys1
-  observe "y2" (Normal mu2 1) ys2
-
--- | ケース 2: 両パラメータが同じ観測に現れる → 事後分布に強反相関
-hardModel :: [Double] -> ModelP ()
-hardModel ys = do
-  alpha <- sample "mu1" (Normal 0 5)
-  beta  <- sample "mu2" (Normal 0 5)
-  observe "y" (Normal (alpha + beta) 1) ys
-
--- ---------------------------------------------------------------------------
--- 合成データ
--- ---------------------------------------------------------------------------
-
--- ケース 1: 真値 μ₁=2, μ₂=-1, n=20
-obsEasy1, obsEasy2 :: [Double]
-obsEasy1 = [2.3,1.8,2.1,1.9,2.5,1.7,2.2,2.0,1.6,2.4
-           ,2.1,1.8,2.3,2.0,1.9,2.2,1.7,2.5,1.8,2.1]
-obsEasy2 = [-0.8,-1.2,-0.9,-1.1,-0.7,-1.3,-1.0,-0.9,-1.2,-1.1
-           ,-1.0,-0.8,-1.2,-1.1,-0.9,-1.0,-1.3,-0.7,-1.1,-0.8]
-
--- ケース 2: 真値 α+β=2, n=20
-obsHard :: [Double]
-obsHard = [1.5,2.3,1.8,2.1,2.5,1.7,2.2,2.0,1.6,2.4
-          ,2.1,1.8,2.3,2.0,1.9,2.2,1.7,2.5,1.8,2.1]
-
--- ---------------------------------------------------------------------------
--- MCMC 設定
--- ---------------------------------------------------------------------------
-
-nIter, nBurnIn :: Int
-nIter   = 5000
-nBurnIn = 1000
-
--- MH (ケース 1): 事後 SD ≈ 0.22 に対してステップ 0.4
-mhEasy :: MCMCConfig
-mhEasy = MCMCConfig
-  { mcmcIterations = nIter
-  , mcmcBurnIn     = nBurnIn
-  , mcmcStepSizes  = Map.fromList [("mu1", 0.4), ("mu2", 0.4)]
-  }
-
--- MH (ケース 2): 短軸 SD ≈ 0.2 に合わせた小ステップ
---   → 受容率は高いが長軸方向は完全なランダムウォーク
-mhHard :: MCMCConfig
-mhHard = MCMCConfig
-  { mcmcIterations = nIter
-  , mcmcBurnIn     = nBurnIn
-  , mcmcStepSizes  = Map.fromList [("mu1", 0.1), ("mu2", 0.1)]
-  }
-
--- HMC (ケース 1)
-hmcEasy :: HMCConfig
-hmcEasy = defaultHMCConfig
-  { hmcIterations    = nIter
-  , hmcBurnIn        = nBurnIn
-  , hmcStepSize      = 0.2
-  , hmcLeapfrogSteps = 10
-  }
-
--- HMC (ケース 2): 長軸を踏破するためステップ数を多く
-hmcHard :: HMCConfig
-hmcHard = defaultHMCConfig
-  { hmcIterations    = nIter
-  , hmcBurnIn        = nBurnIn
-  , hmcStepSize      = 0.05
-  , hmcLeapfrogSteps = 50
-  }
-
--- NUTS (ケース 1)
-nutsEasy :: NUTSConfig
-nutsEasy = defaultNUTSConfig
-  { nutsIterations = nIter
-  , nutsBurnIn     = nBurnIn
-  , nutsStepSize   = 0.2
-  }
-
--- NUTS (ケース 2): U-Turn 判定で軌跡長を自動調整
-nutsHard :: NUTSConfig
-nutsHard = defaultNUTSConfig
-  { nutsIterations = nIter
-  , nutsBurnIn     = nBurnIn
-  , nutsStepSize   = 0.05
-  }
-
--- ---------------------------------------------------------------------------
--- ユーティリティ
--- ---------------------------------------------------------------------------
-
-getESS :: T.Text -> Chain -> Double
-getESS name ch =
-  ess (chainVals name ch)
-
-timed :: IO a -> IO (a, Double)
-timed action = do
-  t0 <- getCurrentTime
-  x  <- action
-  t1 <- getCurrentTime
-  return (x, realToFrac (diffUTCTime t1 t0))
-
-report :: String -> Chain -> Double -> IO ()
-report method ch secs = do
-  let e1   = getESS "mu1" ch
-      e2   = getESS "mu2" ch
-      minE = min e1 e2
-      m1   = maybe 0 id (posteriorMean "mu1" ch)
-      m2   = maybe 0 id (posteriorMean "mu2" ch)
-  printf
-    "  %-5s | acc=%5.3f | mean(μ₁)=%6.3f mean(μ₂)=%6.3f \
-    \| ESS(μ₁)=%5.0f ESS(μ₂)=%5.0f | minESS/s=%6.1f | %5.2fs\n"
-    method (acceptanceRate ch) m1 m2 e1 e2 (minE / secs) secs
-
--- ---------------------------------------------------------------------------
--- Main
--- ---------------------------------------------------------------------------
-
-mEasy :: ModelP ()
-mEasy = easyModel obsEasy1 obsEasy2
-
-mHard :: ModelP ()
-mHard = hardModel obsHard
-
-main :: IO ()
-main = do
-  gen <- createSystemRandom
-
-  let initP = Map.fromList [("mu1", 0.0 :: Double), ("mu2", 0.0)]
-
-  -- ---- ケース 1: 独立 2D 正規 ----
-  let n     = length obsEasy1
-      sigPost = 1 / sqrt (fromIntegral n + 1/25 :: Double)
-
-  putStrLn ""
-  putStrLn "══════════════════════════════════════════════════════════════════"
-  putStrLn " ケース 1: 独立 2D 正規事後分布  (全手法で収束しやすい)"
-  printf   "  真値: μ₁≈2.0, μ₂≈-1.0  事後 SD≈%.3f  ρ=0\n" sigPost
-  putStrLn "══════════════════════════════════════════════════════════════════"
-
-  (ch1, t1) <- timed $ metropolis mEasy mhEasy  initP gen
-  report "MH"   ch1 t1
-  (ch2, t2) <- timed $ hmc  mEasy hmcEasy  initP gen
-  report "HMC"  ch2 t2
-  (ch3, t3) <- timed $ nuts mEasy nutsEasy initP gen
-  report "NUTS" ch3 t3
-
-  -- ---- ケース 2: 強反相関 ----
-  let ybar     = sum obsHard / fromIntegral (length obsHard)
-      n2       = fromIntegral (length obsHard) :: Double
-      -- 事後の短軸/長軸 SD を解析的に計算
-      -- Λ = [[1/25+n, n],[n, 1/25+n]], Σ = Λ^{-1}
-      lam      = 1/25 + n2
-      detLam   = lam*lam - n2*n2
-      sig11    = lam / detLam
-      sig12    = negate n2 / detLam
-      rhoPost  = sig12 / sig11
-      sdShort  = sqrt (sig11 + sig12)  -- SD of (μ₁-μ₂)/√2
-      sdLong   = sqrt (sig11 - sig12)  -- SD of (μ₁+μ₂)/√2
-
-  putStrLn ""
-  putStrLn "══════════════════════════════════════════════════════════════════"
-  putStrLn " ケース 2: 和制約 α+β≈ȳ  (MH で収束しにくい)"
-  printf   "  ȳ=%.2f  事後: 短軸 SD≈%.3f  長軸 SD≈%.2f  ρ≈%.4f\n"
-           ybar sdShort sdLong rhoPost
-  putStrLn "══════════════════════════════════════════════════════════════════"
-
-  (ch4, t4) <- timed $ metropolis mHard mhHard  initP gen
-  report "MH"   ch4 t4
-  (ch5, t5) <- timed $ hmc  mHard hmcHard  initP gen
-  report "HMC"  ch5 t5
-  (ch6, t6) <- timed $ nuts mHard nutsHard initP gen
-  report "NUTS" ch6 t6
-
-  putStrLn ""
-  putStrLn "凡例: acc=受容率  mean=事後平均  ESS=有効サンプル数  minESS/s=効率"
diff --git a/demo/bayesian/CDFTestDemo.hs b/demo/bayesian/CDFTestDemo.hs
deleted file mode 100644
--- a/demo/bayesian/CDFTestDemo.hs
+++ /dev/null
@@ -1,109 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | 全分布の CDF 動作確認 (Beta / Gamma / Cauchy / StudentT 含む)。
---
--- statistics パッケージの CDF (= 信頼できる reference) があれば比較したいが、
--- ここでは既知の数値 (例: 標準正規 0 で 0.5、対称性チェック) で検証する。
-module Main where
-
-import Text.Printf (printf)
-
-import Hanalyze.Model.HBM (Distribution (..), distCDF)
-
--- distCDF Just から値を取り出す
-cdfAt :: Distribution Double -> Double -> Double
-cdfAt d x = case distCDF d x of
-  Just v  -> v
-  Nothing -> 0/0
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  CDF 動作確認 (全分布)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  -- Normal
-  putStrLn "[Normal] N(0, 1)"
-  printf "  F(0) = %.4f  (期待 0.5)\n"   (cdfAt (Normal 0 1) 0)
-  printf "  F(1.96) = %.4f  (期待 ≈ 0.975)\n" (cdfAt (Normal 0 1) 1.96)
-  printf "  F(-1.96) = %.4f  (期待 ≈ 0.025)\n" (cdfAt (Normal 0 1) (-1.96))
-  putStrLn ""
-
-  -- Cauchy (標準)
-  putStrLn "[Cauchy] Cauchy(0, 1)"
-  printf "  F(0) = %.4f  (期待 0.5)\n" (cdfAt (Cauchy 0 1) 0)
-  printf "  F(1) = %.4f  (期待 0.75)\n" (cdfAt (Cauchy 0 1) 1)
-  printf "  F(-1) = %.4f  (期待 0.25)\n" (cdfAt (Cauchy 0 1) (-1))
-  putStrLn ""
-
-  -- HalfCauchy
-  putStrLn "[HalfCauchy] HalfCauchy(1)"
-  printf "  F(0) = %.4f  (期待 0)\n" (cdfAt (HalfCauchy 1) 0)
-  printf "  F(1) = %.4f  (期待 0.5)\n" (cdfAt (HalfCauchy 1) 1)
-  putStrLn ""
-
-  -- Exponential
-  putStrLn "[Exponential] Exp(rate=1)"
-  printf "  F(0) = %.4f  (期待 0)\n" (cdfAt (Exponential 1) 0)
-  printf "  F(1) = %.4f  (期待 ≈ 0.6321)\n" (cdfAt (Exponential 1) 1)
-  printf "  F(2) = %.4f  (期待 ≈ 0.8647)\n" (cdfAt (Exponential 1) 2)
-  putStrLn ""
-
-  -- Uniform
-  putStrLn "[Uniform] U(0, 1)"
-  printf "  F(0.3) = %.4f  (期待 0.3)\n" (cdfAt (Uniform 0 1) 0.3)
-  printf "  F(0.5) = %.4f  (期待 0.5)\n" (cdfAt (Uniform 0 1) 0.5)
-  putStrLn ""
-
-  -- Gamma
-  putStrLn "[Gamma] Gamma(shape=2, rate=1)"
-  printf "  F(0) = %.4f  (期待 0)\n" (cdfAt (Gamma 2 1) 0)
-  printf "  F(2) = %.4f  (期待 ≈ 0.5940)\n" (cdfAt (Gamma 2 1) 2)
-  printf "  F(5) = %.4f  (期待 ≈ 0.9596)\n" (cdfAt (Gamma 2 1) 5)
-  printf "  F(10) = %.4f  (期待 ≈ 0.9995)\n" (cdfAt (Gamma 2 1) 10)
-  putStrLn ""
-  putStrLn "[Gamma] Gamma(shape=0.5, rate=1)  ; これは ½χ²(1) と同じ"
-  printf "  F(0.5) = %.4f  (期待 ≈ 0.6827)\n" (cdfAt (Gamma 0.5 1) 0.5)
-  printf "  F(2) = %.4f  (期待 ≈ 0.9545)\n" (cdfAt (Gamma 0.5 1) 2)
-  putStrLn ""
-
-  -- Beta
-  putStrLn "[Beta] Beta(2, 5)"
-  printf "  F(0) = %.4f  (期待 0)\n" (cdfAt (Beta 2 5) 0)
-  printf "  F(0.5) = %.4f  (期待 ≈ 0.8906)\n" (cdfAt (Beta 2 5) 0.5)
-  printf "  F(1) = %.4f  (期待 1)\n" (cdfAt (Beta 2 5) 1)
-  putStrLn ""
-  putStrLn "[Beta] Beta(1, 1)  ; 一様分布と等価"
-  printf "  F(0.3) = %.4f  (期待 0.3)\n" (cdfAt (Beta 1 1) 0.3)
-  printf "  F(0.7) = %.4f  (期待 0.7)\n" (cdfAt (Beta 1 1) 0.7)
-  putStrLn ""
-
-  -- StudentT
-  putStrLn "[StudentT] t(df=3, mu=0, sigma=1)"
-  printf "  F(0) = %.4f  (期待 0.5)\n" (cdfAt (StudentT 3 0 1) 0)
-  printf "  F(1) = %.4f  (期待 ≈ 0.8044)\n" (cdfAt (StudentT 3 0 1) 1)
-  printf "  F(-1) = %.4f  (期待 ≈ 0.1956)\n" (cdfAt (StudentT 3 0 1) (-1))
-  printf "  F(3.18) = %.4f  (期待 ≈ 0.975 — 95%% CI 上限)\n" (cdfAt (StudentT 3 0 1) 3.18)
-  putStrLn ""
-  putStrLn "[StudentT] t(df=30) ; df 大で標準正規に近づく"
-  printf "  F(0) = %.4f  (期待 0.5)\n" (cdfAt (StudentT 30 0 1) 0)
-  printf "  F(1.96) = %.4f  (期待 ≈ 0.9706 — 標準正規だと 0.975)\n" (cdfAt (StudentT 30 0 1) 1.96)
-  putStrLn ""
-
-  -- LogNormal
-  putStrLn "[LogNormal] LN(0, 1)"
-  printf "  F(1) = %.4f  (期待 0.5)\n" (cdfAt (LogNormal 0 1) 1)
-  printf "  F(exp(1)) = %.4f  (期待 ≈ 0.8413)\n" (cdfAt (LogNormal 0 1) (exp 1))
-  putStrLn ""
-
-  -- HalfNormal
-  putStrLn "[HalfNormal] HN(σ=1)"
-  printf "  F(0) = %.4f  (期待 0)\n" (cdfAt (HalfNormal 1) 0)
-  printf "  F(1) = %.4f  (期待 ≈ 0.6827)\n" (cdfAt (HalfNormal 1) 1)
-  printf "  F(2) = %.4f  (期待 ≈ 0.9545)\n" (cdfAt (HalfNormal 1) 2)
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ 全分布で CDF が動作 (Beta/Gamma/Cauchy/StudentT 含む)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/ClinicalTrial.hs b/demo/bayesian/ClinicalTrial.hs
deleted file mode 100644
--- a/demo/bayesian/ClinicalTrial.hs
+++ /dev/null
@@ -1,207 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | ベイズ A/B テスト (Beta-Binomial モデル)
---
--- 二値アウトカム（例: 新薬投与後の回復）を持つ二群比較。
---
--- モデル:
---   p_ctrl      ~ Beta(1,1)         ← 一様事前分布  (UnitIntervalT → ロジット変換)
---   p_trt       ~ Beta(1,1)
---   y_ctrl      ~ Binomial(n_ctrl,  p_ctrl)
---   y_trt       ~ Binomial(n_trt,   p_trt)
---
--- 解析解 (Beta-Binomial 共役): p|y ~ Beta(1+k, 1+n-k)
---
--- 推論:
---   - 4-chain NUTS でサンプリング
---   - P(p_trt > p_ctrl) をサンプルから推定
---   - HTML レポート生成 (mcmc_report_clinical.html)
---
-module Main where
-
-import Control.Monad (forM_)
-import qualified Data.Map.Strict as Map
-import qualified Data.Text       as T
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.Model.HBM
-import Hanalyze.MCMC.Core  (Chain (..), chainVals, acceptanceRate, posteriorMean
-                  , posteriorQuantile)
-import Hanalyze.MCMC.NUTS  (NUTSConfig (..), defaultNUTSConfig, nutsChains)
--- import Hanalyze.Stat.Distribution (Distribution (..)) -- now from Hanalyze.Model.HBM
-import Hanalyze.Stat.MCMC  (ess, rhat)
-import Hanalyze.Viz.Core   (openInBrowser)
-import Hanalyze.Viz.Report (MCMCReport (..), defaultReport, renderReport)
-
--- ---------------------------------------------------------------------------
--- 合成データ (架空の臨床試験)
--- ---------------------------------------------------------------------------
-
--- 対照群: 50 人中 18 人が回復  → 真値 p_ctrl ≈ 0.36
-nCtrl, kCtrl :: Int
-nCtrl = 50
-kCtrl = 18
-
--- 治療群: 50 人中 31 人が回復  → 真値 p_trt  ≈ 0.62
-nTrt, kTrt :: Int
-nTrt  = 50
-kTrt  = 31
-
--- ---------------------------------------------------------------------------
--- モデル定義
--- ---------------------------------------------------------------------------
-
-clinicalModel :: ModelP ()
-clinicalModel = do
-  pCtrl <- sample "p_ctrl" (Beta 1 1)
-  pTrt  <- sample "p_trt"  (Beta 1 1)
-  observe "y_ctrl" (Binomial nCtrl pCtrl) [fromIntegral kCtrl]
-  observe "y_trt"  (Binomial nTrt  pTrt)  [fromIntegral kTrt]
-
--- ---------------------------------------------------------------------------
--- 解析解 (Beta-Binomial 共役)
--- ---------------------------------------------------------------------------
-
--- Beta(1,1) 事前 + Binomial(n,p) 観測 k → Beta(1+k, 1+n-k) 事後
-analyticMean :: Int -> Int -> Double
-analyticMean k n = fromIntegral (1 + k) / fromIntegral (2 + n)
-
-analyticSD :: Int -> Int -> Double
-analyticSD k n =
-  let a = fromIntegral (1 + k)
-      b = fromIntegral (1 + n - k)
-      s = a + b
-  in sqrt (a * b / (s * s * (s + 1)))
-
--- ---------------------------------------------------------------------------
--- Main
--- ---------------------------------------------------------------------------
-
-m :: ModelP ()
-m = clinicalModel
-
-main :: IO ()
-main = do
-  gen <- createSystemRandom
-  let names = sampleNames m
-
-  -- ── モデル概要 ────────────────────────────────────────────────────────
-  putStrLn "=== Bayesian A/B Test: Clinical Trial ==="
-  putStrLn ""
-  putStrLn "モデル:"
-  putStrLn "  p_ctrl ~ Beta(1,1)"
-  putStrLn "  p_trt  ~ Beta(1,1)"
-  printf "  y_ctrl ~ Binomial(%d, p_ctrl)  観測: %d/%d 回復\n" nCtrl kCtrl nCtrl
-  printf "  y_trt  ~ Binomial(%d, p_trt)   観測: %d/%d 回復\n" nTrt  kTrt  nTrt
-  putStrLn ""
-
-  -- ── 解析解 ────────────────────────────────────────────────────────────
-  let aCtrlMean = analyticMean kCtrl nCtrl
-      aCtrlSD   = analyticSD   kCtrl nCtrl
-      aTrtMean  = analyticMean kTrt  nTrt
-      aTrtSD    = analyticSD   kTrt  nTrt
-
-  putStrLn "=== 解析解 (Beta-Binomial 共役) ==="
-  printf "  p_ctrl | data ~ Beta(%d, %d)  mean=%.4f  SD=%.4f\n"
-    (1+kCtrl) (1+nCtrl-kCtrl) aCtrlMean aCtrlSD
-  printf "  p_trt  | data ~ Beta(%d, %d)  mean=%.4f  SD=%.4f\n"
-    (1+kTrt) (1+nTrt-kTrt) aTrtMean aTrtSD
-  putStrLn ""
-
-  -- ── 4-chain NUTS ──────────────────────────────────────────────────────
-  putStrLn "=== 4-chain NUTS サンプリング ==="
-  let initP = Map.fromList [("p_ctrl", 0.5 :: Double), ("p_trt", 0.5)]
-      cfg   = defaultNUTSConfig
-                { nutsIterations = 2000
-                , nutsBurnIn     = 500
-                , nutsStepSize   = 0.3
-                }
-
-  chains <- nutsChains m cfg 4 initP gen
-
-  forM_ (zip [1::Int ..] chains) $ \(i, ch) ->
-    printf "  chain %d: acceptance=%.3f  p_ctrl=%.4f  p_trt=%.4f\n"
-      i (acceptanceRate ch)
-      (maybe 0 id $ posteriorMean "p_ctrl" ch)
-      (maybe 0 id $ posteriorMean "p_trt"  ch)
-  putStrLn ""
-
-  -- ── 事後サマリー ──────────────────────────────────────────────────────
-  putStrLn "=== 事後サマリー ==="
-  printf "  %-10s  %8s  %8s  %8s  %8s  %8s  %8s  %8s\n"
-    ("param"::String) ("mean"::String) ("SD"::String)
-    ("2.5%"::String) ("97.5%"::String) ("ESS"::String)
-    ("R-hat"::String) ("analytic"::String)
-  let allChains = chains
-  forM_ names $ \p -> do
-    let vals      = concatMap (chainVals p) allChains
-        repChain  = head allChains
-        get f     = maybe 0 id (f p repChain)
-        mean_     = sum vals / fromIntegral (length vals)
-        sd_       = sqrt (sum (map (\v -> (v - mean_)^(2::Int)) vals)
-                          / fromIntegral (length vals))
-        lo        = get (posteriorQuantile 0.025)
-        hi        = get (posteriorQuantile 0.975)
-        ess_      = ess (chainVals p repChain)
-        rhatV     = maybe 0 id (rhat (map (chainVals p) allChains))
-        analytic  = if p == "p_ctrl" then aCtrlMean else aTrtMean
-    printf "  %-10s  %8.4f  %8.4f  %8.4f  %8.4f  %8.0f  %8.4f  %8.4f\n"
-      (T.unpack p) mean_ sd_ lo hi ess_ rhatV analytic
-  putStrLn ""
-
-  -- ── 治療効果の推定 ────────────────────────────────────────────────────
-  putStrLn "=== 治療効果の推定 ==="
-  let ctrlSamples = concatMap (chainVals "p_ctrl") allChains
-      trtSamples  = concatMap (chainVals "p_trt")  allChains
-      diffs       = zipWith (-) trtSamples ctrlSamples
-      probBetter  = fromIntegral (length (filter (> 0) diffs))
-                  / fromIntegral (length diffs) :: Double
-      meanDiff    = sum diffs / fromIntegral (length diffs)
-      sdDiff      = sqrt (sum (map (\d -> (d - meanDiff)^(2::Int)) diffs)
-                         / fromIntegral (length diffs))
-
-  printf "  P(p_trt > p_ctrl) = %.4f  (%.1f%%)\n" probBetter (probBetter * 100)
-  printf "  E[p_trt - p_ctrl] = %.4f  (SD=%.4f)\n" meanDiff sdDiff
-  printf "  95%% CI of差:      [%.4f, %.4f]\n"
-    (quantileOf 0.025 diffs) (quantileOf 0.975 diffs)
-  putStrLn ""
-  printf "  → %s\n" (interpret probBetter :: String)
-  putStrLn ""
-
-  -- ── HTML レポート生成 ────────────────────────────────────────────────
-  putStrLn "=== HTML レポート生成 ==="
-  let graph = buildModelGraph m   -- HBMP: 依存グラフは Track 型で自動抽出
-      report = (defaultReport "Bayesian A/B Test — Clinical Trial" (head chains) names)
-                 { reportGraph  = Just graph
-                 , reportChains = chains
-                 , reportPairs  = [("p_ctrl", "p_trt")]
-                 , reportMaxLag = 40
-                 }
-  renderReport "mcmc_report_clinical.html" report
-  putStrLn "  mcmc_report_clinical.html を生成しました"
-  openInBrowser "mcmc_report_clinical.html"
-
--- ---------------------------------------------------------------------------
--- ヘルパー
--- ---------------------------------------------------------------------------
-
-quantileOf :: Double -> [Double] -> Double
-quantileOf q xs =
-  let sorted = foldr insertSorted [] xs
-      n      = length sorted
-      idx    = min (n - 1) (max 0 (round (q * fromIntegral (n - 1)) :: Int))
-  in sorted !! idx
-  where
-    insertSorted x []     = [x]
-    insertSorted x (y:ys)
-      | x <= y    = x : y : ys
-      | otherwise = y : insertSorted x ys
-
-interpret :: Double -> String
-interpret p
-  | p >= 0.99 = "非常に強いエビデンス: 治療が有効"
-  | p >= 0.95 = "強いエビデンス: 治療が有効"
-  | p >= 0.80 = "中程度のエビデンス: 治療が有効傾向"
-  | p >= 0.50 = "弱いエビデンス: 治療がやや有効"
-  | otherwise = "エビデンスなし: 治療効果は不明瞭"
diff --git a/demo/bayesian/DeterministicDemo.hs b/demo/bayesian/DeterministicDemo.hs
deleted file mode 100644
--- a/demo/bayesian/DeterministicDemo.hs
+++ /dev/null
@@ -1,71 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | pm.Deterministic 相当のデモ。
---
--- σ をサンプリングして、派生量 τ = 1/σ² (precision) と
--- log_sigma = log(σ) も保存する。Posterior summary には latent と
--- derived が同じテーブルに混ざって表示される。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, deterministic,
-                  Distribution (..), augmentChainWithDeterministic)
-import Hanalyze.Viz.MCMC (printPosteriorSummary, posteriorSummaryFile,
-                 tracePlotHDIFile)
-import Hanalyze.Viz.Core (defaultConfig, OutputFormat (..), PlotConfig (..))
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 1500
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.1
-        }
-
-modelWithDeterministic :: ModelP ()
-modelWithDeterministic = do
-  mu  <- sample "mu"    (Normal 0 5)
-  sig <- sample "sigma" (HalfNormal 2)
-  -- 派生量 1: precision = 1/σ²
-  _ <- deterministic "tau"       (1 / (sig * sig))
-  -- 派生量 2: log(σ)
-  _ <- deterministic "log_sigma" (log sig)
-  -- 派生量 3: 信号対雑音比
-  _ <- deterministic "snr"       (mu / sig)
-  observe "y" (Normal mu sig)
-    [1.2, 0.9, 1.4, 0.7, 1.1, 1.0, 1.3, 0.95, 1.05, 1.15,
-     0.85, 1.25, 0.95, 1.18, 1.02]
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  pm.Deterministic デモ (派生量を Chain に保存)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  gen <- createSystemRandom
-  rawCh <- nuts modelWithDeterministic cfg
-                (Map.fromList [("mu", 1), ("sigma", 1)]) gen
-
-  -- 派生量を Chain に注入
-  let ch = augmentChainWithDeterministic modelWithDeterministic rawCh
-  let names = ["mu", "sigma", "tau", "log_sigma", "snr"]
-
-  putStrLn "[1] Posterior summary (latent + derived 混在)"
-  printPosteriorSummary names [ch]
-  putStrLn ""
-
-  -- HTML 出力
-  posteriorSummaryFile "summary-determ.html"
-    "Posterior with deterministic" names [ch]
-  let traceCfg = (defaultConfig "Trace (latent + derived)")
-                   { plotWidth = 700, plotHeight = 90 }
-  tracePlotHDIFile HTML "trace-determ.html" traceCfg 0.94 names ch
-  putStrLn "  → summary-determ.html / trace-determ.html"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Deterministic 派生量が posterior summary / trace に出る"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/DirichletDemo.hs b/demo/bayesian/DirichletDemo.hs
deleted file mode 100644
--- a/demo/bayesian/DirichletDemo.hs
+++ /dev/null
@@ -1,76 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | Dirichlet 事前 + Categorical 観測のデモ。
---
--- 3 カテゴリの観測 (生起頻度: 50, 30, 20) に対し、
--- Dir(1,1,1) (一様事前) を Dirichlet にして π を推定。
---
--- 共役: 事後は Dir(1+50, 1+30, 1+20) = Dir(51, 31, 21) で
--- 平均は (51, 31, 21) / 103 = (0.495, 0.301, 0.204)。
--- これと推定値が一致するかを確認。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, dirichlet, observe, Distribution (..),
-                  augmentChainWithDeterministic)
-import Hanalyze.Viz.MCMC (printPosteriorSummary, posteriorSummaryFile,
-                 tracePlotHDIFile)
-import Hanalyze.Viz.Core (defaultConfig, OutputFormat (..), PlotConfig (..))
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 2000
-        , nutsBurnIn     = 1000
-        , nutsStepSize   = 0.1
-        }
-
--- 生成データ: カテゴリ 0,1,2 の頻度 50, 30, 20
-genObs :: [Double]
-genObs = replicate 50 0 ++ replicate 30 1 ++ replicate 20 2
-
-dirichletModel :: ModelP ()
-dirichletModel = do
-  pis <- dirichlet "pi" [1, 1, 1]   -- Dir(1,1,1) 一様事前
-  observe "y" (Categorical pis) genObs
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Dirichlet 事前 + Categorical 観測"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  putStrLn "観測: カテゴリ 0/1/2 が 50/30/20 件 (合計 100)"
-  putStrLn "事後 (共役): Dir(51, 31, 21)"
-  putStrLn "  期待値: π_0 = 0.495, π_1 = 0.301, π_2 = 0.204"
-  putStrLn ""
-
-  gen <- createSystemRandom
-
-  -- 初期値: Beta が UnitIntervalT 経由で sample されるため
-  -- pi_b0, pi_b1 は (0,1)
-  rawCh <- nuts dirichletModel cfg
-                (Map.fromList [("pi_b0", 0.5), ("pi_b1", 0.5)]) gen
-  let ch = augmentChainWithDeterministic dirichletModel rawCh
-
-  putStrLn "[1] Posterior summary (β: stick-breaking 棒折り、π: 派生量)"
-  let names = [ "pi_b0", "pi_b1"          -- raw latent (Beta)
-              , "pi_0", "pi_1", "pi_2" ]  -- derived simplex π
-  printPosteriorSummary names [ch]
-  putStrLn ""
-
-  -- HTML 出力
-  posteriorSummaryFile "dirichlet-summary.html" "Dirichlet posterior" names [ch]
-  let traceCfg = (defaultConfig "Dirichlet trace (β + π)")
-                   { plotWidth = 700, plotHeight = 90 }
-  tracePlotHDIFile HTML "dirichlet-trace.html" traceCfg 0.94 names ch
-  putStrLn "  → dirichlet-summary.html / dirichlet-trace.html"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Dirichlet が stick-breaking 経由で latent 化"
-  putStrLn "    π_0 + π_1 + π_2 = 1 がサンプル単位で自動的に成立"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/DiscreteObsDemo.hs b/demo/bayesian/DiscreteObsDemo.hs
deleted file mode 100644
--- a/demo/bayesian/DiscreteObsDemo.hs
+++ /dev/null
@@ -1,106 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | Phase 2.2: Bernoulli / Categorical 観測モデルの動作確認デモ。
---
--- どちらも観測分布として使う (潜在変数は連続のまま)。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Data.Maybe (fromMaybe)
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.MCMC.Core (chainSamples, posteriorMean, posteriorSD,
-                  posteriorQuantile, acceptanceRate)
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, Distribution (..))
-
--- ---------------------------------------------------------------------------
--- Bernoulli 観測 (ロジスティック回帰の単純版)
--- ---------------------------------------------------------------------------
--- 真値: p = 0.7
-
-bernoulliData :: [Double]
-bernoulliData = [1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1]
--- 20 件中 15 件成功 → MLE p̂ = 0.75, 真値 0.7 から少しずれ
-
-bernoulliModel :: ModelP ()
-bernoulliModel = do
-  p <- sample "p" (Beta 1 1)             -- 一様事前
-  observe "y" (Bernoulli p) bernoulliData
-
--- ---------------------------------------------------------------------------
--- Categorical 観測
--- ---------------------------------------------------------------------------
--- 真値: probs = [0.5, 0.3, 0.2]
--- 20 観測
-
-categoricalData :: [Double]
-categoricalData = [0,0,0,0,0,0,0,0,0,0, 1,1,1,1,1,1, 2,2,2,2]
--- 0 が 10, 1 が 6, 2 が 4 → MLE [0.5, 0.3, 0.2]
-
-categoricalModel :: ModelP ()
-categoricalModel = do
-  -- 単純化: 3 つの確率を独立にサンプリング (本来は Dirichlet が望ましい)
-  -- HalfNormal 事前で正値、内部で正規化
-  q0 <- sample "q0" (HalfNormal 1)
-  q1 <- sample "q1" (HalfNormal 1)
-  q2 <- sample "q2" (HalfNormal 1)
-  observe "y" (Categorical [q0, q1, q2]) categoricalData
-
--- ---------------------------------------------------------------------------
--- main
--- ---------------------------------------------------------------------------
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 2000
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.1
-        }
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Phase 2.2: 離散観測分布の動作確認"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  -- ── Bernoulli ──
-  putStrLn "[1] Bernoulli(p) 観測"
-  printf "    データ: 20 観測, 15 件成功 (真 p=0.7, MLE p̂=0.75)\n"
-  gen1 <- createSystemRandom
-  ch1 <- nuts bernoulliModel cfg (Map.fromList [("p", 0.5)]) gen1
-  printf "    Acceptance: %.1f%%, samples: %d\n"
-         (acceptanceRate ch1 * 100 :: Double)
-         (length (chainSamples ch1))
-  printf "    p mean=%+.4f  sd=%.4f  95%% CI=[%+.4f, %+.4f]\n"
-         (fromMaybe 0 (posteriorMean "p" ch1))
-         (fromMaybe 0 (posteriorSD   "p" ch1))
-         (fromMaybe 0 (posteriorQuantile 0.025 "p" ch1))
-         (fromMaybe 0 (posteriorQuantile 0.975 "p" ch1))
-  putStrLn "    → Beta(1,1) + Binomial 共役解析: Beta(1+15, 1+5) = Beta(16, 6)"
-  printf "       解析的 mean = 16/22 = %.4f\n" (16/22 :: Double)
-  putStrLn ""
-
-  -- ── Categorical ──
-  putStrLn "[2] Categorical([q0,q1,q2]) 観測"
-  printf "    データ: 20 観測, [0:10, 1:6, 2:4] (真 probs=[0.5, 0.3, 0.2])\n"
-  gen2 <- createSystemRandom
-  let initP = Map.fromList [("q0", 1.0), ("q1", 1.0), ("q2", 1.0)]
-  ch2 <- nuts categoricalModel cfg initP gen2
-  printf "    Acceptance: %.1f%%, samples: %d\n"
-         (acceptanceRate ch2 * 100 :: Double)
-         (length (chainSamples ch2))
-  let q0m = fromMaybe 0 (posteriorMean "q0" ch2)
-      q1m = fromMaybe 0 (posteriorMean "q1" ch2)
-      q2m = fromMaybe 0 (posteriorMean "q2" ch2)
-      total = q0m + q1m + q2m
-  printf "    q0 mean=%.4f  q1 mean=%.4f  q2 mean=%.4f\n" q0m q1m q2m
-  printf "    正規化後: [%.3f, %.3f, %.3f]   ← 真値 [0.500, 0.300, 0.200]\n"
-         (q0m/total) (q1m/total) (q2m/total)
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Bernoulli / Categorical 観測モデルが正常動作"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/EnergyDemo.hs b/demo/bayesian/EnergyDemo.hs
deleted file mode 100644
--- a/demo/bayesian/EnergyDemo.hs
+++ /dev/null
@@ -1,98 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | NUTS の Energy plot / BFMI 診断デモ。
---
--- BFMI < 0.3 → reparameterization 推奨 (典型例: Neal's funnel)。
--- 0.3 以上が望ましく、PyMC の経験則ではしばしば 0.5 を目安にする。
---
--- 例 1: 単純なガウシアンモデル → BFMI 高い (= 健全)
--- 例 2: Neal's funnel (centered) → BFMI 低い ことを期待
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.MCMC.Core (chainEnergy)
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, Distribution (..))
-import Hanalyze.Stat.MCMC (bfmi)
-import Hanalyze.Viz.Core  (PlotConfig (..), defaultConfig, OutputFormat (..))
-import Hanalyze.Viz.MCMC  (energyPlotFile)
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 2000
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.1
-        }
-
--- ---------------------------------------------------------------------------
--- 例 1: 普通の正規回帰
--- ---------------------------------------------------------------------------
-
-healthyModel :: ModelP ()
-healthyModel = do
-  mu  <- sample "mu"    (Normal 0 5)
-  sig <- sample "sigma" (HalfNormal 2)
-  observe "y" (Normal mu sig) [1.2, 0.9, 1.4, 0.7, 1.1, 1.0, 1.3, 0.95, 1.05, 1.15]
-
--- ---------------------------------------------------------------------------
--- 例 2: Neal's funnel (centered) — 病的な階層構造
--- ---------------------------------------------------------------------------
--- v ~ Normal(0, 3),  x | v ~ Normal(0, exp(v/2))
--- v が大きいと x の分散が爆発、小さいと潰れる → エネルギー方向の探索失敗。
-
-funnelModel :: ModelP ()
-funnelModel = do
-  v <- sample "v" (Normal 0 3)
-  _ <- sample "x" (Normal 0 (exp (v / 2)))
-  return ()
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Energy plot / BFMI 診断デモ"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  gen <- createSystemRandom
-
-  -- ── 例 1: 健全 ──
-  putStrLn "[1] 健全な Gaussian モデル"
-  ch1 <- nuts healthyModel cfg
-              (Map.fromList [("mu", 1), ("sigma", 1)]) gen
-  let es1   = chainEnergy ch1
-      bfmi1 = bfmi es1
-  printf "  Energy 列: %d 件、平均 = %.3f\n"
-         (length es1) (sum es1 / fromIntegral (length es1))
-  case bfmi1 of
-    Just v  -> printf "  BFMI = %.3f  (>0.3 で良好、>0.5 で理想)\n" v
-    Nothing -> putStrLn "  BFMI: 計算不能"
-  energyPlotFile HTML "energy-healthy.html"
-    (defaultConfig "Energy plot") { plotTitle = "Energy plot — healthy model"
-                                 , plotWidth = 600, plotHeight = 250 } ch1
-  putStrLn "  → energy-healthy.html"
-  putStrLn ""
-
-  -- ── 例 2: Funnel ──
-  putStrLn "[2] Neal's funnel (centered parameterization)"
-  ch2 <- nuts funnelModel cfg
-              (Map.fromList [("v", 0), ("x", 0)]) gen
-  let es2   = chainEnergy ch2
-      bfmi2 = bfmi es2
-  printf "  Energy 列: %d 件、平均 = %.3f\n"
-         (length es2) (sum es2 / fromIntegral (length es2))
-  case bfmi2 of
-    Just v  -> printf "  BFMI = %.3f  (低い場合は reparameterization 推奨)\n" v
-    Nothing -> putStrLn "  BFMI: 計算不能"
-  energyPlotFile HTML "energy-funnel.html"
-    (defaultConfig "Energy plot") { plotTitle = "Energy plot — Neal's funnel"
-                                 , plotWidth = 600, plotHeight = 250 } ch2
-  putStrLn "  → energy-funnel.html"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Energy plot / BFMI が動作"
-  putStrLn "    NUTS のサンプル列は energy も保持 (chainEnergy フィールド)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/ForestCompareDemo.hs b/demo/bayesian/ForestCompareDemo.hs
deleted file mode 100644
--- a/demo/bayesian/ForestCompareDemo.hs
+++ /dev/null
@@ -1,109 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | Phase 3.1 + 3.3: Forest plot と Pseudo-BMA モデル比較デモ。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.MCMC.Core (Chain)
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, Distribution (..))
-import Hanalyze.Stat.ModelSelect
-  (CompareEntry (..), CompareResult (..), compareModels, chainLogLikMatrix)
-import Hanalyze.Viz.Core (PlotConfig (..), OutputFormat (..))
-import Hanalyze.Viz.MCMC (forestPlotFile)
-
--- ---------------------------------------------------------------------------
--- 3 モデル: 分散事前を変えて比較
--- ---------------------------------------------------------------------------
-
-obs :: [Double]
-obs = [1.5, 2.1, 1.8, 2.5, 1.9, 2.3, 1.7, 2.0, 2.2, 1.6,
-       2.0, 1.7, 2.4, 1.5, 2.1, 1.8, 2.3, 1.9, 2.0, 1.6]
-
-modelHN :: ModelP ()
-modelHN = do
-  mu  <- sample "mu" (Normal 0 10)
-  sig <- sample "sigma" (HalfNormal 5)
-  observe "y" (Normal mu sig) obs
-
-modelHC :: ModelP ()
-modelHC = do
-  mu  <- sample "mu" (Normal 0 10)
-  sig <- sample "sigma" (HalfCauchy 2)
-  observe "y" (Normal mu sig) obs
-
-modelExp :: ModelP ()
-modelExp = do
-  mu  <- sample "mu" (Normal 0 10)
-  sig <- sample "sigma" (Exponential 1)
-  observe "y" (Normal mu sig) obs
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 1500
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.1
-        }
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Phase 3.1/3.3: Forest plot + Model comparison weights"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-  printf "  3 つのモデル (異なる σ 事前):\n"
-  putStrLn "    HN  : sigma ~ HalfNormal(5)"
-  putStrLn "    HC  : sigma ~ HalfCauchy(2)"
-  putStrLn "    Exp : sigma ~ Exponential(1)"
-  putStrLn ""
-
-  gen <- createSystemRandom
-  let initP = Map.fromList [("mu", 0.0), ("sigma", 1.0)]
-
-  putStrLn "[1] 3 モデルを NUTS で推論"
-  ch1 <- nuts modelHN  cfg initP gen
-  ch2 <- nuts modelHC  cfg initP gen
-  ch3 <- nuts modelExp cfg initP gen
-  putStrLn "  完了"
-  putStrLn ""
-
-  -- ── Forest plot ──
-  putStrLn "[2] Forest plot 出力 (forest_compare.html)"
-  let fcfg = PlotConfig "Posterior 95% CI per model" 600 400 Nothing Nothing Nothing
-      -- 各モデルを別 chain として渡すと色分けされる
-      chs  = [ch1, ch2, ch3]
-  forestPlotFile HTML "forest_compare.html" fcfg ["mu", "sigma"] chs
-  putStrLn "  → forest_compare.html"
-  putStrLn ""
-
-  -- ── Pseudo-BMA model comparison ──
-  putStrLn "[3] WAIC/LOO + Pseudo-BMA 重み (compareModels)"
-  let entries =
-        [ CompareEntry "HN"  (chainLogLikMatrix modelHN  ch1)
-        , CompareEntry "HC"  (chainLogLikMatrix modelHC  ch2)
-        , CompareEntry "Exp" (chainLogLikMatrix modelExp ch3)
-        ]
-      results = compareModels entries
-  printf "  %-6s  %10s  %10s  %10s  %10s  %8s  %8s\n"
-         ("model"::String) ("WAIC"::String) ("dWAIC"::String)
-         ("LOO"::String)   ("dLOO"::String) ("SE"::String)
-         ("weight"::String)
-  mapM_ (\r ->
-    printf "  %-6s  %10.3f  %10.3f  %10.3f  %10.3f  %8.3f  %8.3f%s\n"
-           (crLabel r) (crWAIC r) (crDeltaWAIC r)
-           (crLOO r)   (crDeltaLOO r) (crSE r) (crWeight r)
-           ((if crDeltaWAIC r == 0 then " *" else "  ") :: String))
-    results
-  putStrLn ""
-  putStrLn "  解釈:"
-  putStrLn "    weight = Pseudo-BMA 重み (Σ = 1)"
-  putStrLn "    重みが分散している = モデル選択の不確実性が高い"
-  putStrLn "    重みが特定モデルに集中 = そのモデルが圧倒的"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Forest plot + compareModels が正常動作"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/GibbsDemo.hs b/demo/bayesian/GibbsDemo.hs
deleted file mode 100644
--- a/demo/bayesian/GibbsDemo.hs
+++ /dev/null
@@ -1,198 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | Gibbs サンプリング + モデル比較 (WAIC / LOO-CV) デモ
---
--- モデル: 正規分布の平均推定
---   μ ~ Normal(0, σ_prior)        ← 事前分布
---   yᵢ ~ Normal(μ, σ_lik = 2)    ← 尤度、σ は既知
---   真値: μ = 3.0, n = 20
---
--- セクション 1: Gibbs vs NUTS サンプリング比較
---   - Gibbs: normalNormal 共役アップデートで直接サンプリング
---   - 解析解と ESS/秒で比較
---
--- セクション 2: WAIC によるモデル比較
---   - モデル A: μ ~ Normal(0, 10)  [弱情報事前]
---   - モデル B: μ ~ Normal(5,  1)  [情報事前・真値からずれた仮定]
---
--- セクション 3: PSIS-LOO 診断
---   - 各観測値の Pareto k̂ (< 0.5 良好、> 0.7 要注意)
---
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Data.Time.Clock (getCurrentTime, diffUTCTime)
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.Model.HBM
--- import Hanalyze.Stat.Distribution (Distribution (..)) -- now from Hanalyze.Model.HBM
-import Hanalyze.MCMC.Core (chainVals, posteriorMean, posteriorSD)
-import Hanalyze.MCMC.Gibbs (GibbsConfig (..), defaultGibbsConfig, gibbs, normalNormal)
-import Hanalyze.MCMC.NUTS  (NUTSConfig (..), defaultNUTSConfig, nuts)
-import Hanalyze.Stat.MCMC  (ess)
-import Hanalyze.Stat.ModelSelect
-
--- ---------------------------------------------------------------------------
--- 合成データ  (真値 μ = 3, σ = 2, n = 20)
--- ---------------------------------------------------------------------------
-
-sigLik :: Double
-sigLik = 2.0
-
-obsData :: [Double]
-obsData =
-  [ 3.2, 1.8, 4.1, 2.9, 3.5, 2.3, 4.5, 3.1, 2.7, 3.8
-  , 3.3, 2.5, 4.2, 3.0, 2.8, 3.6, 2.4, 4.0, 3.2, 2.9 ]
-
--- ---------------------------------------------------------------------------
--- モデル定義
--- ---------------------------------------------------------------------------
-
--- | モデル A: μ ~ Normal(0, 10) — 弱情報事前分布
-modelA :: ModelP ()
-modelA = do
-  mu <- sample "mu" (Normal 0 10)
-  observe "y" (Normal mu (realToFrac sigLik)) obsData
-
--- | モデル B: μ ~ Normal(5, 1) — 情報事前分布 (真値 μ=3 からずれた仮定)
-modelB :: ModelP ()
-modelB = do
-  mu <- sample "mu" (Normal 5 1)
-  observe "y" (Normal mu (realToFrac sigLik)) obsData
-
--- ---------------------------------------------------------------------------
--- 解析解 (Normal-Normal 共役)
--- ---------------------------------------------------------------------------
-
--- | 解析的事後平均  μ_post = σ_post² × (μ₀/σ₀² + nȳ/σ_lik²)
-analyticPosterior :: Double -> Double -> Double -> Double -> (Double, Double)
-analyticPosterior mu0 sig0 ybar n =
-  let prec0    = 1 / sig0    ^ (2::Int)
-      precLik  = 1 / sigLik  ^ (2::Int)
-      precPost = prec0 + n * precLik
-      sigPost  = sqrt (1 / precPost)
-      muPost   = (mu0 * prec0 + n * ybar * precLik) / precPost
-  in (muPost, sigPost)
-
--- ---------------------------------------------------------------------------
--- ユーティリティ
--- ---------------------------------------------------------------------------
-
-timed :: IO a -> IO (a, Double)
-timed action = do
-  t0 <- getCurrentTime
-  x  <- action
-  t1 <- getCurrentTime
-  return (x, realToFrac (diffUTCTime t1 t0))
-
--- ---------------------------------------------------------------------------
--- Main
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  gen <- createSystemRandom
-
-  let initP = Map.fromList [("mu", 0.0 :: Double)]
-      n     = fromIntegral (length obsData) :: Double
-      ybar  = sum obsData / n
-
-  -- ── 1. Gibbs vs NUTS ─────────────────────────────────────────────────────
-  putStrLn "=== Section 1: Gibbs vs NUTS (Normal 平均推定) ==="
-  putStrLn ""
-  printf "  データ: n=%d, ȳ=%.3f, σ_lik=%.1f (既知), 真値 μ=3.0\n"
-    (length obsData) ybar sigLik
-  putStrLn ""
-
-  -- Gibbs (5000 サンプル)
-  let gibbsUpdates = [ normalNormal "mu" 0 10 obsData sigLik ]
-      gibbsCfg     = defaultGibbsConfig { gibbsIterations = 5000, gibbsBurnIn = 500 }
-  (gibbsCh, tG) <- timed $ gibbs gibbsUpdates gibbsCfg initP gen
-
-  -- NUTS (5000 サンプル)
-  let nutsCfg = defaultNUTSConfig { nutsIterations = 5000, nutsBurnIn = 500, nutsStepSize = 0.5 }
-  (nutsCh, tN) <- timed $ nuts modelA nutsCfg initP gen
-
-  -- 解析解
-  let (muA, sigA) = analyticPosterior 0 10 ybar n
-
-  printf "  %-10s  mean=%7.4f  SD=%7.4f  ESS=%6.0f  ESS/s=%7.1f\n"
-    ("Gibbs"   ::String)
-    (maybe 0 id $ posteriorMean "mu" gibbsCh)
-    (maybe 0 id $ posteriorSD   "mu" gibbsCh)
-    (ess (chainVals "mu" gibbsCh))
-    (ess (chainVals "mu" gibbsCh) / tG)
-  printf "  %-10s  mean=%7.4f  SD=%7.4f  ESS=%6.0f  ESS/s=%7.1f\n"
-    ("NUTS"    ::String)
-    (maybe 0 id $ posteriorMean "mu" nutsCh)
-    (maybe 0 id $ posteriorSD   "mu" nutsCh)
-    (ess (chainVals "mu" nutsCh))
-    (ess (chainVals "mu" nutsCh) / tN)
-  printf "  %-10s  mean=%7.4f  SD=%7.4f\n"
-    ("解析解"  ::String) muA sigA
-  putStrLn ""
-  putStrLn "  → Gibbs は共役モデルで直接サンプリングできるため ESS/s が高い"
-  putStrLn ""
-
-  -- ── 2. WAIC モデル比較 ────────────────────────────────────────────────────
-  putStrLn "=== Section 2: WAIC モデル比較 ==="
-  putStrLn "  モデル A: μ ~ Normal(0, 10)  [弱情報事前: 真値 μ=3 を広くカバー]"
-  putStrLn "  モデル B: μ ~ Normal(5,  1)  [情報事前: μ≈5 を強く仮定、真値からずれ]"
-  putStrLn ""
-
-  -- モデル A の WAIC: NUTS チェーンから
-  let waicA = chainWAIC modelA nutsCh
-
-  -- モデル B を NUTS で推定
-  (nutsChB, _) <- timed $ nuts modelB nutsCfg initP gen
-  let waicB = chainWAIC modelB nutsChB
-      (muB, _) = analyticPosterior 5 1 ybar n
-
-  printf "  %-10s  事後 mean=%.4f (解析=%.4f)  WAIC=%8.3f  lppd=%8.3f  p_waic=%.3f  SE=%.3f\n"
-    ("モデル A"::String) (maybe 0 id $ posteriorMean "mu" nutsCh)  muA
-    (waicValue waicA) (waicLppd waicA) (waicPwaic waicA) (waicSE waicA)
-  printf "  %-10s  事後 mean=%.4f (解析=%.4f)  WAIC=%8.3f  lppd=%8.3f  p_waic=%.3f  SE=%.3f\n"
-    ("モデル B"::String) (maybe 0 id $ posteriorMean "mu" nutsChB) muB
-    (waicValue waicB) (waicLppd waicB) (waicPwaic waicB) (waicSE waicB)
-  putStrLn ""
-
-  let delta = waicValue waicA - waicValue waicB
-  printf "  ΔWAIC(A − B) = %.3f\n" delta
-  if delta < -2
-    then putStrLn "  → モデル A (弱情報事前) の方が良い当てはまり ✓"
-    else if delta > 2
-      then putStrLn "  → モデル B (情報事前) の方が良い当てはまり"
-      else putStrLn "  → 両モデルの差は誤差範囲内"
-  putStrLn ""
-
-  -- ── 3. PSIS-LOO 診断 ──────────────────────────────────────────────────────
-  putStrLn "=== Section 3: PSIS-LOO 診断 ==="
-  putStrLn ""
-
-  let looA = chainLOO modelA nutsCh
-      looB = chainLOO modelB nutsChB
-
-  printf "  モデル A: LOO=%.3f  elpd=%.3f  SE=%.3f  k̂>0.7: %d 観測\n"
-    (looValue looA) (looElpd looA) (looSE looA) (looKHatBad looA)
-  printf "  モデル B: LOO=%.3f  elpd=%.3f  SE=%.3f  k̂>0.7: %d 観測\n"
-    (looValue looB) (looElpd looB) (looSE looB) (looKHatBad looB)
-  putStrLn ""
-
-  let deltaLOO = looValue looA - looValue looB
-  printf "  ΔLOO(A − B) = %.3f\n" deltaLOO
-  putStrLn ""
-
-  putStrLn "  Pareto k̂ 診断 (モデル A, 観測値ごと):"
-  putStrLn "  k̂ < 0.5: 良好  |  0.5–0.7: 許容  |  > 0.7: LOO が不安定"
-  mapM_ (\(i, k) ->
-    printf "    obs %2d: k̂=%.3f  %s\n" (i::Int) k (khatLabel k))
-    (zip [1..] (looKHat looA))
-  putStrLn ""
-  putStrLn "完了"
-
-khatLabel :: Double -> String
-khatLabel k
-  | k < 0.5   = "良好"
-  | k < 0.7   = "許容"
-  | otherwise = "要注意"
diff --git a/demo/bayesian/GibbsHBMDemo.hs b/demo/bayesian/GibbsHBMDemo.hs
deleted file mode 100644
--- a/demo/bayesian/GibbsHBMDemo.hs
+++ /dev/null
@@ -1,123 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | Gibbs サンプラー × HBM DSL 統合デモ
---
--- gibbsFromModel で共役ペアを自動検出し、GibbsUpdate を自動構築する。
--- 検出できた場合は純 Gibbs、できない場合はハイブリッド Gibbs+MH になる。
---
--- 検証する3モデル:
---   1. Gamma-Poisson   : λ ~ Gamma(2,1), y ~ Poisson(λ)       [全パラメータ共役]
---   2. Beta-Binomial   : p ~ Beta(2,2),  y ~ Binomial(10, p)  [全パラメータ共役]
---   3. Normal-Normal+σ : μ ~ Normal(0,10), σ ~ Exponential(1) [μ 共役, σ は MH]
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Data.Text (Text)
-import qualified Data.Text as T
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.Model.HBM
--- import Hanalyze.Stat.Distribution (Distribution (..)) -- now from Hanalyze.Model.HBM
-import Hanalyze.MCMC.Core   (Chain (..), chainVals, posteriorMean, posteriorSD)
-import Hanalyze.MCMC.Gibbs  (GibbsConfig (..), defaultGibbsConfig,
-                    gibbsFromModel, gibbsMH, GibbsUpdate)
-
--- ---------------------------------------------------------------------------
--- モデル定義
--- ---------------------------------------------------------------------------
-
--- Model 1: Gamma-Poisson  (全共役)
-poissonModel :: [Double] -> ModelP ()
-poissonModel ys = do
-  lam <- sample "lambda" (Gamma 2 1)
-  observe "y" (Poisson lam) ys
-  return ()
-
--- Model 2: Beta-Binomial  (全共役; 各 y は 0/1 の Bernoulli)
-binomModel :: Int -> Int -> ModelP ()
-binomModel nTrials nSucc = do
-  p <- sample "p" (Beta 2 2)
-  let ys = replicate nSucc 1.0 ++ replicate (nTrials - nSucc) 0.0
-  observe "y" (Binomial 1 p) ys
-  return ()
-
--- Model 3: Normal 平均推定 (μ 共役, σ は非共役 → MH)
-normalModel :: [Double] -> ModelP ()
-normalModel ys = do
-  mu    <- sample "mu"    (Normal 0 10)
-  sigma <- sample "sigma" (Exponential 1)
-  observe "y" (Normal mu sigma) ys
-  return ()
-
--- ---------------------------------------------------------------------------
--- ヘルパー
--- ---------------------------------------------------------------------------
-
-cfg :: GibbsConfig
-cfg = defaultGibbsConfig { gibbsIterations = 3000, gibbsBurnIn = 500 }
-
--- 各モデルを top-level で構築 (rank-2 type が let-binding に流れないため)
-pModel :: ModelP ()
-pModel = poissonModel (replicate 30 (4.0 :: Double))
-
-bModel :: ModelP ()
-bModel = binomModel 100 70
-
-nModel :: ModelP ()
-nModel = normalModel (map (* 1.5) [-1.5,-1..1.5] ++ [2.0])
-
-printResult :: Text -> Chain -> Double -> IO ()
-printResult name ch truth = do
-  let vals = chainVals name ch
-  let mn   = maybe 0 id (posteriorMean name ch)
-  let sd   = maybe 0 id (posteriorSD   name ch)
-  printf "  %-10s | mean=%7.4f  sd=%7.4f  truth=%7.4f  n=%d\n"
-         (T.unpack name) mn sd truth (length vals)
-
--- ---------------------------------------------------------------------------
--- main
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  gen <- createSystemRandom
-
-  -- ── Model 1: Gamma-Poisson ──────────────────────────────────────────────
-  let trueL  = 4.0 :: Double
-      (gpUpdates, gpMH) = gibbsFromModel pModel :: ([GibbsUpdate IO], [Text])
-
-  putStrLn "\n=== Model 1: Gamma(2,1) + Poisson(λ) ==="
-  printf "  検出: Gibbs=%d ブロック, MH=%d パラメータ\n"
-         (length gpUpdates) (length gpMH)
-
-  ch1 <- gibbsMH pModel cfg Map.empty (Map.singleton "lambda" 1.0) gen
-  printResult "lambda" ch1 trueL
-
-  -- ── Model 2: Beta-Binomial ──────────────────────────────────────────────
-  let trueP   = 0.7 :: Double
-      (bbUpdates, bbMH) = gibbsFromModel bModel :: ([GibbsUpdate IO], [Text])
-
-  putStrLn "\n=== Model 2: Beta(2,2) + Binomial(1,p) ==="
-  printf "  検出: Gibbs=%d ブロック, MH=%d パラメータ\n"
-         (length bbUpdates) (length bbMH)
-
-  ch2 <- gibbsMH bModel cfg Map.empty (Map.singleton "p" 0.5) gen
-  printResult "p" ch2 trueP
-
-  -- ── Model 3: Normal + Exponential (混合) ────────────────────────────────
-  let trueMu  = 2.0 :: Double
-      trueSig = 1.5 :: Double
-      (nnUpdates, nnMH) = gibbsFromModel nModel :: ([GibbsUpdate IO], [Text])
-
-  putStrLn "\n=== Model 3: Normal(0,10) + Exponential(1) [混合モード] ==="
-  printf "  検出: Gibbs=%d ブロック (mu), MH=%d パラメータ (sigma)\n"
-         (length nnUpdates) (length nnMH)
-
-  let mhSteps = Map.singleton "sigma" 0.3
-      init3   = Map.fromList [("mu", 0.0), ("sigma", 1.0)]
-  ch3 <- gibbsMH nModel cfg mhSteps init3 gen
-  printResult "mu"    ch3 trueMu
-  printResult "sigma" ch3 trueSig
-
-  putStrLn "\n✓ 完了"
diff --git a/demo/bayesian/HBMExample.hs b/demo/bayesian/HBMExample.hs
deleted file mode 100644
--- a/demo/bayesian/HBMExample.hs
+++ /dev/null
@@ -1,219 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE ImpredicativeTypes #-}
--- Phase 4 + 5: Small hierarchical model example with MCMC inference
---
--- Hierarchical normal model for test scores across J schools:
---
---   μ      ~ Normal(0, 100)   -- global mean hyperprior
---   τ      ~ Exponential(0.1) -- between-school SD hyperprior
---   θ_j    ~ Normal(μ, τ)     -- school-specific mean  (j = 1..J)
---   y_ij   ~ Normal(θ_j, σ)   -- observations (σ = 5 treated as known)
---
-module Main where
-
-import Control.Monad (forM)
-import qualified Data.Map.Strict as Map
-import qualified Data.Text       as T
-import qualified Data.Text.IO    as TIO
-import Text.Printf (printf)
-
-import Hanalyze.Model.HBM
-import Hanalyze.MCMC.Core
-import Hanalyze.MCMC.MH   (MCMCConfig (..), defaultMCMCConfig, metropolis)
-import Hanalyze.MCMC.NUTS (nutsChains, NUTSConfig (..), defaultNUTSConfig)
-import Hanalyze.Stat.Distribution ()
-import Hanalyze.Stat.MCMC  (ess)
-import Hanalyze.Viz.Core      (openInBrowser)
-import Hanalyze.Viz.Report    (MCMCReport (..), defaultReport, renderReport)
-import System.Random.MWC (createSystemRandom)
-
--- ---------------------------------------------------------------------------
--- Model
--- ---------------------------------------------------------------------------
-
-sigma :: Double
-sigma = 5.0   -- known observation SD
-
--- | Build the hierarchical model for the given group data.
-schoolModel :: [[Double]] -> ModelP ()
-schoolModel groupData = do
-  mu  <- sample "mu"  (Normal 0 100)
-  tau <- sample "tau" (Exponential 0.1)
-  mapM_ (\(j, ys) -> do
-    theta <- sample (T.pack ("theta_" ++ show j)) (Normal mu tau)
-    observe (T.pack ("y_" ++ show j)) (Normal theta (realToFrac sigma)) ys)
-    (zip [1 :: Int ..] groupData)
-
--- ---------------------------------------------------------------------------
--- Synthetic data  (3 schools, n = 4 each)
--- ---------------------------------------------------------------------------
-
-schoolData :: [[Double]]
-schoolData =
-  [ [72, 68, 75, 71]    -- school 1: mean ≈ 71.5
-  , [85, 88, 82, 90]    -- school 2: mean ≈ 86.25
-  , [61, 65, 58, 63]    -- school 3: mean ≈ 61.75
-  ]
-
-schoolMeans :: [Double]
-schoolMeans = map (\ys -> sum ys / fromIntegral (length ys)) schoolData
-
--- | Params near the MLE: global mean = grand mean, tau = inter-school SD,
--- theta_j = school sample mean.
-trueParams :: Params
-trueParams = Map.fromList $
-  [ ("mu",  grandMean)
-  , ("tau", interSD)
-  ] ++
-  zipWith (\j m -> (T.pack ("theta_" ++ show (j :: Int)), m))
-          [1..] schoolMeans
-  where
-    grandMean = sum schoolMeans / fromIntegral (length schoolMeans)
-    interSD   = sqrt (sum (map (\m -> (m - grandMean)^(2::Int)) schoolMeans)
-                      / fromIntegral (length schoolMeans))
-
--- ---------------------------------------------------------------------------
--- Main
--- ---------------------------------------------------------------------------
-
-m :: ModelP ()
-m = schoolModel schoolData
-
-main :: IO ()
-main = do
-
-  -- ── 1. Model structure ─────────────────────────────────────────────────
-  putStrLn "=== Model Structure ==="
-  TIO.putStr (describeModel m)
-  putStrLn $ "Latent variables: " ++ show (sampleNames m)
-  putStrLn ""
-
-  -- ── 1b. Build model graph (HBMP の Track 型で依存を自動抽出) ──────────
-  let graph = buildModelGraph m
-
-  -- ── 2. Log-joint at near-MLE parameters ────────────────────────────────
-  putStrLn "=== Log-joint at near-MLE params ==="
-  printParams trueParams
-  printf "  logJoint      = %.4f\n" (logJoint      m trueParams)
-  printf "  logPrior      = %.4f\n" (logPrior      m trueParams)
-  printf "  logLikelihood = %.4f\n" (logLikelihood m trueParams)
-  putStrLn ""
-
-  -- ── 3. Effect of τ (between-school SD) ────────────────────────────────
-  putStrLn "=== logJoint as τ varies (mu, theta_j fixed at near-MLE) ==="
-  printf "  %-8s  %s\n" ("tau" :: String) ("logJoint" :: String)
-  mapM_ (checkTau m) [0.5, 1, 2, 5, 10, 20, 50]
-  putStrLn ""
-
-  -- ── 4. Effect of μ (global mean) ──────────────────────────────────────
-  putStrLn "=== logJoint as μ varies (others fixed at near-MLE) ==="
-  printf "  %-8s  %s\n" ("mu" :: String) ("logJoint" :: String)
-  mapM_ (checkMu m) [40, 55, 65, 73, 80, 90, 100]
-  putStrLn ""
-
-  -- ── 5. Invalid params ─────────────────────────────────────────────────
-  putStrLn "=== Edge cases ==="
-  let badTau = Map.insert "tau" (-1) trueParams
-  printf "  tau = -1  (outside support): logJoint = %.4f\n" (logJoint m badTau)
-  let missingTheta = Map.delete "theta_2" trueParams
-  printf "  theta_2 missing:             logJoint = %.4f\n" (logJoint m missingTheta)
-  putStrLn ""
-
-  -- ── 6. Random Walk Metropolis ──────────────────────────────────────────
-  putStrLn "=== Random Walk Metropolis (Phase 5) ==="
-  gen <- createSystemRandom
-
-  let names = sampleNames m
-      cfg   = (defaultMCMCConfig names)
-                { mcmcIterations = 5000
-                , mcmcBurnIn     = 1000
-                , mcmcStepSizes  = Map.fromList
-                    [ ("mu",      5.0)
-                    , ("tau",     2.0)
-                    , ("theta_1", 3.0)
-                    , ("theta_2", 3.0)
-                    , ("theta_3", 3.0)
-                    ]
-                }
-
-  chain <- metropolis m cfg trueParams gen
-
-  printf "Acceptance rate: %.3f  (%d / %d)\n"
-    (acceptanceRate chain)
-    (chainAccepted chain)
-    (chainTotal chain)
-  putStrLn ""
-
-  putStrLn "Posterior summaries (mean ± SD, 95% CI, ESS):"
-  printf "  %-12s  %8s  %8s  %8s  %8s  %8s\n"
-    ("param" :: String) ("mean" :: String) ("sd" :: String)
-    ("2.5%" :: String) ("97.5%" :: String) ("ESS" :: String)
-  mapM_ (printSummary chain) names
-  putStrLn ""
-
-  -- ── 7. Single-chain consolidated HTML report ─────────────────────────
-  putStrLn "=== Generating consolidated report (single chain) ==="
-
-  let report = (defaultReport "School Model — MCMC Report" chain names)
-                 { reportGraph  = Just graph
-                 , reportPairs  = [("mu", "tau")]
-                 , reportMaxLag = 40
-                 }
-  renderReport "mcmc_report.html" report
-  putStrLn "  mcmc_report.html  (model graph + summary + diagnostics + autocorr + pair plots)"
-
-  -- ── 8. 4-chain NUTS + multi-chain report ──────────────────────────────
-  putStrLn ""
-  putStrLn "=== 4-chain NUTS (parallel) ==="
-  let nutsCfg = defaultNUTSConfig
-        { nutsIterations = 2000
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.08
-        }
-  multiChains <- nutsChains m nutsCfg 4 trueParams gen
-  mapM_ (\(i, ch) ->
-    printf "  chain %d: accept=%.3f  mu_mean=%.2f  tau_mean=%.2f\n"
-      (i :: Int)
-      (acceptanceRate ch)
-      (maybe 0 id $ posteriorMean "mu"  ch)
-      (maybe 0 id $ posteriorMean "tau" ch)
-    ) (zip [1..] multiChains)
-
-  let multiReport = (defaultReport "School Model — 4-chain NUTS" (head multiChains) names)
-                      { reportGraph  = Just graph
-                      , reportChains = multiChains
-                      , reportPairs  = [("mu", "tau")]
-                      , reportMaxLag = 40
-                      }
-  renderReport "mcmc_report_multi.html" multiReport
-  putStrLn "  mcmc_report_multi.html  (4-chain KDE + colored traces + R-hat)"
-  openInBrowser "mcmc_report_multi.html"
-
--- ---------------------------------------------------------------------------
--- Helpers
--- ---------------------------------------------------------------------------
-
-printParams :: Params -> IO ()
-printParams ps = mapM_ (\(k,v) -> printf "  %-12s = %.4f\n" k v) (Map.toAscList ps)
-
-checkTau :: ModelP () -> Double -> IO ()
-checkTau m tau =
-  let ps = Map.insert "tau" tau trueParams
-  in printf "  %-8.1f  %.4f\n" tau (logJoint m ps)
-
-checkMu :: ModelP () -> Double -> IO ()
-checkMu m mu =
-  let ps = Map.insert "mu" mu trueParams
-  in printf "  %-8.1f  %.4f\n" mu (logJoint m ps)
-
-printSummary :: Chain -> T.Text -> IO ()
-printSummary chain pname =
-  let get f = maybe 0.0 id (f pname chain)
-      mean_ = get posteriorMean
-      sd_   = get posteriorSD
-      lo    = get (posteriorQuantile 0.025)
-      hi    = get (posteriorQuantile 0.975)
-      ess_  = ess (chainVals pname chain)
-  in printf "  %-12s  %8.3f  %8.3f  %8.3f  %8.3f  %8.0f\n"
-       (T.unpack pname) mean_ sd_ lo hi ess_
diff --git a/demo/bayesian/HBMRandomSlopeDemo.hs b/demo/bayesian/HBMRandomSlopeDemo.hs
deleted file mode 100644
--- a/demo/bayesian/HBMRandomSlopeDemo.hs
+++ /dev/null
@@ -1,337 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | HBM のランダム切片 vs ランダム切片+ランダム傾きの比較デモ。
---
--- データ:
---   3 グループ (A, B, C) で **傾きも異なる**
---   * Group A: α=2.0, β=-0.8   (急な右下り)
---   * Group B: α=5.0, β=-0.3   (緩やかな右下り)
---   * Group C: α=8.0, β=+0.2   (わずかに右上り)
---
--- モデル比較:
---   1. M1 (ランダム切片のみ): β を全グループで共有
---      → 単一の β に各グループの異なる傾きを"平均"してしまう
---   2. M2 (ランダム切片+ランダム傾き): β_g をグループごとに推定
---      → 各グループの真の傾きを正しく回復
---
--- WAIC/LOO で M2 が支持されることを示す。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import Data.Maybe (fromMaybe)
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import qualified DataFrame.Internal.Column    as DX
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified DataFrame.Internal.DataFrame as DXD
-
-import Hanalyze.MCMC.Core (Chain (..), chainVals, posteriorMean, posteriorSD,
-                  posteriorQuantile, acceptanceRate)
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, Distribution (..),
-                  buildModelGraph, perObsLogLiks)
-import Hanalyze.Stat.MCMC (ess)
-import Hanalyze.Stat.ModelSelect (waic, loo, WAICResult (..), LOOResult (..))
-
-import Hanalyze.Viz.AnalysisReport
-  ( AnalysisReportConfig (..), defaultAnalysisConfig
-  , FitSummary (..), HBMRegSummary (..), SmoothData (..)
-  , ModelFit (..), NamedPlot (..), CompareEntry (..)
-  , writeAnalysisReport, writeComparisonReport
-  )
-import Hanalyze.Viz.Core (PlotConfig (..))
-import Hanalyze.Viz.MCMC (mcmcDiagnostics, autocorrPlot)
-
--- ---------------------------------------------------------------------------
--- データ生成: グループごとに異なる傾き
--- ---------------------------------------------------------------------------
-
--- Group A: α=2,  β=-0.8 (急な右下り)
-dataA :: [(Double, Double)]
-dataA = zip
-  [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0]
-  -- y_clean: 1.6, 1.2, 0.8, 0.4, 0.0, -0.4, -0.8, -1.2, -1.6, -2.0
-  [1.71, 1.05, 0.92, 0.31, 0.18, -0.51, -0.65, -1.13, -1.74, -1.85]
-
--- Group B: α=5,  β=-0.3 (緩やかな右下り)
-dataB :: [(Double, Double)]
-dataB = zip
-  [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0]
-  -- y_clean: 4.85, 4.70, 4.55, 4.40, 4.25, 4.10, 3.95, 3.80, 3.65, 3.50
-  [4.94, 4.59, 4.66, 4.32, 4.41, 3.96, 4.07, 3.82, 3.51, 3.65]
-
--- Group C: α=8,  β=+0.2 (わずかに右上り)
-dataC :: [(Double, Double)]
-dataC = zip
-  [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0]
-  -- y_clean: 8.10, 8.20, 8.30, 8.40, 8.50, 8.60, 8.70, 8.80, 8.90, 9.00
-  [8.16, 8.05, 8.43, 8.32, 8.61, 8.49, 8.78, 8.71, 9.04, 8.92]
-
-allXs :: [Double]
-allXs = map fst (dataA ++ dataB ++ dataC)
-
-allYs :: [Double]
-allYs = map snd (dataA ++ dataB ++ dataC)
-
-allGroups :: [Text]
-allGroups = replicate (length dataA) "A"
-         ++ replicate (length dataB) "B"
-         ++ replicate (length dataC) "C"
-
-mkDataFrame :: DXD.DataFrame
-mkDataFrame = DX.insertColumn "x"     (DX.fromList (allXs :: [Double]))
-            $ DX.insertColumn "y"     (DX.fromList (allYs :: [Double]))
-            $ DX.insertColumn "group" (DX.fromList (allGroups :: [T.Text]))
-            $ DX.empty
-
--- ---------------------------------------------------------------------------
--- M1: ランダム切片のみ (β は全グループ共通)
--- ---------------------------------------------------------------------------
-
-modelM1 :: ModelP ()
-modelM1 = do
-  muAlpha    <- sample "mu_alpha"    (Normal 0 10)
-  sigmaAlpha <- sample "sigma_alpha" (Exponential 1)
-  beta       <- sample "beta"        (Normal 0 10)
-  sigma      <- sample "sigma"       (Exponential 1)
-  alphaA <- sample "alpha_A" (Normal muAlpha sigmaAlpha)
-  alphaB <- sample "alpha_B" (Normal muAlpha sigmaAlpha)
-  alphaC <- sample "alpha_C" (Normal muAlpha sigmaAlpha)
-  mapM_ (\(x, y) -> let xC = realToFrac x
-                    in observe "y_A" (Normal (alphaA + beta * xC) sigma) [y])
-        dataA
-  mapM_ (\(x, y) -> let xC = realToFrac x
-                    in observe "y_B" (Normal (alphaB + beta * xC) sigma) [y])
-        dataB
-  mapM_ (\(x, y) -> let xC = realToFrac x
-                    in observe "y_C" (Normal (alphaC + beta * xC) sigma) [y])
-        dataC
-
--- ---------------------------------------------------------------------------
--- M2: ランダム切片 + ランダム傾き (β_g もグループ別)
--- ---------------------------------------------------------------------------
-
-modelM2 :: ModelP ()
-modelM2 = do
-  -- 切片の階層
-  muAlpha    <- sample "mu_alpha"    (Normal 0 10)
-  sigmaAlpha <- sample "sigma_alpha" (Exponential 1)
-  -- 傾きの階層
-  muBeta     <- sample "mu_beta"     (Normal 0 5)
-  sigmaBeta  <- sample "sigma_beta"  (Exponential 1)
-  -- 残差
-  sigma      <- sample "sigma"       (Exponential 1)
-  -- グループ別パラメータ
-  alphaA <- sample "alpha_A" (Normal muAlpha sigmaAlpha)
-  alphaB <- sample "alpha_B" (Normal muAlpha sigmaAlpha)
-  alphaC <- sample "alpha_C" (Normal muAlpha sigmaAlpha)
-  betaA  <- sample "beta_A"  (Normal muBeta  sigmaBeta)
-  betaB  <- sample "beta_B"  (Normal muBeta  sigmaBeta)
-  betaC  <- sample "beta_C"  (Normal muBeta  sigmaBeta)
-  mapM_ (\(x, y) -> let xC = realToFrac x
-                    in observe "y_A" (Normal (alphaA + betaA * xC) sigma) [y])
-        dataA
-  mapM_ (\(x, y) -> let xC = realToFrac x
-                    in observe "y_B" (Normal (alphaB + betaB * xC) sigma) [y])
-        dataB
-  mapM_ (\(x, y) -> let xC = realToFrac x
-                    in observe "y_C" (Normal (alphaC + betaC * xC) sigma) [y])
-        dataC
-
--- ---------------------------------------------------------------------------
--- 共通: NUTS 実行 + WAIC/LOO + AnalysisReport 用 ModelFit 構築
--- ---------------------------------------------------------------------------
-
-runHBM
-  :: Text                    -- ^ モデルラベル ("M1" / "M2")
-  -> Text                    -- ^ 出力 HTML ファイル名
-  -> ModelP ()               -- ^ 推論対象モデル
-  -> [Text]                  -- ^ 主要パラメータ名 (β または β_g など)
-  -> Map.Map Text Double     -- ^ 初期値
-  -> [Text]                  -- ^ 全潜在変数名 (事後分布表用)
-  -> NUTSConfig
-  -> IO (Maybe ModelFit)
-runHBM label htmlPath m mainParams initP allParams cfg = do
-  putStrLn $ "  [" ++ T.unpack label ++ "] NUTS サンプリング..."
-  gen <- createSystemRandom
-  chain <- nuts m cfg initP gen
-  printf "    受容率=%.1f%%, サンプル数=%d\n"
-         (acceptanceRate chain * 100 :: Double)
-         (length (chainSamples chain))
-
-  putStrLn $ "  [" ++ T.unpack label ++ "] 主要パラメータ事後:"
-  mapM_ (\n ->
-    printf "    %-10s mean=%+.3f  sd=%.3f  95%% CI=[%+.3f, %+.3f]\n"
-      (T.unpack n)
-      (fromMaybe 0 (posteriorMean n chain))
-      (fromMaybe 0 (posteriorSD   n chain))
-      (fromMaybe 0 (posteriorQuantile 0.025 n chain))
-      (fromMaybe 0 (posteriorQuantile 0.975 n chain)))
-    mainParams
-
-  -- WAIC/LOO
-  let llMat = [ perObsLogLiks m ps | ps <- chainSamples chain ]
-      wRes  = waic llMat
-      lRes  = loo  llMat
-  printf "    WAIC=%.2f  LOO=%.2f  p_WAIC=%.2f\n"
-         (waicValue wRes) (looValue lRes) (waicPwaic wRes)
-
-  -- 全体平均線の事後予測 (μ_α + 平均β · x で構築。M2 では平均β = mu_beta)
-  let alphas = chainVals "mu_alpha" chain
-      -- M1 は "beta", M2 は "mu_beta" を使う
-      betas  = case chainVals "mu_beta" chain of
-                 [] -> chainVals "beta" chain
-                 vs -> vs
-      xMin = minimum allXs
-      xMax = maximum allXs
-      xExt = (xMax - xMin) * 0.1
-      grid = [xMin - xExt + i * (xMax - xMin + 2 * xExt) / 99 | i <- [0..99]]
-      atX x = let ss     = sortAsc (zipWith (\a b -> a + b * x) alphas betas)
-                  n      = length ss
-                  qAt p  = ss !! min (n-1) (max 0 (floor (p * fromIntegral n) :: Int))
-              in (qAt 0.5, qAt 0.025, qAt 0.975)
-      (ysMid, ysLo, ysHi) = unzip3 (map atX grid)
-      smooth = SmoothData
-        { sdXs = grid, sdYs = ysMid, sdLower = ysLo, sdUpper = ysHi
-        , sdHasBand = True
-        }
-
-      bMean    = case posteriorMean "mu_beta" chain of
-                   Just v  -> v
-                   Nothing -> fromMaybe 0 (posteriorMean "beta" chain)
-      aMu      = fromMaybe 0 (posteriorMean "mu_alpha" chain)
-      fitted   = [aMu + bMean * x | x <- allXs]
-      resid    = zipWith (-) allYs fitted
-      yBar     = sum allYs / fromIntegral (length allYs)
-      tss      = sum [(y - yBar) ^ (2::Int) | y <- allYs]
-      rss      = sum [r ^ (2::Int) | r <- resid]
-      r2       = if tss < 1e-12 then 0 else 1 - rss / tss
-
-      modelLabelLong = case label of
-        "M1" -> "HBM (Random intercept only)"
-        "M2" -> "HBM (Random intercept + random slope)"
-        _    -> "HBM"
-
-      formula = case label of
-        "M1" -> "y_g ~ α_g + β · x  (β 共通)"
-        "M2" -> "y_g ~ α_g + β_g · x  (α_g, β_g 階層)"
-        _    -> "y ~ α + β · x"
-
-      fs = FitSummary
-        { fsModelType    = modelLabelLong
-        , fsFormula      = formula
-        , fsCoeffs       = [("μ_α (全体切片)", aMu), ("μ_β (平均傾き)", bMean)]
-        , fsR2           = r2
-        , fsR2Label      = "R² (全体平均線)"
-        , fsFitted       = fitted
-        , fsResiduals    = resid
-        , fsLinkName     = "Normal (identity link)"
-        , fsXColDegs     = [("x", 1)]
-        , fsSmoothData   = Just ("x", smooth)
-        , fsModelSelect  = Just (wRes, lRes)
-        }
-      hs = HBMRegSummary
-        { hbmsFit           = fs
-        , hbmsModelGraph    = buildModelGraph m
-        , hbmsChain         = chain
-        , hbmsParams        = allParams
-        , hbmsPosteriorRows =
-            [ (n, fromMaybe 0 (posteriorMean n chain)
-              , fromMaybe 0 (posteriorSD   n chain)
-              , fromMaybe 0 (posteriorQuantile 0.025 n chain)
-              , fromMaybe 0 (posteriorQuantile 0.975 n chain))
-            | n <- allParams ]
-        }
-      diagCfg = PlotConfig "MCMC 診断 (KDE + トレース)" 760 320 Nothing Nothing Nothing
-      acfCfg  = PlotConfig "自己相関 (lag 0..40)" 760 220 Nothing Nothing Nothing
-      diagPlot = NamedPlot "vl-diag" "MCMC 診断"
-                   (mcmcDiagnostics diagCfg mainParams chain)
-      acfPlot  = NamedPlot "vl-acf"  "自己相関"
-                   (autocorrPlot acfCfg 40 mainParams chain)
-      rptCfg = defaultAnalysisConfig
-                 ("HBM " <> label <> " — " <> modelLabelLong)
-  writeAnalysisReport (T.unpack htmlPath) rptCfg mkDataFrame ["x"] "y"
-                      (HBMFit hs) [diagPlot, acfPlot]
-  putStrLn $ "    → " ++ T.unpack htmlPath
-  return (Just (HBMFit hs))
-
-sortAsc :: [Double] -> [Double]
-sortAsc = qs
-  where
-    qs []     = []
-    qs (p:rs) = qs [x | x <- rs, x <= p] ++ [p] ++ qs [x | x <- rs, x > p]
-
--- ---------------------------------------------------------------------------
--- main
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  HBM ランダム傾き比較: M1 (β 共通) vs M2 (β_g グループ別)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  printf "  3 グループ × 10 観測 = N=%d\n" (length allXs)
-  putStrLn "  真値:"
-  putStrLn "    Group A: α=2.0, β=-0.8  (急な右下り)"
-  putStrLn "    Group B: α=5.0, β=-0.3  (緩やかな右下り)"
-  putStrLn "    Group C: α=8.0, β=+0.2  (わずかに右上り)"
-  putStrLn ""
-
-  let cfg = defaultNUTSConfig
-              { nutsIterations = 800
-              , nutsBurnIn     = 400
-              , nutsStepSize   = 0.05
-              , nutsMaxDepth   = 8
-              }
-      m1Init = Map.fromList
-                 [ ("mu_alpha", 5.0), ("sigma_alpha", 2.0)
-                 , ("beta", 0.0), ("sigma", 0.5)
-                 , ("alpha_A", 2.0), ("alpha_B", 5.0), ("alpha_C", 8.0)
-                 ]
-      m1Params = ["mu_alpha","sigma_alpha","beta","sigma",
-                  "alpha_A","alpha_B","alpha_C"]
-      m2Init = Map.fromList
-                 [ ("mu_alpha", 5.0), ("sigma_alpha", 2.0)
-                 , ("mu_beta", 0.0), ("sigma_beta", 0.5)
-                 , ("sigma", 0.3)
-                 , ("alpha_A", 2.0), ("alpha_B", 5.0), ("alpha_C", 8.0)
-                 , ("beta_A",  -0.5), ("beta_B", -0.5), ("beta_C", 0.0)
-                 ]
-      m2Params = ["mu_alpha","sigma_alpha","mu_beta","sigma_beta","sigma",
-                  "alpha_A","alpha_B","alpha_C",
-                  "beta_A","beta_B","beta_C"]
-
-  putStrLn "[M1] ランダム切片のみ (β 共通):"
-  mFit1 <- runHBM "M1" "rs_m1.html" modelM1 ["beta"] m1Init m1Params cfg
-  putStrLn ""
-
-  putStrLn "[M2] ランダム切片 + ランダム傾き (β_g 階層):"
-  mFit2 <- runHBM "M2" "rs_m2.html" modelM2
-                  ["mu_beta","beta_A","beta_B","beta_C"]
-                  m2Init m2Params cfg
-  putStrLn ""
-
-  -- 統合比較レポート
-  case (mFit1, mFit2) of
-    (Just f1, Just f2) -> do
-      putStrLn "[Compare] M1 vs M2 統合レポート:"
-      let entries =
-            [ CompareEntry "M1 (β 共通)"           "#e41a1c" f1
-            , CompareEntry "M2 (β_g グループ別)"    "#4daf4a" f2
-            ]
-          rptCfg = defaultAnalysisConfig
-                     "HBM Random Intercept vs Random Intercept + Slope"
-      writeComparisonReport "rs_compare.html" rptCfg
-                            mkDataFrame ["x"] "y" entries
-      putStrLn "    → rs_compare.html"
-    _ -> putStrLn "  比較レポート生成スキップ"
-
-  putStrLn ""
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  解釈: M2 (各グループに β_g) のほうが WAIC/LOO が小さくなれば、"
-  putStrLn "        グループ間で傾きが異なる構造をデータが支持していることになる。"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/HBMRegressionDemo.hs b/demo/bayesian/HBMRegressionDemo.hs
deleted file mode 100644
--- a/demo/bayesian/HBMRegressionDemo.hs
+++ /dev/null
@@ -1,244 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | HBM (ベイズ階層モデル) を使った単回帰デモ。
---
--- モデル:
---   alpha ~ Normal(0, 10)        -- 切片
---   beta  ~ Normal(0, 10)        -- 傾き
---   sigma ~ Exponential(1)       -- 観測ノイズ
---   y_i   ~ Normal(alpha + beta * x_i, sigma)
---
--- NUTS で事後サンプリング → AnalysisReport を生成:
---   * モデル概要に DAG (依存グラフ)
---   * 回帰結果に MCMC 診断 (KDE/トレース/自己相関)
---   * 対話的予測 (95% 信用区間バンド付き)
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import Data.List (sort)
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import qualified DataFrame.Internal.Column    as DX
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified DataFrame.Internal.DataFrame as DXD
-import Hanalyze.MCMC.Core (Chain (..), chainVals, posteriorMean, posteriorSD,
-                  posteriorQuantile)
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, Distribution (..),
-                  buildModelGraph)
-import Hanalyze.Stat.MCMC (ess)
-import Hanalyze.Viz.AnalysisReport
-  ( AnalysisReportConfig (..), defaultAnalysisConfig
-  , FitSummary (..), SmoothData (..), HBMRegSummary (..)
-  , ModelFit (..), NamedPlot (..)
-  , writeAnalysisReport
-  )
-import Hanalyze.Viz.Core (PlotConfig (..))
-import Hanalyze.Viz.MCMC (mcmcDiagnostics, autocorrPlot)
-
-import Hanalyze.Model.GLM (Family (..), LinkFn (..))
-
--- ---------------------------------------------------------------------------
--- 合成データ: y = 2 + 3x + ε,  ε ~ N(0, 1.5²)
--- ---------------------------------------------------------------------------
-
-trueAlpha, trueBeta, trueSigma :: Double
-trueAlpha = 2.0
-trueBeta  = 3.0
-trueSigma = 1.5
-
-xs :: [Double]
-xs = [-2.5, -2.1, -1.7, -1.3, -0.9, -0.5, -0.1, 0.3, 0.7, 1.1
-     , 1.5, 1.9, 2.3, 2.7, 3.1, 0.0, 0.6, 1.2, 2.0, 2.8
-     , -1.0, -1.5, 1.0, 1.4, -0.3, 0.2, 1.8, 2.4, -2.0, 0.9]
-
--- 真の関係 + 軽いノイズ (再現性のため固定)
-ys :: [Double]
-ys = zipWith (+) [trueAlpha + trueBeta * x | x <- xs]
-                 [-1.41, 0.83, -0.66, 1.55, 0.27, 1.83, -0.30, 0.45, -1.18, 1.52
-                 , 0.62, -1.25, 1.09, -0.31, 0.74, 0.18, -1.84, 1.43, -0.96, 1.21
-                 , -0.55, 1.79, 0.04, 0.91, -1.43, 0.38, 0.66, -0.80, 0.49, -1.12]
-
--- ---------------------------------------------------------------------------
--- HBM 回帰モデル (top-level: rank-2 type の monomorphisation を回避)
--- ---------------------------------------------------------------------------
-
-regModel :: ModelP ()
-regModel = do
-  alpha <- sample "alpha" (Normal 0 10)
-  beta  <- sample "beta"  (Normal 0 10)
-  sigma <- sample "sigma" (Exponential 1)
-  -- yMean は各観測値で異なるため、observe を観測ごとに発行する
-  -- (1 つの分布で全観測を扱うと、x が分布パラメータに入らないため)
-  mapM_ (\(x, y) ->
-    let xC = realToFrac x
-    in observe "y" (Normal (alpha + beta * xC) sigma) [y])
-    (zip xs ys)
-
--- ---------------------------------------------------------------------------
--- 事後予測曲線 (信用区間付き) を計算
--- ---------------------------------------------------------------------------
-
--- グリッド x* 上で、各事後サンプル (α, β) から μ* = α + β·x* を計算し、
--- その分布の 2.5%/50%/97.5% 分位点を返す。
-makeSmoothData :: Chain -> SmoothData
-makeSmoothData ch =
-  let alphas = chainVals "alpha" ch
-      betas  = chainVals "beta"  ch
-      xMin   = minimum xs
-      xMax   = maximum xs
-      ext    = (xMax - xMin) * 0.5
-      grid   = [xMin - ext + i * (xMax - xMin + 2 * ext) / 99 | i <- [0..99]]
-      atX x  = sort (zipWith (\a b -> a + b * x) alphas betas)
-      qAt p ss = let n = length ss
-                     i = max 0 (min (n - 1) (floor (p * fromIntegral n) :: Int))
-                 in ss !! i
-      ysMean = [ let s = atX x in qAt 0.5 s | x <- grid ]
-      ysLo   = [ let s = atX x in qAt 0.025 s | x <- grid ]
-      ysHi   = [ let s = atX x in qAt 0.975 s | x <- grid ]
-  in SmoothData
-       { sdXs      = grid
-       , sdYs      = ysMean
-       , sdLower   = ysLo
-       , sdUpper   = ysHi
-       , sdHasBand = True
-       }
-
--- ---------------------------------------------------------------------------
--- DataFrame の組み立て
--- ---------------------------------------------------------------------------
-
-mkDataFrame :: DXD.DataFrame
-mkDataFrame = DX.insertColumn "x" (DX.fromList (xs :: [Double]))
-            $ DX.insertColumn "y" (DX.fromList (ys :: [Double]))
-            $ DX.empty
-
--- ---------------------------------------------------------------------------
--- HBM フィット結果から FitSummary を構築
--- ---------------------------------------------------------------------------
-
-mkFitForHBM :: Chain -> FitSummary
-mkFitForHBM ch =
-  let aMean = maybe 0 id (posteriorMean "alpha" ch)
-      bMean = maybe 0 id (posteriorMean "beta"  ch)
-      fitted = [aMean + bMean * x | x <- xs]
-      resid  = zipWith (-) ys fitted
-      yBar   = sum ys / fromIntegral (length ys)
-      tss    = sum [(y - yBar) ^ (2::Int) | y <- ys]
-      rss    = sum [r ^ (2::Int) | r <- resid]
-      r2     = if tss < 1e-12 then 0 else 1 - rss / tss
-      smooth = makeSmoothData ch
-  in FitSummary
-       { fsModelType    = "Bayesian Linear Regression (HBM)"
-       , fsFormula      = "y ~ α + β · x"
-       , fsCoeffs       = [("(Intercept) α", aMean), ("β (x)", bMean)]
-       , fsR2           = r2
-       , fsR2Label      = "R²"
-       , fsFitted       = fitted
-       , fsResiduals    = resid
-       , fsLinkName     = "Normal (identity link)"
-       , fsXColDegs     = [("x", 1)]
-       , fsSmoothData   = Just ("x", smooth)
-       , fsModelSelect  = Nothing
-       }
-
-mkPosteriorRows :: Chain -> [(Text, Double, Double, Double, Double)]
-mkPosteriorRows ch =
-  [ ( name
-    , maybe 0 id (posteriorMean name ch)
-    , maybe 0 id (posteriorSD   name ch)
-    , maybe 0 id (posteriorQuantile 0.025 name ch)
-    , maybe 0 id (posteriorQuantile 0.975 name ch)
-    )
-  | name <- ["alpha", "beta", "sigma"]
-  ]
-
--- ---------------------------------------------------------------------------
--- main
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  putStrLn "=== HBM 単回帰デモ ==="
-  printf "  真値: α=%.2f, β=%.2f, σ=%.2f\n" trueAlpha trueBeta trueSigma
-  printf "  サンプル数: n=%d\n\n" (length xs)
-
-  putStrLn "[NUTS サンプリング (AD 勾配, dual averaging)]"
-  let cfg = defaultNUTSConfig
-              { nutsIterations = 2000
-              , nutsBurnIn     = 500
-              , nutsStepSize   = 0.1
-              }
-      initP = Map.fromList [("alpha", 0.0), ("beta", 0.0), ("sigma", 1.0)]
-  gen <- createSystemRandom
-  chain <- nuts regModel cfg initP gen
-
-  printf "  受容率: %.1f%%\n" (acceptanceRateOf chain * 100)
-  printf "  サンプル数: %d\n\n" (length (chainSamples chain))
-
-  putStrLn "[事後分布サマリー]"
-  printf "  %-8s %10s %10s %10s %10s %10s\n"
-    ("param"::String) ("mean"::String) ("sd"::String)
-    ("2.5%"::String) ("97.5%"::String) ("ESS"::String)
-  mapM_ (\name ->
-    printf "  %-8s %10.4f %10.4f %10.4f %10.4f %10.0f\n"
-      (T.unpack name)
-      (maybe 0 id (posteriorMean name chain))
-      (maybe 0 id (posteriorSD   name chain))
-      (maybe 0 id (posteriorQuantile 0.025 name chain))
-      (maybe 0 id (posteriorQuantile 0.975 name chain))
-      (ess (chainVals name chain)))
-    ["alpha", "beta", "sigma"]
-
-  -- DAG / FitSummary / 診断プロットを構築
-  let graph = buildModelGraph regModel
-      fs    = mkFitForHBM chain
-      hs    = HBMRegSummary
-                { hbmsFit           = fs
-                , hbmsModelGraph    = graph
-                , hbmsChain         = chain
-                , hbmsParams        = ["alpha", "beta", "sigma"]
-                , hbmsPosteriorRows = mkPosteriorRows chain
-                }
-      diagCfg = PlotConfig
-                  { plotTitle  = "MCMC 診断 (KDE + トレース)"
-                  , plotWidth  = 720
-                  , plotHeight = 280
-                  }
-      acfCfg  = PlotConfig
-                  { plotTitle  = "自己相関 (lag 0..40)"
-                  , plotWidth  = 720
-                  , plotHeight = 220
-                  }
-      diagPlot = NamedPlot
-                   { npName  = "vl-hbm-diag"
-                   , npTitle = "MCMC 診断 (KDE + トレース)"
-                   , npSpec  = mcmcDiagnostics diagCfg ["alpha", "beta", "sigma"] chain
-                   }
-      acfPlot  = NamedPlot
-                   { npName  = "vl-hbm-acf"
-                   , npTitle = "パラメータ別 自己相関"
-                   , npSpec  = autocorrPlot acfCfg 40 ["alpha", "beta", "sigma"] chain
-                   }
-      reportCfg = defaultAnalysisConfig "HBM 単回帰 — AnalysisReport"
-      df = mkDataFrame
-
-  putStrLn "\n[HTML レポート生成]"
-  writeAnalysisReport "hbm_regression_report.html" reportCfg df ["x"] "y"
-                       (HBMFit hs) [diagPlot, acfPlot]
-  putStrLn "  hbm_regression_report.html"
-  putStrLn "  (DAG + 事後分布 + MCMC 診断 + 信用区間付き対話的予測)"
-
-acceptanceRateOf :: Chain -> Double
-acceptanceRateOf ch =
-  let t = chainTotal ch
-      a = chainAccepted ch
-  in if t == 0 then 0 else fromIntegral a / fromIntegral t :: Double
-
--- 未使用警告の抑制
-_unused :: (Family, LinkFn)
-_unused = (Gaussian, Identity)
diff --git a/demo/bayesian/LKJ3DDemo.hs b/demo/bayesian/LKJ3DDemo.hs
deleted file mode 100644
--- a/demo/bayesian/LKJ3DDemo.hs
+++ /dev/null
@@ -1,136 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | Phase J1: LKJ 相関行列事前を K=3 で検証。
---
--- 真の相関行列 (sd=1 固定):
---   R = [[1.0, 0.6, 0.3],
---        [0.6, 1.0, 0.4],
---        [0.3, 0.4, 1.0]]
--- から 3D サンプル n=200 を生成し、LKJ(η=1) 事前で
--- 各 3 個の相関 (R[1][0], R[2][0], R[2][1]) を回復する。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-import qualified System.Random.MWC.Distributions as MWC
-
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observeMV, lkjCorrCholesky,
-                  Distribution (..), augmentChainWithDeterministic)
-import Hanalyze.Viz.MCMC (printPosteriorSummary, posteriorSummaryFile)
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 800
-        , nutsBurnIn     = 400
-        , nutsStepSize   = 0.05
-        , nutsMaxDepth   = 7
-        }
-
--- 真の Cholesky factor L (sd=1 仮定)
-trueL :: [[Double]]
-trueL =
-  -- L[0] = (1, 0, 0)
-  -- L[1] = (0.6, sqrt(1-0.36)=0.8, 0)
-  -- L[2] = (0.3, (0.4 - 0.6*0.3)/0.8 = 0.275, sqrt(1 - 0.09 - 0.275^2) = 0.946)
-  [ [1.0, 0.0,    0.0]
-  , [0.6, 0.8,    0.0]
-  , [0.3, 0.275,  sqrt (1 - 0.09 - 0.275 ^ (2::Int)) ]
-  ]
-
-gen3D :: Int -> IO [[Double]]
-gen3D n = do
-  gen <- createSystemRandom
-  let drawOne = do
-        z0 <- MWC.standard gen
-        z1 <- MWC.standard gen
-        z2 <- MWC.standard gen
-        let x0 = head (head trueL)             * z0
-            x1 = (trueL !! 1 !! 0) * z0 + (trueL !! 1 !! 1) * z1
-            x2 = (trueL !! 2 !! 0) * z0 + (trueL !! 2 !! 1) * z1
-                 + (trueL !! 2 !! 2) * z2
-        return [x0, x1, x2]
-  mapM (const drawOne) [1 .. n]
-
--- σ 既知 (=1)、相関のみ LKJ で推定
-lkj3DModel :: [[Double]] -> ModelP ()
-lkj3DModel obs = do
-  l <- lkjCorrCholesky "R" 3 1.0    -- η = 1: uniform 事前
-  let cov = let row i = [ sum [ ((l !! i) !! kk) * ((l !! j) !! kk)
-                              | kk <- [0 .. min i j] ]
-                        | j <- [0, 1, 2] ]
-            in [row i | i <- [0, 1, 2]]
-  m0 <- sample "mu0" (Normal 0 5)
-  m1 <- sample "mu1" (Normal 0 5)
-  m2 <- sample "mu2" (Normal 0 5)
-  observeMV "y" (MvNormal [m0, m1, m2] cov) obs
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  LKJ K=3 検証 (Phase J1)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  putStrLn "真の相関: R[1][0]=0.6, R[2][0]=0.3, R[2][1]=0.4"
-  obs <- gen3D 200
-  -- 標本相関で確認
-  let cols = transpose obs
-      mu c = sum c / fromIntegral (length c)
-      cov ci cj =
-        let mi = mu ci; mj = mu cj
-        in sum (zipWith (\x y -> (x - mi) * (y - mj)) ci cj)
-           / fromIntegral (length ci - 1)
-      sd ci = sqrt (cov ci ci)
-      cor ci cj = cov ci cj / (sd ci * sd cj)
-      [c0, c1, c2] = cols
-  printf "標本: r10=%.3f, r20=%.3f, r21=%.3f\n"
-         (cor c0 c1) (cor c0 c2) (cor c1 c2)
-  putStrLn ""
-
-  gen <- createSystemRandom
-  rawCh <- nuts (lkj3DModel obs) cfg
-                (Map.fromList [ ("R_u1_0", 0.5), ("R_u2_0", 0.5)
-                              , ("R_u2_1", 0.5)
-                              , ("mu0", 0), ("mu1", 0), ("mu2", 0) ])
-                gen
-  let ch = augmentChainWithDeterministic (lkj3DModel obs) rawCh
-
-  -- pc は partial correlations。R 自体の上三角 (j < i) は対応する
-  -- canonical partial correlation だが、L の積で R が決まる。
-  -- 実際の R[i][j] (i > j) は L から再構築できる; ここでは
-  -- pc/L をそのまま表示し、コメントで対応関係を示す。
-  putStrLn "[1] Posterior summary"
-  let names = [ "R_pc1_0"     -- = R[1][0] = ρ_10   (K=2 部分なので一致)
-              , "R_pc2_0"     -- partial corr (NOT直接 ρ_20)
-              , "R_pc2_1"     --
-              , "R_L1_0", "R_L1_1"
-              , "R_L2_0", "R_L2_1", "R_L2_2"
-              , "mu0", "mu1", "mu2"
-              ]
-  printPosteriorSummary names [ch]
-  putStrLn ""
-
-  posteriorSummaryFile "lkj3d-summary.html" "LKJ K=3 posterior" names [ch]
-  putStrLn "  → lkj3d-summary.html"
-  putStrLn ""
-
-  putStrLn "Note: R[i][j] (i>j) は L から再構築:"
-  putStrLn "  R[1][0] = L[1][0]"
-  putStrLn "  R[2][0] = L[2][0]"
-  putStrLn "  R[2][1] = L[1][0]*L[2][0] + L[1][1]*L[2][1]"
-  putStrLn ""
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ LKJ(η=1) が K=3 で動作、3 個の相関を同時推定"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-
-  where
-    transpose :: [[a]] -> [[a]]
-    transpose [] = []
-    transpose xss
-      | all null xss = []
-      | otherwise =
-          let heads = [h | (h:_) <- xss]
-              tails = [t | (_:t) <- xss]
-          in heads : transpose tails
diff --git a/demo/bayesian/LKJDemo.hs b/demo/bayesian/LKJDemo.hs
deleted file mode 100644
--- a/demo/bayesian/LKJDemo.hs
+++ /dev/null
@@ -1,103 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | LKJ 相関行列事前 + MvNormal 観測のデモ (Phase H4)。
---
--- 2D 観測データの相関行列 R を LKJ(η=1) 事前 (uniform on R) で推定。
--- 真の相関 ρ = 0.7 のデータを生成し、posterior の R[1][0]=ρ̂ が
--- 0.7 付近に集中することを確認。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-import qualified System.Random.MWC.Distributions as MWC
-
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observeMV, lkjCorrCholesky,
-                  Distribution (..), augmentChainWithDeterministic)
-import Hanalyze.Viz.MCMC (printPosteriorSummary, posteriorSummaryFile,
-                 pairScatterFile)
-import Hanalyze.Viz.Core (defaultConfig, OutputFormat (..), PlotConfig (..))
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 800
-        , nutsBurnIn     = 400
-        , nutsStepSize   = 0.1
-        , nutsMaxDepth   = 6
-        }
-
--- 真の相関 ρ = 0.7 で 2D サンプル生成
-genCorr :: Int -> Double -> IO [[Double]]
-genCorr n rho = do
-  gen <- createSystemRandom
-  let l11 = sqrt (1 - rho * rho)
-      drawOne = do
-        z0 <- MWC.standard gen
-        z1 <- MWC.standard gen
-        return [z0, rho * z0 + l11 * z1]
-  mapM (const drawOne) [1 .. n]
-
--- σ 既知 (= 1)、相関行列を LKJ 事前で推定
-lkjModel :: [[Double]] -> ModelP ()
-lkjModel obs = do
-  -- 相関行列 R の Cholesky factor L (2×2)
-  l <- lkjCorrCholesky "R" 2 1.0   -- η = 1: uniform 事前
-  -- σ_i 既知 = 1 → cov = L Lᵀ
-  let cov = let row i = [ sum [ ((l !! i) !! kk) * ((l !! j) !! kk)
-                              | kk <- [0 .. min i j] ]
-                        | j <- [0, 1] ]
-            in [row 0, row 1]
-  -- μ も推定
-  m0 <- sample "mu0" (Normal 0 5)
-  m1 <- sample "mu1" (Normal 0 5)
-  observeMV "y" (MvNormal [m0, m1] cov) obs
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  LKJ 相関行列事前 + MvNormal 観測 (Phase H4)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  putStrLn "真値: ρ = 0.7, μ = (0, 0), σ = (1, 1) (固定)"
-  obs <- genCorr 100 0.7
-  let xs    = [head ys | ys <- obs]
-      ys    = [last ys | ys <- obs]
-      n     = length obs
-      mux   = sum xs / fromIntegral n
-      muy   = sum ys / fromIntegral n
-      cxy   = sum (zipWith (\x y -> (x - mux) * (y - muy)) xs ys)
-              / fromIntegral (n - 1)
-      sx    = sqrt (sum [(x - mux)^(2::Int) | x <- xs] / fromIntegral (n-1))
-      sy    = sqrt (sum [(y - muy)^(2::Int) | y <- ys] / fromIntegral (n-1))
-      empRho = cxy / (sx * sy)
-  printf "観測 (n=%d): 標本 ρ = %.3f\n" n empRho
-  putStrLn ""
-
-  gen <- createSystemRandom
-  rawCh <- nuts (lkjModel obs) cfg
-                (Map.fromList [ ("R_u1_0", 0.5)
-                              , ("mu0", 0), ("mu1", 0) ]) gen
-  let ch = augmentChainWithDeterministic (lkjModel obs) rawCh
-
-  putStrLn "[1] Posterior summary"
-  -- pc1_0 は 2u−1 ∈ (-1,1) で、これが ρ そのもの (K=2 の場合)
-  let names = [ "R_u1_0"        -- raw Beta latent
-              , "R_pc1_0"       -- 2u-1 = ρ
-              , "R_L1_0"        -- Cholesky off-diag = ρ (K=2)
-              , "R_L1_1"        -- diag = √(1-ρ²)
-              , "mu0", "mu1" ]
-  printPosteriorSummary names [ch]
-  putStrLn ""
-
-  posteriorSummaryFile "lkj-summary.html" "LKJ posterior" names [ch]
-  let pcfg = (defaultConfig "ρ̂ posterior")
-               { plotWidth = 500, plotHeight = 400 }
-  pairScatterFile HTML "lkj-pair.html" pcfg "mu0" "R_pc1_0" ch
-  putStrLn "  → lkj-summary.html / lkj-pair.html"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ LKJ 事前で ρ ≈ 0.7 を回復、Cholesky factor も派生量化"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/MixtureDemo.hs b/demo/bayesian/MixtureDemo.hs
deleted file mode 100644
--- a/demo/bayesian/MixtureDemo.hs
+++ /dev/null
@@ -1,171 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | Mixture 分布のデモ。
---
--- 3 つの典型用途:
---   1. 2 成分ガウス混合 (二峰性データ)
---   2. ゼロ過剰 (過剰ゼロ + 通常分布)
---   3. 頑健回帰 (Normal + 広い Normal の混合 = 外れ値耐性)
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Data.Maybe (fromMaybe)
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.MCMC.Core (chainSamples, posteriorMean, posteriorSD,
-                  posteriorQuantile, acceptanceRate)
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, Distribution (..))
-import Hanalyze.Stat.PosteriorPredictive (posteriorPredictive)
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 2000
-        , nutsBurnIn     = 800
-        , nutsStepSize   = 0.05
-        }
-
--- ---------------------------------------------------------------------------
--- 例 1: 2 成分ガウス混合 (二峰性データ)
--- ---------------------------------------------------------------------------
--- データ: 2 つの正規分布の混合 (片方は平均 0、もう片方は平均 5)
-
-bimodalData :: [Double]
-bimodalData =
-  [-0.3, 0.2, -0.1, 0.5, -0.2, 0.1, 0.4, -0.4, 0.3, 0.0,    -- 成分 1 中心
-    4.8, 5.2, 4.7, 5.3, 5.0, 4.9, 5.1, 4.6, 5.4, 4.7]     -- 成分 2 中心
-
--- 混合モデル: 重みも推定
-gmmModel :: ModelP ()
-gmmModel = do
-  -- 2 成分の平均を学習 (重みは固定 [0.5, 0.5] で簡単化)
-  mu1 <- sample "mu1"   (Normal 0 5)
-  mu2 <- sample "mu2"   (Normal 0 5)
-  sig <- sample "sigma" (HalfNormal 2)
-  -- 各観測 y は Normal(mu1, sig) と Normal(mu2, sig) の重み 0.5/0.5 混合
-  observe "y" (Mixture [0.5, 0.5]
-                       [Normal mu1 sig, Normal mu2 sig])
-              bimodalData
-
--- ---------------------------------------------------------------------------
--- 例 2: ゼロ過剰モデル (zero-inflated)
--- ---------------------------------------------------------------------------
--- データ: ゼロ過剰のカウント風データ (実装の関係上連続で代用)
--- - ゼロ近傍に確率 q
--- - 通常 Normal(2, 1) に確率 1-q
-
-ziData :: [Double]
-ziData = [0.0, 0.0, 0.0, 0.0, 0.0, 0.01, -0.02, 0.01,   -- 「ゼロ過剰」(8 件)
-          1.8, 2.1, 2.3, 1.9, 2.0, 1.7, 2.2]              -- 「通常」(7 件)
-
--- Normal(0, 0.05) の鋭いピーク + Normal(mu, sig) の混合
-ziModel :: ModelP ()
-ziModel = do
-  q   <- sample "q"     (Beta 1 1)        -- ゼロ過剰割合
-  mu  <- sample "mu"    (Normal 0 5)      -- 通常成分の中心
-  sig <- sample "sigma" (HalfNormal 2)
-  observe "y" (Mixture [q, 1 - q]
-                       [Normal 0 0.05, Normal mu sig])
-              ziData
-
--- ---------------------------------------------------------------------------
--- 例 3: 頑健 Normal-Normal 混合 (外れ値耐性)
--- ---------------------------------------------------------------------------
--- データ: 平均 2 周辺 + 大きな外れ値 1 つ
-
-robData :: [Double]
-robData = [1.9, 2.0, 2.1, 1.8, 2.2, 2.0, 1.7, 2.3, 1.9, 2.1, 15.0]
---                                                            ^外れ値
-
--- 95% 通常分布 + 5% 広い分布 (外れ値モデル) の混合
-robustModel :: ModelP ()
-robustModel = do
-  mu  <- sample "mu"    (Normal 0 10)
-  sig <- sample "sigma" (HalfNormal 2)
-  -- 95% Normal(mu, sig), 5% Normal(mu, 10*sig) の混合
-  observe "y" (Mixture [0.95, 0.05]
-                       [Normal mu sig, Normal mu (sig * 10)])
-              robData
-
--- 比較用: 普通の Normal
-plainModel :: ModelP ()
-plainModel = do
-  mu  <- sample "mu"    (Normal 0 10)
-  sig <- sample "sigma" (HalfNormal 2)
-  observe "y" (Normal mu sig) robData
-
--- ---------------------------------------------------------------------------
--- main
--- ---------------------------------------------------------------------------
-
-prn :: String -> Double -> Double -> IO ()
-prn lbl m s = printf "    %-8s mean=%+.4f  sd=%.4f\n" lbl m s
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Mixture 分布のデモ"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  gen <- createSystemRandom
-
-  -- ── 例 1: 2 成分ガウス混合 ──
-  putStrLn "[1] 2 成分ガウス混合 (二峰性データ)"
-  printf "    観測: 20 件 (片半分は ~0、片半分は ~5)\n"
-  ch1 <- nuts gmmModel cfg
-              (Map.fromList [("mu1", -1.0), ("mu2", 6.0), ("sigma", 1.0)]) gen
-  printf "    Acceptance: %.1f%%\n" (acceptanceRate ch1 * 100 :: Double)
-  prn "mu1"   (fromMaybe 0 (posteriorMean "mu1" ch1)) (fromMaybe 0 (posteriorSD "mu1" ch1))
-  prn "mu2"   (fromMaybe 0 (posteriorMean "mu2" ch1)) (fromMaybe 0 (posteriorSD "mu2" ch1))
-  prn "sigma" (fromMaybe 0 (posteriorMean "sigma" ch1)) (fromMaybe 0 (posteriorSD "sigma" ch1))
-  putStrLn "    → 真値 (mu1, mu2) = (0, 5) を回復"
-  putStrLn ""
-
-  -- ── 例 2: ゼロ過剰 ──
-  putStrLn "[2] ゼロ過剰モデル"
-  printf "    観測: 15 件 (8 件がゼロ近傍、7 件が ~2)\n"
-  ch2 <- nuts ziModel cfg
-              (Map.fromList [("q", 0.5), ("mu", 1.0), ("sigma", 1.0)]) gen
-  printf "    Acceptance: %.1f%%\n" (acceptanceRate ch2 * 100 :: Double)
-  prn "q"     (fromMaybe 0 (posteriorMean "q" ch2)) (fromMaybe 0 (posteriorSD "q" ch2))
-  prn "mu"    (fromMaybe 0 (posteriorMean "mu" ch2)) (fromMaybe 0 (posteriorSD "mu" ch2))
-  prn "sigma" (fromMaybe 0 (posteriorMean "sigma" ch2)) (fromMaybe 0 (posteriorSD "sigma" ch2))
-  printf "    → q ≈ %.2f (理論値 8/15 = 0.53)\n"
-         (fromMaybe 0 (posteriorMean "q" ch2))
-  putStrLn ""
-
-  -- ── 例 3: 頑健回帰 ──
-  putStrLn "[3] 頑健 Normal 混合 vs 普通の Normal (外れ値 15.0 を含む)"
-  ch3 <- nuts robustModel cfg
-              (Map.fromList [("mu", 0.0), ("sigma", 1.0)]) gen
-  ch4 <- nuts plainModel cfg
-              (Map.fromList [("mu", 0.0), ("sigma", 1.0)]) gen
-  putStrLn "  混合 (95% N(μ,σ) + 5% N(μ,10σ)):"
-  prn "mu"    (fromMaybe 0 (posteriorMean "mu" ch3)) (fromMaybe 0 (posteriorSD "mu" ch3))
-  prn "sigma" (fromMaybe 0 (posteriorMean "sigma" ch3)) (fromMaybe 0 (posteriorSD "sigma" ch3))
-  putStrLn "  比較: 普通の Normal:"
-  prn "mu"    (fromMaybe 0 (posteriorMean "mu" ch4)) (fromMaybe 0 (posteriorSD "mu" ch4))
-  prn "sigma" (fromMaybe 0 (posteriorMean "sigma" ch4)) (fromMaybe 0 (posteriorSD "sigma" ch4))
-  printf "    → 真値 μ ≈ 2.0   混合: %.2f  普通: %.2f\n"
-         (fromMaybe 0 (posteriorMean "mu" ch3))
-         (fromMaybe 0 (posteriorMean "mu" ch4))
-  putStrLn ""
-
-  -- ── 事後予測でデモを締めくくる ──
-  putStrLn "[4] 例 1 (GMM) の事後予測サンプリング"
-  postPreds <- posteriorPredictive gmmModel ch1 gen
-  let allYs = concatMap (Map.findWithDefault [] "y") postPreds
-      bin xs = (length (filter (< 2.5) xs), length (filter (>= 2.5) xs))
-      (lo, hi) = bin allYs
-      total = length allYs
-  printf "    生成された予測 %d 件: y < 2.5 が %d (%.1f%%), y >= 2.5 が %d (%.1f%%)\n"
-         total lo (100 * fromIntegral lo / fromIntegral total :: Double)
-         hi (100 * fromIntegral hi / fromIntegral total :: Double)
-  printf "    観測: y < 2.5 が 10 (50%%), y >= 2.5 が 10 (50%%) — 整合\n"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Mixture 分布が正常動作 (混合・ゼロ過剰・頑健)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/MultinomialDemo.hs b/demo/bayesian/MultinomialDemo.hs
deleted file mode 100644
--- a/demo/bayesian/MultinomialDemo.hs
+++ /dev/null
@@ -1,79 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | Multinomial 観測 + Dirichlet 事前のデモ (Phase H2)。
---
--- 1 試行で N 件の対象がカテゴリ K=3 に振り分けられる
--- (例: 投票結果、サイコロ N 回中の出目分布)。
--- そのような実験を T 回繰り返した結果から確率ベクトル π を推定。
--- 共役事後は Dirichlet(α + Σ_t y_t)。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, dirichlet, observeMV, Distribution (..),
-                  augmentChainWithDeterministic, multinomialLogDensity)
-import Hanalyze.Viz.MCMC (printPosteriorSummary, posteriorSummaryFile)
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 1000
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.1
-        , nutsMaxDepth   = 6
-        }
-
--- 試行ごとの観測 (T=5 試行、N=20 件、真の π = (0.5, 0.3, 0.2))
-trials :: [[Double]]
-trials =
-  [ [10, 6, 4]
-  , [11, 5, 4]
-  , [9, 7, 4]
-  , [10, 6, 4]
-  , [12, 5, 3]
-  ]
-
-multinomModel :: ModelP ()
-multinomModel = do
-  pis <- dirichlet "pi" [1, 1, 1]   -- 一様事前
-  observeMV "y" (Multinomial 20 pis) trials
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Multinomial 観測 + Dirichlet 事前 (Phase H2)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  putStrLn "観測 5 試行 (各 N=20):"
-  mapM_ print trials
-  let totals = foldr1 (zipWith (+)) trials
-  printf "合計: %s  (合計 %.0f)\n" (show totals) (sum totals)
-  putStrLn "真値 π = (0.5, 0.3, 0.2)"
-  putStrLn "共役事後: Dir(1+52, 1+29, 1+19) → 平均 (0.520, 0.288, 0.192)"
-  putStrLn ""
-
-  -- 単体テスト
-  let lp = multinomialLogDensity 20 [0.5, 0.3, 0.2] [10, 6, 4] :: Double
-  printf "単体テスト: log P([10,6,4] | n=20, π=(.5,.3,.2)) = %.4f\n" lp
-  putStrLn ""
-
-  gen <- createSystemRandom
-  rawCh <- nuts multinomModel cfg
-                (Map.fromList [("pi_b0", 0.5), ("pi_b1", 0.5)]) gen
-  let ch = augmentChainWithDeterministic multinomModel rawCh
-
-  putStrLn "[1] Posterior summary"
-  printPosteriorSummary ["pi_0", "pi_1", "pi_2"] [ch]
-  putStrLn ""
-
-  posteriorSummaryFile "multinom-summary.html" "Multinomial posterior"
-    ["pi_0", "pi_1", "pi_2"] [ch]
-  putStrLn "  → multinom-summary.html"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Multinomial 観測で π を推定、π_0+π_1+π_2 = 1 が成立"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/MvNormalDemo.hs b/demo/bayesian/MvNormalDemo.hs
deleted file mode 100644
--- a/demo/bayesian/MvNormalDemo.hs
+++ /dev/null
@@ -1,115 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | MvNormal (多変量正規) 観測のデモ。
---
--- PyMC の @pm.MvNormal("y", mu=mu, cov=cov, observed=Y)@ 相当。
---
--- 例 1: 既知の共分散で平均ベクトルを推定
---   y_i ~ MvNormal(μ, Σ),  Σ = [[1, 0.7], [0.7, 1]] (固定)
---   μ ~ Normal(0, 5)        (各成分独立)
---
--- 例 2: 静的検証 (Cholesky / log density 単体テスト)
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Data.Maybe (fromMaybe)
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom, GenIO)
-import qualified System.Random.MWC.Distributions as MWC
-
-import Hanalyze.MCMC.Core (posteriorMean, posteriorSD)
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observeMV, Distribution (..),
-                  mvNormalLogDensity)
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 1500
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.1
-        }
-
--- ---------------------------------------------------------------------------
--- 単体テスト: 既知ケースの log density を比較
--- ---------------------------------------------------------------------------
-
--- | 標準 2 変量正規 N([0,0], I) で y=[0,0]:
---   log p = -k/2 log(2π) = -log(2π) ≈ -1.8379
-test1 :: Double
-test1 = mvNormalLogDensity [0, 0] [[1, 0], [0, 1]] [0, 0]
-
--- | N([0,0], I), y=[1,0]: log p = -log(2π) - 0.5 ≈ -2.3379
-test2 :: Double
-test2 = mvNormalLogDensity [0, 0] [[1, 0], [0, 1]] [1, 0]
-
--- | 相関ありケース Σ=[[1,0.7],[0.7,1]], y=[0,0]:
---   |Σ| = 1 - 0.49 = 0.51, log|Σ| = log 0.51 ≈ -0.6733
---   log p = -log(2π) - 0.5*log(0.51) ≈ -1.8379 + 0.3367 ≈ -1.5013
-test3 :: Double
-test3 = mvNormalLogDensity [0, 0] [[1, 0.7], [0.7, 1]] [0, 0]
-
--- ---------------------------------------------------------------------------
--- 平均推定モデル
--- ---------------------------------------------------------------------------
-
-cov2 :: [[Double]]
-cov2 = [[1.0, 0.7], [0.7, 1.0]]
-
--- | 真の μ = [2, -1] からデータ生成。
-genData :: GenIO -> Int -> IO [[Double]]
-genData gen n = do
-  -- L = [[1,0], [0.7, sqrt(1-0.49)]] = [[1,0],[0.7, 0.7141]]
-  let l00 = 1.0
-      l10 = 0.7
-      l11 = sqrt (1 - 0.49)
-      muTrue = [2.0, -1.0]
-  let drawOne = do
-        z0 <- MWC.standard gen
-        z1 <- MWC.standard gen
-        let y0 = head muTrue + l00 * z0
-            y1 = (muTrue !! 1) + l10 * z0 + l11 * z1
-        return [y0, y1]
-  mapM (const drawOne) [1 .. n]
-
-mvNormalModel :: [[Double]] -> ModelP ()
-mvNormalModel ys = do
-  m1 <- sample "mu1" (Normal 0 5)
-  m2 <- sample "mu2" (Normal 0 5)
-  observeMV "y" (MvNormal [m1, m2] [[1.0, 0.7], [0.7, 1.0]]) ys
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  MvNormal (多変量正規) デモ"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  -- ── 単体テスト ──
-  putStrLn "[A] 単体テスト: log density 既知ケース"
-  let exp1 = -log (2 * pi) :: Double
-      exp2 = -log (2 * pi) - 0.5 :: Double
-      exp3 = -log (2 * pi) - 0.5 * log 0.51 :: Double
-  printf "  N([0,0], I) で y=[0,0]   : %+.4f  (期待 %+.4f = -log(2pi))\n" test1 exp1
-  printf "  N([0,0], I) で y=[1,0]   : %+.4f  (期待 %+.4f)\n"            test2 exp2
-  printf "  N([0,0], cov_corr) y=0   : %+.4f  (期待 %+.4f)\n"            test3 exp3
-  putStrLn ""
-
-  -- ── NUTS で平均ベクトル推定 ──
-  putStrLn "[B] NUTS で μ を推定 (Σ 既知)"
-  putStrLn "    真値 μ = [2.0, -1.0],   Σ = [[1, 0.7], [0.7, 1]]"
-  gen <- createSystemRandom
-  ys <- genData gen 100
-  printf "    観測: %d 件 (k=2)\n" (length ys)
-  ch <- nuts (mvNormalModel ys) cfg
-              (Map.fromList [("mu1", 0), ("mu2", 0)]) gen
-  let m1m = fromMaybe 0 (posteriorMean "mu1" ch)
-      m2m = fromMaybe 0 (posteriorMean "mu2" ch)
-      s1m = fromMaybe 0 (posteriorSD   "mu1" ch)
-      s2m = fromMaybe 0 (posteriorSD   "mu2" ch)
-  printf "  事後 μ1 = %+.3f  ± %.3f  (真値 +2.000)\n" m1m s1m
-  printf "  事後 μ2 = %+.3f  ± %.3f  (真値 -1.000)\n" m2m s2m
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ MvNormal が観測分布として動作 (Cholesky 経由 log density)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/MvNormalLatentDemo.hs b/demo/bayesian/MvNormalLatentDemo.hs
deleted file mode 100644
--- a/demo/bayesian/MvNormalLatentDemo.hs
+++ /dev/null
@@ -1,79 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | MvNormal を latent (事前) として使うデモ。
---
--- 階層モデル:
---   μ_vec ~ MvNormal([0, 0], [[1, 0.8], [0.8, 1]])  -- 2D latent
---   y1 ~ Normal(μ_0, 0.5)
---   y2 ~ Normal(μ_1, 0.5)
---
--- データはわざと相関を持たせて生成し、posterior 上の μ_0 と μ_1 にも
--- 相関が現れるかを pair plot で確認。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, observe, mvNormalLatent,
-                  Distribution (..), augmentChainWithDeterministic)
-import Hanalyze.Viz.MCMC (printPosteriorSummary, posteriorSummaryFile,
-                 pairScatterFile, tracePlotHDIFile)
-import Hanalyze.Viz.Core (defaultConfig, OutputFormat (..), PlotConfig (..))
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 2000
-        , nutsBurnIn     = 1000
-        , nutsStepSize   = 0.1
-        }
-
--- 真の μ ≈ (1.0, -0.5) 付近に集中させる観測
-y1Obs, y2Obs :: [Double]
-y1Obs = [1.1, 0.9, 1.2, 1.0, 0.8, 1.05, 0.95, 1.15, 1.0, 1.08]
-y2Obs = [-0.4, -0.6, -0.5, -0.45, -0.55, -0.5, -0.42, -0.58, -0.48, -0.52]
-
-mvLatentModel :: ModelP ()
-mvLatentModel = do
-  -- 2D latent vector: 強い相関 0.8 を入れた事前
-  mu <- mvNormalLatent "mu" [0, 0] [[1, 0.8], [0.8, 1]]
-  observe "y1" (Normal (mu !! 0) 0.5) y1Obs
-  observe "y2" (Normal (mu !! 1) 0.5) y2Obs
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  MvNormal を latent vector として使う (G6)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  putStrLn "事前: μ ~ MvNormal([0,0], [[1, 0.8], [0.8, 1]])"
-  putStrLn "観測: y1 ≈ 1.0, y2 ≈ -0.5 (n=10 each)"
-  putStrLn ""
-
-  gen <- createSystemRandom
-  rawCh <- nuts mvLatentModel cfg
-                (Map.fromList [("mu_z0", 0), ("mu_z1", 0)]) gen
-  let ch = augmentChainWithDeterministic mvLatentModel rawCh
-
-  putStrLn "[1] Posterior summary"
-  let names = ["mu_z0", "mu_z1", "mu_0", "mu_1"]
-  printPosteriorSummary names [ch]
-  putStrLn ""
-
-  -- HTML 出力
-  posteriorSummaryFile "mvlatent-summary.html"
-    "MvNormal latent — posterior" names [ch]
-  let pcfg = (defaultConfig "mu_0 vs mu_1 (posterior)")
-               { plotWidth = 500, plotHeight = 400 }
-  pairScatterFile HTML "mvlatent-pair.html" pcfg "mu_0" "mu_1" ch
-  let tcfg = (defaultConfig "MvNormal latent — trace")
-               { plotWidth = 700, plotHeight = 90 }
-  tracePlotHDIFile HTML "mvlatent-trace.html" tcfg 0.94 names ch
-  putStrLn "  → mvlatent-summary.html / pair.html / trace.html"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ MvNormal latent vector が NUTS で推論できる"
-  putStrLn "    raw N(0,1) latent (mu_z*) + Cholesky で派生量 (mu_*) を生成"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/NegBinomDemo.hs b/demo/bayesian/NegBinomDemo.hs
deleted file mode 100644
--- a/demo/bayesian/NegBinomDemo.hs
+++ /dev/null
@@ -1,93 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | NegativeBinomial(μ, α) — 過分散カウントデータのデモ。
---
--- 比較: 同じデータに対して Poisson と NegativeBinomial を fit。
--- データは μ=10, α=2 の NB から生成 (var = 10 + 100/2 = 60、Poisson の
--- var = 10 より遥かに大きい)。Poisson モデルでは過分散を捕えられない。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-import qualified System.Random.MWC.Distributions as MWC
-import qualified System.Random.MWC as MWCBase
-
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, Distribution (..))
-import Hanalyze.Viz.MCMC (printPosteriorSummary, posteriorSummaryFile)
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 800
-        , nutsBurnIn     = 300
-        , nutsStepSize   = 0.1
-        , nutsMaxDepth   = 6
-        }
-
--- 真のパラメタで NB データを生成
-genNB :: Int -> Double -> Double -> IO [Double]
-genNB n mu alpha = do
-  gen <- createSystemRandom
-  -- Gamma-Poisson mixture: λ ~ Gamma(α, μ/α), X ~ Poisson(λ)
-  let drawOne = do
-        lam <- MWC.gamma alpha (mu / alpha) gen
-        let knuth k p = do
-              u <- MWCBase.uniform gen :: IO Double
-              let p' = p * u
-              if p' < exp (-lam)
-                then return (fromIntegral k)
-                else knuth (k + 1) p'
-        knuth (0 :: Int) (1 :: Double)
-  mapM (const drawOne) [1 .. n]
-
-poissonModel :: [Double] -> ModelP ()
-poissonModel ys = do
-  lam <- sample "lambda" (Gamma 1 0.1)
-  observe "y" (Poisson lam) ys
-
-nbModel :: [Double] -> ModelP ()
-nbModel ys = do
-  mu    <- sample "mu"    (Gamma 1 0.1)
-  alpha <- sample "alpha" (Gamma 1 0.1)
-  observe "y" (NegativeBinomial mu alpha) ys
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  NegativeBinomial vs Poisson (過分散カウント, Phase H1)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  putStrLn "真値: μ = 10, α = 2  (= var = 60, mean = 10 → 過分散)"
-  ys <- genNB 80 10 2
-  let n     = length ys
-      muSm  = sum ys / fromIntegral n
-      varSm = sum [(y - muSm)^(2::Int) | y <- ys] / fromIntegral (n - 1)
-  printf "観測 (n=%d): 標本平均 = %.2f, 標本分散 = %.2f\n" n muSm varSm
-  printf "  → 分散/平均 = %.2f (≫ 1 なら Poisson は不適合)\n"
-         (varSm / muSm)
-  putStrLn ""
-
-  gen <- createSystemRandom
-
-  putStrLn "[1] Poisson モデル (過分散を捕えない)"
-  ch1 <- nuts (poissonModel ys) cfg
-              (Map.fromList [("lambda", 5)]) gen
-  printPosteriorSummary ["lambda"] [ch1]
-  putStrLn ""
-
-  putStrLn "[2] NegativeBinomial モデル (過分散を捕える)"
-  ch2 <- nuts (nbModel ys) cfg
-              (Map.fromList [("mu", 5), ("alpha", 1)]) gen
-  printPosteriorSummary ["mu", "alpha"] [ch2]
-  putStrLn ""
-
-  posteriorSummaryFile "negbinom-poisson.html"  "Poisson"  ["lambda"]      [ch1]
-  posteriorSummaryFile "negbinom-nb.html"       "NegBinom" ["mu", "alpha"] [ch2]
-  putStrLn "  → negbinom-{poisson,nb}.html"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ NegativeBinomial で μ ≈ 10, α ≈ 2 を回復、過分散を表現"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/NewDistribDemo.hs b/demo/bayesian/NewDistribDemo.hs
deleted file mode 100644
--- a/demo/bayesian/NewDistribDemo.hs
+++ /dev/null
@@ -1,151 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | Phase 2.1 で追加した連続分布の動作確認デモ。
---
--- 各分布を事前分布として使ったモデルを NUTS で推論する。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Maybe (fromMaybe)
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.MCMC.Core (Chain, chainSamples, posteriorMean, posteriorSD,
-                  posteriorQuantile, acceptanceRate)
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, Distribution (..))
-
-obsData :: [Double]
-obsData = [1.5, 2.1, 1.8, 2.5, 1.9, 2.3, 1.7, 2.0, 2.2, 1.6]
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 1500
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.1
-        }
-
--- ---------------------------------------------------------------------------
--- 各モデル定義 (top-level: rank-2 type の monomorphisation 回避)
--- ---------------------------------------------------------------------------
-
-halfNormalModel :: ModelP ()
-halfNormalModel = do
-  mu    <- sample "mu"    (Normal 0 10)
-  sigma <- sample "sigma" (HalfNormal 5)
-  observe "y" (Normal mu sigma) obsData
-
-halfCauchyModel :: ModelP ()
-halfCauchyModel = do
-  mu    <- sample "mu"    (Normal 0 10)
-  sigma <- sample "sigma" (HalfCauchy 2)
-  observe "y" (Normal mu sigma) obsData
-
-studentTObs :: [Double]
-studentTObs = obsData ++ [10.0]  -- 外れ値追加
-
-studentTModel :: ModelP ()
-studentTModel = do
-  mu    <- sample "mu"    (Normal 0 10)
-  sigma <- sample "sigma" (HalfNormal 5)
-  observe "y" (StudentT 3 mu sigma) studentTObs   -- df=3
-
-normalRobustModel :: ModelP ()
-normalRobustModel = do
-  mu    <- sample "mu"    (Normal 0 10)
-  sigma <- sample "sigma" (HalfNormal 5)
-  observe "y" (Normal mu sigma) studentTObs
-
-logNormalObs :: [Double]
-logNormalObs = [exp (1.5 + n) | n <-
-                  [0.20, -0.10, 0.30, -0.05, 0.15, -0.20, 0.05, 0.10, -0.15, 0.0]]
--- 真値: log y ~ Normal(1.5, ~0.16)
-
-logNormalModel :: ModelP ()
-logNormalModel = do
-  mu  <- sample "mu_log"  (Normal 0 10)
-  sig <- sample "sig_log" (HalfNormal 2)
-  observe "y" (LogNormal mu sig) logNormalObs
-
-cauchyPriorModel :: ModelP ()
-cauchyPriorModel = do
-  mu  <- sample "mu" (Cauchy 0 1)
-  sig <- sample "sigma" (HalfNormal 5)
-  observe "y" (Normal mu sig) obsData
-
-uniformPriorModel :: ModelP ()
-uniformPriorModel = do
-  mu  <- sample "mu" (Uniform (-5) 5)
-  sig <- sample "sigma" (HalfNormal 3)
-  observe "y" (Normal mu sig) obsData
-
--- ---------------------------------------------------------------------------
--- 共通ランナー
--- ---------------------------------------------------------------------------
-
-runOne
-  :: String           -- ラベル
-  -> ModelP ()        -- モデル
-  -> Map.Map Text Double  -- 初期値
-  -> [Text]           -- 表示するパラメータ名
-  -> IO ()
-runOne label m initP params = do
-  putStrLn $ "─── " ++ label ++ " ───"
-  gen <- createSystemRandom
-  chain <- nuts m cfg initP gen
-  printf "  Acceptance: %.1f%%, samples: %d\n"
-         (acceptanceRate chain * 100 :: Double)
-         (length (chainSamples chain))
-  mapM_ (printParam chain) params
-
-printParam :: Chain -> Text -> IO ()
-printParam chain p =
-  printf "  %-10s mean=%+.4f  sd=%.4f  95%% CI=[%+.4f, %+.4f]\n"
-         (T.unpack p)
-         (fromMaybe 0 (posteriorMean p chain))
-         (fromMaybe 0 (posteriorSD   p chain))
-         (fromMaybe 0 (posteriorQuantile 0.025 p chain))
-         (fromMaybe 0 (posteriorQuantile 0.975 p chain))
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Phase 2.1: 追加分布の動作確認"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  let init1 = Map.fromList [("mu", 0.0), ("sigma", 1.0)]
-      init4 = Map.fromList [("mu_log", 0.0), ("sig_log", 1.0)]
-
-  putStrLn "[1] HalfNormal 分散事前"
-  runOne "HalfNormal" halfNormalModel init1 ["mu", "sigma"]
-  putStrLn ""
-
-  putStrLn "[2] HalfCauchy 分散事前 (重い裾)"
-  runOne "HalfCauchy" halfCauchyModel init1 ["mu", "sigma"]
-  putStrLn ""
-
-  putStrLn "[3] StudentT 観測 (df=3) — 外れ値ロバスト"
-  putStrLn "    データに 10.0 の外れ値が混入"
-  runOne "StudentT_obs" studentTModel init1 ["mu", "sigma"]
-  putStrLn "    比較: Normal 観測 (外れ値の影響を受けやすい)"
-  runOne "Normal_obs " normalRobustModel init1 ["mu", "sigma"]
-  putStrLn ""
-
-  putStrLn "[4] LogNormal 観測 (真値 mu_log=1.5)"
-  runOne "LogNormal" logNormalModel init4 ["mu_log", "sig_log"]
-  putStrLn ""
-
-  putStrLn "[5] Cauchy 事前"
-  runOne "CauchyPrior" cauchyPriorModel init1 ["mu", "sigma"]
-  putStrLn ""
-
-  putStrLn "[6] Uniform 事前 (mu ∈ [-5, 5])"
-  runOne "UniformPrior" uniformPriorModel init1 ["mu", "sigma"]
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ 全分布が正常にサンプリング可能"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/NewDistribsDemo.hs b/demo/bayesian/NewDistribsDemo.hs
deleted file mode 100644
--- a/demo/bayesian/NewDistribsDemo.hs
+++ /dev/null
@@ -1,116 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | Phase I: 5 つの新規分布をまとめて検証 (sample/observe)。
---
--- - InverseGamma:   分散の共役事前 (Normal-InvGamma)
--- - Weibull:        生存解析の典型 (k=2 でレイリー)
--- - Pareto:         重い裾の冪分布
--- - BetaBinomial:   過分散二項
--- - VonMises:       角度データ (-π, π]
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom, GenIO)
-
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, Distribution (..),
-                  sampleDist)
-import Hanalyze.Viz.MCMC (printPosteriorSummary)
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 800
-        , nutsBurnIn     = 400
-        , nutsStepSize   = 0.1
-        , nutsMaxDepth   = 6
-        }
-
--- ---------------------------------------------------------------------------
--- 単体テスト: sampleDist で分布から N 個ドローして経験統計を確認
--- ---------------------------------------------------------------------------
-
-drawN :: Int -> Distribution Double -> GenIO -> IO [Double]
-drawN n d gen = mapM (const (sampleDist d gen)) [1..n]
-
-stats :: [Double] -> (Double, Double)
-stats xs =
-  let n  = length xs
-      mu = sum xs / fromIntegral n
-      v  = sum [(x - mu)^(2::Int) | x <- xs] / fromIntegral (n - 1)
-  in (mu, sqrt v)
-
--- ---------------------------------------------------------------------------
--- Bayesian: InverseGamma を分散事前として使う Normal モデル
--- ---------------------------------------------------------------------------
-
--- σ² ~ InverseGamma(2, 3) (mean = 3/(2-1) = 3)
--- y ~ Normal(μ, sqrt(σ²))
-invGammaModel :: [Double] -> ModelP ()
-invGammaModel ys = do
-  mu  <- sample "mu"     (Normal 0 5)
-  sig2 <- sample "sigma2" (InverseGamma 2 3)
-  observe "y" (Normal mu (sqrt sig2)) ys
-
--- ---------------------------------------------------------------------------
--- Bayesian: Weibull で生存時間の k と λ を推定
--- ---------------------------------------------------------------------------
-weibullModel :: [Double] -> ModelP ()
-weibullModel ys = do
-  kSh <- sample "k"      (HalfNormal 5)
-  lam <- sample "lambda" (HalfNormal 5)
-  observe "y" (Weibull kSh lam) ys
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Phase I: 新規 5 分布 (InvGamma/Weibull/Pareto/BetaBin/VonMises)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  gen <- createSystemRandom
-
-  -- ── 単体: 各分布から 10000 ドロー → 平均/sd 確認 ──
-  putStrLn "[A] sampleDist 単体 (n=10000)"
-
-  ig <- drawN 10000 (InverseGamma 3 2) gen     -- mean = 2/(3-1) = 1
-  let (m, s) = stats ig
-  printf "  InverseGamma(3, 2): mean=%.3f (期待 1.000), sd=%.3f\n" m s
-
-  wb <- drawN 10000 (Weibull 2 1) gen           -- レイリー: mean = √(π/2)/√2 = 0.886
-  let (m2, s2) = stats wb
-  printf "  Weibull(2, 1):      mean=%.3f (期待 0.886), sd=%.3f\n" m2 s2
-
-  pr <- drawN 10000 (Pareto 3 1) gen            -- mean = 3/(3-1) = 1.5
-  let (m3, s3) = stats pr
-  printf "  Pareto(3, 1):       mean=%.3f (期待 1.500), sd=%.3f\n" m3 s3
-
-  bb <- drawN 10000 (BetaBinomial 20 2 8) gen   -- mean = 20*2/10 = 4
-  let (m4, s4) = stats bb
-  printf "  BetaBin(n=20, 2, 8): mean=%.3f (期待 4.000), sd=%.3f\n" m4 s4
-
-  vm <- drawN 10000 (VonMises 0 4) gen          -- mean = 0、概ね正規 sd ≈ 1/√4 = 0.5
-  let (m5, s5) = stats vm
-  printf "  VonMises(0, κ=4):   mean=%.3f (期待 0.000), sd=%.3f (≈0.5)\n" m5 s5
-  putStrLn ""
-
-  -- ── Bayesian: InverseGamma 事前 ──
-  putStrLn "[B] Normal-InverseGamma 事前で σ² を推定"
-  let ys = [1.2, 0.9, 1.4, 0.7, 1.1, 1.0, 1.3, 0.95, 1.05, 1.15,
-            0.85, 1.25, 0.95, 1.18, 1.02]
-  ch1 <- nuts (invGammaModel ys) cfg
-              (Map.fromList [("mu", 1), ("sigma2", 0.04)]) gen
-  printPosteriorSummary ["mu", "sigma2"] [ch1]
-  putStrLn ""
-
-  -- ── Bayesian: Weibull の k, λ ──
-  putStrLn "[C] Weibull モデルで k, λ を推定 (真値 k=2, λ=2)"
-  weibullObs <- drawN 80 (Weibull 2 2) gen
-  ch2 <- nuts (weibullModel weibullObs) cfg
-              (Map.fromList [("k", 2), ("lambda", 2)]) gen
-  printPosteriorSummary ["k", "lambda"] [ch2]
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ 5 つの新規分布が sample/observe 両方で動作"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/NonCenteredDemo.hs b/demo/bayesian/NonCenteredDemo.hs
deleted file mode 100644
--- a/demo/bayesian/NonCenteredDemo.hs
+++ /dev/null
@@ -1,118 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | 非中心化パラメタ化 (non-centered) のデモ。
---
--- Neal's funnel:
---   v ~ Normal(0, 3)
---   x | v ~ Normal(0, exp(v/2))
---
--- Centered: x を直接 sample → v が大きいと x のスケールが爆発、
---           小さいと潰れて HMC の事後分布が病的に。
--- Non-centered: x_raw ~ Normal(0, 1) と v は独立にサンプル、
---               x = exp(v/2) * x_raw を派生量として出す。
---
--- BFMI 値の改善で診断する (Phase E の energyPlot を流用)。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Data.Maybe (fromMaybe)
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.MCMC.Core (chainEnergy, chainDivergences)
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, Distribution (..),
-                  nonCenteredNormal, augmentChainWithDeterministic)
-import Hanalyze.Stat.MCMC (bfmi)
-import Hanalyze.Viz.Core  (defaultConfig, OutputFormat (..), PlotConfig (..))
-import Hanalyze.Viz.MCMC  (energyPlotFile, posteriorSummaryFile,
-                  printPosteriorSummary, pairScatterDivFile)
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 2000
-        , nutsBurnIn     = 1000
-        , nutsStepSize   = 0.1
-        }
-
--- ---------------------------------------------------------------------------
--- Centered: x ~ Normal(0, exp(v/2))
--- ---------------------------------------------------------------------------
-centeredFunnel :: ModelP ()
-centeredFunnel = do
-  v <- sample "v" (Normal 0 3)
-  _ <- sample "x" (Normal 0 (exp (v / 2)))
-  return ()
-
--- ---------------------------------------------------------------------------
--- Non-centered: x_raw ~ Normal(0,1) → x = exp(v/2) * x_raw
--- ---------------------------------------------------------------------------
-nonCenteredFunnel :: ModelP ()
-nonCenteredFunnel = do
-  v <- sample "v" (Normal 0 3)
-  _ <- nonCenteredNormal "x" 0 (exp (v / 2))
-  return ()
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  非中心化パラメタ化 vs centered (Neal's funnel)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  gen <- createSystemRandom
-
-  -- ── Centered ──
-  putStrLn "[1] Centered: x ~ Normal(0, exp(v/2))"
-  ch1 <- nuts centeredFunnel cfg
-              (Map.fromList [("v", 0), ("x", 0)]) gen
-  let bfmi1 = fromMaybe (0/0) (bfmi (chainEnergy ch1))
-  printf "  BFMI = %.3f\n" bfmi1
-  printPosteriorSummary ["v", "x"] [ch1]
-  putStrLn ""
-
-  -- ── Non-centered ──
-  putStrLn "[2] Non-centered: x_raw ~ Normal(0,1), x = exp(v/2) * x_raw"
-  ch2raw <- nuts nonCenteredFunnel cfg
-                 (Map.fromList [("v", 0), ("x_raw", 0)]) gen
-  let ch2   = augmentChainWithDeterministic nonCenteredFunnel ch2raw
-      bfmi2 = fromMaybe (0/0) (bfmi (chainEnergy ch2raw))
-  printf "  BFMI = %.3f\n" bfmi2
-  printPosteriorSummary ["v", "x_raw", "x"] [ch2]
-  putStrLn ""
-
-  -- ── 可視化: Energy plot 比較 ──
-  let ecfg t = (defaultConfig t)
-                 { plotWidth = 600, plotHeight = 250 }
-  energyPlotFile HTML "funnel-centered-energy.html"
-    (ecfg "Centered funnel") ch1
-  energyPlotFile HTML "funnel-noncenter-energy.html"
-    (ecfg "Non-centered funnel") ch2raw
-  putStrLn "  → funnel-centered-energy.html / funnel-noncenter-energy.html"
-
-  posteriorSummaryFile "funnel-centered.html" "Centered funnel"
-    ["v", "x"] [ch1]
-  posteriorSummaryFile "funnel-noncenter.html" "Non-centered funnel"
-    ["v", "x_raw", "x"] [ch2]
-  putStrLn "  → funnel-centered.html / funnel-noncenter.html"
-
-  -- ── Divergence overlay ──
-  let divs1 = chainDivergences ch1
-      divs2 = chainDivergences ch2raw
-  printf "  Centered     divergences: %d 件\n" (length divs1)
-  printf "  Non-centered divergences: %d 件\n" (length divs2)
-  let divCfg t = (defaultConfig t)
-                   { plotWidth = 500, plotHeight = 400 }
-  pairScatterDivFile HTML "funnel-centered-pair.html"
-    (divCfg "Centered funnel — pair (divergences in red)")
-    "v" "x" ch1 divs1
-  pairScatterDivFile HTML "funnel-noncenter-pair.html"
-    (divCfg "Non-centered — pair (v vs x_raw, divergences in red)")
-    "v" "x_raw" ch2raw divs2
-  putStrLn "  → funnel-centered-pair.html / funnel-noncenter-pair.html"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Non-centered では x_raw が posterior に保存され、"
-  putStrLn "    x は派生量として記録される。BFMI で改善度を比較。"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/PPCDemo.hs b/demo/bayesian/PPCDemo.hs
deleted file mode 100644
--- a/demo/bayesian/PPCDemo.hs
+++ /dev/null
@@ -1,127 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | Phase 2.3: 事前予測 / 事後予測サンプリングのデモ。
---
--- - prior predictive: データを見る前に「モデルが何を予測するか」確認
--- - posterior predictive: フィット後に「観測されたデータと整合するか」確認
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Data.List (sort)
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.MCMC.Core (chainSamples)
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, Distribution (..))
-import Hanalyze.Stat.PosteriorPredictive
-  (priorPredictive, posteriorPredictive, posteriorPredictiveSummary)
-
-obsData :: [Double]
-obsData = [1.5, 2.1, 1.8, 2.5, 1.9, 2.3, 1.7, 2.0, 2.2, 1.6]
-
--- 真値: μ ≈ 1.96, σ ≈ 0.30
-linearModel :: ModelP ()
-linearModel = do
-  mu    <- sample "mu"    (Normal 0 10)
-  sigma <- sample "sigma" (HalfNormal 5)
-  observe "y" (Normal mu sigma) obsData
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 2000
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.1
-        }
-
--- ---------------------------------------------------------------------------
--- ヘルパー: 統計量
--- ---------------------------------------------------------------------------
-
-stats :: [Double] -> (Double, Double, Double, Double)
-stats xs =
-  let s   = sort xs
-      n   = length s
-      mu  = sum xs / fromIntegral n
-      q p = s !! min (n-1) (max 0 (floor (p * fromIntegral n) :: Int))
-  in (mu, q 0.025, q 0.975, sqrt (sum [(x-mu)^(2::Int) | x <- xs] / fromIntegral n))
-
--- ---------------------------------------------------------------------------
--- main
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Phase 2.3: 事前予測 / 事後予測サンプリング"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  printf "  モデル: μ ~ N(0,10), σ ~ HalfN(5), y ~ N(μ,σ)\n"
-  printf "  観測: %d 件 (mean=%.2f, sd=%.2f)\n\n"
-         (length obsData) (sum obsData / fromIntegral (length obsData))
-         (sqrt (sum [(x - sum obsData / fromIntegral (length obsData))^(2::Int) | x <- obsData] / fromIntegral (length obsData)))
-
-  -- ── 事前予測 ──
-  putStrLn "[1] 事前予測サンプリング (priorPredictive)"
-  putStrLn "    データ観測前のモデルが予測する y の分布を確認"
-  gen <- createSystemRandom
-  prior <- priorPredictive linearModel 2000 gen
-  let priorYs = concatMap (Map.findWithDefault [] "y") prior
-      (pMean, pLo, pHi, pSD) = stats priorYs
-  printf "    事前予測: mean=%+.3f  sd=%.3f  95%% CI=[%+.3f, %+.3f]\n"
-         pMean pSD pLo pHi
-  printf "    → 事前 μ ~ N(0,10) が広いため事前予測は広く散らばる (期待通り)\n\n"
-
-  -- ── NUTS で事後をサンプリング ──
-  putStrLn "[2] 事後分布サンプリング (NUTS)"
-  ch <- nuts linearModel cfg
-              (Map.fromList [("mu", 0.0), ("sigma", 1.0)])
-              gen
-  printf "    samples=%d\n\n" (length (chainSamples ch))
-
-  -- ── 事後予測 ──
-  putStrLn "[3] 事後予測サンプリング (posteriorPredictive)"
-  putStrLn "    観測データと整合的か検証"
-  postPreds <- posteriorPredictive linearModel ch gen
-  let postYs = concatMap (Map.findWithDefault [] "y") postPreds
-      (poMean, poLo, poHi, poSD) = stats postYs
-  printf "    事後予測: mean=%+.3f  sd=%.3f  95%% CI=[%+.3f, %+.3f]\n"
-         poMean poSD poLo poHi
-  printf "    観測値:   mean=%+.3f  sd=%.3f  range=[%.2f, %.2f]\n"
-         (sum obsData / fromIntegral (length obsData))
-         (let mn = sum obsData / fromIntegral (length obsData)
-          in sqrt (sum [(x-mn)^(2::Int) | x <- obsData] / fromIntegral (length obsData)))
-         (minimum obsData) (maximum obsData)
-  putStrLn "    → 事後予測の中心が観測平均近くに来ている (モデル妥当)"
-  putStrLn ""
-
-  -- ── 観測位置ごとの事後予測 95% CI ──
-  putStrLn "[4] 観測位置ごとの事後予測区間 (posteriorPredictiveSummary)"
-  let summary = posteriorPredictiveSummary postPreds
-  case Map.lookup "y" summary of
-    Just rows -> do
-      printf "    %-3s  %8s  %10s  %12s\n"
-             ("i"::String) ("y_obs"::String)
-             ("yhat_mean"::String) ("95% CI"::String)
-      mapM_ (\(i, (y_obs, (m, lo, hi))) ->
-              printf "    %-3d  %8.3f  %10.3f  [%+5.2f, %+5.2f]\n"
-                     (i::Int) y_obs m lo hi)
-            (zip [1..] (zip obsData rows))
-    Nothing -> putStrLn "    no predictions"
-  putStrLn ""
-
-  -- ── PPC ベイズ p 値風診断 ──
-  putStrLn "[5] PPC 整合性チェック (Bayesian p-value)"
-  let obsMean = sum obsData / fromIntegral (length obsData)
-      meansFromPred = [ let ys = Map.findWithDefault [] "y" p
-                        in sum ys / fromIntegral (length ys)
-                      | p <- postPreds ]
-      pVal = fromIntegral (length (filter (> obsMean) meansFromPred))
-            / fromIntegral (length meansFromPred) :: Double
-  printf "    観測平均: %.3f\n" obsMean
-  printf "    P(事後予測平均 > 観測平均) = %.3f\n" pVal
-  printf "    (0.05 < p < 0.95 ならモデルとデータが整合)\n"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ 事前/事後予測サンプリングが正常動作"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/Phase37A0VerifyDemo.hs b/demo/bayesian/Phase37A0VerifyDemo.hs
deleted file mode 100644
--- a/demo/bayesian/Phase37A0VerifyDemo.hs
+++ /dev/null
@@ -1,277 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | Phase 37-A0 doc 拡充の検証用 demo。
---
--- docs/bayesian/02-probabilistic-model.ja.md の追加節
--- (形式 A / B / C / random slope / multi-level / crossed / prior choice)
--- に載せる sample code をそのまま入れて、 build + 小規模 NUTS で実行可能性を
--- 確認する。 doc に貼る code はここから写経する。
---
--- 各モデルは独立に小さい NUTS で 1 回まわす (50 iter / 25 burn) ので、 doc に
--- 載せる code が "本当にコンパイル + 実行できる" ことの証拠になる。
-module Main where
-
-import           Control.Monad             (forM, forM_)
-import qualified Data.Map.Strict           as Map
-import           Data.Maybe                (fromMaybe)
-import qualified Data.Text                 as T
-import           System.Random.MWC         (createSystemRandom)
-import           Text.Printf               (printf)
-
-import           Hanalyze.MCMC.Core        (acceptanceRate, posteriorMean)
-import           Hanalyze.MCMC.NUTS        (NUTSConfig (..), defaultNUTSConfig,
-                                            nuts)
-import           Hanalyze.Model.HBM        (Distribution (..), ModelP,
-                                            indexed, nonCenteredNormal,
-                                            observe, sample)
-
--- ===========================================================================
--- 形式 A: 群ごとデータが分かれている (現 Pattern 5)
--- ===========================================================================
-
--- | μ ~ Normal(0, 10), τ ~ HalfNormal(5),
---   θ_j ~ Normal(μ, τ),  y_ij ~ Normal(θ_j, σ=1)
-schoolModelA :: [[Double]] -> ModelP ()
-schoolModelA groupData = do
-  mu  <- sample "mu"  (Normal 0 10)
-  tau <- sample "tau" (HalfNormal 5)
-  forM_ (zip [1 :: Int ..] groupData) $ \(j, ys) -> do
-    theta <- sample (indexed "theta" j) (Normal mu tau)
-    observe (indexed "y" j) (Normal theta 1) ys
-
-groupDataA :: [[Double]]
-groupDataA =
-  [ [ 1.1, 0.8, 1.3, 1.0 ]
-  , [ 4.9, 5.2, 4.7, 5.1 ]
-  , [ 9.0, 8.7, 9.3, 8.9 ]
-  ]
-
-initA :: Map.Map T.Text Double
-initA = Map.fromList
-  [ ("mu", 5), ("tau", 3)
-  , ("theta_1", 1), ("theta_2", 5), ("theta_3", 9) ]
-
--- ===========================================================================
--- 形式 B: long-format (各観測が gid を持つ)
--- ===========================================================================
-
--- | gid と y の縦持ち data を受け、 per-group θ を先に forM で全部展開してから
---   観測する。 同じ gid を持つ観測を集めて 1 度に observe するのが効率的。
-schoolModelB :: [Int] -> [Double] -> ModelP ()
-schoolModelB gids ys = do
-  let nG = maximum gids + 1
-  mu  <- sample "mu"  (Normal 0 10)
-  tau <- sample "tau" (HalfNormal 5)
-  thetas <- forM [0 .. nG - 1] $ \j ->
-    sample (indexed "theta" j) (Normal mu tau)
-  forM_ [0 .. nG - 1] $ \j -> do
-    let ysG = [y | (g, y) <- zip gids ys, g == j]
-    observe (indexed "y" j)
-            (Normal (thetas !! j) 1) ysG
-
-gidsB :: [Int]
-gidsB = [0,0,0,0, 1,1,1,1, 2,2,2,2]
-
-ysB :: [Double]
-ysB = [1.1, 0.8, 1.3, 1.0,  4.9, 5.2, 4.7, 5.1,  9.0, 8.7, 9.3, 8.9]
-
-initB :: Map.Map T.Text Double
-initB = initA
-
--- ===========================================================================
--- 形式 C: non-centered グループ (funnel 回避)
--- ===========================================================================
-
--- | θ_j ~ Normal(μ, τ) を θ_j_raw ~ Normal(0,1), θ_j = μ + τ·θ_j_raw に書き直す。
---   `nonCenteredNormal` がその差し替えを 1 行で提供する。 latent 名は
---   "theta_j_raw" になり、 推論後 augmentChainWithDeterministic で θ_j を復元
---   できる (doc 中では derived 部は割愛しても OK)。
-schoolModelC :: [[Double]] -> ModelP ()
-schoolModelC groupData = do
-  mu  <- sample "mu"  (Normal 0 10)
-  tau <- sample "tau" (HalfNormal 5)
-  forM_ (zip [1 :: Int ..] groupData) $ \(j, ys) -> do
-    theta <- nonCenteredNormal (indexed "theta" j) mu tau
-    observe (indexed "y" j) (Normal theta 1) ys
-
-initC :: Map.Map T.Text Double
-initC = Map.fromList
-  [ ("mu", 5), ("tau", 3)
-  , ("theta_1_raw", 0), ("theta_2_raw", 0), ("theta_3_raw", 0) ]
-
--- ===========================================================================
--- random slope (α_j, β_j 両方を階層化)
--- ===========================================================================
-
--- | y_ij ~ Normal(α_j + β_j · x_ij, σ),
---   α_j ~ Normal(μ_α, τ_α),  β_j ~ Normal(μ_β, τ_β)。
---   入力は (x, y) のグループごとのペアリスト。
-randomSlope :: [[(Double, Double)]] -> ModelP ()
-randomSlope groupData = do
-  muA  <- sample "mu_alpha"    (Normal 0 10)
-  tauA <- sample "tau_alpha"   (HalfNormal 5)
-  muB  <- sample "mu_beta"     (Normal 0 5)
-  tauB <- sample "tau_beta"    (HalfNormal 5)
-  sig  <- sample "sigma"       (Exponential 1)
-  forM_ (zip [1 :: Int ..] groupData) $ \(j, pts) -> do
-    alpha <- sample (indexed "alpha" j) (Normal muA tauA)
-    beta  <- sample (T.pack ("beta_"  ++ show j)) (Normal muB tauB)
-    forM_ pts $ \(x, y) ->
-      observe (indexed "y" j)
-              (Normal (alpha + beta * realToFrac x) sig) [y]
-
-rsData :: [[(Double, Double)]]
-rsData =
-  [ zip [0.5, 1.0, 1.5, 2.0] [1.6, 1.2, 0.8, 0.4]
-  , zip [0.5, 1.0, 1.5, 2.0] [4.85, 4.70, 4.55, 4.40]
-  , zip [0.5, 1.0, 1.5, 2.0] [8.10, 8.20, 8.30, 8.40]
-  ]
-
-initRS :: Map.Map T.Text Double
-initRS = Map.fromList
-  [ ("mu_alpha", 5), ("tau_alpha", 3)
-  , ("mu_beta", 0),  ("tau_beta", 1)
-  , ("sigma", 0.3)
-  , ("alpha_1", 2), ("alpha_2", 5), ("alpha_3", 8)
-  , ("beta_1", -0.8), ("beta_2", -0.3), ("beta_3", 0.2) ]
-
--- ===========================================================================
--- multi-level (3-level nested): district → school → students
--- ===========================================================================
-
--- | 地区 d 内に学校 (d,s) があり、 その中に生徒 (d, s, i) がいる。
---   μ ← Normal(0, 10)
---   τ_d ← HalfNormal(5)               (地区間 SD)
---   τ_s ← HalfNormal(5)               (学校間 SD、 地区共通)
---   δ_d ← Normal(μ, τ_d)              (地区効果)
---   θ_{d,s} ← Normal(δ_d, τ_s)        (学校効果)
---   y_{d,s,i} ← Normal(θ_{d,s}, 1)
---
---   入力: 各地区につき、 学校ごとの観測リストのリスト。
-multiLevel :: [[[Double]]] -> ModelP ()
-multiLevel byDistrict = do
-  mu  <- sample "mu"    (Normal 0 10)
-  tD  <- sample "tau_d" (HalfNormal 5)
-  tS  <- sample "tau_s" (HalfNormal 5)
-  forM_ (zip [1 :: Int ..] byDistrict) $ \(d, schools) -> do
-    delta <- sample (indexed "delta" d) (Normal mu tD)
-    forM_ (zip [1 :: Int ..] schools) $ \(s, ys) -> do
-      theta <- sample (T.pack (concat ["theta_", show d, "_", show s]))
-                      (Normal delta tS)
-      observe (T.pack (concat ["y_", show d, "_", show s]))
-              (Normal theta 1) ys
-
-mlData :: [[[Double]]]
-mlData =
-  [ [ [1.1, 0.8, 1.3], [1.5, 1.2, 1.7] ]   -- 地区 1 に学校 2 つ
-  , [ [4.9, 5.2, 4.7], [5.5, 5.3, 5.7] ]   -- 地区 2 に学校 2 つ
-  , [ [8.9, 9.0, 8.7], [8.4, 8.6, 8.5] ]   -- 地区 3 に学校 2 つ
-  ]
-
-initML :: Map.Map T.Text Double
-initML = Map.fromList $
-  [ ("mu", 5), ("tau_d", 3), ("tau_s", 1)
-  , ("delta_1", 1), ("delta_2", 5), ("delta_3", 9) ]
-  ++ [ (T.pack (concat ["theta_", show d, "_", show s]), v)
-     | (d, v) <- [(1::Int, 1.3), (2, 5.4), (3, 8.7)]
-     , s <- [1 :: Int .. 2] ]
-
--- ===========================================================================
--- crossed random effects: school × year
--- ===========================================================================
-
--- | 学校 s と年度 t が **交差** (どの (s, t) ペアでも観測される)。
---   α_s ← Normal(μ_α, τ_α)
---   γ_t ← Normal(0, τ_γ)
---   y_{s,t,i} ← Normal(α_s + γ_t, σ)
---
---   入力: [(sid, tid, y)] の long-format。
-crossed :: Int -> Int -> [(Int, Int, Double)] -> ModelP ()
-crossed nS nT obs = do
-  muA <- sample "mu_alpha" (Normal 0 10)
-  tA  <- sample "tau_a"    (HalfNormal 5)
-  tG  <- sample "tau_g"    (HalfNormal 5)
-  sig <- sample "sigma"    (Exponential 1)
-  alphas <- forM [0 .. nS - 1] $ \s ->
-    sample (indexed "alpha" s) (Normal muA tA)
-  gammas <- forM [0 .. nT - 1] $ \t ->
-    sample (indexed "gamma" t) (Normal 0 tG)
-  forM_ obs $ \(s, t, y) ->
-    observe (T.pack (concat ["y_", show s, "_", show t]))
-            (Normal (alphas !! s + gammas !! t) sig) [y]
-
-crossedObs :: [(Int, Int, Double)]
-crossedObs =
-  [ (0,0, 1.0), (0,1, 1.3), (0,2, 0.9)
-  , (1,0, 4.8), (1,1, 5.2), (1,2, 5.0)
-  , (2,0, 8.7), (2,1, 9.1), (2,2, 8.9)
-  ]
-
-initCrossed :: Map.Map T.Text Double
-initCrossed = Map.fromList
-  [ ("mu_alpha", 5), ("tau_a", 3), ("tau_g", 0.3), ("sigma", 0.3)
-  , ("alpha_0", 1), ("alpha_1", 5), ("alpha_2", 9)
-  , ("gamma_0", 0), ("gamma_1", 0), ("gamma_2", 0) ]
-
--- ===========================================================================
--- prior choice: HalfNormal vs HalfCauchy on τ
--- ===========================================================================
-
--- | 弱情報事前 HalfNormal(5) 版 (Gelman 2006 推奨)。
-priorHalfNormal :: [[Double]] -> ModelP ()
-priorHalfNormal = schoolModelA  -- HalfNormal(5) を使う実装は schoolModelA と同じ
-
--- | 重い裾 HalfCauchy(2.5) 版。
-priorHalfCauchy :: [[Double]] -> ModelP ()
-priorHalfCauchy groupData = do
-  mu  <- sample "mu"  (Normal 0 10)
-  tau <- sample "tau" (HalfCauchy 2.5)
-  forM_ (zip [1 :: Int ..] groupData) $ \(j, ys) -> do
-    theta <- sample (indexed "theta" j) (Normal mu tau)
-    observe (indexed "y" j) (Normal theta 1) ys
-
--- ===========================================================================
--- 検証 runner
--- ===========================================================================
-
-verify
-  :: T.Text
-  -> ModelP ()
-  -> Map.Map T.Text Double
-  -> [T.Text]   -- ^ 確認したい posterior mean パラメータ
-  -> IO ()
-verify label model initP showVars = do
-  gen <- createSystemRandom
-  ch  <- nuts model smallCfg initP gen
-  printf "[%s] accept=%.2f  " (T.unpack label) (acceptanceRate ch)
-  forM_ showVars $ \v ->
-    printf " %s=%.2f" (T.unpack v) (fromMaybe (0 :: Double) (posteriorMean v ch))
-  putStrLn ""
-  where
-    smallCfg = defaultNUTSConfig
-      { nutsIterations = 100
-      , nutsBurnIn     = 50
-      , nutsStepSize   = 0.05
-      , nutsMaxDepth   = 6
-      }
-
-main :: IO ()
-main = do
-  putStrLn "═══ Phase 37 A0 sample code verification ═══"
-  verify "A (per-group data)"   (schoolModelA  groupDataA) initA
-         ["mu", "tau", "theta_1", "theta_2", "theta_3"]
-  verify "B (long-format)"      (schoolModelB  gidsB ysB)  initB
-         ["mu", "tau", "theta_1"]
-  verify "C (non-centered)"     (schoolModelC  groupDataA) initC
-         ["mu", "tau"]
-  verify "random slope"         (randomSlope   rsData)     initRS
-         ["mu_alpha", "mu_beta", "beta_1", "beta_3"]
-  verify "multi-level (3-lvl)"  (multiLevel    mlData)     initML
-         ["mu", "tau_d", "tau_s", "delta_1", "delta_3"]
-  verify "crossed (S × T)"      (crossed 3 3 crossedObs)   initCrossed
-         ["mu_alpha", "alpha_0", "alpha_2", "gamma_1"]
-  verify "prior HalfNormal(5)"  (priorHalfNormal groupDataA) initA
-         ["mu", "tau"]
-  verify "prior HalfCauchy(2.5)" (priorHalfCauchy groupDataA) initA
-         ["mu", "tau"]
-  putStrLn "═══ all 8 models compiled + ran ═══"
diff --git a/demo/bayesian/PlateNotationDemo.hs b/demo/bayesian/PlateNotationDemo.hs
deleted file mode 100644
--- a/demo/bayesian/PlateNotationDemo.hs
+++ /dev/null
@@ -1,95 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Phase 40 plate 記法のデモ。 8-schools + nested 多レベルモデルの
--- mermaid HTML と graphviz DOT を出力する。
---
--- 実行:
---
--- > cabal run plate-notation-demo
---
--- 生成物 (demo-output/ 下):
---
--- - @8schools.html@   ブラウザで開くと mermaid plate (subgraph 囲い) で表示
--- - @8schools.dot@    @dot -Tpng 8schools.dot -o 8schools.png@ で PNG 化
--- - @multilevel.html@ nested plate (school × student)
--- - @multilevel.dot@  nested cluster
-module Main where
-
-import Control.Monad (forM_, forM)
-import qualified Data.Text as T
-import qualified Data.Text.IO as TIO
-import System.Directory (createDirectoryIfMissing)
-
-import qualified Hanalyze.Model.HBM as HBM
-import qualified Hanalyze.Viz.ModelGraph as VMG
-import qualified Hanalyze.Viz.ModelGraphDot as VMGD
-
--- ---------------------------------------------------------------------------
--- モデル 1: 8-schools (Gelman et al.)
--- ---------------------------------------------------------------------------
-
-eightSchools :: HBM.ModelP ()
-eightSchools = do
-  mu  <- HBM.sample "mu"  (HBM.Normal 0 5)
-  tau <- HBM.sample "tau" (HBM.HalfCauchy 5)
-  _ <- HBM.plate "school" 8 $ forM [0..7 :: Int] $ \j -> do
-    eta <- HBM.sample ("eta_" <> T.pack (show j)) (HBM.Normal 0 1)
-    HBM.observe ("y_" <> T.pack (show j))
-                (HBM.Normal (mu + tau * eta) 1)
-                [realToFrac j]
-  return ()
-
--- ---------------------------------------------------------------------------
--- モデル 2: nested multi-level (school × student)
--- ---------------------------------------------------------------------------
-
-multilevel :: HBM.ModelP ()
-multilevel = do
-  mu  <- HBM.sample "mu" (HBM.Normal 0 5)
-  tau <- HBM.sample "tau" (HBM.HalfNormal 1)
-  _ <- HBM.plate "school" 3 $ forM_ [0..2 :: Int] $ \j -> do
-    theta <- HBM.sample ("theta_" <> T.pack (show j))
-                        (HBM.Normal mu tau)
-    _ <- HBM.plate "student" 2 $ forM_ [0..1 :: Int] $ \i ->
-           HBM.observe ("y_" <> T.pack (show j) <> "_" <> T.pack (show i))
-                       (HBM.Normal theta 1)
-                       [realToFrac (j * 2 + i)]
-    return ()
-  return ()
-
--- ---------------------------------------------------------------------------
--- main
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  createDirectoryIfMissing True "demo-output"
-  -- 1. 8-schools (expanded = N 個列挙)
-  let g1   = HBM.buildModelGraph eightSchools
-      g1c  = HBM.collapseIndexedPlateNodes g1   -- PyMC 同等の集約
-  VMG.renderModelGraph "demo-output/8schools-expanded.html"
-    "8 schools - expanded (8 個列挙)" g1
-  VMG.renderModelGraph "demo-output/8schools-collapsed.html"
-    "8 schools - collapsed (PyMC 同等)" g1c
-  VMGD.writeModelGraphDot "demo-output/8schools-expanded.dot"  g1
-  VMGD.writeModelGraphDot "demo-output/8schools-collapsed.dot" g1c
-  TIO.putStrLn "[1] 8-schools:"
-  TIO.putStrLn "    展開 (Phase 40 旧):"
-  TIO.putStrLn "    - demo-output/8schools-expanded.html / .dot"
-  TIO.putStrLn "    集約 (Phase 40-A8 = PyMC 同等):"
-  TIO.putStrLn "    - demo-output/8schools-collapsed.html / .dot"
-  TIO.putStrLn $ "    mgPlates = " <> T.pack (show (HBM.mgPlates g1))
-  TIO.putStrLn $ "    集約後ノード数 = " <> T.pack (show (length (HBM.mgNodes g1c)))
-  -- 2. nested multilevel
-  let g2  = HBM.buildModelGraph multilevel
-      g2c = HBM.collapseIndexedPlateNodes g2
-  VMG.renderModelGraph "demo-output/multilevel-expanded.html"
-    "school × student - expanded" g2
-  VMG.renderModelGraph "demo-output/multilevel-collapsed.html"
-    "school × student - collapsed (PyMC 同等)" g2c
-  VMGD.writeModelGraphDot "demo-output/multilevel-expanded.dot"  g2
-  VMGD.writeModelGraphDot "demo-output/multilevel-collapsed.dot" g2c
-  TIO.putStrLn "[2] nested multi-level:"
-  TIO.putStrLn "    展開:    demo-output/multilevel-expanded.html / .dot"
-  TIO.putStrLn "    集約:    demo-output/multilevel-collapsed.html / .dot"
-  TIO.putStrLn $ "    mgPlates = " <> T.pack (show (HBM.mgPlates g2))
-  TIO.putStrLn $ "    集約後ノード数 = " <> T.pack (show (length (HBM.mgNodes g2c)))
diff --git a/demo/bayesian/PotentialDemo.hs b/demo/bayesian/PotentialDemo.hs
deleted file mode 100644
--- a/demo/bayesian/PotentialDemo.hs
+++ /dev/null
@@ -1,171 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | Potential プリミティブのデモ (PyMC `pm.Potential` 相当)。
---
--- 任意の log-prob 項を log-joint に加える機能。3 つの典型用途を例示:
---   1. ソフト順序制約 (μ_1 < μ_2)
---   2. ベイズ的な L2 正則化 (ridge)
---   3. カスタム尤度 (既存分布で表せない観測モデル)
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Data.Maybe (fromMaybe)
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.MCMC.Core (chainSamples, posteriorMean, posteriorSD,
-                  posteriorQuantile, acceptanceRate)
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, potential, Distribution (..))
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 1500
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.1
-        }
-
--- ---------------------------------------------------------------------------
--- 例 1: ソフト順序制約 mu1 < mu2
--- ---------------------------------------------------------------------------
--- 2 群のデータ。Potential で μ_1 < μ_2 を ソフトに強制する。
--- 制約違反時は -1000 の罰則を加える (実質ゼロ確率)。
-
-obs1 :: [Double]
-obs1 = [1.5, 2.0, 1.8, 2.1, 1.6]
-obs2 :: [Double]
-obs2 = [3.5, 3.8, 3.2, 3.6, 3.9]
-
--- 制約なし版
-unconstrainedModel :: ModelP ()
-unconstrainedModel = do
-  mu1 <- sample "mu1" (Normal 0 10)
-  mu2 <- sample "mu2" (Normal 0 10)
-  sigma <- sample "sigma" (HalfNormal 5)
-  observe "y1" (Normal mu1 sigma) obs1
-  observe "y2" (Normal mu2 sigma) obs2
-
--- 制約付き版: Potential で μ_1 < μ_2 を強制
-orderedModel :: ModelP ()
-orderedModel = do
-  mu1 <- sample "mu1" (Normal 0 10)
-  mu2 <- sample "mu2" (Normal 0 10)
-  sigma <- sample "sigma" (HalfNormal 5)
-  -- ソフト制約: mu1 >= mu2 なら大きな罰則
-  potential "order" (if mu1 < mu2 then 0 else -1000)
-  observe "y1" (Normal mu1 sigma) obs1
-  observe "y2" (Normal mu2 sigma) obs2
-
--- ---------------------------------------------------------------------------
--- 例 2: ベイズ的な L2 正則化 (ridge regression)
--- ---------------------------------------------------------------------------
--- 通常 β ~ Normal(0, σ_β) と書くのと等価だが、Potential で直接記述すると
--- 自由度がある (例えば lambda を別に決められる)。
-
-xs2 :: [Double]
-xs2 = [-2.0, -1.0, 0.0, 1.0, 2.0, -1.5, 0.5, 1.5, -0.5, 0.0]
-ys2 :: [Double]
-ys2 = [-3.5, -1.8, 0.2, 2.1, 4.0, -2.7, 1.0, 3.2, -0.9, 0.1]
-
-ridgeModel :: ModelP ()
-ridgeModel = do
-  alpha <- sample "alpha" (Normal 0 100)   -- 切片はフラット事前
-  beta  <- sample "beta"  (Normal 0 100)   -- 傾きもフラット
-  sigma <- sample "sigma" (HalfNormal 5)
-  -- Ridge ペナルティ: -0.5 * lambda * beta^2 (lambda=2.0)
-  let lambda = 2.0
-  potential "ridge" (-0.5 * lambda * beta * beta)
-  -- 観測尤度
-  mapM_ (\(x, y) -> let xC = realToFrac x
-                    in observe "y" (Normal (alpha + beta * xC) sigma) [y])
-        (zip xs2 ys2)
-
--- ---------------------------------------------------------------------------
--- 例 3: カスタム尤度 — Laplace ノイズ (頑健回帰)
--- ---------------------------------------------------------------------------
--- 既存の Distribution に Laplace は無いので、Potential で直接記述する。
--- log p(y|μ,b) = -log(2b) - |y - μ| / b
-
-xs3, ys3 :: [Double]
-xs3 = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 2.5]
-ys3 = [0.1, 1.2, 2.0, 3.3, 4.1, 5.0, 8.0]   -- (2.5, 8.0) は外れ値
-
-laplaceRegModel :: ModelP ()
-laplaceRegModel = do
-  alpha <- sample "alpha" (Normal 0 10)
-  beta  <- sample "beta"  (Normal 0 10)
-  b     <- sample "b"     (HalfNormal 3)   -- スケール
-  -- Laplace 尤度を Potential で記述
-  let logLapl mu y = -log (2 * b) - abs (realToFrac y - mu) / b
-  mapM_ (\(x, y) -> let mu = alpha + beta * realToFrac x
-                    in potential "laplace_lik" (logLapl mu y))
-        (zip xs3 ys3)
-
--- ---------------------------------------------------------------------------
--- main
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Potential プリミティブのデモ (PyMC pm.Potential 相当)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  -- ── 例 1 ──
-  putStrLn "[1] ソフト順序制約 mu1 < mu2"
-  printf "    観測: y1 = %s (低)\n          y2 = %s (高)\n"
-         (show obs1) (show obs2)
-  gen <- createSystemRandom
-
-  putStrLn "  制約なし: μ_1, μ_2 は独立にサンプリングされる"
-  ch1 <- nuts unconstrainedModel cfg
-              (Map.fromList [("mu1", 0.0), ("mu2", 0.0), ("sigma", 1.0)]) gen
-  printf "    mu1 = %+.4f ± %.4f   mu2 = %+.4f ± %.4f\n"
-         (fromMaybe 0 (posteriorMean "mu1" ch1)) (fromMaybe 0 (posteriorSD "mu1" ch1))
-         (fromMaybe 0 (posteriorMean "mu2" ch1)) (fromMaybe 0 (posteriorSD "mu2" ch1))
-
-  putStrLn "  制約付き: Potential で μ_1 < μ_2 を強制"
-  ch2 <- nuts orderedModel cfg
-              (Map.fromList [("mu1", 0.0), ("mu2", 4.0), ("sigma", 1.0)]) gen
-  printf "    mu1 = %+.4f ± %.4f   mu2 = %+.4f ± %.4f\n"
-         (fromMaybe 0 (posteriorMean "mu1" ch2)) (fromMaybe 0 (posteriorSD "mu1" ch2))
-         (fromMaybe 0 (posteriorMean "mu2" ch2)) (fromMaybe 0 (posteriorSD "mu2" ch2))
-  -- 制約違反サンプル数
-  let violations = length [() | s <- chainSamples ch2
-                              , let m1 = Map.findWithDefault 0 "mu1" s
-                                    m2 = Map.findWithDefault 0 "mu2" s
-                              , m1 >= m2]
-  printf "    制約違反 (mu1 ≥ mu2) のサンプル数: %d / %d\n"
-         violations (length (chainSamples ch2))
-  putStrLn ""
-
-  -- ── 例 2 ──
-  putStrLn "[2] Ridge 正則化 (Potential で -0.5 * λ * β²)"
-  printf "    データ: 直線 y ≈ 1.8x (10 点)\n"
-  ch3 <- nuts ridgeModel cfg
-              (Map.fromList [("alpha", 0.0), ("beta", 0.0), ("sigma", 1.0)]) gen
-  printf "    alpha = %+.4f ± %.4f\n"
-         (fromMaybe 0 (posteriorMean "alpha" ch3)) (fromMaybe 0 (posteriorSD "alpha" ch3))
-  printf "    beta  = %+.4f ± %.4f   (Ridge により 0 寄りに縮小)\n"
-         (fromMaybe 0 (posteriorMean "beta"  ch3)) (fromMaybe 0 (posteriorSD "beta"  ch3))
-  printf "    sigma = %+.4f ± %.4f\n"
-         (fromMaybe 0 (posteriorMean "sigma" ch3)) (fromMaybe 0 (posteriorSD "sigma" ch3))
-  putStrLn ""
-
-  -- ── 例 3 ──
-  putStrLn "[3] カスタム Laplace 尤度 (頑健回帰)"
-  printf "    データ: y ≈ x + ε (7 点中 (2.5, 8.0) は外れ値)\n"
-  ch4 <- nuts laplaceRegModel cfg
-              (Map.fromList [("alpha", 0.0), ("beta", 1.0), ("b", 1.0)]) gen
-  printf "    alpha = %+.4f ± %.4f\n"
-         (fromMaybe 0 (posteriorMean "alpha" ch4)) (fromMaybe 0 (posteriorSD "alpha" ch4))
-  printf "    beta  = %+.4f ± %.4f   (外れ値に頑健 → 真値 1.0 に近い)\n"
-         (fromMaybe 0 (posteriorMean "beta"  ch4)) (fromMaybe 0 (posteriorSD "beta"  ch4))
-  printf "    b     = %+.4f ± %.4f   (Laplace スケール)\n"
-         (fromMaybe 0 (posteriorMean "b"     ch4)) (fromMaybe 0 (posteriorSD "b"     ch4))
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Potential が 3 つの典型用途で動作 (制約・正則化・カスタム尤度)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/PyMCStatusDemo.hs b/demo/bayesian/PyMCStatusDemo.hs
deleted file mode 100644
--- a/demo/bayesian/PyMCStatusDemo.hs
+++ /dev/null
@@ -1,148 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | PyMC との機能比較を可視化するレポート (棒グラフ + テキスト)。
---
--- カテゴリ別に ✅ 実装済み / 🚧 部分実装 / ❌ 未実装 の件数を
--- 積み上げ棒グラフで表示し、最近のブランチで追加されたものを強調する。
-module Main where
-
-import qualified Data.Text as T
-import Data.Text (Text)
-import Text.Printf (printf)
-
-import Hanalyze.Viz.Bar  (stackedBar)
-import Hanalyze.Viz.Core (PlotConfig (..), defaultConfig, OutputFormat (..), writeSpec)
-
--- ---------------------------------------------------------------------------
--- データ: PyMC 機能カテゴリ別の実装状況 (このブランチ完了時点)
--- ---------------------------------------------------------------------------
-
--- (カテゴリ, 実装済 ✅, 部分実装 🚧, 未実装 ❌)
--- Phase 29 完了 + Phase 37 計画反映 (2026-05-30 更新)。
--- A2 (連続 7) + A3 (離散 6) + A4 (多変量 3) = 16 の分布が Phase 37 計画上で
--- 未実装、 これを missing にカウントする (旧 Bound 1 も含めて 17)。
-statusByCategory :: [(Text, Int, Int, Int)]
-statusByCategory =
-  [ -- 分布: Base12 + (Mixture, Truncated, Censored, MvNormal, Dirichlet,
-    --        LKJ, Multinomial, NegBinom, ZIP, ZIB, InvGamma, Weibull,
-    --        Pareto, BetaBinom, VonMises) = 27 ✅
-    -- + Phase 37-A2 (4) + A3 (5) + A4 (2) ✅ → 38
-    -- 残 Phase 37 計画: A2 残 (3) + A3 残 (1) + A4 残 Wishart (1) + Bound (1) = 6 ❌
-    ("分布",          38, 0,  6)
-  , -- サンプラー: NUTS/HMC/MH/Gibbs/Slice/ADVI/SMC/Full-rank ADVI = 8 ✅
-    -- 残: 正規化フロー (Stretch) = 1 ❌
-    ("サンプラー",     8, 0, 1 )
-  , -- 事後 Workflow: PPC/PriorPC/Potential/set_data/Deterministic = 5 ✅
-    ("事後 Workflow",  5, 0, 0 )
-  , -- 可視化・診断: trace/posterior/pair/acf/forest/energy/BFMI/HDI-trace
-    --              /rank/ppc/summary/divergence-overlay/ESS-R̂表 = 13 ✅
-    --              A7 はナビ整理のみで実装 gap なし
-    ("可視化・診断",   13, 0, 0 )
-  , -- モデル比較: WAIC/LOO/Pseudo-BMA/真BMA/BayesFactor (Bridge) = 5 ✅
-    ("モデル比較",     5, 0, 0 )
-  , -- プリミティブ: 階層/ランダム切片・傾き/Mixture/Trunc/Censored/Potential
-    --              /Deterministic/non-centered/AR/MvN-latent/Dirichlet/LKJ = 12 ✅
-    -- + Phase 37-A6: glmmRandomIntercept ✅ → 13
-    -- 残 Phase 39: hmmLatent / dpStickBreaking = 2 ❌
-    -- Stretch: ODE 尤度 / Bayes NN = 2 ❌
-    ("プリミティブ",   13, 1, 4 )  -- GP 部分; hmm/dp + ODE/BNN
-  ]
-
--- 完了したフェーズ (Phase 29 まで)
-addedThisBranch :: [(Text, Text)]
-addedThisBranch =
-  [ ("Phase A-J",   "本ブランチ初期: 27 分布 + サンプラー基盤 + 5 viz")
-  , ("Phase 29-A1", "SMC (annealing + bridge 経路)")
-  , ("Phase 29-A2", "Bridge Sampling + Bayes Factor")
-  , ("Phase 29-A3", "真の BMA (周辺尤度ベース)")
-  , ("Phase 37-A0", "HBM 書き方 doc (グループ別 3 形式 + multi-level + crossed)")
-  , ("Phase 37-A1", "本 PyMC 比較 doc を Phase 29 反映 + 残 gap 確定")
-  , ("Phase 37-A2", "連続分布 +4: SkewNormal / Logistic / Gumbel / AsymmetricLaplace")
-  , ("Phase 37-A3", "離散分布 +5: OrderedLogistic / DiscreteUniform / Geometric / HyperGeometric / ZeroInflatedNegativeBinomial")
-  , ("Phase 37-A4", "多変量分布 +2: MvStudentT / DirichletMultinomial (Wishart 後回し)")
-  , ("Phase 37-A5", "Full-rank ADVI (q = N(μ, LLᵀ)、 viCovU で L 出力)")
-  , ("Phase 37-A6", "glmmRandomIntercept helper (Gaussian/Binomial/Poisson)")
-  ]
-
--- 残課題 (Phase 37 計画分 + Stretch)
-todoStretch :: [Text]
-todoStretch =
-  [ "[A2 残] 連続分布 3 (低優先): Triangular/Kumaraswamy/Rice"
-  , "[A3 残] 離散 1 (低優先): DiscreteWeibull"
-  , "[A4 残] 多変量 1: Wishart (LKJ 代替可、 後回し)"
-  , "[Phase 39 候補] 残 helper: hmmLatent / dpStickBreaking (Phase 37 から繰越)"
-  , "[Stretch] 正規化フロー / ODE 尤度 / ベイズ NN (研究レベル、 別 Phase)"
-  ]
-
--- ---------------------------------------------------------------------------
--- 可視化
--- ---------------------------------------------------------------------------
-
-statusChart :: IO ()
-statusChart = do
-  let cats   = [c | (c, _, _, _) <- statusByCategory]
-      vDone  = [d | (_, d, _, _) <- statusByCategory]
-      vPart  = [p | (_, _, p, _) <- statusByCategory]
-      vMiss  = [m | (_, _, _, m) <- statusByCategory]
-
-      -- stackedBar: 各カテゴリに 3 行 (Done/Partial/Missing) を持たせる
-      xs   = concatMap (replicate 3) cats
-      vals = concat $ zipWith3 (\d p m -> [fromIntegral d, fromIntegral p, fromIntegral m])
-                               vDone vPart vMiss
-      kinds = concat $ replicate (length cats) ["Done (✅)", "Partial (🚧)", "Missing (❌)"]
-
-      cfg = (defaultConfig "PyMC parity status — hanalyze")
-              { plotWidth = 700, plotHeight = 350 }
-  writeSpec HTML "pymc-status.html"
-    (stackedBar cfg "category" "count" "status" xs vals kinds)
-  putStrLn "  → pymc-status.html (カテゴリ別 stacked bar)"
-
--- ---------------------------------------------------------------------------
--- テキストレポート
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  PyMC parity ステータスレポート"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  -- カテゴリ別件数
-  putStrLn "[1] カテゴリ別 実装状況"
-  printf "  %-15s   %4s  %4s  %4s   %s\n" ("Category" :: String)
-         ("Done" :: String) ("Part" :: String) ("Miss" :: String)
-         ("Total" :: String)
-  printf "  %s\n" (replicate 50 '-' :: String)
-  let total = sum [d + p + m | (_, d, p, m) <- statusByCategory]
-      tDone = sum [d | (_, d, _, _) <- statusByCategory]
-      tPart = sum [p | (_, _, p, _) <- statusByCategory]
-      tMiss = sum [m | (_, _, _, m) <- statusByCategory]
-  mapM_ (\(c, d, p, m) ->
-            printf "  %-15s   %4d  %4d  %4d   %4d\n"
-                   (T.unpack c) d p m (d + p + m))
-        statusByCategory
-  printf "  %s\n" (replicate 50 '-' :: String)
-  printf "  %-15s   %4d  %4d  %4d   %4d   (%.1f%% complete)\n"
-         ("TOTAL" :: String) tDone tPart tMiss total
-         (100 * fromIntegral tDone / fromIntegral total :: Double)
-  putStrLn ""
-
-  -- 追加した機能
-  putStrLn "[2] このブランチで追加された機能"
-  mapM_ (\(p, d) -> printf "  %-9s  %s\n" (T.unpack p) (T.unpack d))
-        addedThisBranch
-  putStrLn ""
-
-  -- TODO (Stretch のみ)
-  putStrLn "[3] 残課題 TODO (Stretch — 主要ギャップは完了)"
-  mapM_ (\t -> putStrLn ("    [ ] " ++ T.unpack t)) todoStretch
-  putStrLn ""
-
-  -- 可視化
-  putStrLn "[4] 可視化"
-  statusChart
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  詳細表は docs/08-pymc-comparison.ja.md を参照"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/SetDataDemo.hs b/demo/bayesian/SetDataDemo.hs
deleted file mode 100644
--- a/demo/bayesian/SetDataDemo.hs
+++ /dev/null
@@ -1,85 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | pm.set_data 相当のデモ。
---
--- Haskell では「データを差し替え可能なモデル」を表す自然な方法は
--- データを引数にとるモデル関数 `mkModel :: [Double] -> ModelP ()` を作ること。
--- これは PyMC の `pm.Data` + `pm.set_data` ワークフローと同じ意図を
--- 構文的に表現する。
---
--- DSL レベルでは更に `dataNamed` / `withData` を提供している。
--- これらは Free monad の構造を直接書き換えるので、構造が動的に決まる
--- 場合や、モデル定義部から多くのコードを共有したい場合に便利。
--- ただし polymorphic な ModelP r に対する `withData` の適用は
--- 型システム的に煩雑なので、本デモでは parametric 化パターンを示す。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, dataNamed, withData,
-                  Distribution (..))
-import Hanalyze.Stat.PosteriorPredictive (posteriorPredictive)
-import Hanalyze.Viz.MCMC (printPosteriorSummary, ppcPlotFile)
-import Hanalyze.Viz.Core (defaultConfig, OutputFormat (..), PlotConfig (..))
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 1500
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.1
-        }
-
-trainData, testData :: [Double]
-trainData =
-  [1.2, 0.9, 1.4, 0.7, 1.1, 1.0, 1.3, 0.95, 1.05, 1.15,
-   0.85, 1.25, 0.95, 1.18, 1.02]
-testData = [1.6, 1.4, 1.5, 1.7, 1.3, 1.5, 1.55, 1.45, 1.48, 1.52]
-
--- | データを引数にとるモデル。同じ構造を異なるデータで再利用するための
--- 標準パターン (= pm.set_data 相当)。`dataNamed` で名前付きプレースホルダ
--- としても保存しておく (構造分析時に「この観測は y という名前」と判明する)。
-mkModel :: [Double] -> ModelP ()
-mkModel ys = do
-  yObs <- dataNamed "y" ys
-  mu   <- sample "mu"    (Normal 0 5)
-  sig  <- sample "sigma" (HalfNormal 2)
-  observe "y" (Normal mu sig) yObs
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  pm.set_data デモ — データを差し替えて事後予測を取る"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  gen <- createSystemRandom
-
-  -- ── 訓練データで推論 ──
-  putStrLn "[1] 訓練データで NUTS 実行 (μ ≈ 1.0 期待)"
-  ch <- nuts (mkModel trainData) cfg
-              (Map.fromList [("mu", 1), ("sigma", 1)]) gen
-  printPosteriorSummary ["mu", "sigma"] [ch]
-  putStrLn ""
-
-  -- ── 同じモデル構造をテストデータで適用 (pm.set_data に相当) ──
-  -- Phase H5: withData が直接 ModelP に適用できるようになった。
-  putStrLn "[2] withData でテストデータに直接差し替え (Rank-2 多相対応版)"
-  let testModel :: ModelP ()
-      testModel = withData "y" testData (mkModel trainData)
-  preds <- posteriorPredictive testModel ch gen
-  let yReps = [Map.findWithDefault [] "y" m | m <- preds]
-  let ppcCfg = (defaultConfig "PP check — train posterior on test data")
-                 { plotWidth = 700, plotHeight = 280 }
-  ppcPlotFile HTML "set-data-ppc.html" ppcCfg testData yReps 50
-  putStrLn "  → set-data-ppc.html"
-  putStrLn "    観測 (青) はテストデータ μ≈1.5、予測 (オレンジ) は"
-  putStrLn "    訓練データから得た posterior の予測 → 中心が ≈1.0 で"
-  putStrLn "    乖離 → 訓練分布と異なるサンプルだと判明。"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ withData が ModelP r → ModelP r に対応 (Phase H5)"
-  putStrLn "    型注釈 :: ModelP () を let に付ければそのまま使える"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/SimpsonParadoxDemo.hs b/demo/bayesian/SimpsonParadoxDemo.hs
deleted file mode 100644
--- a/demo/bayesian/SimpsonParadoxDemo.hs
+++ /dev/null
@@ -1,397 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | シンプソンのパラドックスを LM / GLMM / HBM で比較するデモ。
---
--- データ:
---   * 3 グループ (A, B, C)、各グループ内では負の傾き (右下り)
---   * グループを無視すると正の傾き (右上り) に見える
---
--- 期待される結果:
---   * LM (グループ無視): β > 0  → 誤った結論
---   * GLMM (ランダム切片): β < 0 → 正しい結論
---   * HBM (階層モデル): β < 0   → 正しい結論 + 不確実性付き
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import Data.Maybe (fromMaybe)
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import qualified DataFrame.Internal.Column    as DX
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified DataFrame.Internal.DataFrame as DXD
-import Hanalyze.Model.Core    (Band (..), coefficientsV)
-import Hanalyze.Model.LM      (fitPolyWithSmooth, SmoothFit (..), polyDesignMatrix)
-import Hanalyze.Model.GLMM    (fitLMEDataFrame, GLMMResult (..))
-import Hanalyze.Model.GLM     (Family (..), LinkFn (..))
-import qualified Numeric.LinearAlgebra as LA
-import Hanalyze.Stat.ModelSelect (lmPosteriorLogLiks, lmePosteriorLogLiks, waic, loo,
-                         WAICResult (..), LOOResult (..))
-
-import Hanalyze.MCMC.Core (Chain (..), chainVals, posteriorMean, posteriorSD,
-                  posteriorQuantile, acceptanceRate)
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, Distribution (..),
-                  buildModelGraph, perObsLogLiks)
-import Hanalyze.Stat.MCMC (ess)
-
-import Hanalyze.Viz.AnalysisReport
-  ( AnalysisReportConfig (..), defaultAnalysisConfig
-  , FitSummary (..), GLMMSummary (..), HBMRegSummary (..), SmoothData (..)
-  , ModelFit (..), NamedPlot (..)
-  , CompareEntry (..)
-  , mkFitSummary, mkGLMMSummary
-  , writeAnalysisReport, writeComparisonReport
-  )
-import Hanalyze.Viz.Core (PlotConfig (..))
-import Hanalyze.Viz.MCMC (mcmcDiagnostics, autocorrPlot)
-
--- ---------------------------------------------------------------------------
--- データ生成 (Simpson's Paradox)
--- ---------------------------------------------------------------------------
--- 各グループ内: y = α_g - 0.5·x + ノイズ  (負の傾き)
--- グループ A: α=2, x ∈ [0.2, 3.0]
--- グループ B: α=5, x ∈ [3.5, 6.0]
--- グループ C: α=8, x ∈ [6.4, 9.0]
--- 全体としては正の関係に見える (グループの x 平均と y 平均が正相関)
-
-dataA, dataB, dataC :: [(Double, Double)]
-dataA = zip
-  [0.2, 0.6, 1.0, 1.4, 1.8, 2.0, 2.4, 2.6, 2.8, 3.0]
-  -- y_clean: 1.90, 1.70, 1.50, 1.30, 1.10, 1.00, 0.80, 0.70, 0.60, 0.50
-  [1.93, 1.62, 1.55, 1.27, 1.18, 0.92, 0.85, 0.74, 0.55, 0.43]
-
-dataB = zip
-  [3.4, 3.8, 4.2, 4.5, 4.8, 5.0, 5.3, 5.6, 5.8, 6.0]
-  -- y_clean: 3.30, 3.10, 2.90, 2.75, 2.60, 2.50, 2.35, 2.20, 2.10, 2.00
-  [3.39, 3.04, 2.95, 2.62, 2.71, 2.41, 2.30, 2.27, 2.04, 1.91]
-
-dataC = zip
-  [6.4, 6.8, 7.0, 7.3, 7.5, 7.8, 8.0, 8.3, 8.5, 9.0]
-  -- y_clean: 4.80, 4.60, 4.50, 4.35, 4.25, 4.10, 4.00, 3.85, 3.75, 3.50
-  [4.86, 4.51, 4.58, 4.30, 4.19, 4.18, 3.93, 3.79, 3.62, 3.44]
-
-allXs :: [Double]
-allXs = map fst (dataA ++ dataB ++ dataC)
-
-allYs :: [Double]
-allYs = map snd (dataA ++ dataB ++ dataC)
-
-allGroups :: [Text]
-allGroups = replicate (length dataA) "A"
-         ++ replicate (length dataB) "B"
-         ++ replicate (length dataC) "C"
-
-mkDataFrame :: DXD.DataFrame
-mkDataFrame = DX.insertColumn "x"     (DX.fromList (allXs :: [Double]))
-            $ DX.insertColumn "y"     (DX.fromList (allYs :: [Double]))
-            $ DX.insertColumn "group" (DX.fromList (allGroups :: [Text]))
-            $ DX.empty
-
--- ---------------------------------------------------------------------------
--- HBM 階層モデル (varying intercept)
--- ---------------------------------------------------------------------------
-
-hbmModel :: ModelP ()
-hbmModel = do
-  muAlpha    <- sample "mu_alpha"    (Normal 0 10)
-  sigmaAlpha <- sample "sigma_alpha" (Exponential 1)
-  beta       <- sample "beta"        (Normal 0 10)
-  sigma      <- sample "sigma"       (Exponential 1)
-  alphaA     <- sample "alpha_A"     (Normal muAlpha sigmaAlpha)
-  alphaB     <- sample "alpha_B"     (Normal muAlpha sigmaAlpha)
-  alphaC     <- sample "alpha_C"     (Normal muAlpha sigmaAlpha)
-  mapM_ (\(x, y) -> let xC = realToFrac x
-                    in observe "y_A" (Normal (alphaA + beta * xC) sigma) [y])
-        dataA
-  mapM_ (\(x, y) -> let xC = realToFrac x
-                    in observe "y_B" (Normal (alphaB + beta * xC) sigma) [y])
-        dataB
-  mapM_ (\(x, y) -> let xC = realToFrac x
-                    in observe "y_C" (Normal (alphaC + beta * xC) sigma) [y])
-        dataC
-
--- ---------------------------------------------------------------------------
--- レポート 1: LM (プールド回帰、グループ無視)
--- ---------------------------------------------------------------------------
-
-reportLM :: IO (Maybe ModelFit)
-reportLM = do
-  let df = mkDataFrame
-  case fitPolyWithSmooth (CI 0.95) 100 df "x" "y" of
-    Nothing -> do putStrLn "  LM fit failed"; return Nothing
-    Just (res, sf) -> do
-      let beta = coefficientsV res
-          slope = LA.atIndex beta 1
-          intercept = LA.atIndex beta 0
-      printf "  LM:    intercept=%+.3f  slope=%+.3f  R²=%.3f\n"
-             intercept slope (computeR2Local df res)
-
-      -- WAIC/LOO: フラット事前で β,σ² の事後を解析的にサンプリング
-      gen <- createSystemRandom
-      let yVec = LA.fromList allYs
-          dm   = polyDesignMatrix 1 (V.fromList allXs)
-          nSamples = 1000 :: Int
-      llMat <- lmPosteriorLogLiks dm yVec res nSamples gen
-      let wRes = waic llMat
-          lRes = loo  llMat
-      printf "         WAIC=%.2f  LOO=%.2f  p_WAIC=%.2f\n"
-             (waicValue wRes) (looValue lRes) (waicPwaic wRes)
-
-      let smooth = SmoothData
-            { sdXs      = sfX sf
-            , sdYs      = sfFit sf
-            , sdLower   = sfLower sf
-            , sdUpper   = sfUpper sf
-            , sdHasBand = sfHasBand sf
-            }
-          summary = mkFitSummary Gaussian Identity [("x", 1)] (Just ("x", smooth)) res
-          summary' = summary
-            { fsModelType    = "LM (Pooled — group 無視)"
-            , fsFormula      = "y ~ α + β · x"
-            , fsLinkName     = "Identity (Gaussian)"
-            , fsModelSelect  = Just (wRes, lRes)
-            }
-          rptCfg = defaultAnalysisConfig
-                     "Simpson Paradox — LM (Pooled regression)"
-      writeAnalysisReport "simpson_lm.html" rptCfg df ["x"] "y"
-                          (RegFit summary') []
-      putStrLn "  → simpson_lm.html"
-      return (Just (RegFit summary'))
-
--- ---------------------------------------------------------------------------
--- レポート 2: GLMM (LME, ランダム切片 by group)
--- ---------------------------------------------------------------------------
-
-reportGLMM :: IO (Maybe ModelFit)
-reportGLMM = do
-  let df = mkDataFrame
-  case fitLMEDataFrame [("x", 1)] "group" "y" df of
-    Nothing -> do putStrLn "  GLMM fit failed"; return Nothing
-    Just gr -> do
-      let beta = coefficientsV (glmmFixed gr)
-          slope = LA.atIndex beta 1
-          intercept = LA.atIndex beta 0
-      printf "  GLMM:  intercept=%+.3f  slope=%+.3f  σ²_u=%.3f  σ²=%.3f  ICC=%.3f\n"
-             intercept slope
-             (glmmRandVar gr) (glmmResidVar gr) (glmmICC gr)
-      mapM_ (\(g, b) -> printf "         BLUP[%s] = %+.3f\n" (T.unpack g) b)
-            (zip (V.toList (glmmGroups gr)) (V.toList (glmmBLUPs gr)))
-
-      -- WAIC/LOO (条件付き: BLUP 固定で β,σ² のみ事後サンプリング)
-      gen <- createSystemRandom
-      let groupLabels = V.toList (glmmGroups gr)
-          blupsList   = V.toList (glmmBLUPs gr)
-          blupMap     = zip groupLabels blupsList
-          offsets     = [ maybe 0 id (lookup g blupMap) | g <- allGroups ]
-          dm          = polyDesignMatrix 1 (V.fromList allXs)
-          yVec        = LA.fromList allYs
-          nSamples    = 1000 :: Int
-      llMat <- lmePosteriorLogLiks dm yVec offsets (glmmFixed gr) nSamples gen
-      let wRes = waic llMat
-          lRes = loo  llMat
-      printf "         WAIC=%.2f  LOO=%.2f  p_WAIC=%.2f  (条件付き: BLUP 固定)\n"
-             (waicValue wRes) (looValue lRes) (waicPwaic wRes)
-
-      -- 固定効果のみで smoothData を構築 (β_0 + β_1·x_grid)
-      let xMin = minimum allXs
-          xMax = maximum allXs
-          xExt = (xMax - xMin) * 0.1
-          grid = [xMin - xExt + i * (xMax - xMin + 2 * xExt) / 99 | i <- [0..99]]
-          ysGrid = [intercept + slope * x | x <- grid]
-          smooth = SmoothData
-            { sdXs      = grid
-            , sdYs      = ysGrid
-            , sdLower   = ysGrid
-            , sdUpper   = ysGrid
-            , sdHasBand = False
-            }
-          baseSummary = mkGLMMSummary Gaussian Identity [("x", 1)] "group"
-                                       (Just ("x", smooth)) gr
-          summary = baseSummary { gsModelSelect = Just (wRes, lRes) }
-          rptCfg = defaultAnalysisConfig
-                     "Simpson Paradox — GLMM (LME, random intercept by group)"
-      writeAnalysisReport "simpson_glmm.html" rptCfg df ["x"] "y"
-                          (MixFit summary) []
-      putStrLn "  → simpson_glmm.html"
-      return (Just (MixFit summary))
-
--- ---------------------------------------------------------------------------
--- レポート 3: HBM (階層ベイズ)
--- ---------------------------------------------------------------------------
-
-reportHBM :: IO (Maybe ModelFit)
-reportHBM = do
-  let df  = mkDataFrame
-      cfg = defaultNUTSConfig
-              { nutsIterations = 800
-              , nutsBurnIn     = 400
-              , nutsStepSize   = 0.05
-              , nutsMaxDepth   = 8
-              }
-      initP = Map.fromList
-                [ ("mu_alpha", 5.0), ("sigma_alpha", 2.0)
-                , ("beta", 0.0), ("sigma", 0.5)
-                , ("alpha_A", 2.0), ("alpha_B", 5.0), ("alpha_C", 8.0)
-                ]
-  gen <- createSystemRandom
-  chain <- nuts hbmModel cfg initP gen
-
-  let bMean = fromMaybe 0 (posteriorMean "beta" chain)
-      bSD   = fromMaybe 0 (posteriorSD   "beta" chain)
-  printf "  HBM:   β = %+.3f ± %.3f  (95%% CI: %+.3f, %+.3f)\n"
-         bMean bSD
-         (fromMaybe 0 (posteriorQuantile 0.025 "beta" chain))
-         (fromMaybe 0 (posteriorQuantile 0.975 "beta" chain))
-  mapM_ (\g -> let nm = "alpha_" <> g
-               in printf "         %-9s mean=%+.3f  sd=%.3f\n"
-                    (T.unpack nm)
-                    (fromMaybe 0 (posteriorMean nm chain))
-                    (fromMaybe 0 (posteriorSD   nm chain)))
-        ["A", "B", "C"]
-  printf "         受容率=%.1f%%\n" (acceptanceRate chain * 100)
-  let llMatPreview = [ perObsLogLiks hbmModel ps | ps <- chainSamples chain ]
-      wPrev = waic llMatPreview
-      lPrev = loo  llMatPreview
-  printf "         WAIC=%.2f  LOO=%.2f  p_WAIC=%.2f\n"
-         (waicValue wPrev) (looValue lPrev) (waicPwaic wPrev)
-
-  -- Smooth: 全体曲線 (mu_alpha + beta * x) を信用区間付きで描画
-  let alphas = chainVals "mu_alpha" chain
-      betas  = chainVals "beta"     chain
-      xMin = minimum allXs
-      xMax = maximum allXs
-      xExt = (xMax - xMin) * 0.1
-      grid = [xMin - xExt + i * (xMax - xMin + 2 * xExt) / 99 | i <- [0..99]]
-      atX x = let ss = zipWith (\a b -> a + b * x) alphas betas
-                  sorted = sortAsc ss
-                  n      = length sorted
-                  qAt p  = sorted !! min (n-1) (max 0 (floor (p * fromIntegral n) :: Int))
-              in (qAt 0.5, qAt 0.025, qAt 0.975)
-      (ysMid, ysLo, ysHi) = unzip3 (map atX grid)
-      smooth = SmoothData
-        { sdXs      = grid
-        , sdYs      = ysMid
-        , sdLower   = ysLo
-        , sdUpper   = ysHi
-        , sdHasBand = True
-        }
-      -- HBM 用 FitSummary (回帰スタイル)
-      aMu = fromMaybe 0 (posteriorMean "mu_alpha" chain)
-      fitted = [aMu + bMean * x | x <- allXs]
-      resid  = zipWith (-) allYs fitted
-      yBar   = sum allYs / fromIntegral (length allYs)
-      tss    = sum [(y - yBar) ^ (2::Int) | y <- allYs]
-      rss    = sum [r ^ (2::Int) | r <- resid]
-      r2     = if tss < 1e-12 then 0 else 1 - rss / tss
-      -- WAIC/LOO: 各 MCMC サンプルで perObsLogLiks を評価
-      -- (HBM では log-likelihood をモデルから直接得られる)
-      llMatHBM = [ perObsLogLiks hbmModel ps | ps <- chainSamples chain ]
-      wRes = waic llMatHBM
-      lRes = loo  llMatHBM
-      fs = FitSummary
-             { fsModelType    = "Hierarchical Bayesian Regression (HBM)"
-             , fsFormula      = "y_g ~ α_g + β · x,  α_g ~ N(μ_α, σ_α)"
-             , fsCoeffs       = [("μ_α (全体平均)", aMu), ("β (傾き)", bMean)]
-             , fsR2           = r2
-             , fsR2Label      = "R² (全体平均線)"
-             , fsFitted       = fitted
-             , fsResiduals    = resid
-             , fsLinkName     = "Normal (identity link)"
-             , fsXColDegs     = [("x", 1)]
-             , fsSmoothData   = Just ("x", smooth)
-             , fsModelSelect  = Just (wRes, lRes)
-             }
-      hs = HBMRegSummary
-             { hbmsFit           = fs
-             , hbmsModelGraph    = buildModelGraph hbmModel
-             , hbmsChain         = chain
-             , hbmsParams        = paramNames
-             , hbmsPosteriorRows = [ (n, fromMaybe 0 (posteriorMean n chain)
-                                    , fromMaybe 0 (posteriorSD   n chain)
-                                    , fromMaybe 0 (posteriorQuantile 0.025 n chain)
-                                    , fromMaybe 0 (posteriorQuantile 0.975 n chain))
-                                  | n <- paramNames ]
-             }
-      paramNames = ["mu_alpha", "sigma_alpha", "beta", "sigma",
-                    "alpha_A", "alpha_B", "alpha_C"]
-      diagCfg = PlotConfig "MCMC 診断 (KDE + トレース)" 760 320 Nothing Nothing Nothing
-      acfCfg  = PlotConfig "自己相関 (lag 0..40)" 760 220 Nothing Nothing Nothing
-      diagPlot = NamedPlot "vl-hbm-diag" "MCMC 診断 (β / α_g / σ)"
-                   (mcmcDiagnostics diagCfg ["beta", "alpha_A", "alpha_B", "alpha_C", "sigma"] chain)
-      acfPlot  = NamedPlot "vl-hbm-acf" "パラメータ別 自己相関"
-                   (autocorrPlot acfCfg 40 ["beta", "alpha_A", "alpha_B", "alpha_C"] chain)
-      rptCfg = defaultAnalysisConfig
-                 "Simpson Paradox — HBM (Hierarchical Bayesian)"
-
-  writeAnalysisReport "simpson_hbm.html" rptCfg df ["x"] "y"
-                       (HBMFit hs) [diagPlot, acfPlot]
-  putStrLn "  → simpson_hbm.html"
-  return (Just (HBMFit hs))
-
-sortAsc :: [Double] -> [Double]
-sortAsc xs = let go [] = []
-                 go (p:rest) = go [x | x <- rest, x <= p]
-                            ++ [p]
-                            ++ go [x | x <- rest, x > p]
-             in go xs
-
--- ---------------------------------------------------------------------------
--- 解析的に R² を計算 (Main.hs の res に R² が含まれていない場合の保険)
--- ---------------------------------------------------------------------------
-
-computeR2Local :: DXD.DataFrame -> a -> Double
-computeR2Local _ _ = 0  -- mkFitSummary が R² を上書きするので未使用
-
--- ---------------------------------------------------------------------------
--- main
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  シンプソンのパラドックス: LM vs GLMM vs HBM"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  printf "  3 グループ (A, B, C) × 10 観測 = N=%d\n" (length allXs)
-  putStrLn "  各グループ内: 真の傾き β_within = -0.5"
-  putStrLn "  グループ無視: 見かけの傾き β_pooled ≈ +0.5  ← パラドックス"
-  putStrLn ""
-
-  putStrLn "[1] LM (Pooled) — グループを無視した単回帰:"
-  mLm   <- reportLM
-  putStrLn ""
-
-  putStrLn "[2] GLMM (LME) — グループをランダム切片として導入:"
-  mGlmm <- reportGLMM
-  putStrLn ""
-
-  putStrLn "[3] HBM (Hierarchical) — α_g を階層的に推定 + 不確実性:"
-  mHbm  <- reportHBM
-  putStrLn ""
-
-  -- 統合比較レポート (LM/GLMM/HBM が揃っていれば生成)
-  case (mLm, mGlmm, mHbm) of
-    (Just lm, Just glmm, Just hbm) -> do
-      putStrLn "[4] 統合比較レポート — LM/GLMM/HBM を 1 つの HTML に並べ:"
-      let entries =
-            [ CompareEntry "LM (Pooled)"       "#e41a1c" lm     -- 赤
-            , CompareEntry "GLMM (LME)"        "#377eb8" glmm   -- 青
-            , CompareEntry "HBM (Hierarchical)" "#4daf4a" hbm   -- 緑
-            ]
-          rptCfg = defaultAnalysisConfig
-                     "Simpson Paradox — LM vs GLMM vs HBM 比較レポート"
-      writeComparisonReport "simpson_compare.html" rptCfg
-                            mkDataFrame ["x"] "y" entries
-      putStrLn "  → simpson_compare.html"
-      putStrLn ""
-    _ -> putStrLn "  [統合比較レポート] 一部モデルの fit に失敗したためスキップ"
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  結果: LM は β > 0 (誤った正の傾き)、"
-  putStrLn "        GLMM/HBM は β < 0 (正しい負の傾き) を回復する"
-  putStrLn "  比較レポート simpson_compare.html で 3 モデルの予測曲線・係数・"
-  putStrLn "  WAIC/LOO を一覧できる"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/SliceDemo.hs b/demo/bayesian/SliceDemo.hs
deleted file mode 100644
--- a/demo/bayesian/SliceDemo.hs
+++ /dev/null
@@ -1,83 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | Slice sampler のデモ (Phase J3)。
---
--- Slice sampling (Neal 2003) はステップサイズ調整不要で、
--- log-density を評価できれば任意分布から sample できる univariate 法。
--- 多変量モデルは coordinate-wise sweep で扱う。
---
--- ここでは MH/NUTS と同じ Normal モデルで比較し、Slice の利点
--- (受容率調整不要、概ね高 ESS) を確認する。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.MCMC.MH    (metropolis, defaultMCMCConfig, MCMCConfig (..))
-import Hanalyze.MCMC.NUTS  (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.MCMC.Slice (slice, defaultSliceConfig, SliceConfig (..))
-import Hanalyze.MCMC.Core  (acceptanceRate)
-import Hanalyze.Model.HBM  (ModelP, sample, observe, Distribution (..))
-import Hanalyze.Viz.MCMC   (printPosteriorSummary)
-
-simpleModel :: ModelP ()
-simpleModel = do
-  mu  <- sample "mu"    (Normal 0 5)
-  sig <- sample "sigma" (HalfNormal 2)
-  observe "y" (Normal mu sig)
-    [1.2, 0.9, 1.4, 0.7, 1.1, 1.0, 1.3, 0.95, 1.05, 1.15,
-     0.85, 1.25, 0.95, 1.18, 1.02]
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Slice sampler vs Metropolis vs NUTS (Phase J3)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  gen <- createSystemRandom
-  let init0 = Map.fromList [("mu", 1.0), ("sigma", 0.2)]
-
-  -- ── Slice ──
-  putStrLn "[1] Slice sampler (1000 iter sweep, 200 burn-in)"
-  let scfg = (defaultSliceConfig ["mu", "sigma"])
-               { sliceIterations = 1000
-               , sliceBurnIn     = 200
-               , sliceWidths     = Map.fromList
-                   [("mu", 0.5), ("sigma", 0.2)]
-               }
-  chSlice <- slice simpleModel scfg init0 gen
-  printPosteriorSummary ["mu", "sigma"] [chSlice]
-  printf "  受容数 (sweep 内全 update のうち accept): %d\n"
-         (case acceptanceRate chSlice of
-            r -> round (r * 100 * 2 :: Double) :: Int)
-  putStrLn ""
-
-  -- ── Metropolis ──
-  putStrLn "[2] Random Walk Metropolis (1500 iter, 500 burn-in)"
-  let mcfg = (defaultMCMCConfig ["mu", "sigma"])
-               { mcmcIterations = 1500
-               , mcmcBurnIn     = 500
-               , mcmcStepSizes  = Map.fromList
-                   [("mu", 0.1), ("sigma", 0.05)]
-               }
-  chMH <- metropolis simpleModel mcfg init0 gen
-  printPosteriorSummary ["mu", "sigma"] [chMH]
-  printf "  受容率: %.1f%%\n" (acceptanceRate chMH * 100)
-  putStrLn ""
-
-  -- ── NUTS ──
-  putStrLn "[3] NUTS (1000 iter, 500 burn-in)"
-  let ncfg = defaultNUTSConfig
-               { nutsIterations = 1000
-               , nutsBurnIn     = 500
-               , nutsStepSize   = 0.1
-               }
-  chNUTS <- nuts simpleModel ncfg init0 gen
-  printPosteriorSummary ["mu", "sigma"] [chNUTS]
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Slice sampler が動作 (ステップサイズ自動調整、勾配不要)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/SummaryDemo.hs b/demo/bayesian/SummaryDemo.hs
deleted file mode 100644
--- a/demo/bayesian/SummaryDemo.hs
+++ /dev/null
@@ -1,100 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | Posterior summary table (az.summary 相当) のデモ。
---
--- 単一チェーン: mean / sd / 94% HDI / ESS
--- 多チェーン:    + R-hat (split R-hat、< 1.01 で収束)
-module Main where
-
-import qualified Data.Map.Strict as Map
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.MCMC.NUTS (nuts, nutsChains, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, Distribution (..))
-import Hanalyze.Viz.MCMC (printPosteriorSummary, posteriorSummaryFile,
-                 tracePlotHDIFile, rankPlotFile, ppcPlotFile,
-                 pairScatterDivFile)
-import Hanalyze.Stat.PosteriorPredictive (posteriorPredictive)
-import Hanalyze.Viz.Core (defaultConfig, OutputFormat (..), PlotConfig (..))
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 1000
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.1
-        }
-
-obsData :: [Double]
-obsData =
-  [1.2, 0.9, 1.4, 0.7, 1.1, 1.0, 1.3, 0.95, 1.05, 1.15,
-   0.85, 1.25, 0.95, 1.18, 1.02]
-
-simpleModel :: ModelP ()
-simpleModel = do
-  mu  <- sample "mu"    (Normal 0 5)
-  sig <- sample "sigma" (HalfNormal 2)
-  observe "y" (Normal mu sig) obsData
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Posterior summary (az.summary 相当)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  gen <- createSystemRandom
-
-  -- ── 単一チェーン ──
-  putStrLn "[1] 単一チェーン"
-  ch <- nuts simpleModel cfg
-              (Map.fromList [("mu", 1), ("sigma", 1)]) gen
-  printPosteriorSummary ["mu", "sigma"] [ch]
-  putStrLn ""
-
-  -- ── 多チェーン (R-hat 付き) ──
-  putStrLn "[2] 多チェーン (R-hat 付き)"
-  chs <- nutsChains simpleModel cfg 4
-                    (Map.fromList [("mu", 1), ("sigma", 1)]) gen
-  printPosteriorSummary ["mu", "sigma"] chs
-  putStrLn ""
-
-  -- ── HTML 出力 ──
-  posteriorSummaryFile "summary-single.html"
-    "Posterior summary (single chain)" ["mu", "sigma"] [ch]
-  posteriorSummaryFile "summary-multi.html"
-    "Posterior summary (4 chains, R-hat)" ["mu", "sigma"] chs
-  putStrLn "  → summary-single.html / summary-multi.html"
-  putStrLn ""
-
-  -- ── HDI 帯付きトレース ──
-  let traceCfg = (defaultConfig "Trace with 94% HDI")
-                   { plotWidth = 700, plotHeight = 90 }
-  tracePlotHDIFile HTML "trace-hdi.html" traceCfg 0.94 ["mu", "sigma"] ch
-  putStrLn "  → trace-hdi.html (HDI 帯付きトレース)"
-
-  -- ── Rank plot (多チェーン収束診断) ──
-  let rankCfg = (defaultConfig "Rank plot — chain uniformity")
-                  { plotWidth = 700, plotHeight = 100 }
-  rankPlotFile HTML "rank.html" rankCfg 20 ["mu", "sigma"] chs
-  putStrLn "  → rank.html (Rank plot, 4 chains)"
-
-  -- ── Posterior predictive check ──
-  preds <- posteriorPredictive simpleModel ch gen
-  let yReps = [Map.findWithDefault [] "y" m | m <- preds]
-  let ppcCfg = (defaultConfig "Posterior predictive check (y)")
-                 { plotWidth = 700, plotHeight = 280 }
-  ppcPlotFile HTML "ppc.html" ppcCfg obsData yReps 50
-  putStrLn "  → ppc.html (PP check, 観測 vs 予測 50 ドロー)"
-
-  -- ── Divergence overlay (Phase F5; Phase G4 で NUTS から自動取得予定) ──
-  -- 現状はモック divergent indices [10, 50, 200, 500] で描画機構を検証。
-  let divCfg = (defaultConfig "Pair plot — divergence overlay (mock)")
-                 { plotWidth = 500, plotHeight = 400 }
-      mockDiv = [10, 50, 200, 500]
-  pairScatterDivFile HTML "pair-div.html" divCfg "mu" "sigma" ch mockDiv
-  putStrLn "  → pair-div.html (4 mock divergent points)"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Posterior summary が動作 (mean/sd/HDI/ESS/R-hat)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/TestHMCNUTS.hs b/demo/bayesian/TestHMCNUTS.hs
deleted file mode 100644
--- a/demo/bayesian/TestHMCNUTS.hs
+++ /dev/null
@@ -1,141 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- 1次元ガウスモデルで HMC / NUTS の動作を確認する。
---
--- モデル: μ ~ Normal(0, 10), y | μ ~ Normal(μ, 1), data = [1, 2, 3]
--- 解析的事後分布: μ|y ~ Normal(μ_post, σ_post)
---   σ_post^2 = 1 / (1/10^2 + 3/1^2) ≈ 0.332  → σ_post ≈ 0.577
---   μ_post   = σ_post^2 * (0/10^2 + 6/1)    ≈ 1.993
-module Main where
-
-import qualified Data.Map.Strict as Map
-import qualified Data.Text as T
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.Model.HBM
-import Hanalyze.MCMC.Core (Chain (..), chainVals, posteriorMean, posteriorSD, acceptanceRate)
-import Hanalyze.MCMC.HMC
-import Hanalyze.MCMC.NUTS
-import Hanalyze.Stat.Distribution ()
-import Hanalyze.Stat.MCMC (rhat)
-
--- モデル1: μ のみ (unconstrained)
-gaussModel :: [Double] -> ModelP ()
-gaussModel ys = do
-  mu <- sample "mu" (Normal 0 10)
-  observe "y" (Normal mu 1) ys
-
--- モデル2: sigma ~ Exponential(1) (constrained: sigma > 0)
--- データ: [1,2,3], 真値 sigma=1
--- 解析解は複雑だが sigma の事後平均は 1 付近に収束するはず
-scaledModel :: [Double] -> ModelP ()
-scaledModel ys = do
-  sigma <- sample "sigma" (Exponential 1)
-  observe "y" (Normal 0 sigma) ys
-
-observed :: [Double]
-observed = [1.0, 2.0, 3.0]
-
-initP :: Map.Map T.Text Double
-initP = Map.fromList [("mu", 0.0)]
-
-initP2 :: Map.Map T.Text Double
-initP2 = Map.fromList [("sigma", 1.5)]
-
-m :: ModelP ()
-m = gaussModel observed
-
-m2 :: ModelP ()
-m2 = scaledModel observed
-
-main :: IO ()
-main = do
-  gen <- createSystemRandom
-
-  putStrLn "=== HMC (unconstrained μ) ==="
-  let hmcCfg = defaultHMCConfig
-        { hmcIterations    = 3000
-        , hmcBurnIn        = 500
-        , hmcStepSize      = 0.3
-        , hmcLeapfrogSteps = 5
-        }
-  ch1 <- hmc m hmcCfg initP gen
-  printf "  acceptance rate : %.3f\n"  (acceptanceRate ch1)
-  printf "  posterior mean  : %.4f  (expect ≈ 1.993)\n"
-    (maybe 0 id $ posteriorMean "mu" ch1)
-  printf "  posterior SD    : %.4f  (expect ≈ 0.577)\n"
-    (maybe 0 id $ posteriorSD   "mu" ch1)
-
-  putStrLn ""
-  putStrLn "=== NUTS (unconstrained μ) ==="
-  let nutsCfg = defaultNUTSConfig
-        { nutsIterations = 3000
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.3
-        }
-  ch2 <- nuts m nutsCfg initP gen
-  printf "  acceptance rate : %.3f\n"  (acceptanceRate ch2)
-  printf "  posterior mean  : %.4f  (expect ≈ 1.993)\n"
-    (maybe 0 id $ posteriorMean "mu" ch2)
-  printf "  posterior SD    : %.4f  (expect ≈ 0.577)\n"
-    (maybe 0 id $ posteriorSD   "mu" ch2)
-
-  -- 制約付きパラメータのテスト: sigma ~ Exponential (正値制約)
-  putStrLn ""
-  putStrLn "=== HMC (constrained σ ~ Exponential, PositiveT) ==="
-  let hmcCfg2 = defaultHMCConfig
-        { hmcIterations    = 3000
-        , hmcBurnIn        = 500
-        , hmcStepSize      = 0.1
-        , hmcLeapfrogSteps = 10
-        }
-  ch3 <- hmc m2 hmcCfg2 initP2 gen
-  printf "  acceptance rate : %.3f\n"  (acceptanceRate ch3)
-  printf "  posterior mean σ: %.4f  (expect > 0)\n"
-    (maybe 0 id $ posteriorMean "sigma" ch3)
-  printf "  posterior SD σ  : %.4f\n"
-    (maybe 0 id $ posteriorSD "sigma" ch3)
-  let samples3 = map (Map.findWithDefault 0 "sigma") (chainSamples ch3)
-      minSigma = minimum samples3
-  printf "  min σ sample    : %.6f  (must be > 0)\n" minSigma
-
-  putStrLn ""
-  putStrLn "=== NUTS (constrained σ ~ Exponential, PositiveT) ==="
-  let nutsCfg2 = defaultNUTSConfig
-        { nutsIterations = 3000
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.1
-        }
-  ch4 <- nuts m2 nutsCfg2 initP2 gen
-  printf "  acceptance rate : %.3f\n"  (acceptanceRate ch4)
-  printf "  posterior mean σ: %.4f  (expect > 0)\n"
-    (maybe 0 id $ posteriorMean "sigma" ch4)
-  printf "  posterior SD σ  : %.4f\n"
-    (maybe 0 id $ posteriorSD "sigma" ch4)
-  let samples4 = map (Map.findWithDefault 0 "sigma") (chainSamples ch4)
-      minSigma4 = minimum samples4
-  printf "  min σ sample    : %.6f  (must be > 0)\n" minSigma4
-
-  -- 並列チェーン + R-hat テスト
-  putStrLn ""
-  putStrLn "=== 4-chain NUTS (parallel) + split-R-hat ==="
-  putStrLn "  Model: μ ~ Normal(0,10), y|μ ~ Normal(μ,1), data=[1,2,3]"
-  let nutsCfgR = defaultNUTSConfig
-        { nutsIterations = 2000
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.3
-        }
-  chains <- nutsChains m nutsCfgR 4 initP gen
-  let muVals = map (chainVals "mu") chains
-      rhatMu = rhat muVals
-  mapM_ (\(i, ch) ->
-    printf "  chain %d: mean=%.4f  SD=%.4f  accept=%.3f\n"
-      (i :: Int)
-      (maybe 0 id $ posteriorMean "mu" ch)
-      (maybe 0 id $ posteriorSD   "mu" ch)
-      (acceptanceRate ch)
-    ) (zip [1..] chains)
-  case rhatMu of
-    Nothing -> putStrLn "  R-hat: N/A"
-    Just r  -> printf "  split-R-hat (μ): %.4f  (< 1.01 = converged)\n" r
diff --git a/demo/bayesian/TruncCensorDemo.hs b/demo/bayesian/TruncCensorDemo.hs
deleted file mode 100644
--- a/demo/bayesian/TruncCensorDemo.hs
+++ /dev/null
@@ -1,129 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | Truncated / Censored 分布のデモ。
---
--- - Truncated: 観測が範囲内のみで、範囲外は観測されない (打ち切り)。
---   推定で正規化定数を補正する必要がある。
--- - Censored: 範囲外の値もデータに含まれるが「しきい値以下/以上」とのみ判明
---   (Tobit 風)。CDF/SF を尤度に使う。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Data.Maybe (fromMaybe)
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.MCMC.Core (chainSamples, posteriorMean, posteriorSD,
-                  posteriorQuantile, acceptanceRate)
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, Distribution (..))
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 1500
-        , nutsBurnIn     = 500
-        , nutsStepSize   = 0.1
-        }
-
-prn :: String -> Double -> Double -> IO ()
-prn lbl m s = printf "    %-8s mean=%+.4f  sd=%.4f\n" lbl m s
-
--- ---------------------------------------------------------------------------
--- 例 1: Truncated Exponential (生存時間モデル、観測は [0, 5] のみ)
--- ---------------------------------------------------------------------------
--- 真値: Exponential(rate=0.5) を [0, 5] で truncate (観測終了時刻 5)。
--- 範囲外の長い生存は観測されない → 無視すると rate を過小推定 (生存時間を短く見積もる)。
-
-truncObs :: [Double]
-truncObs =
-  [0.5, 1.2, 2.0, 0.3, 4.5, 1.8, 0.8, 3.2, 2.5, 1.5,
-   0.4, 2.8, 4.2, 1.1, 0.9, 3.5, 2.1, 0.6, 1.7, 4.0]
-
--- truncate 補正あり版
-truncatedModel :: ModelP ()
-truncatedModel = do
-  rate <- sample "rate" (HalfNormal 2)
-  observe "y" (Truncated (Exponential rate) (Just 0) (Just 5)) truncObs
-
--- 補正なし版 (誤った推論)
-naiveModel :: ModelP ()
-naiveModel = do
-  rate <- sample "rate" (HalfNormal 2)
-  observe "y" (Exponential rate) truncObs
-
--- ---------------------------------------------------------------------------
--- 例 2: Censored Normal (Tobit 回帰 — 検出限界あり)
--- ---------------------------------------------------------------------------
--- データ: 真の値 N(3, 1.5) に対し、検出下限 = 1 (下限以下は 1 と記録される)
---   検出下限 1 で打ち切られた値: 1.0 (3 件)
---   普通に観測された値: 1.5..
-
-censObs :: [Double]
-censObs =
-  [1.0, 1.0, 1.0,                        -- 検出下限で打ち切り (真値は < 1)
-   1.5, 2.2, 3.3, 3.8, 4.1, 2.9, 3.5,
-   2.7, 4.0, 3.1, 3.9, 2.5, 4.2, 3.0]
-
-censoredModel :: ModelP ()
-censoredModel = do
-  mu  <- sample "mu"    (Normal 0 5)
-  sig <- sample "sigma" (HalfNormal 3)
-  observe "y" (Censored (Normal mu sig) (Just 1.0) Nothing) censObs
-
--- 単純に「1.0 を観測値」として扱う誤った版
-ignoreCensorModel :: ModelP ()
-ignoreCensorModel = do
-  mu  <- sample "mu"    (Normal 0 5)
-  sig <- sample "sigma" (HalfNormal 3)
-  observe "y" (Normal mu sig) censObs
-
--- ---------------------------------------------------------------------------
--- main
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Truncated / Censored 分布のデモ"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  gen <- createSystemRandom
-
-  -- ── 例 1: 片側 Truncated (生存時間モデル) ──
-  putStrLn "[1] Truncated Exponential (生存時間): y ~ Exp(rate) truncated to [0, 5]"
-  printf "    観測: %d 件、真値 rate=0.5 (= 平均生存 2.0)\n" (length truncObs)
-  ch1 <- nuts truncatedModel cfg
-              (Map.fromList [("rate", 0.5)]) gen
-  putStrLn "  Truncated 補正あり (正しいモデル):"
-  prn "rate" (fromMaybe 0 (posteriorMean "rate" ch1)) (fromMaybe 0 (posteriorSD "rate" ch1))
-  ch1n <- nuts naiveModel cfg
-                (Map.fromList [("rate", 0.5)]) gen
-  putStrLn "  Truncated 補正なし (= 普通の Exponential、誤推論):"
-  prn "rate" (fromMaybe 0 (posteriorMean "rate" ch1n)) (fromMaybe 0 (posteriorSD "rate" ch1n))
-  putStrLn "    (補正なしは rate を過大推定 → 生存時間を短く見積もる)"
-  putStrLn ""
-
-  -- ── 例 2: Censored ──
-  putStrLn "[2] Censored Normal: 検出下限 1.0 (Tobit 風)"
-  printf "    観測: 17 件中 3 件は y=1.0 (検出下限 = 真値は 1 未満だが分からない)\n"
-  ch2 <- nuts censoredModel cfg
-              (Map.fromList [("mu", 1.0), ("sigma", 1.0)]) gen
-  putStrLn "  Censored 補正あり (正しいモデル):"
-  prn "mu"    (fromMaybe 0 (posteriorMean "mu" ch2)) (fromMaybe 0 (posteriorSD "mu" ch2))
-  prn "sigma" (fromMaybe 0 (posteriorMean "sigma" ch2)) (fromMaybe 0 (posteriorSD "sigma" ch2))
-  ch2n <- nuts ignoreCensorModel cfg
-                (Map.fromList [("mu", 1.0), ("sigma", 1.0)]) gen
-  putStrLn "  Censored 補正なし (1.0 を真値扱い、誤推論):"
-  prn "mu"    (fromMaybe 0 (posteriorMean "mu" ch2n)) (fromMaybe 0 (posteriorSD "mu" ch2n))
-  prn "sigma" (fromMaybe 0 (posteriorMean "sigma" ch2n)) (fromMaybe 0 (posteriorSD "sigma" ch2n))
-  putStrLn "    (補正なしは μ を上方バイアス、σ を過小推定)"
-  putStrLn ""
-
-  putStrLn "  注: 両側 Truncated (区間 [a,b]) は log-density 不連続性が強く、"
-  putStrLn "      NUTS で収束が難しい場合がある (MH やリジェクション法を併用要)。"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Truncated / Censored が動作 (正しい推論で σ/μ のバイアス回避)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/bayesian/VIDemo.hs b/demo/bayesian/VIDemo.hs
deleted file mode 100644
--- a/demo/bayesian/VIDemo.hs
+++ /dev/null
@@ -1,224 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE ImpredicativeTypes #-}
--- | 変分推論 (ADVI) vs NUTS 比較デモ
---
--- 2 つのモデルで VI と NUTS を比較する。
---
--- モデル 1: Beta-Binomial (臨床試験)
---   p_ctrl ~ Beta(1,1),  y_ctrl ~ Binomial(50, p_ctrl),  観測: 18 回復
---   p_trt  ~ Beta(1,1),  y_trt  ~ Binomial(50, p_trt),   観測: 31 回復
---   → 解析解が存在するため精度の検証が可能
---
--- モデル 2: 階層正規モデル (3 校)
---   μ ~ Normal(0,100), τ ~ Exponential(0.1), θ_j ~ Normal(μ,τ)
---   → 強い相関がある事後分布で VI の限界を確認
---
-module Main where
-
-import Control.Monad (forM_)
-import qualified Data.Map.Strict as Map
-import qualified Data.Text as T
-import Data.Time.Clock (getCurrentTime, diffUTCTime)
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.Model.HBM
-import Hanalyze.Stat.Distribution ()
-import Hanalyze.MCMC.Core (chainVals, posteriorMean, posteriorSD)
-import Hanalyze.MCMC.NUTS (NUTSConfig (..), defaultNUTSConfig, nuts)
-import Hanalyze.Stat.VI
-
--- ---------------------------------------------------------------------------
--- モデル 1: Beta-Binomial (臨床試験)
--- ---------------------------------------------------------------------------
-
-nCtrl, kCtrl, nTrt, kTrt :: Int
-nCtrl = 50; kCtrl = 18
-nTrt  = 50; kTrt  = 31
-
-clinicalModel :: ModelP ()
-clinicalModel = do
-  pCtrl <- sample "p_ctrl" (Beta 1 1)
-  pTrt  <- sample "p_trt"  (Beta 1 1)
-  observe "y_ctrl" (Binomial nCtrl pCtrl) [fromIntegral kCtrl]
-  observe "y_trt"  (Binomial nTrt  pTrt)  [fromIntegral kTrt]
-
-m1 :: ModelP ()
-m1 = clinicalModel
-
-m2 :: ModelP ()
-m2 = schoolModelI schoolData
-
--- 解析解: Beta(1,1) + Binomial → Beta(1+k, 1+n-k)
-betaMean :: Int -> Int -> Double
-betaMean k n = fromIntegral (1 + k) / fromIntegral (2 + n)
-
-betaSD :: Int -> Int -> Double
-betaSD k n =
-  let a = fromIntegral (1 + k); b = fromIntegral (1 + n - k); s = a + b
-  in sqrt (a * b / (s * s * (s + 1)))
-
--- ---------------------------------------------------------------------------
--- モデル 2: 階層正規モデル (3 校)
--- ---------------------------------------------------------------------------
-
-sigma :: Double
-sigma = 5.0
-
-schoolData :: [[Double]]
-schoolData =
-  [ [72, 68, 75, 71]
-  , [85, 88, 82, 90]
-  , [61, 65, 58, 63]
-  ]
-
--- schoolModel を添字付きで作る
-schoolModelI :: [[Double]] -> ModelP ()
-schoolModelI groupData = do
-  mu  <- sample "mu"  (Normal 0 100)
-  tau <- sample "tau" (Exponential 0.1)
-  forM_ (zip [1::Int ..] groupData) $ \(j, ys) -> do
-    theta <- sample (T.pack ("theta_" ++ show j)) (Normal mu tau)
-    observe (T.pack ("y_" ++ show j)) (Normal theta (realToFrac sigma)) ys
-
--- ---------------------------------------------------------------------------
--- ユーティリティ
--- ---------------------------------------------------------------------------
-
-timed :: IO a -> IO (a, Double)
-timed action = do
-  t0 <- getCurrentTime
-  x  <- action
-  t1 <- getCurrentTime
-  return (x, realToFrac (diffUTCTime t1 t0))
-
--- ---------------------------------------------------------------------------
--- Main
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  gen <- createSystemRandom
-
-  -- ════════════════════════════════════════════════════════════════════════
-  putStrLn "=== モデル 1: Beta-Binomial (臨床試験) ==="
-  putStrLn "    解析解が存在するモデルで VI の精度を検証する"
-  putStrLn ""
-
-  let initP1 = Map.fromList [("p_ctrl", 0.5 :: Double), ("p_trt", 0.5)]
-
-  -- VI
-  let viCfg1 = defaultVIConfig
-                 { viIterations = 500
-                 , viSamples    = 10
-                 , viNumDraws   = 5000
-                 }
-  (viRes1, tVI1) <- timed $ advi m1 viCfg1 initP1 gen
-
-  -- NUTS
-  let nutsCfg1 = defaultNUTSConfig
-                   { nutsIterations = 2000
-                   , nutsBurnIn     = 500
-                   , nutsStepSize   = 0.3
-                   }
-  (nutsC1, tNUTS1) <- timed $ nuts m1 nutsCfg1 initP1 gen
-
-  -- 解析解
-  let analCtrlMu = betaMean kCtrl nCtrl;  analCtrlSD = betaSD kCtrl nCtrl
-      analTrtMu  = betaMean kTrt  nTrt;   analTrtSD  = betaSD kTrt  nTrt
-
-  let get f p = Map.findWithDefault 0 p (f viRes1)
-
-  printf "  %-12s  %-12s  %-12s  %-12s\n"
-    ("" :: String) ("p_ctrl" :: String) ("p_trt" :: String) ("時間" :: String)
-  printf "  %-12s  mean=%.4f SD=%.4f  mean=%.4f SD=%.4f  %.3fs\n"
-    ("VI" :: String)
-    (get viPostMeans "p_ctrl") (get viPostSDs "p_ctrl")
-    (get viPostMeans "p_trt")  (get viPostSDs "p_trt")
-    tVI1
-  printf "  %-12s  mean=%.4f SD=%.4f  mean=%.4f SD=%.4f  %.3fs\n"
-    ("NUTS" :: String)
-    (maybe 0 id $ posteriorMean "p_ctrl" nutsC1)
-    (maybe 0 id $ posteriorSD   "p_ctrl" nutsC1)
-    (maybe 0 id $ posteriorMean "p_trt"  nutsC1)
-    (maybe 0 id $ posteriorSD   "p_trt"  nutsC1)
-    tNUTS1
-  printf "  %-12s  mean=%.4f SD=%.4f  mean=%.4f SD=%.4f\n"
-    ("解析解" :: String)
-    analCtrlMu analCtrlSD analTrtMu analTrtSD
-  putStrLn ""
-
-  -- ELBO 収束の表示
-  putStrLn "  ELBO 収束 (初期 → 最終):"
-  let elboHist = viElboHistory viRes1
-      n        = length elboHist
-      steps    = [1, n `div` 4, n `div` 2, 3 * n `div` 4, n]
-  forM_ steps $ \i ->
-    when (i > 0 && i <= n) $
-      printf "    iter %4d: ELBO = %.3f\n" i (elboHist !! (i - 1))
-  putStrLn ""
-
-  -- P(p_trt > p_ctrl) の推定
-  let vDraws1  = viDraws viRes1
-      diffVI   = [ Map.findWithDefault 0 "p_trt"  d
-                 - Map.findWithDefault 0 "p_ctrl" d | d <- vDraws1 ]
-      probVI   = fromIntegral (length (filter (> 0) diffVI)) / fromIntegral (length diffVI) :: Double
-      diffNUTS = zipWith (-) (chainVals "p_trt" nutsC1) (chainVals "p_ctrl" nutsC1)
-      probNUTS = fromIntegral (length (filter (> 0) diffNUTS)) / fromIntegral (length diffNUTS) :: Double
-
-  printf "  P(p_trt > p_ctrl): VI=%.4f  NUTS=%.4f\n" probVI probNUTS
-  putStrLn ""
-
-  -- ════════════════════════════════════════════════════════════════════════
-  putStrLn "=== モデル 2: 階層正規モデル (3 校) ==="
-  putStrLn "    相関の強い事後分布で VI の近似誤差を確認する"
-  putStrLn ""
-
-  let initP2 = Map.fromList
-                 [ ("mu", 73.0), ("tau", 10.0)
-                 , ("theta_1", 71.5), ("theta_2", 86.25), ("theta_3", 61.75)
-                 ]
-      names2 = sampleNames m2
-
-  -- VI
-  let viCfg2 = defaultVIConfig
-                 { viIterations   = 1000
-                 , viSamples      = 10
-                 , viNumDraws     = 5000
-                 , viLearningRate = 0.05
-                 }
-  (viRes2, tVI2) <- timed $ advi m2 viCfg2 initP2 gen
-
-  -- NUTS
-  let nutsCfg2 = defaultNUTSConfig
-                   { nutsIterations = 2000
-                   , nutsBurnIn     = 500
-                   , nutsStepSize   = 0.05
-                   }
-  (nutsC2, tNUTS2) <- timed $ nuts m2 nutsCfg2 initP2 gen
-
-  putStrLn "  事後サマリー:"
-  printf "  %-12s  %8s  %8s  |  %8s  %8s\n"
-    ("param" :: String) ("VI 平均" :: String) ("VI SD" :: String)
-    ("NUTS 平均" :: String) ("NUTS SD" :: String)
-  forM_ names2 $ \p ->
-    printf "  %-12s  %8.3f  %8.3f  |  %8.3f  %8.3f\n"
-      (T.unpack p)
-      (Map.findWithDefault 0 p (viPostMeans viRes2))
-      (Map.findWithDefault 0 p (viPostSDs   viRes2))
-      (maybe 0 id $ posteriorMean p nutsC2)
-      (maybe 0 id $ posteriorSD   p nutsC2)
-  putStrLn ""
-
-  printf "  実行時間: VI=%.3fs  NUTS=%.3fs  (VI は NUTS の %.1f 倍速)\n"
-    tVI2 tNUTS2 (tNUTS2 / tVI2)
-  putStrLn ""
-  putStrLn "  注: 平均場 VI は各パラメータ間の相関を無視するため、"
-  putStrLn "      階層モデルでは SD を過小評価する傾向がある (過信)"
-  putStrLn ""
-  putStrLn "完了"
-
-when :: Bool -> IO () -> IO ()
-when True  action = action
-when False _      = return ()
diff --git a/demo/bayesian/ZeroInflatedDemo.hs b/demo/bayesian/ZeroInflatedDemo.hs
deleted file mode 100644
--- a/demo/bayesian/ZeroInflatedDemo.hs
+++ /dev/null
@@ -1,94 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- | ZeroInflatedPoisson のデモ (Phase H3)。
---
--- 真値: ψ = 0.4 (40% は構造的ゼロ), λ = 5
--- 期待 mean = (1-ψ)λ = 3.0
--- データには余分なゼロが多く出現 → 普通の Poisson モデルだと λ を低く推定する。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-import qualified System.Random.MWC as MWCBase
-
-import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
-import Hanalyze.Model.HBM (ModelP, sample, observe, Distribution (..))
-import Hanalyze.Viz.MCMC (printPosteriorSummary, posteriorSummaryFile)
-
-cfg :: NUTSConfig
-cfg = defaultNUTSConfig
-        { nutsIterations = 800
-        , nutsBurnIn     = 400
-        , nutsStepSize   = 0.1
-        , nutsMaxDepth   = 6
-        }
-
--- 真値 ψ=0.4, λ=5 で生成
-genZIP :: Int -> Double -> Double -> IO [Double]
-genZIP n psi lam = do
-  gen <- createSystemRandom
-  let drawOne = do
-        u <- MWCBase.uniform gen :: IO Double
-        if u < psi
-          then return 0
-          else do
-            -- Knuth Poisson
-            let go k p = do
-                  v <- MWCBase.uniform gen :: IO Double
-                  let p' = p * v
-                  if p' < exp (-lam)
-                    then return (fromIntegral k)
-                    else go (k+1) p'
-            go (0 :: Int) (1 :: Double)
-  mapM (const drawOne) [1 .. n]
-
-poissonModel :: [Double] -> ModelP ()
-poissonModel ys = do
-  lam <- sample "lambda" (Gamma 1 0.1)
-  observe "y" (Poisson lam) ys
-
-zipModel :: [Double] -> ModelP ()
-zipModel ys = do
-  psi <- sample "psi"    (Beta 1 1)        -- 一様事前
-  lam <- sample "lambda" (Gamma 1 0.1)
-  observe "y" (ZeroInflatedPoisson psi lam) ys
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ZeroInflatedPoisson vs Poisson (ゼロ過剰, Phase H3)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  putStrLn "真値: ψ = 0.4, λ = 5  → 観測平均 ≈ (1-0.4)*5 = 3.0"
-  ys <- genZIP 100 0.4 5
-  let nZero = length (filter (== 0) ys)
-      n     = length ys
-      muSm  = sum ys / fromIntegral n
-  printf "観測 (n=%d): 平均 = %.2f, ゼロ件数 = %d (%.0f%%)\n"
-         n muSm nZero (100 * fromIntegral nZero / fromIntegral n :: Double)
-  putStrLn ""
-
-  gen <- createSystemRandom
-
-  putStrLn "[1] Poisson (ゼロ過剰を捕えない)"
-  ch1 <- nuts (poissonModel ys) cfg
-              (Map.fromList [("lambda", 3)]) gen
-  printPosteriorSummary ["lambda"] [ch1]
-  putStrLn ""
-
-  putStrLn "[2] ZeroInflatedPoisson (構造的ゼロを分離)"
-  ch2 <- nuts (zipModel ys) cfg
-              (Map.fromList [("psi", 0.3), ("lambda", 5)]) gen
-  printPosteriorSummary ["psi", "lambda"] [ch2]
-  putStrLn ""
-
-  posteriorSummaryFile "zip-poisson.html" "Poisson"          ["lambda"]      [ch1]
-  posteriorSummaryFile "zip-zip.html"     "ZeroInflated Poi" ["psi","lambda"] [ch2]
-  putStrLn "  → zip-{poisson,zip}.html"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ ZIP で ψ ≈ 0.4 (構造的ゼロ率) と λ ≈ 5 を分離回復"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/doe-optim/BayesOptDemo.hs b/demo/doe-optim/BayesOptDemo.hs
deleted file mode 100644
--- a/demo/doe-optim/BayesOptDemo.hs
+++ /dev/null
@@ -1,71 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Phase V: Bayesian Optimization のデモ。
---
--- 1. 単一目的 BO で sin 関数の最小値を探す
--- 2. 多目的 BO (NSGA-II 内側) で 2 目的問題の Pareto 近似を構築
-module Main where
-
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.Optim.BayesOpt
-import Hanalyze.Model.GP (Kernel (..))
-
--- 単一目的: f(x) = sin(3x) + (x - 2)² / 5
--- 真の最小は x ≈ 0.96 で y ≈ -0.789
-trueF :: Double -> Double
-trueF x = sin (3 * x) + (x - 2) ^ (2 :: Int) / 5
-
--- 多目的:
---   y_1 = x²
---   y_2 = (x - 2)²
-trueMO :: [Double] -> [Double]
-trueMO [x] = [x * x, (x - 2) ^ (2 :: Int)]
-trueMO _ = error "1D"
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Phase V: Bayesian Optimization"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  gen <- createSystemRandom
-
-  -- ── 1. 単一目的 BO ──
-  putStrLn "[1] 単一目的 BO: f(x) = sin(3x) + (x-2)²/5 on [0, 4]"
-  putStrLn "    真の最小 ≈ (0.96, -0.789)"
-  let cfg = defaultBayesOptConfig
-              { boIterations = 15
-              , boInitPoints = 4
-              , boUCBBeta    = 2.0
-              }
-  (history, (xBest, yBest)) <- bayesOpt cfg (return . trueF) (0, 4) gen
-  printf "  評価回数: %d (= 初期 %d + BO %d)\n"
-         (length history) (boInitPoints cfg) (boIterations cfg)
-  printf "  推定最良: x* = %.4f, y* = %.4f\n" xBest yBest
-  printf "  履歴の最終 5 評価:\n"
-  mapM_ (\(x, y) -> printf "    (%.4f, %.4f)\n" x y)
-        (drop (length history - 5) history)
-  putStrLn ""
-
-  -- ── 2. 多目的 BO ──
-  putStrLn "[2] 多目的 BO: y_1 = x², y_2 = (x-2)² on [0, 2]"
-  putStrLn "    真の Pareto front: x ∈ [0, 2] で連続"
-  history2 <- bayesOptMOWithNSGA 12 5 RBF (return . trueMO)
-                                 [(0, 2)] gen
-  printf "  評価回数: %d\n" (length history2)
-  printf "  最終 5 評価:\n"
-  mapM_ (\(x, y) -> printf "    x=%s, y=%s\n"
-                           (show (map round3 x))
-                           (show (map round3 y)))
-        (drop (length history2 - 5) history2)
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Bayesian Optimization が動作 (単目的 BO + NSGA-II 内側)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-
-  where
-    round3 :: Double -> Double
-    round3 v = fromIntegral (round (v * 1000) :: Int) / 1000
diff --git a/demo/doe-optim/CISImplantWorkflowDemo.hs b/demo/doe-optim/CISImplantWorkflowDemo.hs
deleted file mode 100644
--- a/demo/doe-optim/CISImplantWorkflowDemo.hs
+++ /dev/null
@@ -1,395 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns      #-}
--- | CMOS Image Sensor (CIS) PD implant 工程の workflow デモ。
---
--- マニュアル `docs/manual/semiconductor-design-workflow.md` の付録 B 相当を、
--- ユーザ実用ケース (3 因子 + tilt 離散 + CIS 応答) で動作可能形にしたもの。
---
--- ## モチーフ
---
--- CMOS Image Sensor のフォトダイオード (PD) implant 工程を題材とし、 注入条件
--- (dose、 energy、 tilt) が画素特性に与える影響を Custom Design で評価する。
---
--- 3 因子:
---
---   * dose   (1e13 .. 5e13 cm^-2、 5 水準)
---   * energy (5 .. 50 keV、 5 水準)
---   * tilt   (0 / 7 / 15 / 30 deg、 装置制約で 4 水準離散)
---
--- 3 応答:
---
---   * defect  (画素欠陥カウント、 数万オーダの自然数、 Poisson GLM)
---   * fwc     (Full Well Capacity [e-]、 連続、 二次 RSM、 maximize)
---   * dark    (Dark Current [pA/cm^2]、 連続 log-scale、 LM、 minimize)
---
--- ## フロー
---
--- 1. Custom Design I-optimal で 23 runs を生成
--- 2. AddCenter 2 で強制中心行を追加 → 計 25 runs (1 ロット枠)
--- 3. 合成 Sim (本来は実機 / TCAD) で 3 応答を測定
--- 4. defect → Poisson GLM (Log link)、 fwc → RSM 二次、 dark → log-LM
--- 5. Desirability で多目的統合スコアを評価、 最適条件を特定
---
--- ## 数値合成
---
--- 各応答は **dose / energy / tilt の物理直感に沿った合成関数** + 小さい
--- 確定的揺らぎ (run 番号由来) で生成する。 実機データ取得を模した骨組み。
-module Main where
-
-import qualified Data.Text                          as T
-import qualified Numeric.LinearAlgebra              as LA
-import           Text.Printf                        (printf)
-
-import qualified Hanalyze.Design.Custom.Factor      as DF
-import qualified Hanalyze.Design.Custom.Model       as DM
-import qualified Hanalyze.Design.Custom.Coordinate  as DC
-import qualified Hanalyze.Design.Custom.Augment     as DA
-import qualified Hanalyze.Design.Optimal            as DO
-import qualified Hanalyze.Design.RSM                as RSM
-import qualified Hanalyze.Model.Core                as Core
-import qualified Hanalyze.Model.LM                  as LM
-import qualified Hanalyze.Model.GLM                 as GLM
-import qualified Hanalyze.Optim.Desirability        as Des
-import qualified Hanalyze.Model.LiNGAM.Direct       as LNG
-import qualified Hanalyze.Model.DAG                 as DAG
-import qualified Data.Text.IO                       as TIO
-import qualified Data.Vector                        as V
-import           System.Directory                   (createDirectoryIfMissing)
-
--- ===========================================================================
--- 因子定義
--- ===========================================================================
-
-doseLo, doseHi :: Double
-doseLo = 1e13
-doseHi = 5e13
-
-energyLo, energyHi :: Double
-energyLo = 5
-energyHi = 50
-
-tiltLevels :: [Double]
-tiltLevels = [0, 7, 15, 30]
-
-factors :: [DF.Factor]
-factors =
-  [ DF.Factor "dose"   (DF.Continuous   doseLo   doseHi)   DF.Controllable
-  , DF.Factor "energy" (DF.Continuous   energyLo energyHi) DF.Controllable
-  , DF.Factor "tilt"   (DF.DiscreteNum  tiltLevels)        DF.Controllable
-  ]
-
--- | 二次モデル: main + 2-way interactions + pure quadratic
---   (10 項、 23 runs で十分推定可能)
-quadModel :: DM.Model
-quadModel = DM.Model
-  { DM.mTerms =
-      [ DM.TIntercept
-      , DM.TMain "dose"
-      , DM.TMain "energy"
-      , DM.TMain "tilt"
-      , DM.TInter ["dose", "energy"]
-      , DM.TInter ["dose", "tilt"]
-      , DM.TInter ["energy", "tilt"]
-      , DM.TPower "dose"   2
-      , DM.TPower "energy" 2
-      , DM.TPower "tilt"   2
-      ]
-  , DM.mNorm = DM.NCoded
-  }
-
--- ===========================================================================
--- 合成応答 (synthetic ground truth)
--- ===========================================================================
---
--- 物理直感ベースの合成関数:
---   * defect: 高 dose で増、 高 energy で増、 tilt 中央で最小 (チャネリング)
---             → Poisson λ ≈ exp(10 .. 11) 程度 → 数万カウント
---   * fwc:    energy で増、 dose 中央で極大、 tilt 弱影響
---   * dark:   高 dose で増、 高 energy で増 (損傷)、 tilt 中央で最小
---             → log-scale で扱う
-
-codedDose :: Double -> Double
-codedDose x = 2 * (x - (doseLo + doseHi) / 2) / (doseHi - doseLo)
-
-codedEnergy :: Double -> Double
-codedEnergy x = 2 * (x - (energyLo + energyHi) / 2) / (energyHi - energyLo)
-
-codedTilt :: Double -> Double
-codedTilt x = (x - 13) / 17    -- tilt 範囲 0..30 を ~[-0.76, 1] にざっくり
-
--- | 引数は **既に coded された値** (dose / energy ∈ [-1,1])、 ただし
---   tilt は raw 値 (DiscreteNum 因子はライブラリの内部表現も raw)。
---   ここで tilt のみ coded に変換する。
-syntheticResp :: (Double, Double, Double) -> (Int, Double, Double)
-syntheticResp (dC, eC, tiltRaw) =
-  let !tC = codedTilt tiltRaw
-      -- defect: Poisson λ。 中心 ~exp(10.5) ≈ 36300
-      !logLam = 10.5 + 0.8*dC + 0.4*eC + 0.30*tC*tC - 0.20*dC*eC
-      !lam    = exp logLam
-      !defect = round lam :: Int
-      -- fwc (e-): 中心 ~12000、 energy で増、 dose 中央極大 (dose^2 で減少)
-      !fwc = 12000 + 1000*eC - 800*dC*dC - 200*tC + 50*dC*eC
-      -- dark (pA/cm^2): log-scale
-      !logDark = -1.0 + 0.5*dC + 0.3*eC + 0.4*tC*tC
-      !dark    = exp logDark
-  in (defect, fwc, dark)
-
--- ===========================================================================
--- ヘルパ
--- ===========================================================================
-
--- | 設計行列各行から (dose, energy, tilt) を取り出す
-rowToFactors :: LA.Matrix Double -> Int -> (Double, Double, Double)
-rowToFactors m i =
-  ( LA.atIndex m (i, 0)
-  , LA.atIndex m (i, 1)
-  , LA.atIndex m (i, 2)
-  )
-
--- | coded 値の行列に変換 (analysis 用)。
---   ライブラリの cdMatrix: Continuous は既に coded ±1、 DiscreteNum (tilt) は raw。
---   ここで tilt のみ codedTilt に通す。
-toCodedMatrix :: LA.Matrix Double -> LA.Matrix Double
-toCodedMatrix m =
-  let n   = LA.rows m
-      dC  = LA.fromList [ LA.atIndex m (i,0)               | i <- [0..n-1] ]
-      eC  = LA.fromList [ LA.atIndex m (i,1)               | i <- [0..n-1] ]
-      tC  = LA.fromList [ codedTilt (LA.atIndex m (i,2))   | i <- [0..n-1] ]
-  in LA.fromColumns [dC, eC, tC]
-
--- | coded dose/energy を raw 単位に戻す (表示用)
-rawDose :: Double -> Double
-rawDose c = (doseLo + doseHi) / 2 + c * (doseHi - doseLo) / 2
-
-rawEnergy :: Double -> Double
-rawEnergy c = (energyLo + energyHi) / 2 + c * (energyHi - energyLo) / 2
-
--- | 二次モデル設計行列を coded 行列から構築 (intercept + 3 main + 3 inter + 3 quad)
-buildQuadDesign :: LA.Matrix Double -> LA.Matrix Double
-buildQuadDesign xCoded =
-  let n = LA.rows xCoded
-      d = LA.flatten (xCoded LA.¿ [0])
-      e = LA.flatten (xCoded LA.¿ [1])
-      t = LA.flatten (xCoded LA.¿ [2])
-      ones = LA.fromList (replicate n 1)
-  in LA.fromColumns
-       [ ones
-       , d, e, t
-       , d * e, d * t, e * t
-       , d * d, e * e, t * t
-       ]
-
-quadTermLabels :: [String]
-quadTermLabels =
-  [ "intercept"
-  , "dose", "energy", "tilt"
-  , "dose*energy", "dose*tilt", "energy*tilt"
-  , "dose^2", "energy^2", "tilt^2"
-  ]
-
--- ===========================================================================
--- main
--- ===========================================================================
-
-main :: IO ()
-main = do
-  let bar = replicate 75 '='
-  putStrLn bar
-  putStrLn "  CMOS Image Sensor PD implant workflow demo"
-  putStrLn "  3 因子 (dose / energy / tilt 離散) × 25 runs (23 + 2 center)"
-  putStrLn bar
-  putStrLn ""
-
-  -- ── 1. Custom Design I-optimal 23 runs ──
-  putStrLn "[1] Custom Design I-optimal で 23 runs を生成中 ..."
-  let spec = DC.CustomDesignSpec
-        { DC.cdsFactors      = factors
-        , DC.cdsModel        = quadModel
-        , DC.cdsConstraints  = []
-        , DC.cdsNRuns        = 23
-        , DC.cdsCriterion    = DO.IOpt
-        , DC.cdsBudget       = DC.defaultBudget
-        , DC.cdsSeed         = Just 20260530
-        , DC.cdsInitial      = Nothing
-        , DC.cdsDJConvention = False
-        }
-  eDesign <- DC.coordinateExchange spec
-  case eDesign of
-    Left err -> putStrLn ("  FAIL: " ++ T.unpack err)
-    Right cd -> do
-      let base    = DC.cdMatrix cd
-          report  = DC.cdReport cd
-      printf "  ✓ runs=%d, restarts=%d, conv=%s, crit=%.6g\n"
-        (LA.rows base) (DC.crRestarts report)
-        (show (DC.crConverged report)) (DC.crCriterionValue report)
-      putStrLn ""
-
-      -- ── 2. AddCenter 2 で 25 runs に ──
-      putStrLn "[2] AddCenter 2 で強制中心 2 行を追加 → 計 25 runs"
-      let specWithBase = spec { DC.cdsInitial = Just base }
-      eAug <- DA.augmentMenu specWithBase (DA.AddCenter 2)
-      case eAug of
-        Left err  -> putStrLn ("  FAIL: " ++ T.unpack err)
-        Right amr -> do
-          let full = DA.amrMatrix amr
-          printf "  ✓ 最終 runs=%d (= %d + center %d)\n"
-            (LA.rows full) (LA.rows base) (DA.amrAdded amr)
-          putStrLn ""
-
-          -- ── 3. 合成 Sim で応答取得 ──
-          putStrLn "[3] 合成 Sim による応答取得 (defect / fwc / dark)"
-          let n = LA.rows full
-              triples = [ syntheticResp (rowToFactors full i)
-                        | i <- [0..n-1] ]
-              defects = [ d | (d, _, _) <- triples ]
-              fwcs    = [ f | (_, f, _) <- triples ]
-              darks   = [ k | (_, _, k) <- triples ]
-          printf "  defect: min=%d  max=%d  mean=%.0f\n"
-            (minimum defects) (maximum defects)
-            (fromIntegral (sum defects) / fromIntegral n :: Double)
-          printf "  fwc:    min=%.0f  max=%.0f  mean=%.1f\n"
-            (minimum fwcs) (maximum fwcs) (sum fwcs / fromIntegral n)
-          printf "  dark:   min=%.3g  max=%.3g  mean=%.3g\n"
-            (minimum darks) (maximum darks) (sum darks / fromIntegral n)
-          putStrLn ""
-
-          -- ── 4. 解析 ──
-          let xCoded    = toCodedMatrix full
-              xQuad     = buildQuadDesign xCoded
-              yDefect   = LA.fromList (map fromIntegral defects)
-              yFwc      = LA.fromList fwcs
-              yLogDark  = LA.fromList (map log darks)
-
-          -- 4a. defect: Poisson GLM (LogLink)
-          putStrLn "[4a] defect → Poisson GLM (LogLink)"
-          let (glmRes, _glmCov) = GLM.fitGLMFull GLM.Poisson GLM.Log xQuad yDefect
-          printFitCoefs quadTermLabels glmRes
-          putStrLn ""
-
-          -- 4b. fwc: 二次 RSM
-          putStrLn "[4b] fwc → 二次 RSM (canonical analysis)"
-          let qFit = RSM.fitQuadratic (LA.toLists xCoded) (LA.toList yFwc)
-              (xStar, yStar, eigs) = RSM.optimumPoint qFit
-          printf "  推定極値座標 (coded): %s\n" (show xStar)
-          printf "  そこでの fwc: %.3g e-\n" yStar
-          printf "  eigenvalues: %s\n" (show eigs)
-          let nearZero = any (\v -> abs v < 1e-6) eigs
-          if nearZero
-            then putStrLn "  (注: 1 つの eigenvalue が ~0 → quadratic に効かない\n\
-                          \   軸あり。 fwc 合成式が dose のみ quadratic、 energy/tilt\n\
-                          \   は線形であることを canonical analysis が正しく示している)"
-            else pure ()
-          let curvature :: String
-              curvature
-                | all (> 0) eigs = "局所極小 (応答最小)"
-                | all (< 0) eigs = "局所極大 (応答最大)"
-                | otherwise      = "鞍点 (mixed sign)"
-          printf "  → %s\n" curvature
-          putStrLn ""
-
-          -- 4c. dark: log-LM (log-scale 応答に対する線形モデル)
-          putStrLn "[4c] dark (log-scale) → LM"
-          let lmFit = LM.fitLMVec xQuad yLogDark
-          printFitCoefs quadTermLabels lmFit
-          putStrLn ""
-
-          -- ── 5. Desirability で多目的統合スコア ──
-          putStrLn "[5] Desirability で多目的統合スコア"
-          --   defect: minimize、 上限 50000、 目標 10000
-          --   fwc:    maximize、 下限 10000、 目標 14000
-          --   dark:   minimize、 上限 5.0、 目標 0.5
-          -- 閾値は実データ範囲を踏まえ動的に設定 (デモ用)
-          let defMin = fromIntegral (minimum defects) :: Double
-              defMax = fromIntegral (maximum defects) :: Double
-              fwcMin = minimum fwcs
-              fwcMax = maximum fwcs
-              darkMin = minimum darks
-              darkMax = maximum darks
-              dTypes =
-                [ Des.Minimize defMax  defMin
-                , Des.Maximize fwcMin  fwcMax
-                , Des.Minimize darkMax darkMin
-                ]
-              scorePerRun =
-                [ Des.overallDesirability dTypes
-                    [ fromIntegral (defects !! i)
-                    , fwcs !! i
-                    , darks !! i
-                    ]
-                | i <- [0..n-1]
-                ]
-              bestIdx = argmax scorePerRun
-              bestRow = rowToFactors full bestIdx
-          printf "  best run idx = %d (score = %.4f)\n"
-            bestIdx (scorePerRun !! bestIdx)
-          let (bdC, beC, btR) = bestRow
-          printf "  best 条件 (raw):  dose=%.2e  energy=%.2f keV  tilt=%.1f deg\n"
-            (rawDose bdC) (rawEnergy beC) btR
-          printf "  best 条件 (coded): dose=%+.3f  energy=%+.3f  tilt(raw)=%.1f\n"
-            bdC beC btR
-          let (bd, bf, bk) = syntheticResp bestRow
-          printf "  best 応答: defect=%d  fwc=%.0f  dark=%.3g\n" bd bf bk
-          putStrLn ""
-
-          -- ── 6. LiNGAM 因果探索 (3 応答間の因果構造を観測データから推定) ──
-          putStrLn "[6] LiNGAM 因果探索 (defect / fwc / dark 間の因果構造)"
-          -- 3 応答を縦に並べた n × 3 行列を組む。 dark は log-scale。
-          let respMat = LA.fromColumns
-                [ LA.fromList (map fromIntegral defects)
-                , yFwc
-                , yLogDark
-                ]
-              lingamFit = LNG.fitDirectLiNGAM LNG.defaultDirectLiNGAMConfig respMat
-              respLabels = V.fromList
-                [ T.pack "defect", T.pack "fwc", T.pack "log_dark" ]
-              dag = DAG.withNames respLabels
-                      (LNG.dlDAG LNG.defaultDirectLiNGAMConfig lingamFit)
-          printf "  causal order: %s\n" (show (LNG.dlOrder lingamFit))
-          putStrLn "  推定 B 行列 (係数):"
-          let b = LNG.dlB lingamFit
-              rows = [ (i, j, LA.atIndex b (i, j))
-                     | i <- [0..2], j <- [0..2], i /= j
-                     , abs (LA.atIndex b (i, j)) > 0.05 ]
-          mapM_ (\(i, j, w) ->
-                  printf "    %s ← %s (%+.3f)\n"
-                    (T.unpack (DAG.dagNodeName dag i))
-                    (T.unpack (DAG.dagNodeName dag j))
-                    w) rows
-          putStrLn ""
-          printf "  DAG acyclic? %s\n" (show (DAG.isAcyclic dag))
-          printf "  topological sort: %s\n"
-            (case DAG.topoSort dag of
-               Just ord -> show ord ++ " ("
-                          ++ unwords [T.unpack (DAG.dagNodeName dag i) | i <- ord]
-                          ++ ")"
-               Nothing  -> "(循環あり)")
-          putStrLn ""
-
-          -- ── 7. DOT エクスポート (Graphviz で可視化) ──
-          putStrLn "[7] DOT エクスポート"
-          createDirectoryIfMissing True "demo-output"
-          let dotPath = "demo-output/cis-implant-dag.dot"
-              dotText = DAG.toDOT dag
-          TIO.writeFile dotPath dotText
-          printf "  → %s に出力 (graphviz: dot -Tpng %s -o dag.png)\n"
-            dotPath dotPath
-          putStrLn ""
-
-          putStrLn bar
-          putStrLn "  CIS implant workflow demo 完了"
-          putStrLn bar
-
--- | 係数ベクトルを項ラベル付きで表示。 単一応答 FitResult (q=1) を仮定し
---   coefficients の 1 列目を取り出す。
-printFitCoefs :: [String] -> Core.FitResult -> IO ()
-printFitCoefs labels res = do
-  let !beta = Core.coefficients res
-      cs    = if LA.cols beta > 0
-                then LA.toList (LA.flatten (beta LA.¿ [0]))
-                else []
-  mapM_ (\(lbl, c) -> printf "  %-14s %+12.4g\n" lbl c)
-        (zip labels cs)
-
-argmax :: Ord a => [a] -> Int
-argmax xs = snd $ foldr1 (\a b -> if fst a >= fst b then a else b)
-                         (zip xs [0..])
diff --git a/demo/doe-optim/DOEDemo.hs b/demo/doe-optim/DOEDemo.hs
deleted file mode 100644
--- a/demo/doe-optim/DOEDemo.hs
+++ /dev/null
@@ -1,106 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Design of Experiments デモ (Phase O)。
---
--- 全要因/部分要因/ラテン方格/乱塊法/ANOVA/Power/質指標を一括検証。
-module Main where
-
-import Text.Printf (printf)
-
-import qualified Hanalyze.Design.Factorial as DF
-import qualified Hanalyze.Design.Block     as DB
-import qualified Hanalyze.Design.Mixed     as DM
-import qualified Hanalyze.Design.Anova     as DA
-import qualified Hanalyze.Design.Power     as DP
-import qualified Hanalyze.Design.Quality   as DQ
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Design of Experiments デモ (Phase O)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  -- ── 1. 完全要因 2³ ──
-  putStrLn "[1] 完全要因 2³ (3 因子各 2 水準 = 8 試行)"
-  let d23 = DF.twoLevelFactorial 3
-  printRows d23
-  printf "  直交度スコア = %.4f (1 で完全直交)\n" (DQ.orthogonalityScore d23)
-  printf "  D-efficiency = %.4f\n" (DQ.dEfficiency d23)
-  printf "  条件数        = %.4f\n" (DQ.conditionNumber d23)
-  putStrLn ""
-
-  -- ── 2. 部分要因 2^(4-1): D=ABC ──
-  putStrLn "[2] 部分要因 2^(4-1) (D = ABC)"
-  let d4m1 = DF.fractionalFactorial 4 [[1, 2, 3]]
-  printRows d4m1
-  printf "  試行数: %d (= 完全 16 の半分)\n" (length d4m1)
-  printf "  直交度スコア = %.4f\n" (DQ.orthogonalityScore d4m1)
-  putStrLn ""
-
-  -- ── 3. ラテン方格 4×4 ──
-  putStrLn "[3] ラテン方格 4×4"
-  let ls = DB.latinSquare 4
-  mapM_ print ls
-  putStrLn ""
-
-  -- ── 4. 混合水準 2² × 3 ──
-  putStrLn "[4] 混合水準 2² × 3 (= 12 試行)"
-  let dMix = DF.mixedFactorial [2, 2, 3]
-  printRows (take 4 dMix)
-  putStrLn (printf "  ... (合計 %d 試行)" (length dMix) :: String)
-  putStrLn ""
-
-  -- ── 5. 乱塊法 (4 ブロック × 5 処理) ──
-  putStrLn "[5] 乱塊法 4 ブロック × 5 処理 (各ブロック内ランダム順)"
-  let rb = DB.randomizedBlock 4 5 42
-  mapM_ (\(i, blk) -> printf "  Block %d: %s\n" (i :: Int) (show blk))
-        (zip [1..] rb)
-  putStrLn ""
-
-  -- ── 6. ANOVA (一元配置) ──
-  putStrLn "[6] 一元配置 ANOVA (3 群、各 5 観測)"
-  let labels = concat [replicate 5 g | g <- ["A", "B", "C"]]
-      vals   = [4.1, 4.5, 4.0, 4.3, 4.4   -- group A: mean 4.26
-              , 5.0, 5.3, 5.2, 5.4, 4.9   -- group B: mean 5.16
-              , 5.5, 5.8, 5.6, 5.9, 5.7] -- group C: mean 5.70
-  DA.printAnovaTable (DA.oneWayAnova labels vals)
-  putStrLn ""
-
-  -- ── 7. 検出力解析 ──
-  putStrLn "[7] 検出力解析"
-  let d   = DP.cohensD 0 0.5 1.0       -- d = 0.5 (medium)
-      pwr = DP.powerTTest d 30 30 0.05
-  printf "  t 検定 (n=30 each, d=0.5, α=0.05): power = %.3f\n" pwr
-  let n   = DP.sampleSizeTTest 0.5 0.8 0.05
-  printf "  d=0.5, target power=0.8 → n = %d (each group)\n" n
-
-  let f      = DP.cohensF [4.26, 5.16, 5.70] 0.30
-      anovaP = DP.powerOneWayAnova f 3 5 0.05
-  printf "  ANOVA (k=3, n=5/group, f=%.3f): power = %.3f\n" f anovaP
-  putStrLn ""
-
-  -- ── 8. 設計の質 ──
-  putStrLn "[8] 設計の質 (2³ 完全要因に対する各指標)"
-  printf "  直交?           %s\n" (show (DQ.isOrthogonal 1e-9 d23))
-  printf "  直交度スコア    = %.4f\n" (DQ.orthogonalityScore d23)
-  printf "  条件数          = %.4f\n" (DQ.conditionNumber d23)
-  printf "  D-efficiency    = %.4f\n" (DQ.dEfficiency d23)
-  printf "  A-efficiency    = %.4f\n" (DQ.aEfficiency d23)
-  printf "  VIF (各列)      = %s\n"
-         (show (map (\v -> read (printf "%.2f" v :: String) :: Double)
-                    (DQ.vifList d23)))
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ 完全要因/部分要因/ラテン方格/乱塊法/ANOVA/Power/品質"
-  putStrLn "    全て動作"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-
-  where
-    printRows :: [[Double]] -> IO ()
-    printRows rs = do
-      mapM_ (\r -> putStrLn ("  " ++ showRow r)) rs
-    showRow = unwords . map (printf "%+5.1f")
-
-    -- DM.crossDesign suppress unused warning
-    _ = DM.crossDesign [[1]] [[2]]
diff --git a/demo/doe-optim/MaterialsMOODemo.hs b/demo/doe-optim/MaterialsMOODemo.hs
deleted file mode 100644
--- a/demo/doe-optim/MaterialsMOODemo.hs
+++ /dev/null
@@ -1,110 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Phase W: 統合デモ — 材料科学シナリオ。
---
--- シナリオ: 合金組成 (x ∈ [0, 1] が銅含有率) を最適化。
---   - 強度 (高いほうが良い): strength(x) = 100 * sin(3x) + 50x + 20
---   - コスト (低いほうが良い): cost(x) = 50 + 100*x
---   - 重量 (低いほうが良い): weight(x) = 10 + 5*x
---
--- 全 3 目的を NSGA-II で同時最適化、Pareto front を可視化。
-module Main where
-
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.Optim.NSGA   (Solution (..), NSGAConfig (..), defaultNSGAConfig,
-                     nsga2)
-import Hanalyze.Optim.Pareto (hypervolume)
-import Hanalyze.Viz.Pareto   (parallelCoordinatesFile, paretoPairFile,
-                              solutionsToPlotData)
-import Hanalyze.Viz.Core     (defaultConfig, OutputFormat (..), PlotConfig (..))
-
--- 材料科学シナリオ: x ∈ [0, 1] (合金中の銅含有率)
--- すべて最小化問題に統一 (強度は -strength)
-materialsObjective :: [Double] -> [Double]
-materialsObjective [x] =
-  let strength = 100 * sin (3 * x) + 50 * x + 20    -- 最大化 → 最小化のため -符号
-      cost     = 50 + 100 * x
-      weight   = 10 + 5 * x
-  in [-strength, cost, weight]
-materialsObjective _ = error "1D"
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Phase W: 材料科学 統合デモ"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-  putStrLn "シナリオ: 合金の銅含有率 x ∈ [0, 1] を最適化"
-  putStrLn "  目的 1 (-strength): 100·sin(3x) + 50x + 20 を最大化"
-  putStrLn "  目的 2 (cost):       50 + 100x を最小化"
-  putStrLn "  目的 3 (weight):     10 + 5x を最小化"
-  putStrLn "  全 3 目的を最小化に統一して NSGA-II"
-  putStrLn ""
-
-  gen <- createSystemRandom
-
-  -- NSGA-II で 3 目的最適化
-  let cfg = defaultNSGAConfig
-              { nsgaPopSize = 80
-              , nsgaGenerations = 150
-              }
-  front <- nsga2 cfg materialsObjective [(0, 1)] gen
-  printf "Pareto front サイズ: %d\n" (length front)
-  putStrLn ""
-
-  putStrLn "[1] Pareto front の代表点 (5 個)"
-  let sortedFront = sortByObj 0 front
-      idxs = [0, length sortedFront `div` 4
-             , length sortedFront `div` 2
-             , 3 * length sortedFront `div` 4
-             , length sortedFront - 1]
-      reps = [sortedFront !! i | i <- idxs, i < length sortedFront]
-  printf "  %-15s %-15s %-15s %-15s\n"
-         ("x (Cu 比率)" :: String) ("strength" :: String)
-         ("cost" :: String) ("weight" :: String)
-  mapM_ (\s -> do
-            let [x] = solDecision s
-                [neg_str, c, w] = solObjectives s
-            printf "  %14.4f  %14.2f  %14.2f  %14.2f\n"
-                   x (-neg_str) c w)
-        reps
-  putStrLn ""
-
-  -- HV 評価
-  let allObjs = map solObjectives front
-      refPt = [-(-50.0), 200.0, 16.0]   -- 各目的の悪い値
-  printf "[2] HV (ref = %s) = %.3f\n" (show refPt) (hypervolume refPt allObjs)
-  putStrLn ""
-
-  -- 可視化
-  putStrLn "[3] 可視化"
-  let vCfg t = (defaultConfig t)
-                 { plotWidth = 700, plotHeight = 350 }
-  -- 130 規約: Solution → PlotData に変換してから Viz に渡す
-  let labels = ["-strength", "cost", "weight"]
-      pdFront = solutionsToPlotData labels front
-  parallelCoordinatesFile HTML "materials-parallel.html"
-    (vCfg "材料 Pareto front — 並行座標 (-strength / cost / weight)")
-    labels pdFront
-  paretoPairFile HTML "materials-pair.html"
-    (vCfg "材料 Pareto front — ペア散布")
-    labels pdFront
-  putStrLn "  → materials-parallel.html / materials-pair.html"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ 材料 3 目的最適化が完了"
-  putStrLn "    Pareto front から要件に応じて 1 点を選ぶ:"
-  putStrLn "    - 強度重視: 銅含有率 高、コスト・重量増 (右端)"
-  putStrLn "    - コスト重視: 銅含有率 低、強度低 (左端)"
-  putStrLn "    - バランス: 中央付近"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-
-  where
-    sortByObj :: Int -> [Solution] -> [Solution]
-    sortByObj j = qs
-      where qs []     = []
-            qs (p:xs) = qs [x | x <- xs, solObjectives x !! j <= solObjectives p !! j]
-                       ++ [p]
-                       ++ qs [x | x <- xs, solObjectives x !! j > solObjectives p !! j]
diff --git a/demo/doe-optim/MultiRSMDemo.hs b/demo/doe-optim/MultiRSMDemo.hs
deleted file mode 100644
--- a/demo/doe-optim/MultiRSMDemo.hs
+++ /dev/null
@@ -1,80 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Phase U: 多目的 RSM + Desirability の動作確認。
-module Main where
-
-import qualified Numeric.LinearAlgebra as LA
-import Text.Printf (printf)
-
-import Hanalyze.Design.RSM (centralCompositeRotatable)
-import Hanalyze.Design.MultiRSM
-import Hanalyze.Optim.Desirability
-
--- 真の関数 (3 応答):
---   y_1 = (x_1 - 0.5)² + (x_2)² + 1                      (最小化したい、極小は (0.5, 0))
---   y_2 = -x_1² - (x_2 - 0.5)² + 5                       (最大化したい、極大は (0, 0.5))
---   y_3 = (x_1 + x_2 - 1)²                              (target = 0、x_1 + x_2 = 1 で達成)
-trueY :: [Double] -> [Double]
-trueY [x1, x2] =
-  [ (x1 - 0.5)^(2::Int) + x2^(2::Int) + 1
-  , - x1^(2::Int) - (x2 - 0.5)^(2::Int) + 5
-  , (x1 + x2 - 1)^(2::Int)
-  ]
-trueY _ = error "2D"
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Phase U: 多目的 RSM + Desirability"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  -- CCD k=2、3 応答を生成
-  let design = centralCompositeRotatable 2 3   -- 11 試行
-      ys     = LA.fromLists [trueY r | r <- design]
-
-  printf "計画サイズ: %d 試行 (CCD rotatable)\n" (length design)
-  printf "応答数: %d\n" (LA.cols ys)
-  putStrLn ""
-
-  -- 多目的二次回帰
-  let mqFit = fitMultiQuadratic design ys
-      opts  = optimumPointsMulti mqFit
-  putStrLn "[1] 各応答の個別最適点 (二次回帰の解析解)"
-  mapM_ (\(j, (x, y, eigs)) -> do
-            printf "  y_%d: x* = %s, y* = %.3f\n"
-                   (j :: Int) (show (map (round3) x)) y
-            printf "        Hessian eigs = %s\n"
-                   (show (map round3 eigs)))
-        (zip [1..] opts)
-  putStrLn ""
-
-  -- Desirability 設計
-  putStrLn "[2] Desirability 設計"
-  putStrLn "  y_1 を最小化 (1 ≤ y ≤ 2 で desirable)"
-  putStrLn "  y_2 を最大化 (4 ≤ y ≤ 5 で desirable)"
-  putStrLn "  y_3 を target=0 (許容 -1 ≤ y ≤ 1)"
-  let dts = [ Minimize 2 1
-            , Maximize 4 5
-            , Target 0 (-1) 1 ]
-  -- 既存の test 点で D を評価
-  let testPoints = [[0.5, 0.5], [0.0, 0.5], [0.5, 0.0], [1.0, 0.0]]
-  putStrLn "  各点での総合 desirability D:"
-  mapM_ (\xp -> do
-            let yp = trueY xp
-                ds = zipWith individualDesirability dts yp
-                d  = overallDesirability dts yp
-            printf "    x=%s, y=%s, d=%s, D=%.3f\n"
-                   (show (map round3 xp))
-                   (show (map round3 yp))
-                   (show (map round3 ds))
-                   d)
-        testPoints
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ MultiRSM (q 応答の個別二次解析) + Desirability 集約 動作"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-
-  where
-    round3 :: Double -> Double
-    round3 v = fromIntegral (round (v * 1000) :: Int) / 1000
diff --git a/demo/doe-optim/NSGADemo.hs b/demo/doe-optim/NSGADemo.hs
deleted file mode 100644
--- a/demo/doe-optim/NSGADemo.hs
+++ /dev/null
@@ -1,144 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Phase S4: NSGA-II 本体の動作確認。
---
--- 古典的ベンチマーク ZDT1 と Schaffer 関数で Pareto front を再現。
--- 結果は HV / IGD で評価。
-module Main where
-
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-
-import Hanalyze.Optim.NSGA   (Solution (..), NSGAConfig (..), defaultNSGAConfig,
-                     nsga2)
-import Hanalyze.Optim.Pareto (hypervolume, igd)
-import Hanalyze.Viz.Pareto    (paretoCompareFile, parallelCoordinatesFile,
-                               solutionsToPlotData)
-import Hanalyze.Viz.PlotData  (PlotData (..), fromMixedColumns)
-import Hanalyze.Viz.Core      (defaultConfig, OutputFormat (..), PlotConfig (..))
-import qualified Data.Vector  as V
-import qualified Data.Text    as T
-
--- ---------------------------------------------------------------------------
--- ZDT1 (Zitzler-Deb-Thiele 2000):
---   f1(x) = x_1
---   f2(x) = g(x) * (1 - sqrt(f1/g))
---   g(x)  = 1 + 9 * (sum x_2..x_n) / (n-1)
--- 真の Pareto front: f1 ∈ [0, 1], f2 = 1 - sqrt(f1)
--- 全変数 [0, 1]
--- ---------------------------------------------------------------------------
-
-zdt1 :: Int -> [Double] -> [Double]
-zdt1 n x =
-  let f1 = head x
-      g  = 1 + 9 * sum (drop 1 x) / fromIntegral (n - 1)
-      f2 = g * (1 - sqrt (f1 / g))
-  in [f1, f2]
-
-zdt1TrueFront :: Int -> [[Double]]
-zdt1TrueFront k =
-  [ [f1, 1 - sqrt f1]
-  | i <- [0 .. k - 1]
-  , let f1 = fromIntegral i / fromIntegral (k - 1) ]
-
--- ---------------------------------------------------------------------------
--- Schaffer 関数 (Schaffer 1985):
---   f1(x) = x²
---   f2(x) = (x - 2)²
--- 真の Pareto front: x ∈ [0, 2]
--- ---------------------------------------------------------------------------
-
-schaffer :: [Double] -> [Double]
-schaffer [x] = [x * x, (x - 2) ** 2]
-schaffer _   = error "schaffer: 1 dim"
-
-schafferTrueFront :: Int -> [[Double]]
-schafferTrueFront k =
-  [ [x * x, (x - 2) ** 2]
-  | i <- [0 .. k - 1]
-  , let x = 2 * fromIntegral i / fromIntegral (k - 1) ]
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Phase S4: NSGA-II 本体の動作確認"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  gen <- createSystemRandom
-
-  -- ── Schaffer (1D, 簡易) ──
-  putStrLn "[1] Schaffer 関数 (1 変数, 2 目的)"
-  let cfg1 = defaultNSGAConfig { nsgaPopSize = 50, nsgaGenerations = 100 }
-  front1 <- nsga2 cfg1 schaffer [(0, 2)] gen
-  let objs1 = map solObjectives front1
-  printf "  最終 front サイズ: %d\n" (length front1)
-  printf "  HV (ref [4.5, 4.5]) = %.4f\n" (hypervolume [4.5, 4.5] objs1)
-  printf "  IGD (vs 真の front) = %.4f\n"
-         (igd (schafferTrueFront 100) objs1)
-  printf "  サンプル: %s\n" (show (take 3 (map (round2 . solObjectives) front1)))
-  putStrLn ""
-
-  -- ── ZDT1 (10 変数) ──
-  putStrLn "[2] ZDT1 (10 変数, 2 目的)"
-  let n = 10
-      cfg2 = defaultNSGAConfig { nsgaPopSize = 100, nsgaGenerations = 200 }
-  front2 <- nsga2 cfg2 (zdt1 n) (replicate n (0, 1)) gen
-  let objs2 = map solObjectives front2
-  printf "  最終 front サイズ: %d\n" (length front2)
-  printf "  HV (ref [1.1, 1.1]) = %.4f (真値 ~ 0.66)\n"
-         (hypervolume [1.1, 1.1] objs2)
-  printf "  IGD (vs 真の front) = %.4f (小さいほど良い)\n"
-         (igd (zdt1TrueFront 200) objs2)
-  printf "  端点 (f1 最小): %s\n"
-         (show (round2 (head (sortBy12 objs2))))
-  printf "  端点 (f2 最小): %s\n"
-         (show (round2 (last (sortBy12 objs2))))
-  putStrLn ""
-
-  -- ── 可視化 (130 規約: PlotData 経由) ──
-  let cmpCfg t = (defaultConfig t)
-                   { plotWidth = 600, plotHeight = 400 }
-      -- estimated front + true front を 1 つの PlotData に束ね、"src" 列で分ける
-      buildCompare estObjs trueFront =
-        let f1s = [ head o | o <- estObjs ]
-                ++ [ head p | p <- trueFront ]
-            f2s = [ o !! 1  | o <- estObjs ]
-                ++ [ p !! 1  | p <- trueFront ]
-            srcs = replicate (length estObjs) (T.pack "estimated")
-                ++ replicate (length trueFront) (T.pack "true")
-        in fromMixedColumns
-             [ (T.pack "f1", V.fromList f1s)
-             , (T.pack "f2", V.fromList f2s)
-             ]
-             [ (T.pack "src", V.fromList srcs) ]
-
-  paretoCompareFile HTML "nsga-schaffer.html"
-    (cmpCfg "Schaffer — NSGA-II 推定 vs 真の Pareto front")
-    ("f1", "f2") "src"
-    (buildCompare objs1 (schafferTrueFront 100))
-
-  paretoCompareFile HTML "nsga-zdt1.html"
-    (cmpCfg "ZDT1 (10D) — NSGA-II 推定 vs 真の Pareto front")
-    ("f1", "f2") "src"
-    (buildCompare objs2 (zdt1TrueFront 200))
-  putStrLn "  → nsga-schaffer.html / nsga-zdt1.html"
-
-  parallelCoordinatesFile HTML "nsga-zdt1-parallel.html"
-    ((defaultConfig "ZDT1 final population — parallel coordinates")
-       { plotWidth = 700, plotHeight = 350 })
-    ["f1", "f2"] (solutionsToPlotData ["f1", "f2"] front2)
-  putStrLn "  → nsga-zdt1-parallel.html (並行座標)"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ NSGA-II 本体が動作 (Schaffer, ZDT1 で Pareto front 再現)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  where
-    round2 :: [Double] -> [Double]
-    round2 = map (\v -> fromIntegral (round (v * 10000) :: Int) / 10000)
-    sortBy12 :: [[Double]] -> [[Double]]
-    sortBy12 = qs
-      where qs []     = []
-            qs (p:xs) = qs [x | x <- xs, head x <= head p]
-                       ++ [p]
-                       ++ qs [x | x <- xs, head x > head p]
diff --git a/demo/doe-optim/NSGASmokeDemo.hs b/demo/doe-optim/NSGASmokeDemo.hs
deleted file mode 100644
--- a/demo/doe-optim/NSGASmokeDemo.hs
+++ /dev/null
@@ -1,178 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Phase S1 — 非優越ソート + crowding distance の動作確認。
---
--- 既知の入力で出力が正しいことを 5 ケースで検証。Phase S 全体の
--- 基礎となる関数なので、ここで誤りを潰す。
-module Main where
-
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-import Hanalyze.Optim.NSGA (Solution (..), dominates, paretoDominates,
-                   nonDominatedSort, crowdingDistance,
-                   sbxCrossover, polynomialMutation, randomInBounds,
-                   binaryTournament, crowdedCompare)
-
-mkSol :: [Double] -> [Double] -> Double -> Solution
-mkSol = Solution
-
-assertBool :: String -> Bool -> IO ()
-assertBool label ok = do
-  putStrLn (if ok then "  ✓ " ++ label
-                  else "  ✗ FAIL " ++ label)
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Phase S1: 非優越ソート + crowding distance の動作確認"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  -- ── Test 1: paretoDominates の基本ケース ──
-  putStrLn "[1] paretoDominates"
-  assertBool "(1, 2) dominates (2, 3)"     (paretoDominates [1, 2] [2, 3])
-  assertBool "(1, 2) NOT dominates (1, 3)? = はい (= に注意)"
-             (paretoDominates [1, 2] [1, 3])  -- 1<=1 かつ 2<3 なので支配
-  assertBool "(1, 2) NOT dominates (1, 2)" (not (paretoDominates [1, 2] [1, 2]))
-  assertBool "(1, 3) NOT dominates (2, 2)" (not (paretoDominates [1, 3] [2, 2]))
-  putStrLn ""
-
-  -- ── Test 2: dominates with constraints ──
-  putStrLn "[2] dominates (制約あり)"
-  let s1 = mkSol [] [1, 1] 0     -- feasible
-      s2 = mkSol [] [0, 0] 5     -- infeasible (better obj but violates)
-      s3 = mkSol [] [10, 10] 2   -- infeasible, smaller violation
-      s4 = mkSol [] [10, 10] 8   -- infeasible, larger violation
-  assertBool "feasible dominates infeasible" (dominates s1 s2)
-  assertBool "infeasible NOT dominates feasible" (not (dominates s2 s1))
-  assertBool "smaller violation dominates" (dominates s3 s4)
-  putStrLn ""
-
-  -- ── Test 3: nonDominatedSort 基本 ──
-  putStrLn "[3] nonDominatedSort"
-  -- 4 点で 2 つの front:
-  --   F1 = {(1,4), (2,2), (4,1)} (Pareto front)
-  --   F2 = {(3,3)} (支配される)
-  let pop3 = [ mkSol [] [1, 4] 0
-             , mkSol [] [2, 2] 0
-             , mkSol [] [4, 1] 0
-             , mkSol [] [3, 3] 0
-             ]
-      fronts3 = nonDominatedSort pop3
-  printf "  front 数: %d (期待 2)\n" (length fronts3)
-  printf "  F_1 サイズ: %d (期待 3)\n" (length (head fronts3))
-  printf "  F_2 サイズ: %d (期待 1)\n" (length (fronts3 !! 1))
-  let f2obj = solObjectives (head (fronts3 !! 1))
-  assertBool ("F_2 の点が (3,3): " ++ show f2obj) (f2obj == [3, 3])
-  putStrLn ""
-
-  -- ── Test 4: nonDominatedSort 線形 (全部非優越) ──
-  putStrLn "[4] nonDominatedSort: 全点が非優越 (front は 1 つ)"
-  let pop4 = [mkSol [] [fromIntegral i, fromIntegral (5 - i)] 0
-             | i <- [0 .. 5 :: Int]]
-      fronts4 = nonDominatedSort pop4
-  printf "  front 数: %d (期待 1)\n" (length fronts4)
-  printf "  F_1 サイズ: %d (期待 6)\n" (length (head fronts4))
-  putStrLn ""
-
-  -- ── Test 5: crowdingDistance ──
-  putStrLn "[5] crowdingDistance: 5 点線形 front で端点 ∞、中央点 ~ 0.5"
-  -- (0,4), (1,3), (2,2), (3,1), (4,0) - perfect linear front
-  let front5 = [mkSol [] [fromIntegral i, fromIntegral (4 - i)] 0
-               | i <- [0 .. 4 :: Int]]
-      sorted = crowdingDistance front5
-  putStrLn "  ソート済 (距離降順):"
-  mapM_ (\s -> printf "    %s\n" (show (solObjectives s))) sorted
-  -- 端点 (0,4) と (4,0) が距離 ∞ で最初に来るはず
-  let firstTwo = take 2 sorted
-      firstObjs = map solObjectives firstTwo
-  assertBool "先頭 2 個は端点 (0,4) と (4,0)"
-             (sort2 firstObjs == [[0, 4], [4, 0]])
-  putStrLn ""
-
-  -- ── Test 6: crowdingDistance with all-equal objectives ──
-  putStrLn "[6] crowdingDistance: 全て同じ目的値 (range=0 で 0 距離)"
-  let front6 = replicate 4 (mkSol [] [1, 2] 0)
-      sorted6 = crowdingDistance front6
-  printf "  入出力長一致: %s (length 4)\n"
-         (show (length sorted6))
-  putStrLn ""
-
-  -- ── Phase S3: 遺伝的演算子 ──
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Phase S3: 遺伝的演算子の動作確認"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  gen <- createSystemRandom
-
-  -- Test 7: SBX
-  putStrLn "[7] sbxCrossover"
-  let bounds3 = [(0, 10), (-5, 5)]
-      p1 = [3.0, 1.0]
-      p2 = [7.0, -2.0]
-  (c1, c2) <- sbxCrossover 15 bounds3 p1 p2 gen
-  printf "  parents:  %s, %s\n" (show p1) (show p2)
-  printf "  children: %s, %s\n" (show c1) (show c2)
-  assertBool "c1 in bounds (dim 0)" (head c1 >= 0 && head c1 <= 10)
-  assertBool "c1 in bounds (dim 1)" (c1 !! 1 >= -5 && c1 !! 1 <= 5)
-  assertBool "c2 in bounds (dim 0)" (head c2 >= 0 && head c2 <= 10)
-  assertBool "c2 in bounds (dim 1)" (c2 !! 1 >= -5 && c2 !! 1 <= 5)
-  -- 同一親なら同一子
-  (c1', c2') <- sbxCrossover 15 bounds3 p1 p1 gen
-  assertBool "同一親 → 同一子 (dim 0)"
-             (abs (head c1' - 3.0) < 1e-12 && abs (head c2' - 3.0) < 1e-12)
-  putStrLn ""
-
-  -- Test 8: polynomial mutation
-  putStrLn "[8] polynomialMutation"
-  let xs = [3.0, 1.0]
-  -- pMut=1 で必ず変異、bounds 内に留まる
-  ys <- polynomialMutation 20 1.0 bounds3 xs gen
-  printf "  before: %s, after: %s\n" (show xs) (show ys)
-  assertBool "ys in bounds (dim 0)" (head ys >= 0 && head ys <= 10)
-  assertBool "ys in bounds (dim 1)" (ys !! 1 >= -5 && ys !! 1 <= 5)
-  -- pMut=0 で変異なし
-  ysNo <- polynomialMutation 20 0.0 bounds3 xs gen
-  assertBool "pMut=0 で不変" (ysNo == xs)
-  putStrLn ""
-
-  -- Test 9: randomInBounds
-  putStrLn "[9] randomInBounds"
-  rs <- mapM (const (randomInBounds bounds3 gen)) [1 .. 50 :: Int]
-  let dim0Vals = map head rs
-      dim1Vals = map (!! 1) rs
-      inRange0 = all (\v -> v >= 0 && v <= 10) dim0Vals
-      inRange1 = all (\v -> v >= -5 && v <= 5) dim1Vals
-  assertBool "dim 0 すべて [0, 10] に収まる" inRange0
-  assertBool "dim 1 すべて [-5, 5] に収まる" inRange1
-  putStrLn ""
-
-  -- Test 10: crowdedCompare
-  putStrLn "[10] crowdedCompare"
-  assertBool "rank 0 < rank 1"      (crowdedCompare (0, 0)   (1, 100) == LT)
-  assertBool "rank 同 → 距離大が良い" (crowdedCompare (0, 5.0) (0, 1.0) == LT)
-  assertBool "rank 同 → 距離小は劣"   (crowdedCompare (0, 1.0) (0, 5.0) == GT)
-  assertBool "完全同じ"               (crowdedCompare (0, 5.0) (0, 5.0) == EQ)
-  putStrLn ""
-
-  -- Test 11: binaryTournament
-  putStrLn "[11] binaryTournament (常に小さい数値が勝つ comparator)"
-  -- pop = [1..10], 「数値小=良い」順なら勝者は最小の方の index に近い
-  -- 確率的なので 100 回試行して平均が真ん中より小さいことを確認
-  results <- mapM
-    (const (binaryTournament [1..10 :: Int] compare gen))
-    [1..100 :: Int]
-  let meanRes = fromIntegral (sum results) / 100 :: Double
-  printf "  100 回トーナメント平均: %.2f (期待 < 5.5 = single-pick mean)\n"
-         meanRes
-  assertBool "平均 < 5.5 (= 良い方が選ばれる傾向)" (meanRes < 5.5)
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Phase S1 + S3: 全テスト通過"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-
-  where
-    sort2 :: [[Double]] -> [[Double]]
-    sort2 [a, b] = if a < b then [a, b] else [b, a]
-    sort2 xs = xs
diff --git a/demo/doe-optim/OptimalDOEDemo.hs b/demo/doe-optim/OptimalDOEDemo.hs
deleted file mode 100644
--- a/demo/doe-optim/OptimalDOEDemo.hs
+++ /dev/null
@@ -1,77 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | 最適計画 (D-optimal / A-optimal) のデモ (Phase P2)。
---
--- 候補集合 (3 水準グリッド) から指定試行数の部分集合を Fedorov 交換で
--- 最適化する。線形 vs 二次モデルの両方で確認。
-module Main where
-
-import Text.Printf (printf)
-
-import qualified Hanalyze.Design.Optimal as DO
-import qualified Hanalyze.Design.Quality as DQ
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  最適計画 (D-optimal / A-optimal) — Phase P2"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  -- ── 1. 線形モデル: k=3 因子、3 水準グリッド (27 候補) から 8 行選ぶ ──
-  putStrLn "[1] 線形モデル (k=3 因子)"
-  putStrLn "    候補: 3 水準グリッド = 27 候補。8 試行を選ぶ。"
-  putStrLn ""
-  let cands1 = DO.candidateGrid 3 3
-      n1     = 8
-  printf "  候補集合サイズ: %d\n" (length cands1)
-  let (idxD, designD) = DO.dOptimal cands1 n1 42
-      (idxA, designA) = DO.aOptimal cands1 n1 42
-  printf "  D-optimal 選定: %s\n" (show idxD)
-  printf "    D-eff       = %.4f\n" (DQ.dEfficiency designD)
-  printf "    A-eff       = %.4f\n" (DQ.aEfficiency designD)
-  printf "    条件数      = %.4f\n" (DQ.conditionNumber designD)
-  putStrLn ""
-  printf "  A-optimal 選定: %s\n" (show idxA)
-  printf "    D-eff       = %.4f\n" (DQ.dEfficiency designA)
-  printf "    A-eff       = %.4f\n" (DQ.aEfficiency designA)
-  printf "    条件数      = %.4f\n" (DQ.conditionNumber designA)
-  putStrLn ""
-  putStrLn "  選ばれた D-optimal 設計:"
-  mapM_ printRow designD
-  putStrLn ""
-
-  -- ── 2. 二次モデル: 候補は [1, x_i, x_i², x_i x_j] 拡張済 ──
-  putStrLn "[2] 二次モデル (k=2 因子, 1 + 2 + 2 + 1 = 6 列)"
-  putStrLn "    候補: 5 水準グリッド = 25 候補。10 試行を選ぶ。"
-  putStrLn ""
-  let cands2 = DO.quadraticCandidates 2 5
-      n2     = 10
-  printf "  候補集合サイズ: %d, 列数 (二次拡張後): %d\n"
-         (length cands2) (length (head cands2))
-  let (_, qDesign) = DO.dOptimal cands2 n2 7
-  printf "  D-eff       = %.4f\n" (DQ.dEfficiency qDesign)
-  printf "  条件数      = %.4f\n" (DQ.conditionNumber qDesign)
-  putStrLn "  最初 5 行 (拡張済):"
-  mapM_ printRow (take 5 qDesign)
-  putStrLn ""
-
-  -- ── 3. ランダム選択との比較 ──
-  putStrLn "[3] 改善度比較 (D-optimal vs ランダム選択, k=3 線形, n=8)"
-  let randDesigns = [ map (cands1 !!) (take n1 (DO.pseudoShuffle seed [0 .. length cands1 - 1]))
-                    | seed <- [1..5] ]
-      randDEffs = map DQ.dEfficiency randDesigns
-      avgRand   = sum randDEffs / fromIntegral (length randDEffs)
-  printf "  ランダム選択 5 種の平均 D-eff: %.4f\n" avgRand
-  printf "  D-optimal:                     %.4f\n" (DQ.dEfficiency designD)
-  printf "  改善率: %.1fx\n" (DQ.dEfficiency designD / avgRand)
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Fedorov 交換で D-/A-optimal 設計を構築"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-
-  where
-    printRow row = putStrLn ("    " ++ unwords (map (printf "%+6.3f") row))
-
--- (Optimal モジュールに pseudoShuffleI を export してないので、
---  ここでは pseudoShuffle を直接使う代わりに)
diff --git a/demo/doe-optim/ParetoSmokeDemo.hs b/demo/doe-optim/ParetoSmokeDemo.hs
deleted file mode 100644
--- a/demo/doe-optim/ParetoSmokeDemo.hs
+++ /dev/null
@@ -1,87 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Phase S2 — Pareto utilities (HV, IGD, GD) の動作確認。
-module Main where
-
-import Text.Printf (printf)
-import Hanalyze.Optim.Pareto (isNonDominated, paretoFront, hypervolume, igd, gd)
-
-approxEq :: Double -> Double -> Bool
-approxEq a b = abs (a - b) < 1e-6
-
-assertBool :: String -> Bool -> IO ()
-assertBool label ok = putStrLn (if ok then "  ✓ " ++ label else "  ✗ FAIL " ++ label)
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Phase S2: Pareto utilities の動作確認"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  -- Test 1: paretoFront
-  putStrLn "[1] paretoFront"
-  let pop = [[1, 4], [2, 2], [4, 1], [3, 3]]   -- (3,3) is dominated by (2,2)
-      pf  = paretoFront pop
-  printf "  入力 %s → front %s\n" (show pop) (show pf)
-  assertBool "front 長 3" (length pf == 3)
-  assertBool "front に (3,3) 含まない" ([3, 3] `notElem` pf)
-  putStrLn ""
-
-  -- Test 2: isNonDominated
-  putStrLn "[2] isNonDominated"
-  let ps = [[1, 4], [2, 2], [4, 1]]
-  assertBool "(0, 0) は非優越 (誰も支配していない)"
-             (isNonDominated [0, 0] ps)
-  assertBool "(3, 3) は被支配"
-             (not (isNonDominated [3, 3] ps))
-  putStrLn ""
-
-  -- Test 3: hypervolume 2D 既知ケース
-  putStrLn "[3] hypervolume (2D)"
-  -- 単一点 (1, 2), ref (3, 4): HV = (3-1) × (4-2) = 4
-  let hv1 = hypervolume [3, 4] [[1, 2]]
-  printf "  単一点 (1,2), ref (3,4): HV = %.3f (期待 4.0)\n" hv1
-  assertBool "hv1 = 4.0" (approxEq hv1 4.0)
-
-  -- 2 点 (1, 2), (2, 1), ref (3, 3):
-  -- (1, 2): 寄与 (3-1)(3-2) = 2  だが (2, 1) も計算に入る
-  -- 階段状で計算: x 昇順 (1,2), (2,1)
-  --   (1, 2): 幅 (3-1) = 2、高さ (3-2) = 1 → 2
-  --   (2, 1): 幅 (3-2) = 1、高さ (2-1) = 1 → 1   (前の y=2 から下がった分)
-  -- 合計 3
-  let hv2 = hypervolume [3, 3] [[1, 2], [2, 1]]
-  printf "  2 点 (1,2),(2,1), ref (3,3): HV = %.3f (期待 3.0)\n" hv2
-  assertBool "hv2 = 3.0" (approxEq hv2 3.0)
-
-  -- 全点が ref より悪い → HV = 0
-  let hv3 = hypervolume [1, 1] [[2, 2]]
-  printf "  ref より悪い点: HV = %.3f (期待 0)\n" hv3
-  assertBool "hv3 = 0" (approxEq hv3 0.0)
-  putStrLn ""
-
-  -- Test 4: hypervolume 3D
-  putStrLn "[4] hypervolume (3D)"
-  -- 単一点 (1, 1, 1), ref (2, 2, 2): HV = 1×1×1 = 1
-  let hv3D = hypervolume [2, 2, 2] [[1, 1, 1]]
-  printf "  単一点 (1,1,1), ref (2,2,2): HV = %.3f (期待 1.0)\n" hv3D
-  assertBool "hv3D = 1.0" (approxEq hv3D 1.0)
-  putStrLn ""
-
-  -- Test 5: IGD
-  putStrLn "[5] igd / gd"
-  let trueF = [[0, 4], [1, 3], [2, 2], [3, 1], [4, 0]]
-      estF  = [[0.1, 4.1], [2.1, 2.1], [4.1, 0.1]]
-      igdV  = igd trueF estF
-      gdV   = gd  trueF estF
-  printf "  IGD = %.4f, GD = %.4f\n" igdV gdV
-  assertBool "IGD > 0" (igdV > 0)
-  assertBool "GD > 0" (gdV > 0)
-  -- 完全一致なら IGD = GD = 0
-  let igdSame = igd trueF trueF
-  printf "  IGD(自分自身) = %.6f (期待 0)\n" igdSame
-  assertBool "IGD(self) = 0" (approxEq igdSame 0.0)
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Phase S2: Pareto utilities 動作確認"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/doe-optim/RSMDemo.hs b/demo/doe-optim/RSMDemo.hs
deleted file mode 100644
--- a/demo/doe-optim/RSMDemo.hs
+++ /dev/null
@@ -1,97 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | RSM デモ (Phase P1)。
---
--- - CCD/Box-Behnken の設計行列を表示
--- - 既知の二次関数 y = 5 - (x1-1)² - 2(x2+0.5)² + ε から fit
--- - 極値を解析的に求めて真値と比較
-module Main where
-
-import Text.Printf (printf)
-import qualified Numeric.LinearAlgebra as LA
-import System.Random.MWC (createSystemRandom)
-import qualified System.Random.MWC.Distributions as MWC
-
-import qualified Hanalyze.Design.RSM as RSM
-import qualified Hanalyze.Design.Quality as DQ
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Response Surface Methodology (Phase P1)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  -- ── 1. CCD (rotatable, k=2) ──
-  putStrLn "[1] CCD rotatable, k=2, 中心点 nC=3"
-  let ccd2 = RSM.centralCompositeRotatable 2 3
-      alpha = sqrt (sqrt 4) :: Double  -- (2^2)^(1/4) = √2 ≈ 1.414
-  printf "  α = (2²)^(1/4) = %.4f\n" alpha
-  printf "  試行数: %d (factorial 4 + 軸 4 + 中心 3)\n" (length ccd2)
-  mapM_ printRow ccd2
-  putStrLn ""
-
-  -- ── 2. CCD 種類比較 (k=3, nC=2) ──
-  putStrLn "[2] CCD 種類比較 (k=3, nC=2)"
-  let ccc = RSM.centralCompositeRotatable 3 2
-      ccf = RSM.centralComposite 3 RSM.CCF 2
-  printf "  Circumscribed (rotatable): %d 試行, D-eff = %.4f\n"
-         (length ccc) (DQ.dEfficiency ccc)
-  printf "  Face-centered:             %d 試行, D-eff = %.4f\n"
-         (length ccf) (DQ.dEfficiency ccf)
-  putStrLn ""
-
-  -- ── 3. Box-Behnken k=3 ──
-  putStrLn "[3] Box-Behnken k=3, nC=3 (= 12 + 3 = 15 試行)"
-  let bb = RSM.boxBehnken 3 3
-  printf "  試行数: %d, D-eff = %.4f\n" (length bb) (DQ.dEfficiency bb)
-  putStrLn "  最初 6 行:"
-  mapM_ printRow (take 6 bb)
-  putStrLn ""
-
-  -- ── 4. 二次回帰 fit ──
-  -- 真の関数: y = 5 - (x1-1)² - 2(x2+0.5)² + ε
-  -- 極大は (1, -0.5) で y=5
-  putStrLn "[4] 二次回帰: y = 5 - (x1-1)² - 2(x2+0.5)² + N(0, 0.1)"
-  putStrLn "    真の極大: x* = (1.0, -0.5), y* = 5.0"
-  let trueF [x1, x2] = 5 - (x1 - 1)^(2::Int) - 2 * (x2 + 0.5)^(2::Int)
-      trueF _ = 0
-  gen <- createSystemRandom
-  ys <- mapM (\row -> do
-                 e <- MWC.normal 0 0.1 gen
-                 return (trueF row + e))
-             ccd2
-  printf "    観測 n=%d (CCD k=2)\n" (length ys)
-
-  let fit = RSM.fitQuadratic ccd2 ys
-  let names = RSM.quadraticTermNames 2
-      betas = LA.toList (RSM.qfBeta fit)
-  putStrLn ""
-  putStrLn "  Fit 結果:"
-  printf "    R² = %.4f\n" (RSM.qfR2 fit)
-  mapM_ (\(n, b) -> printf "    %-8s = %+8.4f\n" n b)
-        (zip (map (\t -> read (show t) :: String) names) betas)
-  putStrLn ""
-
-  -- ── 5. 極値推定 ──
-  let (xStar, yStar, eigs) = RSM.optimumPoint fit
-  putStrLn "[5] 極値の解析解 (∂ŷ/∂x = 0 → x* = -½ B⁻¹ b)"
-  printf "  x* = [%.4f, %.4f]   (真値 [1.0, -0.5])\n"
-         (head xStar) (xStar !! 1)
-  printf "  y* = %.4f             (真値 5.0)\n" yStar
-  printf "  Hessian 固有値 = %s\n" (show (map (\e -> read (printf "%.4f" e :: String) :: Double) eigs))
-  let allNeg = all (< 0) eigs
-      allPos = all (> 0) eigs
-      kind :: String
-      kind = if allNeg then "極大 (concave)"
-               else if allPos then "極小 (convex)"
-                 else "鞍点 (saddle)"
-  printf "  → %s\n" kind
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ CCD / Box-Behnken / 二次回帰 / 極値推定すべて動作"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-
-  where
-    printRow row =
-      putStrLn ("    " ++ unwords (map (printf "%+6.3f") row))
diff --git a/demo/doe-optim/SingleOptBench.hs b/demo/doe-optim/SingleOptBench.hs
deleted file mode 100644
--- a/demo/doe-optim/SingleOptBench.hs
+++ /dev/null
@@ -1,187 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | 単目的最適化ベンチマーク。
---
--- 5 アルゴリズム × 3 ベンチ関数で収束履歴を比較し、HTML レポートを出力。
---
--- アルゴリズム: Nelder-Mead / L-BFGS / Brent (1D 専用) / DE / CMA-ES
--- ベンチ:       Sphere (凸 5D) / Rosenbrock (2D) / Rastrigin (5D 多峰)
---
--- 出力: trash/single_opt_bench.html
-module Main where
-
-import qualified Data.Text as T
-import Text.Printf (printf)
-import qualified System.Random.MWC as MWC
-
-import qualified Hanalyze.Optim.Common              as OC
-import qualified Hanalyze.Optim.NelderMead          as NM
-import qualified Hanalyze.Optim.LBFGS               as LBFGS
-import qualified Hanalyze.Optim.LineSearch          as LS
-import qualified Hanalyze.Optim.DifferentialEvolution as DE
-import qualified Hanalyze.Optim.CMAES               as CMAES
-import qualified Hanalyze.Viz.ReportBuilder         as RB
-import Graphics.Vega.VegaLite hiding (filter, name, sphere)
-
--- ベンチ関数
-sphere, rosen, rastrigin :: [Double] -> Double
-sphere     xs = sum [x*x | x <- xs]
-rosen      [x, y] = (1-x)^(2::Int) + 100 * (y - x*x)^(2::Int)
-rosen      _      = error "rosen: 2D"
-rastrigin  xs =
-  10 * fromIntegral (length xs) +
-  sum [x*x - 10 * cos (2 * pi * x) | x <- xs]
-
--- L2 距離
-l2 :: [Double] -> [Double] -> Double
-l2 a b = sqrt (sum (zipWith (\x y -> (x-y)^(2::Int)) a b))
-
--- 1 アルゴリズム実行結果
-data Run = Run
-  { runName    :: T.Text
-  , runValue   :: Double
-  , runDist    :: Double           -- 最適点との距離
-  , runIters   :: Int
-  , runHistory :: [Double]
-  } deriving Show
-
-main :: IO ()
-main = do
-  gen <- MWC.createSystemRandom
-
-  -- ===== Sphere 5D =====
-  putStrLn "=== Sphere 5D (truth = origin) ==="
-  let x0_5 = [3, -2, 1, 0.5, -1.5]
-      truth5 = [0, 0, 0, 0, 0]
-  rNM <- NM.runNelderMeadWith
-           (NM.defaultNMConfig { NM.nmStop = OC.defaultStopCriteria { OC.stMaxIter = 800 } })
-           sphere x0_5
-  rLB <- LBFGS.runLBFGSNumeric LBFGS.defaultLBFGSConfig sphere x0_5
-  rDE <- DE.runDEWith
-           ((DE.defaultDEConfig (replicate 5 (-5, 5)))
-              { DE.deStop = OC.defaultStopCriteria { OC.stMaxIter = 200 } })
-           sphere gen
-  rCM <- CMAES.runCMAESWith
-           (CMAES.defaultCMAESConfig { CMAES.cmStop = OC.defaultStopCriteria { OC.stMaxIter = 200 } })
-           sphere x0_5 gen
-  let sphereRuns =
-        [ runOf "Nelder-Mead" rNM truth5
-        , runOf "L-BFGS"      rLB truth5
-        , runOf "DE"          rDE truth5
-        , runOf "CMA-ES"      rCM truth5
-        ]
-  mapM_ printRun sphereRuns
-
-  -- ===== Rosenbrock 2D =====
-  putStrLn "\n=== Rosenbrock 2D (truth = (1,1)) ==="
-  let x0_2 = [-1.2, 1.0]
-      truth2 = [1, 1]
-  rNM2 <- NM.runNelderMeadWith
-            (NM.defaultNMConfig { NM.nmStop = OC.defaultStopCriteria { OC.stMaxIter = 5000 } })
-            rosen x0_2
-  rLB2 <- LBFGS.runLBFGSNumeric
-            (LBFGS.defaultLBFGSConfig { LBFGS.lbStop = OC.defaultStopCriteria { OC.stMaxIter = 500 } })
-            rosen x0_2
-  rDE2 <- DE.runDEWith
-            ((DE.defaultDEConfig (replicate 2 (-3, 3)))
-              { DE.deStop = OC.defaultStopCriteria { OC.stMaxIter = 300 } })
-            rosen gen
-  rCM2 <- CMAES.runCMAESWith
-            (CMAES.defaultCMAESConfig { CMAES.cmStop = OC.defaultStopCriteria { OC.stMaxIter = 300 } })
-            rosen x0_2 gen
-  let rosenRuns =
-        [ runOf "Nelder-Mead" rNM2 truth2
-        , runOf "L-BFGS"      rLB2 truth2
-        , runOf "DE"          rDE2 truth2
-        , runOf "CMA-ES"      rCM2 truth2
-        ]
-  mapM_ printRun rosenRuns
-
-  -- ===== Rastrigin 5D =====
-  putStrLn "\n=== Rastrigin 5D (truth = origin, multimodal) ==="
-  rNM3 <- NM.runNelderMeadWith
-            (NM.defaultNMConfig { NM.nmStop = OC.defaultStopCriteria { OC.stMaxIter = 2000 } })
-            rastrigin x0_5
-  rDE3 <- DE.runDEWith
-            ((DE.defaultDEConfig (replicate 5 (-5.12, 5.12)))
-              { DE.deStop = OC.defaultStopCriteria { OC.stMaxIter = 500 } })
-            rastrigin gen
-  rCM3 <- CMAES.runCMAESWith
-            (CMAES.defaultCMAESConfig { CMAES.cmStop = OC.defaultStopCriteria { OC.stMaxIter = 500 } })
-            rastrigin x0_5 gen
-  let rastriginRuns =
-        [ runOf "Nelder-Mead" rNM3 truth5
-        , runOf "DE"          rDE3 truth5
-        , runOf "CMA-ES"      rCM3 truth5
-        ]
-  mapM_ printRun rastriginRuns
-
-  -- ===== Brent 1D =====
-  putStrLn "\n=== Brent 1D (parabola, truth = 2.5) ==="
-  let pf = LS.brent LS.defaultBrentConfig (\[x] -> (x - 2.5)^(2::Int) + 1) 0 5
-  printf "  Brent: x=%.6f  f=%.6f  iters=%d\n"
-    (head (OC.orBest pf)) (OC.orValue pf) (OC.orIters pf)
-
-  -- ===== HTML レポート =====
-  let cfg = RB.defaultReportConfig "Single-objective optimizer benchmark"
-      sections =
-        [ RB.secMarkdown "Overview"
-            "5 アルゴリズム × 3 ベンチで収束履歴を比較。各表で best value と truth との距離を表示。"
-        , RB.secTable "Sphere 5D (truth = origin)"
-            ["Algorithm", "Value", "‖x* - truth‖", "Iterations", "Converged"]
-            (map runRow sphereRuns)
-        , RB.secVega "Sphere 5D 収束履歴" (convergenceSpec sphereRuns)
-        , RB.secTable "Rosenbrock 2D (truth = (1,1))"
-            ["Algorithm", "Value", "‖x* - truth‖", "Iterations", "Converged"]
-            (map runRow rosenRuns)
-        , RB.secVega "Rosenbrock 2D 収束履歴" (convergenceSpec rosenRuns)
-        , RB.secTable "Rastrigin 5D (truth = origin, multimodal)"
-            ["Algorithm", "Value", "‖x* - truth‖", "Iterations", "Converged"]
-            (map runRow rastriginRuns)
-        , RB.secVega "Rastrigin 5D 収束履歴" (convergenceSpec rastriginRuns)
-        , RB.secMarkdown "Brent 1D"
-            (T.pack (printf "x* = %.6f, f(x*) = %.6f, iterations = %d"
-                       (head (OC.orBest pf)) (OC.orValue pf) (OC.orIters pf)))
-        ]
-  RB.renderReport "trash/single_opt_bench.html" cfg sections
-  putStrLn "\nWrote trash/single_opt_bench.html"
-
-runOf :: T.Text -> OC.OptimResult -> [Double] -> Run
-runOf nm r truth = Run nm (OC.orValue r) (l2 (OC.orBest r) truth) (OC.orIters r) (OC.orHistory r)
-
-printRun :: Run -> IO ()
-printRun r =
-  printf "  %-12s  value=%10.4g  dist=%8.4g  iters=%d\n"
-    (T.unpack (runName r)) (runValue r) (runDist r) (runIters r)
-
-runRow :: Run -> [T.Text]
-runRow r =
-  [ runName r
-  , T.pack (printf "%.4g" (runValue r))
-  , T.pack (printf "%.4g" (runDist r))
-  , T.pack (show (runIters r))
-  , T.pack (show (runIters r > 0))   -- 雑な印
-  ]
-
--- | 各アルゴリズムの best 値推移をライン重ね描き。
-convergenceSpec :: [Run] -> VegaLite
-convergenceSpec runs =
-  let rows = concat
-        [ [ dataRow [ ("alg",   Str (runName r))
-                    , ("iter",  Number (fromIntegral i))
-                    , ("value", Number v)
-                    ] []
-          | (i, v) <- zip [0::Int ..] (runHistory r) ]
-        | r <- runs ]
-      dat = dataFromRows [] (concat rows)
-  in toVegaLite
-       [ title "Best value vs iteration" []
-       , dat
-       , mark Line [MStrokeWidth 2]
-       , encoding
-           . position X [PName "iter",  PmType Quantitative, PTitle "iteration"]
-           . position Y [PName "value", PmType Quantitative, PTitle "best value (log)", PScale [SType ScLog]]
-           . color    [MName "alg", MmType Nominal]
-           $ []
-       , width 700
-       , height 350
-       ]
diff --git a/demo/doe/ImplantSequentialDemo.hs b/demo/doe/ImplantSequentialDemo.hs
deleted file mode 100644
--- a/demo/doe/ImplantSequentialDemo.hs
+++ /dev/null
@@ -1,365 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE PartialTypeSignatures #-}
-{-# OPTIONS_GHC -Wno-partial-type-signatures #-}
--- | 逐次 DOE デモ: 半導体インプラ条件の最適化 (30 因子スクリーニング → 試作 RSM)。
---
--- ストーリー (実務の逐次 DOE を再現):
---   * 動かせる因子 = 10 インプラ工程 × { dose (連続) / energy (連続) / tilt (2 水準) }
---     = **30 因子**。 前タイプ recipe (= ref・中心条件) では新タイプの **spec 未達**。
---   * 真の応答はスパース: 10 工程中 **3 工程だけ活性**、 各活性工程で dose×energy /
---     dose×tilt の **within-implant 交互作用** (= 物理的交絡) が強い。 残り 7 工程は inert。
---   * **Phase 1 (sim スクリーニング)**: 30 因子は D-最適座標交換だと 1 設計 ~160 秒で非現実的
---     (実測)。 → **DSD (Definitive Screening Design・61 run)** を使う。 61×31 の主効果
---     モデル行列は rank 31・条件数 2.86 = ほぼ直交で主効果を疎に同定できる (実測)。
---     → 効果 Pareto で活性工程を絞り込む。
---   * **Phase 2 (試作 RSM)**: 絞り込んだ支配 2 工程の dose/energy = 4 因子で 2 次応答曲面。
---     4 lot = 4 block・各 lot に **センター 2 枚必須** (runsheet に付加)。 D-最適 (座標交換・
---     小因子なら数秒) で設計 → RSM fit → 停留点で最適条件 → **spec 達成**を数値で示す。
---
--- 出力図 (demo-output/doe/・git ignore):
---   * implant-screening-pareto.svg  — Phase 1 効果 Pareto (どの工程が効くか)
---   * implant-rsm-contour.svg        — Phase 2 応答曲面 contour + ref/最適点
---   * implant-rsm-profiler.svg       — Phase 2 予測プロファイラ (絞り込み因子・CI 帯)
-module Main where
-
-import qualified Numeric.LinearAlgebra as LA
-import qualified Data.Text as T
-import           Data.Text (Text)
-import           Data.List (sortBy, nub)
-import           Data.Ord (comparing, Down (..))
-import           Text.Printf (printf)
-import           System.Directory (createDirectoryIfMissing)
-
-import           Hanalyze.Plot
-                   ( customDesign, customSpec, contFactor, quadratic, blocked
-                   , designTable, designModel, profiler, contourOf
-                   , rsmAnalysis, RSMReport (..), RSMNature (..)
-                   , Design (..), toPlot, (|->) )
-import           Hanalyze.Design.Workflow (CustomSpec (..))
-import           Hanalyze.Design.DSD (dsdDesign, DSDResult (..))
-import           Hgg.Plot.Spec
-                   ( ColData (..), layer, bar, scatterPoints, Point2 (..)
-                   , inline, inlineCat, colorBy, color
-                   , title, subtitle, xLabel, yLabel, width, height
-                   , xAxis, axisRotate
-                   , theme, ThemeName (..) )
-import           Hgg.Plot.Frame ((|>>))
-import           Hgg.Plot.Color (fromHex)
-import           Hgg.Plot.Backend.SVG (saveSVGBound)
-
--- ===========================================================================
--- Phase 0: 因子・真の応答 (ground truth)・ref・spec
--- ===========================================================================
-
-nImplant :: Int
-nImplant = 10
-
--- 因子 index j (0..29): implant = j `div` 3、 role = j `mod` 3 (0=dose,1=energy,2=tilt)。
-implantOf, roleOf :: Int -> Int
-implantOf j = j `div` 3
-roleOf    j = j `mod` 3
-
--- 因子の自然単位レンジ。 dose = ions/cm²、 energy = keV、 tilt = 2 水準 {0,7} deg。
-factorRange :: Int -> (Double, Double)
-factorRange j = case roleOf j of
-  0 -> (1.0e14, 5.0e14)   -- dose
-  1 -> (10, 100)          -- energy (keV)
-  _ -> (0, 7)             -- tilt (deg・2 水準)
-
--- coded c∈[-1,1] → 自然単位 (中心 0 が範囲中点)。
-toNat :: Int -> Double -> Double
-toNat j c = let (lo, hi) = factorRange j in lo + (c + 1) / 2 * (hi - lo)
-
--- 因子名 (compact ラベル): "I3 dose" 等 (implant は 1-based 表示)。
-roleName :: Int -> Text
-roleName j = case roleOf j of { 0 -> "dose"; 1 -> "energy"; _ -> "tilt" }
-
-factorLabel :: Int -> Text
-factorLabel j = "I" <> T.pack (show (implantOf j + 1)) <> " " <> roleName j
-
--- formula / 因子名で使う安全な変数名 (空白なし): "I3_dose"。
-factorVar :: Int -> Text
-factorVar j = "I" <> T.pack (show (implantOf j + 1)) <> "_" <> roleName j
-
--- --- スパースな真の応答 (性能指数 P、 高いほど良い) --------------------------
--- 活性工程 = implant index {2, 5, 8} (= 1-based の 3・6・9)。 各活性工程で
--- dose/energy 主効果 + within-implant 2FI (dose×energy, dose×tilt) + 負の 2 次
--- (⇒ 内点に最大)。 tilt は小さな主効果。 他 7 工程は inert (係数 0)。
-
--- (implant index, bDose, bEnergy, bTilt, bDoseEnergy, bDoseTilt, qDose, qEnergy)
-activeImplants :: [(Int, Double, Double, Double, Double, Double, Double, Double)]
-activeImplants =
-  [ (2, 8.0, 6.0, 1.5, 3.0, 2.0, 8.0, 6.0)   -- 支配工程 (I3)
-  , (5, 5.0, 4.0, 1.0, 2.0, 1.0, 5.0, 4.0)   -- 中位工程 (I6)
-  , (8, 3.0, 2.5, 0.5, 1.0, 0.0, 2.5, 2.0)   -- 弱い工程 (I9)
-  ]
-
-baseP :: Double
-baseP = 50.0
-
--- 真の応答 g(coded 30 次元)。 coded 値を直接使う (screening 係数と同単位)。
-gCoded :: [Double] -> Double
-gCoded x = baseP + sum (map contrib activeImplants)
-  where
-    contrib (i, bd, be, bt, bde, bdt, qd, qe) =
-      let xd = x !! (3*i);  xe = x !! (3*i + 1);  xt = x !! (3*i + 2)
-      in bd*xd + be*xe + bt*xt + bde*xd*xe + bdt*xd*xt - qd*xd*xd - qe*xe*xe
-
--- ref 条件 = 前タイプ recipe = 全因子 coded 0 (中心条件)。
-refCoded :: [Double]
-refCoded = replicate (3 * nImplant) 0
-
--- spec: 性能指数 P >= specP。 ref (= baseP=50) は未達。
-specP :: Double
-specP = 60.0
-
--- ===========================================================================
--- 決定的 PRNG → Gaussian (Box-Muller) — RSMSampleSizeDemo と同型
--- ===========================================================================
-
-lcg :: Int -> Int
-lcg s = (1103515245 * s + 12345) `mod` 2147483648
-
-u01 :: Int -> Double
-u01 s = fromIntegral s / 2147483648
-
-gaussians :: Int -> [Double]
-gaussians seed = go (lcg seed)
-  where
-    go s = let s1 = lcg s; s2 = lcg s1
-               u1 = max 1e-12 (u01 s1); u2 = u01 s2
-           in sqrt (-2 * log u1) * cos (2 * pi * u2) : go s2
-
-noiseSd :: Double
-noiseSd = 0.8
-
--- OLS: beta = pinv(X) y。
-ols :: [[Double]] -> [Double] -> [Double]
-ols xs ys = LA.toList (LA.flatten (LA.pinv (LA.fromLists xs) LA.<> LA.asColumn (LA.fromList ys)))
-
--- ===========================================================================
--- Phase 1: DSD スクリーニング (61 run)
--- ===========================================================================
-
--- 効果 (因子 index, |係数|, 係数) を降順で返す + DSD 情報。
-screening :: (Int, Int, Bool, [(Int, Double, Double)])
-screening =
-  let dsd       = either (error . T.unpack) id (dsdDesign (3 * nImplant))
-      rows      = LA.toLists (dsdMatrix dsd)                 -- 61 × 30 (coded {-1,0,1})
-      ys        = [ gCoded r + noiseSd * z
-                  | (r, z) <- zip rows (gaussians 83001) ]
-      xs        = [ 1 : r | r <- rows ]                      -- 主効果モデル [1 | 30]
-      beta      = ols xs ys
-      effects   = [ (j, abs (beta !! (j+1)), beta !! (j+1)) | j <- [0 .. 3*nImplant - 1] ]
-      ranked    = sortBy (comparing (Down . (\(_,a,_) -> a))) effects
-  in (dsdNRuns dsd, LA.rank (LA.fromLists xs), dsdHasOptimal dsd, ranked)
-
--- 活性因子 index の集合 (ground truth 由来・図の色分け用)。
-activeFactorIdxs :: [Int]
-activeFactorIdxs =
-  concat [ [3*i, 3*i+1, 3*i+2] | (i,_,_,_,_,_,_,_) <- activeImplants ]
-
--- Phase 1 図: 効果 Pareto (top 15・活性/inert 色分け)。
-screeningFigure :: [(Int, Double, Double)] -> IO ()
-screeningFigure ranked = do
-  let topN   = 15
-      top    = take topN ranked
-      -- bar の categorical 軸はラベルをアルファベット順に並べる (hgg に
-      -- xCatOrder 未実装)。 Pareto は降順必須ゆえ、 順位をゼロ埋め前置して
-      -- アルファベット順 = |効果| 降順に一致させる ("01 I3 dose" 等)。
-      labels = [ T.pack (printf "%02d " k) <> factorLabel j
-               | (k, (j,_,_)) <- zip [1 :: Int ..] top ]
-      vals   = [ a | (_,a,_) <- top ]
-      cats   = [ if j `elem` activeFactorIdxs then "活性 (真に効く)" else "inert (ノイズ)"
-               | (j,_,_) <- top ] :: [Text]
-      spec'  = layer ( bar (inlineCat labels) (inline vals)
-                     <> colorBy (inlineCat cats) )
-                <> title    "Phase 1: 効果 Pareto (DSD 61 run で 30 因子スクリーニング)"
-                <> subtitle "|主効果係数| 降順 top 15。 活性 3 工程の dose/energy が上位に立つ"
-                <> xLabel   "因子 (I<工程> <パラメタ>)"
-                <> yLabel   "|効果| (coded 単位)"
-                <> xAxis (axisRotate 90)     -- 横軸ラベルを y 軸タイトルと同じ向き (CCW 90°・下→上読み)
-                <> width 720 <> height 460
-                <> theme ThemeGrey
-      noDf   = [] :: [(Text, ColData)]
-  createDirectoryIfMissing True "demo-output/doe"
-  saveSVGBound "demo-output/doe/implant-screening-pareto.svg" (noDf |>> spec')
-  putStrLn "  → wrote demo-output/doe/implant-screening-pareto.svg"
-
--- ===========================================================================
--- Phase 2: 試作 RSM (blocked 4 lot・center 2/lot・D-最適)
--- ===========================================================================
-
--- coded → 自然単位の逆 (自然 → coded)。 stationary(自然) を coded に戻し true P を評価。
-toCoded :: Int -> Double -> Double
-toCoded j nat = let (lo, hi) = factorRange j in 2 * (nat - lo) / (hi - lo) - 1
-
-nLot :: Int
-nLot = 4
-
-centerPerLot :: Int
-centerPerLot = 2
-
--- screening から RSM に carry する因子を導出:
---   * dose/energy で |効果| > 2.0 の工程 = 「支配工程」 → その dose/energy を carry。
--- 非 carry で |効果| > 0.35 の因子 (= 活性 tilt) は screening の符号方向に固定 (背景条件)。
-selectedImplants :: [(Int, Double, Double)] -> [Int]
-selectedImplants ranked =
-  nub [ implantOf j | (j, a, _) <- ranked, roleOf j /= 2, a > 2.0 ]
-
-carriedIdxs :: [(Int, Double, Double)] -> [Int]
-carriedIdxs ranked =
-  concat [ [3*i, 3*i + 1] | i <- selectedImplants ranked ]  -- 各支配工程の dose, energy
-
--- 背景 coded (非 carry 因子): 活性 tilt 等は screening 符号方向に固定、 他は ref(0)。
-backgroundCoded :: [(Int, Double, Double)] -> [Int] -> [Double]
-backgroundCoded ranked carried =
-  [ bgAt j | j <- [0 .. 3*nImplant - 1] ]
-  where
-    effOf j = head ([ c | (k, _, c) <- ranked, k == j ] ++ [0])
-    bgAt j | j `elem` carried        = 0                       -- carry は design が上書き
-           | abs (effOf j) > 0.35    = signum (effOf j)        -- 有意非 carry (tilt) を固定
-           | otherwise               = 0                       -- inert は ref
-
--- Phase 2 本体。 screening ranked を受け、 設計 → sim → RSM fit → 最適条件を返す。
-data RSMOut = RSMOut
-  { roCarried   :: ![Int]              -- carry した因子 index
-  , roNFree     :: !Int                -- 自由 run 数 (block D-最適)
-  , roNCenter   :: !Int                -- center 数 (2/lot × 4)
-  , roReport    :: !RSMReport          -- rsmAnalysis 結果
-  , roTruePOpt  :: !Double             -- 見つけた最適条件での「真の」P
-  , roNames     :: ![Text]             -- carry 因子の safe 変数名
-  }
-
-runPhase2 :: [(Int, Double, Double)] -> (RSMOut, IO ())
-runPhase2 ranked =
-  let carried  = carriedIdxs ranked
-      names    = map factorVar carried
-      facs     = [ contFactor (factorVar j) (factorRange j) | j <- carried ]
-      nFree    = 44
-      plan     = customDesign ((customSpec facs (quadratic names) nFree 83002)
-                                 { csStructure = blocked nLot })
-      -- 設計の coded 行 (自由 run) + center (2/lot = 8 行・全 0)。
-      codedFree   = dsCoded plan
-      nCenter     = nLot * centerPerLot
-      codedCenter = replicate nCenter (replicate (length carried) 0)
-      codedAug    = codedFree ++ codedCenter
-      -- 自然単位 runsheet (図の data frame 用)。
-      tbl      = designTable plan
-      colOf c  = maybe (error (T.unpack c)) id (lookup c tbl)
-      mids     = [ toNat j 0 | j <- carried ]                     -- center の自然値 = 中点
-      rowsAug  = [ (nm, colOf nm ++ replicate nCenter mid)
-                 | (nm, mid) <- zip names mids ]
-      -- sim 応答 (真の応答 + noise)。 coded 行を full-30 に埋め込み。
-      bg       = backgroundCoded ranked carried
-      idxPos   = zip carried [0 :: Int ..]
-      embed r6 = [ maybe (bg !! j) (r6 !!) (lookup j idxPos) | j <- [0 .. 3*nImplant - 1] ]
-      ysAug    = [ gCoded (embed r6) + noiseSd * z
-                 | (r6, z) <- zip codedAug (gaussians 83003) ]
-      -- RSM 解析 (coded augmented を持つ Design で停留点を自然単位へ)。
-      report   = rsmAnalysis (plan { dsCoded = codedAug }) ysAug
-      -- 見つけた最適条件 (自然) を coded に戻し、 背景に埋め込んで「真の」P を評価。
-      optCoded = [ maybe 0 (\pos -> toCoded (carried !! pos)
-                                       (maybe 0 id (lookup (factorVar (carried !! pos))
-                                                           (rsmStationary report))))
-                          (lookup j idxPos)
-                 | j <- [0 .. 3*nImplant - 1] ]
-      -- 背景 + carry 最適 を合成した full coded で真の P。
-      optFull  = [ if j `elem` carried then optCoded !! j else bg !! j
-                 | j <- [0 .. 3*nImplant - 1] ]
-      truePOpt = gCoded optFull
-      -- 図 (contour + profiler)。 designModel は data frame で fit (plan は formula のみ)。
-      model    = (("P", ysAug) : rowsAug) |-> designModel plan "P"
-      figs     = do
-        createDirectoryIfMissing True "demo-output/doe"
-        let noDf = [] :: [(Text, ColData)]
-            -- 支配 2 因子 (I3 dose × I3 energy) で contour。
-            v1 = names !! 0; v2 = names !! 1
-            refD = toNat (carried !! 0) 0; refE = toNat (carried !! 1) 0
-            optD = maybe refD id (lookup v1 (rsmStationary report))
-            optE = maybe refE id (lookup v2 (rsmStationary report))
-            contourSpec =
-              (noDf |>> ( contourOf model v1 v2
-                          <> layer (scatterPoints [Point2 refD refE] <> color (fromHex "#333333"))
-                          <> layer (scatterPoints [Point2 optD optE] <> color (fromHex "#d73027"))
-                          <> title    "Phase 2: 応答曲面 contour (支配工程 I3 dose × energy)"
-                          <> subtitle "灰=ref(中心・spec未達) / 赤=RSM 最適点。 他因子は中央値固定"
-                          <> xLabel (factorLabel (carried !! 0))
-                          <> yLabel (factorLabel (carried !! 1))
-                          <> theme ThemeGrey ))
-            profSpec =
-              (noDf |>> ( toPlot (profiler [("P", model)] names)
-                          <> title "Phase 2: 予測プロファイラ (絞り込み 6 因子・95% CI 帯)"
-                          <> width 900 <> height 360
-                          <> theme ThemeGrey ))
-        saveSVGBound "demo-output/doe/implant-rsm-contour.svg" contourSpec
-        putStrLn "  → wrote demo-output/doe/implant-rsm-contour.svg"
-        saveSVGBound "demo-output/doe/implant-rsm-profiler.svg" profSpec
-        putStrLn "  → wrote demo-output/doe/implant-rsm-profiler.svg"
-  in ( RSMOut carried nFree nCenter report truePOpt names
-     , figs )
-
--- ===========================================================================
--- main (Phase 0 + Phase 1 + Phase 2)
--- ===========================================================================
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  逐次 DOE デモ: 半導体インプラ条件最適化 (30 因子 → 試作 RSM)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  printf "  因子: 10 工程 × {dose, energy, tilt} = %d 因子\n" (3 * nImplant)
-  printf "  真の活性工程 = I3 / I6 / I9 (残り 7 工程は inert)\n"
-  printf "  ref (中心条件) の性能指数 P = %.1f、 spec = P >= %.1f → ref は未達\n\n"
-         (gCoded refCoded) specP
-
-  putStrLn "── Phase 1: DSD スクリーニング ──────────────────────────────"
-  let (nRun, rnk, hasOpt, ranked) = screening
-  printf "  DSD: %d run・主効果モデル行列 rank = %d (= 31 なら全主効果推定可)・%s\n"
-         nRun rnk (if hasOpt then "verified" else "structural 近似" :: String)
-  putStrLn "  効果 Pareto (top 10):"
-  printf "  %-10s | %8s | %s\n" ("因子" :: String) ("|効果|" :: String) ("活性?" :: String)
-  putStrLn "  -----------|----------|------"
-  mapM_ (\(j, a, _) ->
-           printf "  %-10s | %8.3f | %s\n" (T.unpack (factorLabel j)) a
-                  (if j `elem` activeFactorIdxs then "●" else "" :: String))
-        (take 10 ranked)
-  putStrLn ""
-  screeningFigure ranked
-
-  putStrLn ""
-  putStrLn "── Phase 2: 試作 RSM (絞り込み → blocked D-最適 → 最適条件) ──"
-  let (out, figs) = runPhase2 ranked
-      sel  = selectedImplants ranked
-      rep  = roReport out
-  printf "  絞り込み: 支配工程 = %s → carry 因子 (dose/energy) = %d 個\n"
-         (unwords [ "I" ++ show (i+1) | i <- sel ]) (length (roCarried out))
-  printf "  試作設計: %d lot × (自由 %d + center %d) = %d 枚 (D-最適・block=lot)\n"
-         nLot (roNFree out `div` nLot) centerPerLot
-         (roNFree out + roNCenter out)
-  printf "  RSM fit: R² = %.3f・停留点の性質 = %s・領域内 = %s\n"
-         (rsmR2 rep)
-         (case rsmNature rep of RMaximum -> "極大"; RMinimum -> "極小"; RSaddle -> "鞍点" :: String)
-         (if rsmInRegion rep then "yes" else "no (外挿)" :: String)
-  putStrLn "  最適条件 (自然単位):"
-  mapM_ (\(nm, v) -> printf "    %-10s = %s\n" (T.unpack nm) (fmtNat nm v))
-        (rsmStationary rep)
-  putStrLn ""
-  printf "  ── ストーリー closure ──────────────────────\n"
-  printf "  ref (前タイプ・中心条件)   P = %6.2f  → spec %.0f %s\n"
-         (gCoded refCoded) specP (verdict (gCoded refCoded))
-  printf "  RSM 予測 (最適条件)         P = %6.2f\n" (rsmPredicted rep)
-  printf "  真の応答 (最適条件で検証)   P = %6.2f  → spec %.0f %s\n"
-         (roTruePOpt out) specP (verdict (roTruePOpt out))
-  putStrLn ""
-  figs
-  putStrLn ""
-  putStrLn "  完了: 3 図を demo-output/doe/ に出力。"
-  where
-    verdict p = if p >= specP then "達成 ✓" else "未達 ✗" :: String
-    -- 自然単位の見やすい整形 (dose は指数、 他は小数)。
-    fmtNat :: Text -> Double -> String
-    fmtNat nm v
-      | "_dose" `T.isSuffixOf` nm = printf "%.2e ions/cm²" v
-      | "_energy" `T.isSuffixOf` nm = printf "%.1f keV" v
-      | otherwise = printf "%.2f" v
diff --git a/demo/doe/RSMSampleSizeDemo.hs b/demo/doe/RSMSampleSizeDemo.hs
deleted file mode 100644
--- a/demo/doe/RSMSampleSizeDemo.hs
+++ /dev/null
@@ -1,199 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE PartialTypeSignatures #-}
-{-# OPTIONS_GHC -Wno-partial-type-signatures #-}
--- | DOE demo: RSM の予測精度 vs 実験数 (= 必要実験数の見極め)。
---
--- 温度 (3 水準・Num)・エネルギー (3 水準・Num)・角度 (連続・Cont) の
--- 二次応答曲面を対象に:
---   1. @customDesign@ (二次モデル) で n-run の D-最適設計を作る
---   2. 既知の真の曲面 + Gaussian noise で応答を sim
---   3. 二次 OLS を fit
---   4. 独立テスト集合で 真の曲面 vs 予測 の RMSE を測る (設計シード × noise 反復で平均)
---   5. n を振り、 hgg で「RMSE vs n」の折れ線 (noise sd 参照線つき) を SVG 出力
---
--- 真の曲面には二次モデルで表せない @0.8·angle³@ を混ぜてあり、 n を増やしても
--- 消えない bias 床を作る (= 実務の「これ以上は実験でなくモデルを増やせ」を再現)。
-module Main where
-
-import qualified Data.Vector as V
-import qualified Numeric.LinearAlgebra as LA
-import qualified Data.Text as T
-import           Data.Text (Text)
-import           Text.Printf (printf)
-import           System.Directory (createDirectoryIfMissing)
-
-import           Hanalyze.Plot
-                   ( customDesign, customSpec, numFactor, contFactor, quadratic, designFrame
-                   , designTable, designModel, profiler, toPlot, (|->) )
-import           Hanalyze.DataIO.Convert (getDoubleVec)
-import           Hgg.Plot.Spec
-                   ( ColData (..), layer, linePoints, scatterPoints, Point2 (..)
-                   , color, markWidth
-                   , refHorizontal, title, subtitle, xLabel, yLabel, width, height
-                   , theme, ThemeName (..) )
-import           Hgg.Plot.Frame ((|>>))
-import           Hgg.Plot.Color (fromHex)
-import           Hgg.Plot.Backend.SVG (saveSVG, saveSVGBound)
-
--- === 真の応答曲面とモデル ==================================================
-
--- 正規化座標 (各 ~[-1,1])。
-ut, ue, ua :: Double -> Double
-ut t = (t - 165) / 15
-ue e = (e - 20) / 10
-ua a = (a - 45) / 45
-
--- 真の曲面 = 二次 + 0.8·angle³ (二次モデルでは捉えられない bias 源)。
-gTrue :: Double -> Double -> Double -> Double
-gTrue t e a =
-  let x = ut t; y = ue e; z = ua a
-  in 50 + 8*x + 5*y + 6*z - 4*x*x - 3*y*y - 5*z*z + 2*x*y + 1.5*x*z - 1*y*z
-       + 0.8*z*z*z
-
--- 二次モデルの特徴ベクトル (10 項: 切片 + 主 3 + 二乗 3 + 交互 3)。
-feat :: Double -> Double -> Double -> [Double]
-feat t e a = let x = ut t; y = ue e; z = ua a
-             in [1, x, y, z, x*x, y*y, z*z, x*y, x*z, y*z]
-
-noiseSd :: Double
-noiseSd = 1.5
-
--- === 決定的 PRNG → Gaussian (Box-Muller) ==================================
-
-lcg :: Int -> Int
-lcg s = (1103515245 * s + 12345) `mod` 2147483648
-
-u01 :: Int -> Double
-u01 s = fromIntegral s / 2147483648
-
-gaussians :: Int -> [Double]
-gaussians seed = go (lcg seed)
-  where
-    go s = let s1 = lcg s; s2 = lcg s1
-               u1 = max 1e-12 (u01 s1); u2 = u01 s2
-           in sqrt (-2 * log u1) * cos (2 * pi * u2) : go s2
-
--- === 設計・fit・評価 ======================================================
-
--- 温度/エネルギー = 3 水準 Num、 角度 = 連続。 二次モデルで D-最適設計。
-design n dseed = customDesign (customSpec
-  [ numFactor "temp"   [150, 165, 180]
-  , numFactor "energy" [10, 20, 30]
-  , contFactor "angle" (0, 90) ]
-  (quadratic ["temp", "energy", "angle"]) n dseed)
-
--- 設計 (designFrame) から (temp, energy, angle) 実値の行を取り出す。
-rowsOf :: Int -> Int -> [(Double, Double, Double)]
-rowsOf n dseed =
-  let df = designFrame (design n dseed)
-      col :: Text -> [Double]
-      col c = maybe (error (T.unpack c)) V.toList (getDoubleVec c df)
-  in zip3 (col "temp") (col "energy") (col "angle")
-
--- 固定・独立なテスト集合 (2000 点): 離散 temp/energy + 連続 angle。
-testSet :: [(Double, Double, Double)]
-testSet = take 2000
-  [ ([150,165,180] !! i, [10,20,30] !! j, 90 * u01 (lcg (lcg (7919*k + 13))))
-  | (i, j, k) <- zip3 (cycle [0,1,2,1,0,2,2,0,1]) (cycle [1,2,0,2,1,0,1,2,0]) [1 ..] ]
-
--- OLS: beta = pinv(X) y。
-ols :: [[Double]] -> [Double] -> [Double]
-ols xs ys = LA.toList (LA.flatten (LA.pinv (LA.fromLists xs) LA.<> LA.asColumn (LA.fromList ys)))
-
--- 1 fit (設計シード dseed・noise 反復 rep) の テスト RMSE。
-rmseFor :: Int -> Int -> Int -> Double
-rmseFor n dseed rep =
-  let rws  = rowsOf n dseed
-      xs   = [ feat t e a | (t, e, a) <- rws ]
-      ys   = [ gTrue t e a + noiseSd * z
-             | ((t, e, a), z) <- zip rws (gaussians (n*1000 + dseed*37 + rep)) ]
-      beta = ols xs ys
-      errs = [ sum (zipWith (*) (feat t e a) beta) - gTrue t e a | (t, e, a) <- testSet ]
-  in sqrt (sum (map (^ (2 :: Int)) errs) / fromIntegral (length errs))
-
--- 各 n を 6 設計シード × 6 noise 反復 = 36 fit で平均。
-meanRmse :: Int -> Double
-meanRmse n = sum [ rmseFor n ds rp | ds <- [1..6], rp <- [1..6] ] / 36
-
--- === 効果プロット (CI 帯) : n による信頼区間の変化 =======================
-
--- 応答 y を design frame に載せて二次モデルを当てはめ、 profiler 用モデルを返す。
---   @(("y", ys) : designTable plan) |-> designModel plan "y"@ が慣用形。
---   各 n は別々の設計・データ → 別々のモデル (データはモデル内に同梱される)。
-modelN :: Int -> Int -> _
-modelN n dseed =
-  let plan   = design n dseed
-      rs     = designTable plan
-      colD :: Text -> [Double]
-      colD c = maybe (error (T.unpack c)) id (lookup c rs)
-      ts = colD "temp"; es = colD "energy"; as = colD "angle"
-      ys = [ gTrue t e a + noiseSd * z
-           | (t, e, a, z) <- zip4 ts es as (gaussians (n*1000 + 1)) ]
-  in (("y", ys) : rs) |-> designModel plan "y"
-  where
-    zip4 (a:as') (b:bs) (c:cs) (d:ds) = (a, b, c, d) : zip4 as' bs cs ds
-    zip4 _ _ _ _                      = []
-
--- 効果プロット図: 横 = 因子・縦 = 応答、 1 行 3 列 (因子) を n ごとに縦積み。
--- **profiler** が予測線 + 95% CI 帯 + 実測散布を (応答×因子) で自動描画する (JMP 流)。
--- 行 = models リスト (= n)、 列 = 因子。 各 n は別モデルで、 データはモデル内に同梱
--- されるため 束ねは空 df でよい。 n=10 は係数10個=飽和 (df=0) だが、 CI 帯計算の
--- df<=0 ガード (Phase 82・LM.hs) により例外を出さず帯が線に潰れる (= CI 不能を素直に表現)。
-effectGridFigure :: IO ()
-effectGridFigure = do
-  let dseed = 20260708
-      spec  = toPlot (profiler [ ("n=10", modelN 10 dseed)
-                               , ("n=20", modelN 20 dseed)
-                               , ("n=40", modelN 40 dseed) ]
-                               ["temp", "energy", "angle"])
-                <> title "RSM 効果プロット: n で信頼区間がどう変わるか (行=n・列=因子)"
-                <> width 780 <> height 720       -- 3×3 grid (意図的な非既定サイズ)
-                <> theme ThemeGrey
-      noDf = [] :: [(Text, ColData)]             -- profiler のデータはモデル内・束ねは空
-  createDirectoryIfMissing True "demo-output/doe"
-  saveSVGBound "demo-output/doe/rsm-effects-by-n.svg" (noDf |>> spec)
-  putStrLn "  → wrote demo-output/doe/rsm-effects-by-n.svg"
-
--- === main ================================================================
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  RSM 予測精度 vs 実験数 (温度3水準 × エネルギー3水準 × 角度連続)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  printf "  真の曲面 = 二次 + 0.8·angle³ (bias 源), noise sd = %.2f\n" noiseSd
-  printf "  各 n を 6 設計シード × 6 noise 反復 = 36 fit で平均、 テスト %d 点\n\n"
-         (length testSet)
-  let ns    = [10, 12, 14, 16, 20, 24, 30, 40, 60, 80]
-      rmses = [ (n, meanRmse n) | n <- ns ]
-  printf "  %4s | %8s | %8s | %s\n" ("n" :: String) ("RMSE" :: String)
-         ("noise比" :: String) ("前点比 改善%" :: String)
-  putStrLn "  -----|----------|----------|-----------"
-  mapM_ (\((n, r), prev) ->
-           let imp = case prev of
-                       Nothing         -> ""
-                       Just (_, pr)    -> printf "%.1f%%" (100 * (pr - r) / pr) :: String
-           in printf "  %4d | %8.4f | %8.2f | %s\n" n r (r / noiseSd) imp)
-        (zip rmses (Nothing : map Just rmses))
-  putStrLn ""
-
-  -- === hgg で RMSE vs n を描画 ===
-  createDirectoryIfMissing True "demo-output/doe"
-  let pts   = [ Point2 (fromIntegral n) r | (n, r) <- rmses ]
-      curve = fromHex "#2c7fb8"
-      spec  = layer (linePoints pts <> color curve <> markWidth 2.5)
-           <> layer (scatterPoints pts <> color curve)
-           <> refHorizontal noiseSd                     -- noise sd の水平参照線
-           <> title    "RSM 予測精度 vs 実験数"
-           <> subtitle "参照線 = noise sd (1 測定の誤差)。 下回れば曲面が生データより正確"
-           <> xLabel   "実験数 n"
-           <> yLabel   "予測 RMSE (真の曲面 vs 予測)"
-           -- サイズは指定せず既定 (468×288pt = 624×384px・README 図と同寸) に合わせる
-           <> theme ThemeGrey
-  saveSVG "demo-output/doe/rsm-samplesize.svg" spec
-  putStrLn "  → wrote demo-output/doe/rsm-samplesize.svg"
-
-  -- === 効果プロット (n=10/20/40 で CI 帯がどう変わるか) ===
-  putStrLn ""
-  putStrLn "  効果プロット (行=n · 列=因子・CI 帯) を生成..."
-  effectGridFigure
diff --git a/demo/io/DirtyDataDemo.hs b/demo/io/DirtyDataDemo.hs
deleted file mode 100644
--- a/demo/io/DirtyDataDemo.hs
+++ /dev/null
@@ -1,142 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | 汚いデータを @data\/dirty\/@ から読み込み、'Hanalyze.DataIO.CSV.loadAutoSafeWith'
--- がどんな警告コード (W001…W008) や情報コード (I010…I012) を出すかを
--- 19 ファイル分一覧表示するショーケース demo。
---
--- 実行方法:
---
--- @
--- cabal run dirty-data-demo
--- @
---
--- 出力例:
---
--- @
--- ───────────────────────────────────────────────────────────────────
---   data/dirty/02_no_header.csv
--- ───────────────────────────────────────────────────────────────────
---   [WARN]  W001: 列名が全て数値です: 1.0, 2.0 — ヘッダ行が無いファイル
---                  の可能性。ヒント: --no-header を指定してください。
---   → 同ファイルを LoadOpts { loNoHeader = True } で再読込:
---   [INFO]  I012: --no-header: ヘッダ 2 列 (col0...) を生成しました。
--- @
---
--- 19 ファイル全件処理し、最後にまとめテーブルを出力する。
-module Main where
-
-import qualified Data.Text    as T
-import qualified Data.Text.IO as TIO
-import Control.Monad (forM_, forM)
-import Data.List (sort)
-import Text.Printf (printf)
-
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified DataFrame.Operations.Core     as DX
-import qualified Hanalyze.DataIO.CSV    as CSV
-import qualified Hanalyze.DataIO.Log    as Log
-
-dataDir :: FilePath
-dataDir = "data/dirty"
-
--- | (ファイル名, 期待される W コード, 「修復策」のオプション)。
-fixtures :: [(FilePath, [T.Text], Maybe CSV.LoadOpts)]
-fixtures =
-  -- 期待 W コードは Sniff (loSniff=True デフォルト) 適用後の値。
-  -- Sniff で自動修復される ([] になる) ものはコメントで元の警告を残す。
-  [ ("01_clean.csv",              [],            Nothing)
-  , ("02_no_header.csv",          [], -- sniff: header off で W001 → 0
-       Just (CSV.defaultLoadOpts { CSV.loNoHeader = True }))
-  , ("03_preamble.csv",           [], -- sniff: skip=3 で W002 → 0
-       Just (CSV.defaultLoadOpts { CSV.loSkip = 3 }))
-  , ("04_ragged.csv",             [],            Nothing)
-  , ("05_dup_header.csv",         ["W004", "W004"],                       Nothing)
-  , ("06_blank_unnamed.csv",      ["W004", "W004", "W004", "W004"],       Nothing)
-  , ("07_mixed_na.csv",           ["W003", "W006"],                       Nothing)
-  , ("08_thousands_currency.csv", ["W008"],      Nothing)
-  , ("09_quotes_commas.csv",      [],            Nothing)
-  , ("10_bom.csv",                [],            Nothing)
-  , ("11_semicolon_eu.csv",       ["W008", "W008", "W008"], -- sniff で ;、ただし "1,5" が桁区切りに誤検出 (Phase C 課題)
-       Nothing)
-  , ("12_real.tsv",               [],            Nothing)
-  , ("13_crlf.csv",               [], -- sniff で tab → 0
-       Nothing)
-  , ("14_wrong_ext.csv",          [], -- sniff で tab → 0
-       Nothing)
-  , ("15_trailing_blank.csv",     [],            Nothing)
-  , ("16_dates_units.csv",        ["W007"],      Nothing)
-  , ("17_empty.csv",              ["LeftError"], Nothing)
-  , ("18_header_only.csv",        ["LeftError"], Nothing)
-  , ("19_whitespace.csv",         [],            Nothing)
-  ]
-
-main :: IO ()
-main = do
-  putStrLn "==========================================================="
-  putStrLn " Dirty Data Demo — Hanalyze.DataIO.CSV.loadAutoSafeWith showcase"
-  putStrLn "==========================================================="
-  putStrLn ""
-
-  results <- forM fixtures $ \(name, expectCodes, mFix) -> do
-    let path = dataDir <> "/" <> name
-    sep
-    putStrLn $ "  " ++ path
-    sep
-    actualCodes <- describeOne path CSV.defaultLoadOpts
-    -- 期待コードと突き合わせて簡易判定
-    let expectedSet = sort expectCodes
-        actualSet   = sort actualCodes
-        ok = expectedSet == actualSet
-    printf "  期待 W コード: %s\n" (showCodes expectedSet)
-    printf "  実測 W コード: %s\n"
-           (showCodes actualSet ++ if ok then "  [OK]" else "  [DIFF]")
-
-    -- 修復策があれば再ロードして I コードを出す
-    case mFix of
-      Just lo -> do
-        putStrLn ""
-        putStrLn "  → 修復案で再読込:"
-        _ <- describeOne path lo
-        return ()
-      Nothing -> return ()
-
-    putStrLn ""
-    return (name, ok)
-
-  -- 集計
-  putStrLn "==========================================================="
-  putStrLn " Summary"
-  putStrLn "==========================================================="
-  let nOK = length (filter snd results)
-  printf "  期待コード一致: %d / %d\n" nOK (length results)
-  forM_ results $ \(name, ok) ->
-    printf "    %-32s %s\n" name ((if ok then "OK" else "DIFF") :: String)
-
-sep :: IO ()
-sep = putStrLn "-----------------------------------------------------------"
-
--- | 1 ファイルを読み、ログを stdout に出して、得られた W/I コードのリストを返す。
--- Left の場合は ["LeftError"] を返してテストの突き合わせに使う。
-describeOne :: FilePath -> CSV.LoadOpts -> IO [T.Text]
-describeOne path lo = do
-  r <- CSV.loadAutoSafeWith lo path
-  case r of
-    Left err -> do
-      printf "  [Parse error] %s\n" err
-      return ["LeftError"]
-    Right (df, lg) -> do
-      let (nrows, _) = DX.dimensions df
-          ncols      = length (DX.columnNames df)
-      printf "  Rows / Cols : %d × %d\n" nrows ncols
-      let es = Log.entries lg
-      if null es
-        then putStrLn "  (no warnings)"
-        else mapM_ (TIO.putStrLn . ("  " <>) . Log.prettyEntry) es
-      return [ Log.lgCode e | e <- es, sevWarn (Log.lgSev e) ]
-
-sevWarn :: Log.Severity -> Bool
-sevWarn Log.Warn = True
-sevWarn _        = False
-
-showCodes :: [T.Text] -> String
-showCodes [] = "(none)"
-showCodes xs = T.unpack (T.intercalate ", " xs)
diff --git a/demo/io/ExternalIODemo.hs b/demo/io/ExternalIODemo.hs
deleted file mode 100644
--- a/demo/io/ExternalIODemo.hs
+++ /dev/null
@@ -1,70 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Hanalyze.DataIO.External のデモ。
---
--- Hackage 'dataframe' ライブラリ経由で CSV を読み込み:
--- - 列ごとの自動型推論結果
--- - 欠損値の検出
--- - imputeMean で欠損補完
-import qualified Data.Text as T
-
-import Hanalyze.DataIO.CSV         (loadCSV)
-import Hanalyze.DataIO.Preprocess  (countMissing, imputeMean)
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified DataFrame.Operations.Core     as DX
-import qualified DataFrame.Internal.DataFrame as DXD
-import qualified DataFrame.Internal.Column as DXC
-import Text.Printf        (printf)
-
-testCSV :: String
-testCSV = unlines
-  [ "name,age,score,group"
-  , "Alice,30,95.5,A"
-  , "Bob,25,88.0,B"
-  , "Carol,35,,A"            -- score 欠損
-  , "Dave,,77.2,B"            -- age 欠損
-  , "Eve,42,NA,C"             -- score "NA"
-  ]
-
-main :: IO ()
-main = do
-  let path = "/tmp/external_demo.csv"
-  writeFile path testCSV
-
-  putStrLn "=================================="
-  putStrLn " Hanalyze.DataIO.External Demo"
-  putStrLn "=================================="
-  putStrLn ""
-
-  putStrLn "--- loadCSV (Hackage dataframe) ---"
-  Right df <- loadCSV path
-  printDFTypes df
-  putStrLn ""
-
-  putStrLn "--- countMissing ---"
-  mapM_ (\(c, m) ->
-    if m > 0 then printf "  %s: %d missing\n" (T.unpack c) m
-             else printf "  %s: complete\n"   (T.unpack c))
-    (countMissing df)
-  putStrLn ""
-
-  putStrLn "--- imputeMean \"score\" ---"
-  case imputeMean "score" df of
-    Just df3 -> do
-      printDFTypes df3
-      printf "  → score is now numeric (mean-imputed for NA rows)\n"
-    Nothing -> putStrLn "  imputeMean failed"
-  putStrLn ""
-
-  putStrLn "Done."
-
-printDFTypes :: DXD.DataFrame -> IO ()
-printDFTypes df = do
-  let (rows, ncols) = DX.dimensions df
-  printf "  Rows: %d, Columns: %d\n" rows ncols
-  mapM_ (\n -> case DXD.getColumn n df of
-           Just c  -> printf "    %-10s : %s (len=%d)\n"
-                        (T.unpack n)
-                        (DXC.columnTypeString c)
-                        (DXC.columnLength c)
-           Nothing -> printf "    %-10s : <missing>\n" (T.unpack n))
-        (DX.columnNames df)
diff --git a/demo/io/PotentialMultiKR.hs b/demo/io/PotentialMultiKR.hs
deleted file mode 100644
--- a/demo/io/PotentialMultiKR.hs
+++ /dev/null
@@ -1,89 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | 多出力 RBF カーネルリッジ回帰デモ。
---
--- データ: data/io/potential_wide.csv  (21 dose 行 × 100 z 出力列)
--- モデル: ŷ_j(d) = Σ_i K_h(d, d_i) · α_{ij} ;  α = (K + λI)⁻¹ Y
--- HP    : LOOCV 解析解で h, λ をグリッド最適化
--- 出力 : trash/potential_multikr.html
-module Main where
-
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import qualified Numeric.LinearAlgebra as LA
-import Text.Printf (printf)
-
-import qualified Hanalyze.DataIO.CSV as IO
-import qualified Hanalyze.DataIO.Convert as Conv
-import qualified Hanalyze.Model.Kernel as K
-import Hanalyze.Viz.ReportBuilder
-
-zGrid :: [Double]
-zGrid =
-  let step = 200.0 / 99.0
-  in [ fromIntegral i * step | i <- [0 .. 99 :: Int] ]
-
-main :: IO ()
-main = do
-  Right df <- IO.loadAuto "data/io/potential_wide.csv"
-  let yColNames = [ T.pack (printf "y_z%03d" (i :: Int)) | i <- [1..100] ]
-      Just doseV = Conv.getDoubleVec "dose" df
-      yCols     = map (\c -> case Conv.getDoubleVec c df of
-                               Just v  -> v
-                               Nothing -> error ("missing column: " ++ T.unpack c))
-                      yColNames
-      n = V.length doseV
-      q = length yCols
-      ys = LA.fromLists
-             [ [ (yCols !! j) V.! i | j <- [0 .. q - 1] ]
-             | i <- [0 .. n - 1] ]
-      hs   = K.defaultHGrid doseV
-      lams = K.defaultLamGrid
-      (fit, bestH, bestL, looMSE) =
-        K.autoTuneKernelRidgeMulti K.Gaussian doseV ys hs lams
-      yhat = K.fittedKernelRidgeMulti fit
-      r2v  = K.r2Multi ys yhat
-      res  = ys - yhat
-      rmse = sqrt (LA.sumElements (res * res) / fromIntegral (n * q))
-
-  putStrLn "=== Multi-output Kernel Ridge (RBF, dose only) ==="
-  printf "  N (rows)     = %d\n" n
-  printf "  q (outputs)  = %d\n" q
-  printf "  best h       = %.4f\n" bestH
-  printf "  best lambda  = %.6g\n" bestL
-  printf "  LOO MSE      = %.6f\n" looMSE
-  printf "  RMSE (train) = %.4f\n" rmse
-  printf "  R^2 mean     = %.4f  (min %.4f, max %.4f)\n"
-    (V.sum r2v / fromIntegral q)
-    (V.minimum r2v) (V.maximum r2v)
-
-  let xObs   = V.toList doseV
-      yObs   = [ [ (yCols !! j) V.! i | j <- [0 .. q - 1] ]
-               | i <- [0 .. n - 1] ]
-      alpha2 = [ LA.toList (LA.flatten (K.krmAlpha fit LA.? [i]))
-               | i <- [0 .. n - 1] ]   -- n × q (行抽出)
-      dMin = minimum xObs - 2.0
-      dMax = maximum xObs + 2.0
-      dMid = 0.5 * (dMin + dMax)
-      imo  = mkInteractiveMOKernelRBF "dose" "potential V" "z [nm]"
-                                      zGrid xObs yObs
-                                      xObs alpha2 bestH
-                                      (dMin, dMid, dMax)
-      sections =
-        [ secModelOverview "Multi-output Kernel Ridge (RBF)"
-            "$\\hat{y}_j(d) = \\sum_i \\exp(-\\frac{(d-d_i)^2}{2h^2}) \\, \\alpha_{ij}$"
-            Nothing
-        , secStatRow
-            [ ("N", T.pack (show n))
-            , ("q (outputs)", T.pack (show q))
-            , ("best h", T.pack (printf "%.3f" bestH))
-            , ("best λ", T.pack (printf "%.2g" bestL))
-            , ("LOO MSE", T.pack (printf "%.4g" looMSE))
-            , ("RMSE", T.pack (printf "%.4f" rmse))
-            , ("R^2 mean", T.pack (printf "%.4f"
-                (V.sum r2v / fromIntegral q :: Double)))
-            ]
-        , secInteractiveMultiOut "予測曲線 (dose スライダ)" imo
-        ]
-      cfg = defaultReportConfig "Potential — Multi-output Kernel Ridge (RBF)"
-  renderReport "trash/potential_multikr.html" cfg sections
-  putStrLn "Wrote trash/potential_multikr.html"
diff --git a/demo/io/PotentialMultiOut.hs b/demo/io/PotentialMultiOut.hs
deleted file mode 100644
--- a/demo/io/PotentialMultiOut.hs
+++ /dev/null
@@ -1,81 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | 多出力線形回帰デモ (案 B1)、ReportBuilder 経由の対話的レポート。
---
--- データ: data/io/potential_wide.csv  (21 dose 行 × 100 z 出力列)
--- モデル: Y (n×100) = X (n×2 [1,dose]) · B (2×100)
--- 出力 : trash/potential_multiout.html
-module Main where
-
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import qualified Numeric.LinearAlgebra as LA
-import Text.Printf (printf)
-
-import qualified Hanalyze.DataIO.CSV as IO
-import qualified Hanalyze.DataIO.Convert as Conv
-import qualified Hanalyze.Model.MultiLM as ML
-import Hanalyze.Model.Core (FitResult (..))
-import Hanalyze.Viz.ReportBuilder
-
-zGrid :: [Double]
-zGrid =
-  let step = 200.0 / 99.0
-  in [ fromIntegral i * step | i <- [0 .. 99 :: Int] ]
-
-main :: IO ()
-main = do
-  Right df <- IO.loadAuto "data/io/potential_wide.csv"
-  let yColNames = [ T.pack (printf "y_z%03d" (i :: Int)) | i <- [1..100] ]
-      Just doseV = Conv.getDoubleVec "dose" df
-      yCols     = map (\c -> case Conv.getDoubleVec c df of
-                               Just v  -> v
-                               Nothing -> error ("missing column: " ++ T.unpack c))
-                      yColNames
-      n = V.length doseV
-      q = length yCols
-      x = LA.fromLists [ [1.0, doseV V.! i] | i <- [0 .. n - 1] ]
-      y = LA.fromLists
-            [ [ (yCols !! j) V.! i | j <- [0 .. q - 1] ]
-            | i <- [0 .. n - 1] ]
-      mf = ML.fitMultiLM x y
-      betaB = coefficients (ML.mfFit mf)   -- (2 × q)
-      res  = residuals (ML.mfFit mf)
-      rmse = sqrt (LA.sumElements (res * res) / fromIntegral (n * q))
-      r2v  = rSquared (ML.mfFit mf)
-
-  putStrLn "=== Multi-output Linear Regression (B1: dose only) ==="
-  printf "  N (rows)     = %d\n" n
-  printf "  q (outputs)  = %d\n" q
-  printf "  RMSE overall = %.4f\n" rmse
-  printf "  R^2 mean     = %.4f  (min %.4f, max %.4f)\n"
-    (LA.sumElements r2v / fromIntegral q)
-    (LA.minElement r2v) (LA.maxElement r2v)
-
-  let intercepts = LA.toList (betaB LA.! 0)
-      slopes     = LA.toList (betaB LA.! 1)
-      xObs       = V.toList doseV
-      yObs       = [ [ (yCols !! j) V.! i | j <- [0 .. q - 1] ]
-                   | i <- [0 .. n - 1] ]
-      dMin = minimum xObs - 2.0
-      dMax = maximum xObs + 2.0
-      dMid = 0.5 * (dMin + dMax)
-      imo  = mkInteractiveMOLinear "dose" "potential V" "z [nm]"
-                                   zGrid xObs yObs
-                                   intercepts slopes
-                                   (dMin, dMid, dMax)
-      sections =
-        [ secModelOverview "Multi-output Linear Regression"
-            "$Y_{n\\times q} = X_{n\\times 2} B_{2\\times q} + E$"
-            Nothing
-        , secStatRow
-            [ ("N", T.pack (show n))
-            , ("q (outputs)", T.pack (show q))
-            , ("RMSE", T.pack (printf "%.4f" rmse))
-            , ("R^2 mean", T.pack (printf "%.4f"
-                (LA.sumElements r2v / fromIntegral q)))
-            ]
-        , secInteractiveMultiOut "予測曲線 (dose スライダ)" imo
-        ]
-      cfg = defaultReportConfig "Potential — Multi-output OLS (B1)"
-  renderReport "trash/potential_multiout.html" cfg sections
-  putStrLn "Wrote trash/potential_multiout.html"
diff --git a/demo/io/PreprocessDemo.hs b/demo/io/PreprocessDemo.hs
deleted file mode 100644
--- a/demo/io/PreprocessDemo.hs
+++ /dev/null
@@ -1,158 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeApplications #-}
--- | Hanalyze.DataIO.Preprocess の総合デモ。
---
--- - NA 文字列を含む CSV をロード (Hackage dataframe 経由)
--- - countMissing で欠損列を確認
--- - dropMissingRows / imputeMean / imputeMedian / imputeConstant の比較
--- - filterRowsByNumeric / mapNumeric / deriveNumeric の使用例
-module Main where
-
-import qualified Data.Map.Strict as Map
-import qualified Data.Text as T
-
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified DataFrame.Operators           as DX
-import qualified DataFrame.Operations.Core     as DX
-import qualified DataFrame.Internal.DataFrame as DXD
-import Hanalyze.DataIO.CSV         (loadCSV)
-import Hanalyze.DataIO.Preprocess
-
-import System.IO          (hPutStrLn, stderr)
-import System.Exit        (exitFailure)
-import Text.Printf        (printf)
-
-testCSV :: String
-testCSV = unlines
-  [ "group,age,income"
-  , "A,25,40000"
-  , "A,NA,42000"
-  , "B,32,"
-  , "B,28,55000"
-  , "C,,38000"
-  , "A,45,NA"
-  , "B,30,48000"
-  , "C,55,72000"
-  ]
-
-main :: IO ()
-main = do
-  let path = "/tmp/preprocess_demo.csv"
-  writeFile path testCSV
-
-  result <- loadCSV path
-  case result of
-    Left err -> do
-      hPutStrLn stderr ("Parse error: " ++ err)
-      exitFailure
-    Right df -> runDemo df
-
-runDemo :: DXD.DataFrame -> IO ()
-runDemo df = do
-  putStrLn "=================================="
-  putStrLn " Hanalyze.DataIO.Preprocess Demo"
-  putStrLn "=================================="
-  putStrLn ""
-  let (nrows, _) = DX.dimensions df
-  printf "Loaded %d rows, columns: %s\n"
-         nrows (T.unpack (T.intercalate ", " (DX.columnNames df)))
-  putStrLn ""
-
-  putStrLn "--- countMissing ---"
-  mapM_ (\(c, m) ->
-    if m > 0 then printf "  %s: %d missing\n" (T.unpack c) m
-             else printf "  %s: complete\n"   (T.unpack c))
-    (countMissing df)
-  putStrLn ""
-
-  putStrLn "--- dropMissingRows [\"age\", \"income\"] ---"
-  let df1 = dropMissingRows ["age", "income"] df
-      (nrows1, _) = DX.dimensions df1
-  printf "  After: %d rows (was %d)\n" nrows1 nrows
-  putStrLn ""
-
-  putStrLn "--- parseNumericColumn ---"
-  case parseNumericColumn "age" df1 >>= parseNumericColumn "income" of
-    Nothing -> putStrLn "  (already numeric or parse failed; OK if Hackage parsed it)"
-    Just df2 -> do
-      printf "  Both age/income are now numeric\n"
-      showNumericStats df2 "age"
-      showNumericStats df2 "income"
-  putStrLn ""
-
-  putStrLn "--- imputeMean / imputeMedian on age ---"
-  case imputeMean "age" df of
-    Just df3 -> do
-      let (n3, _) = DX.dimensions df3
-      printf "  imputeMean produces %d numeric rows\n" n3
-      showNumericStats df3 "age"
-    Nothing -> putStrLn "  imputeMean failed"
-  case imputeMedian "income" df of
-    Just df4 -> do
-      let (n4, _) = DX.dimensions df4
-      printf "  imputeMedian produces %d numeric rows\n" n4
-      showNumericStats df4 "income"
-    Nothing -> putStrLn "  imputeMedian failed"
-  putStrLn ""
-
-  putStrLn "--- filterRowsByNumeric (age >= 30) ---"
-  let dfNum = case imputeMean "age" df >>= imputeMean "income" of
-                Just d  -> d
-                Nothing -> df
-      dfFilt = filterRowsByNumeric "age" (>= 30) dfNum
-      (nNum, _)  = DX.dimensions dfNum
-      (nFilt, _) = DX.dimensions dfFilt
-  printf "  After: %d rows (was %d)\n" nFilt nNum
-  putStrLn ""
-
-  putStrLn "--- mapNumeric \"income\" (/1000) ---"
-  let dfMap = mapNumeric "income" (/ 1000) dfNum
-  showNumericStats dfMap "income"
-  putStrLn ""
-
-  putStrLn "--- deriveNumeric \"ratio\" = income / age ---"
-  let dfDeriv = deriveNumeric "ratio"
-                  (\row -> case (Map.lookup "income" row, Map.lookup "age" row) of
-                             (Just (VNum i), Just (VNum a)) | a > 0 -> i / a
-                             _ -> 0)
-                  dfNum
-  showNumericStats dfDeriv "ratio"
-  putStrLn ""
-
-  putStrLn "--- selectColumns [\"group\", \"age\"] ---"
-  let dfSel = selectColumns ["group", "age"] dfNum
-  printf "  columns: %s\n" (T.unpack (T.intercalate ", " (DX.columnNames dfSel)))
-  putStrLn ""
-
-  putStrLn "Done."
-
-showNumericStats :: DXD.DataFrame -> T.Text -> IO ()
-showNumericStats df name =
-  case readNum name df of
-    Nothing -> printf "  %s: not numeric\n" (T.unpack name)
-    Just xs -> do
-      let m  = length xs
-          mean = sum xs / fromIntegral m
-          mn = minimum xs
-          mx = maximum xs
-      printf "  %-10s n=%d  min=%.2f  max=%.2f  mean=%.2f\n"
-             (T.unpack name) m mn mx mean
-
-readNum :: T.Text -> DXD.DataFrame -> Maybe [Double]
-readNum name df =
-  case DXD.getColumn name df of
-    Nothing -> Nothing
-    Just _  ->
-      case tryReadDouble name df of
-        Just xs -> Just xs
-        Nothing -> tryReadIntAsDouble name df
-
-tryReadDouble :: T.Text -> DXD.DataFrame -> Maybe [Double]
-tryReadDouble name df = either (const Nothing) Just $
-  fmap (map (id :: Double -> Double)) $
-    Right (DX.columnAsList (DX.col @Double name) df)
-
-tryReadIntAsDouble :: T.Text -> DXD.DataFrame -> Maybe [Double]
-tryReadIntAsDouble name df = either (const Nothing) Just $
-  fmap (map (fromIntegral :: Int -> Double)) $
-    Right (DX.columnAsList (DX.col @Int name) df)
diff --git a/demo/io/RegridBenchDemo.hs b/demo/io/RegridBenchDemo.hs
deleted file mode 100644
--- a/demo/io/RegridBenchDemo.hs
+++ /dev/null
@@ -1,219 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
--- | Regrid 機能のベンチマークデモ。
---
--- 1. 真の関数 V(z; D) を物理モデル (PotentialGen と同じ) で生成
--- 2. 観測点を歯抜け化 (20% drop + z ズレ ±15 nm) → long-form
--- 3. 3 補間 (Linear / NaturalSpline / PCHIP) × 2 grid (Uniform / Adaptive) で
---    共通 grid に揃える
--- 4. grid 上で真値と比較し RMSE を計算
--- 5. 全結果を 1 つの HTML レポートにまとめて出力
-module Main where
-
-import qualified Data.Text             as T
-import           Data.Text             (Text)
-import           System.Random.MWC     (createSystemRandom, GenIO, uniformR)
-import qualified System.Random.MWC.Distributions as MWCD
-import           Text.Printf           (printf)
-import           Control.Monad         (forM)
-import           Data.List             (sort)
-
-import qualified DataFrame.Internal.Column    as DX
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified Hanalyze.DataIO.Preprocess     as Pp
-import qualified Hanalyze.Stat.Interpolate      as Interp
-import qualified Hanalyze.Stat.AdaptiveGrid     as AG
-import qualified Hanalyze.Viz.ReportBuilder     as RB
-
--- ---------------------------------------------------------------------------
--- 真の物理モデル (PotentialGen.hs と同じ)
--- ---------------------------------------------------------------------------
-
-projectedRange :: Double -> Double
-projectedRange e = 1.5 * (e ** 0.7)
-
-straggle :: Double -> Double
-straggle e = 0.4 * projectedRange e
-
-surfaceL, implantK, doseRef, doseAlpha, fixedE :: Double
-surfaceL  = 30.0
-implantK  = 8.0
-doseRef   = 10.0
-doseAlpha = 0.26
-fixedE    = 100.0
-
-trueV :: Double -> Double -> Double
-trueV d z =
-  let rp  = projectedRange fixedE
-      sg  = straggle fixedE
-      amp = implantK * ((d / doseRef) ** doseAlpha)
-      surf = 3.5 * exp (negate z / surfaceL)
-      well = amp * exp (negate ((z - rp) ** 2) / (2 * sg * sg))
-  in surf - well
-
-doses :: [Double]
-doses = [6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0]
-
-zRange :: (Double, Double)
-zRange = (0, 200)
-
-zPoints :: Int
-zPoints = 80
-
--- ---------------------------------------------------------------------------
--- 歯抜けデータの生成
--- ---------------------------------------------------------------------------
-
-genJaggedRows :: GenIO -> Double -> IO [(Double, Double)]
-genJaggedRows gen d = do
-  let (zlo, zhi) = zRange
-      base       = (zhi - zlo) / fromIntegral (zPoints - 1)
-      jitter     = base * 2.5
-  zs <- forM [0 .. zPoints - 1] $ \i -> do
-    let zb = zlo + fromIntegral i * base
-    j <- uniformR (-jitter, jitter) gen
-    return (max zlo (min zhi (zb + j)))
-  let zsSorted = sort zs
-  -- 20% を欠損化
-  pts <- forM zsSorted $ \z -> do
-    drop' <- uniformR (0, 1 :: Double) gen
-    if drop' < 0.20
-      then return Nothing
-      else do
-        eps <- MWCD.normal 0 0.1 gen
-        return (Just (z, trueV d z + eps))
-  return [p | Just p <- pts]
-
-condId :: Double -> Text
-condId d = T.pack (printf "D%.0f" d)
-
--- ---------------------------------------------------------------------------
--- 補間 + RMSE 計測
--- ---------------------------------------------------------------------------
-
-data Bench = Bench
-  { bInterp   :: Interp.InterpKind
-  , bGrid     :: AG.GridKind
-  , bRMSE     :: Double
-  , bNGrid    :: Int
-  , bResult   :: Pp.RegridResult
-  }
-
-interpName :: Interp.InterpKind -> Text
-interpName Interp.Linear        = "Linear"
-interpName Interp.NaturalSpline = "NaturalSpline"
-interpName Interp.PCHIP         = "PCHIP"
-
-gridName :: AG.GridKind -> Text
-gridName AG.Uniform  = "Uniform"
-gridName AG.Adaptive = "Adaptive"
-
-runBench :: DX.DataFrame -> Interp.InterpKind -> AG.GridKind -> Bench
-runBench df ik gk =
-  let opts = Pp.defaultRegridOpts
-               { Pp.roInterp      = ik
-               , Pp.roGridKind    = gk
-               , Pp.roN           = 30
-               , Pp.roZBoundsMode = Pp.ZIntersection
-               }
-      rr   = Pp.regridLong "id" "z" "y" opts df
-      -- grid 上の予測 vs 真値
-      sqErrs =
-        [ let yTrue = trueV (read (drop 1 (T.unpack i)) :: Double) z
-              yHat  = f z
-          in (yHat - yTrue) ** 2
-        | (i, _, f) <- Pp.rrPerIdInterp rr
-        , z <- Pp.rrZGrid rr
-        ]
-      rmse = if null sqErrs then 0
-             else sqrt (sum sqErrs / fromIntegral (length sqErrs))
-  in Bench ik gk rmse (length (Pp.rrZGrid rr)) rr
-
--- ---------------------------------------------------------------------------
--- レポート生成
--- ---------------------------------------------------------------------------
-
-mkBenchReport :: [Bench] -> [RB.ReportSection]
-mkBenchReport benches =
-  let cmpRows = [ [ interpName (bInterp b) <> " / " <> gridName (bGrid b)
-                  , T.pack (printf "%.4f" (bRMSE b))
-                  , T.pack (show (bNGrid b))
-                  ]
-                | b <- benches ]
-      cmpTable = RB.secTable "RMSE benchmark (vs true V(z; D))"
-                   ["Method", "RMSE", "Grid N"] cmpRows
-      detailSections =
-        [ RB.secInterpolation (irFromBench b)
-        | b <- benches ]
-  in cmpTable : detailSections
-
-irFromBench :: Bench -> RB.InterpReport
-irFromBench b =
-  let rr   = bResult b
-      perObs   = [ (i, pts) | (i, pts, _) <- Pp.rrPerIdInterp rr ]
-      perInterp = [ (i, [(z, f z) | z <- Pp.rrZGrid rr])
-                  | (i, _, f) <- Pp.rrPerIdInterp rr ]
-      perSummary = [ (Pp.piId s, Pp.piNObserved s
-                    , Pp.piZMin s, Pp.piZMax s
-                    , Pp.piExtrapBelow s, Pp.piExtrapAbove s
-                    , Pp.piResidualMax s)
-                   | s <- Pp.rrPerIdStats rr ]
-  in RB.InterpReport
-       { RB.irTitle         = interpName (bInterp b) <> " / "
-                              <> gridName (bGrid b)
-                              <> " — RMSE "
-                              <> T.pack (printf "%.4f" (bRMSE b))
-       , RB.irInterpKind    = interpName (bInterp b)
-       , RB.irGridKind      = gridName (bGrid b)
-       , RB.irN             = bNGrid b
-       , RB.irZBoundsMode   = "intersect"
-       , RB.irZMin          = Pp.rrZMin rr
-       , RB.irZMax          = Pp.rrZMax rr
-       , RB.irPerIdObserved = perObs
-       , RB.irPerIdInterpY  = perInterp
-       , RB.irGrid          = Pp.rrZGrid rr
-       , RB.irDensity       = Pp.rrDensity rr
-       , RB.irPerIdSummary  = perSummary
-       , RB.irExtraEnabled  = False
-       , RB.irPerIdYRange   = []
-       }
-
--- ---------------------------------------------------------------------------
--- main
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  gen <- createSystemRandom
-  putStrLn "Regrid benchmark — 6 methods × 9 dose levels"
-  -- 全 dose の歯抜けデータを 1 つの long DataFrame にまとめる
-  perDoseData <- forM doses $ \d -> do
-    pts <- genJaggedRows gen d
-    return (condId d, pts)
-  let allRows = concat
-        [ [ (i, z, y) | (z, y) <- pts ]
-        | (i, pts) <- perDoseData ]
-      ids    = map (\(i,_,_) -> i) allRows
-      zs     = map (\(_,z,_) -> z) allRows
-      ys     = map (\(_,_,y) -> y) allRows
-      df     = DX.insertColumn "y"  (DX.fromList ys)
-             $ DX.insertColumn "z"  (DX.fromList zs)
-             $ DX.insertColumn "id" (DX.fromList ids)
-             $ DX.empty
-  printf "  Generated %d rows from %d ids\n" (length allRows) (length doses)
-  -- 6 組合せでベンチマーク
-  let kinds = [Interp.Linear, Interp.NaturalSpline, Interp.PCHIP]
-      grids = [AG.Uniform, AG.Adaptive]
-      benches = [ runBench df ik gk | ik <- kinds, gk <- grids ]
-  putStrLn "RMSE results (vs true V(z; D)):"
-  mapM_ (\b -> printf "  %-15s / %-9s : RMSE = %.4f (N=%d)\n"
-                  (T.unpack (interpName (bInterp b)))
-                  (T.unpack (gridName (bGrid b)))
-                  (bRMSE b)
-                  (bNGrid b))
-        benches
-  let outPath = "trash/regrid_bench.html"
-  RB.renderReport outPath
-                  (RB.defaultReportConfig "Regrid benchmark — 3 interp × 2 grid")
-                  (mkBenchReport benches)
-  putStrLn $ "Wrote " ++ outPath
diff --git a/demo/regression/AnalysisCompareDemo.hs b/demo/regression/AnalysisCompareDemo.hs
deleted file mode 100644
--- a/demo/regression/AnalysisCompareDemo.hs
+++ /dev/null
@@ -1,404 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | AnalysisReport vs ReportBuilder の比較デモ。
---
--- LM / GLM / GLMM / GP / HBM の 5 モデルそれぞれで:
---   1. 既存 'Hanalyze.Viz.AnalysisReport' で HTML を生成
---   2. 新 'Hanalyze.Viz.ReportBuilder' で同等の HTML を生成
--- → trash/ 以下に 10 ファイルが出力されるので、ブラウザで開いて見比べる。
-module Main where
-
-import qualified Data.Map.Strict as Map
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import qualified Numeric.LinearAlgebra as LA
-import System.Random.MWC (createSystemRandom)
-import Text.Printf (printf)
-
-import qualified DataFrame.Internal.Column    as DX
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified DataFrame.Operations.Core     as DX
-import qualified DataFrame.Internal.DataFrame as DXD
-import Hanalyze.DataIO.Convert      (getDoubleVec, getTextVec)
-import Hanalyze.DataIO.CSV          (loadAuto)
-import qualified Hanalyze.Model.Core as Core
-import qualified Hanalyze.Model.LM   as LM
-import qualified Hanalyze.Model.GLM  as GLM
-import qualified Hanalyze.Model.GLMM as GLMM
-import qualified Hanalyze.Model.GP   as GP
-import qualified Hanalyze.Model.HBM  as HBM
-import qualified Hanalyze.MCMC.NUTS  as NUTS
-import qualified Hanalyze.MCMC.Core  as MCMCcore
-import qualified Hanalyze.Stat.MCMC  as StatMCMC
-
-import Hanalyze.Model.Core (residualsV, fittedList, coeffList, rSquared1)
-
-import qualified Hanalyze.Viz.AnalysisReport as AR
-import qualified Hanalyze.Viz.ReportBuilder  as RB
-import qualified Hanalyze.Viz.ReportInstances as RI
-import qualified Hanalyze.Viz.ModelGraph     as VMG
-
--- ---------------------------------------------------------------------------
--- Helpers
--- ---------------------------------------------------------------------------
-
-makeGrid :: V.Vector Double -> Int -> [Double]
-makeGrid v n =
-  let lo = V.minimum v
-      hi = V.maximum v
-  in [ lo + fromIntegral i * (hi - lo) / fromIntegral (n - 1)
-     | i <- [0 .. n - 1] ]
-
-sortAsc :: [Double] -> [Double]
-sortAsc [] = []
-sortAsc (p:rs) = sortAsc [x | x <- rs, x <= p]
-              ++ [p]
-              ++ sortAsc [x | x <- rs, x > p]
-
-main :: IO ()
-main = do
-  putStrLn "============================================================"
-  putStrLn " AnalysisReport vs ReportBuilder Comparison Demo"
-  putStrLn "============================================================"
-  putStrLn ""
-
-  -- データロード
-  Right dfLM   <- loadAuto "data/regression/test_lm.csv"
-  Right dfPois <- loadAuto "data/regression/test_poisson.csv"
-
-  putStrLn "Loaded:"
-  putStrLn $ "  data/regression/test_lm.csv      ("
-             ++ show ((fst (DX.dimensions dfLM))) ++ " rows)"
-  putStrLn $ "  data/regression/test_poisson.csv ("
-             ++ show ((fst (DX.dimensions dfPois))) ++ " rows)"
-  putStrLn ""
-
-  doLMDemo  dfLM
-  doGLMDemo dfPois
-  doGLMMDemo
-  doGPDemo  dfLM
-  doHBMDemo dfLM
-
-  putStrLn ""
-  putStrLn "============================================================"
-  putStrLn " All 10 reports written to trash/."
-  putStrLn " Open in a browser:"
-  putStrLn "   trash/cmp_lm_AR.html      vs trash/cmp_lm_RB.html"
-  putStrLn "   trash/cmp_glm_AR.html     vs trash/cmp_glm_RB.html"
-  putStrLn "   trash/cmp_glmm_AR.html    vs trash/cmp_glmm_RB.html"
-  putStrLn "   trash/cmp_gp_AR.html      vs trash/cmp_gp_RB.html"
-  putStrLn "   trash/cmp_hbm_AR.html     vs trash/cmp_hbm_RB.html"
-  putStrLn "============================================================"
-
--- ---------------------------------------------------------------------------
--- LM
--- ---------------------------------------------------------------------------
-
-doLMDemo :: DXD.DataFrame -> IO ()
-doLMDemo df = do
-  putStrLn "--- LM ---"
-  case (getDoubleVec "x" df, getDoubleVec "y" df) of
-    (Just xVec, Just yVec) -> do
-      writeARLM df
-      writeRBLM df xVec yVec
-    _ -> putStrLn "  (LM data not loaded)"
-
-writeARLM :: DXD.DataFrame -> IO ()
-writeARLM df = do
-  case LM.fitPolyWithSmooth (Core.CI 0.95) 100 df "x" "y" of
-    Just (fit, sf) -> do
-      let smoothData = Just ("x", AR.SmoothData
-                              { AR.sdXs    = LM.sfX sf
-                              , AR.sdYs    = LM.sfFit sf
-                              , AR.sdLower = LM.sfLower sf
-                              , AR.sdUpper = LM.sfUpper sf
-                              , AR.sdHasBand = LM.sfHasBand sf })
-          summary = AR.mkFitSummary GLM.Gaussian GLM.Identity [("x", 1)]
-                                    smoothData fit
-          rcfg = AR.AnalysisReportConfig "LM (AnalysisReport)"
-      AR.writeAnalysisReport "trash/cmp_lm_AR.html" rcfg df ["x"] "y"
-        (AR.RegFit summary) []
-      putStrLn "  AR: trash/cmp_lm_AR.html"
-    Nothing -> putStrLn "  AR: fit failed"
-
-writeRBLM :: DXD.DataFrame -> V.Vector Double -> V.Vector Double -> IO ()
-writeRBLM df _xVec _yVec = do
-  appendixSec <- RB.secAppendixFromMd "付録: モデルの原理"
-                   "docs/principles/lm.ja.md"
-  case LM.fitPolyWithSmooth (Core.CI 0.95) 100 df "x" "y" of
-    Just (fit, sf) -> do
-      let cfg      = RB.defaultReportConfig "LM (ReportBuilder)"
-          report   = RI.LMReport fit (Just sf)
-          sections = RB.toReport cfg df ["x"] "y" report ++ [appendixSec]
-      RB.renderReport "trash/cmp_lm_RB.html" cfg sections
-      putStrLn "  RB: trash/cmp_lm_RB.html (Reportable LMReport instance)"
-    Nothing -> putStrLn "  RB: fit failed"
-
--- ---------------------------------------------------------------------------
--- GLM (Poisson)
--- ---------------------------------------------------------------------------
-
-doGLMDemo :: DXD.DataFrame -> IO ()
-doGLMDemo df = do
-  putStrLn "--- GLM (Poisson) ---"
-  -- データの y 列名を判別
-  let yCol = if columnInDF "count" df then "count"
-             else if columnInDF "y" df then "y" else "count"
-      xCol = "x"
-  case (getDoubleVec xCol df, getDoubleVec yCol df) of
-    (Just xVec, Just yVec) -> do
-      writeARGLM df xCol yCol
-      writeRBGLM df xVec yVec xCol yCol
-    _ -> putStrLn $ "  (columns " ++ T.unpack xCol ++ "/"
-                                  ++ T.unpack yCol ++ " not numeric)"
-
-columnInDF :: T.Text -> DXD.DataFrame -> Bool
-columnInDF c df = c `elem` DX.columnNames df
-
-writeARGLM :: DXD.DataFrame -> T.Text -> T.Text -> IO ()
-writeARGLM df xCol yCol = do
-  case GLM.fitGLMWithSmooth GLM.Poisson GLM.Log [(xCol, 1)]
-                              Core.NoBand 100 df yCol of
-    Just (fit, mSmooth) -> do
-      let sm = case mSmooth of
-            Nothing -> Nothing
-            Just sf -> Just (xCol, AR.SmoothData
-                              { AR.sdXs = LM.sfX sf
-                              , AR.sdYs = LM.sfFit sf
-                              , AR.sdLower = LM.sfLower sf
-                              , AR.sdUpper = LM.sfUpper sf
-                              , AR.sdHasBand = LM.sfHasBand sf })
-          summary = AR.mkFitSummary GLM.Poisson GLM.Log [(xCol, 1)] sm fit
-          rcfg = AR.AnalysisReportConfig "GLM Poisson (AnalysisReport)"
-      AR.writeAnalysisReport "trash/cmp_glm_AR.html" rcfg df [xCol] yCol
-        (AR.RegFit summary) []
-      putStrLn "  AR: trash/cmp_glm_AR.html"
-    Nothing -> putStrLn "  AR: fit failed"
-
-writeRBGLM :: DXD.DataFrame -> V.Vector Double -> V.Vector Double
-           -> T.Text -> T.Text -> IO ()
-writeRBGLM df _xVec _yVec xCol yCol = do
-  appendixSec <- RB.secAppendixFromMd "付録: モデルの原理"
-                   "docs/principles/glm.ja.md"
-  case GLM.fitGLMWithSmooth GLM.Poisson GLM.Log [(xCol, 1)]
-                              Core.NoBand 100 df yCol of
-    Just (fit, mSmooth) -> do
-      let cfg      = RB.defaultReportConfig "GLM Poisson (ReportBuilder)"
-          report   = RI.GLMReport fit GLM.Poisson GLM.Log mSmooth
-          sections = RB.toReport cfg df [xCol] yCol report ++ [appendixSec]
-      RB.renderReport "trash/cmp_glm_RB.html" cfg sections
-      putStrLn "  RB: trash/cmp_glm_RB.html (Reportable GLMReport instance)"
-    Nothing -> putStrLn "  RB: fit failed"
-
--- ---------------------------------------------------------------------------
--- GLMM
--- ---------------------------------------------------------------------------
-
-doGLMMDemo :: IO ()
-doGLMMDemo = do
-  putStrLn "--- GLMM (LME) ---"
-  let xs = V.fromList [1,2,3,4, 1,2,3,4, 1,2,3,4 :: Double]
-      ys = V.fromList [7.1,6.9,7.0,7.0, 5.0,4.9,5.1,5.0, 3.0,2.9,3.1,3.0]
-      gs = V.fromList ["A","A","A","A","B","B","B","B","C","C","C","C"]
-      df = DX.insertColumn "x"     (DX.fromList (V.toList xs :: [Double]))
-         $ DX.insertColumn "y"     (DX.fromList (V.toList ys :: [Double]))
-         $ DX.insertColumn "group" (DX.fromList (V.toList gs :: [T.Text]))
-         $ DX.empty
-  case GLMM.fitLMEDataFrame [("x", 1)] "group" "y" df of
-    Just gr -> do
-      writeARGLMM df gr
-      writeRBGLMM df gr
-    Nothing -> putStrLn "  GLMM fit failed"
-
-writeARGLMM :: DXD.DataFrame -> GLMM.GLMMResult -> IO ()
-writeARGLMM df gr = do
-  let summary = AR.mkGLMMSummary GLM.Gaussian GLM.Identity [("x", 1)]
-                                  "group" Nothing gr
-      rcfg = AR.AnalysisReportConfig "LME (AnalysisReport)"
-  AR.writeAnalysisReport "trash/cmp_glmm_AR.html" rcfg df ["x"] "y"
-    (AR.MixFit summary) []
-  putStrLn "  AR: trash/cmp_glmm_AR.html"
-
-writeRBGLMM :: DXD.DataFrame -> GLMM.GLMMResult -> IO ()
-writeRBGLMM df gr = do
-  appendixSec <- RB.secAppendixFromMd "付録: モデルの原理"
-                   "docs/principles/glmm.ja.md"
-  let cfg      = RB.defaultReportConfig "LME (ReportBuilder)"
-      rep      = RI.GLMMReport gr GLM.Gaussian GLM.Identity "group"
-      sections = RB.toReport cfg df ["x"] "y" rep ++ [appendixSec]
-  RB.renderReport "trash/cmp_glmm_RB.html" cfg sections
-  putStrLn "  RB: trash/cmp_glmm_RB.html (Reportable GLMMReport instance)"
-
--- ---------------------------------------------------------------------------
--- GP
--- ---------------------------------------------------------------------------
-
-doGPDemo :: DXD.DataFrame -> IO ()
-doGPDemo df = do
-  putStrLn "--- GP (RBF) ---"
-  case (getDoubleVec "x" df, getDoubleVec "y" df) of
-    (Just xVec, Just yVec) -> do
-      let xs = V.toList xVec
-          ys = V.toList yVec
-          p0 = GP.initParamsFromData xs ys
-          paramsOpt = GP.optimizeGP GP.RBF xs ys p0
-          model = GP.GPModel GP.RBF paramsOpt
-          gridX = let lo = V.minimum xVec
-                      hi = V.maximum xVec
-                      ex = (hi - lo) * 0.5
-                  in [ (lo - ex) + fromIntegral i * ((hi - lo) * 2) / 99
-                     | i <- [0..99::Int] ]   -- ±50% 外挿対応
-          res = GP.fitGP model xs ys gridX
-      writeARGP df xs ys res model paramsOpt
-      writeRBGP df xs ys gridX res paramsOpt
-    _ -> putStrLn "  (GP data not loaded)"
-
-writeARGP :: DXD.DataFrame -> [Double] -> [Double]
-          -> GP.GPResult -> GP.GPModel -> GP.GPParams -> IO ()
-writeARGP df xs ys res model params = do
-  let pd = GP.gpPredData model xs ys
-      kfit = AR.GPKernelFit
-              { AR.gkLabel    = "RBF"
-              , AR.gkKernel   = GP.RBF
-              , AR.gkParams   = params
-              , AR.gkResult   = res
-              , AR.gkLML      = GP.logMarginalLikelihood xs ys GP.RBF params
-              , AR.gkPredData = pd
-              }
-      gfSummary = AR.GPFitSummary
-                    { AR.gfKernelFits = [kfit]
-                    , AR.gfXCol       = "x"
-                    , AR.gfYCol       = "y"
-                    , AR.gfTrainXs    = xs
-                    , AR.gfTrainYs    = ys
-                    }
-      rcfg = AR.AnalysisReportConfig "GP RBF (AnalysisReport)"
-  AR.writeAnalysisReport "trash/cmp_gp_AR.html" rcfg df ["x"] "y"
-    (AR.GPFit gfSummary) []
-  putStrLn "  AR: trash/cmp_gp_AR.html"
-
-writeRBGP :: DXD.DataFrame -> [Double] -> [Double] -> [Double]
-          -> GP.GPResult -> GP.GPParams -> IO ()
-writeRBGP df xs ys gridX res params = do
-  appendixSec <- RB.secAppendixFromMd "付録: モデルの原理"
-                   "docs/principles/gp.ja.md"
-  let cfg      = RB.defaultReportConfig "GP RBF (ReportBuilder)"
-      lml      = GP.logMarginalLikelihood xs ys GP.RBF params
-      rep      = RI.GPReport GP.RBF params res gridX xs ys lml
-      sections = RB.toReport cfg df ["x"] "y" rep ++ [appendixSec]
-  RB.renderReport "trash/cmp_gp_RB.html" cfg sections
-  putStrLn "  RB: trash/cmp_gp_RB.html (Reportable GPReport instance)"
-
--- ---------------------------------------------------------------------------
--- HBM (Bayesian linear regression via NUTS)
--- ---------------------------------------------------------------------------
-
-hbmModel :: [Double] -> [Double] -> HBM.ModelP ()
-hbmModel xs ys = do
-  a <- HBM.sample "alpha" (HBM.Normal 0 10)
-  b <- HBM.sample "beta"  (HBM.Normal 0 10)
-  s <- HBM.sample "sigma" (HBM.Exponential 1)
-  mapM_ (\(x, y) -> HBM.observe "y" (HBM.Normal (a + b * realToFrac x) s) [y])
-        (zip xs ys)
-
-doHBMDemo :: DXD.DataFrame -> IO ()
-doHBMDemo df = do
-  putStrLn "--- HBM (Bayesian LM via NUTS) ---"
-  case (getDoubleVec "x" df, getDoubleVec "y" df) of
-    (Just xVec, Just yVec) -> do
-      let xs = V.toList xVec
-          ys = V.toList yVec
-      gen <- createSystemRandom
-      chain <- NUTS.nuts (hbmModel xs ys)
-        (NUTS.defaultNUTSConfig { NUTS.nutsIterations = 1000
-                                 , NUTS.nutsBurnIn = 200
-                                 , NUTS.nutsStepSize = 0.05 })
-        (Map.fromList [("alpha", 0.0), ("beta", 0.0), ("sigma", 1.0)])
-        gen
-      writeARHBM df xs ys chain
-      writeRBHBM df xs ys chain
-    _ -> putStrLn "  (HBM data not loaded)"
-
-makeHBMSmoothAR :: [Double] -> MCMCcore.Chain -> AR.SmoothData
-makeHBMSmoothAR xs chain =
-  let alphas = MCMCcore.chainVals "alpha" chain
-      betas  = MCMCcore.chainVals "beta"  chain
-      xMin   = minimum xs
-      xMax   = maximum xs
-      ext    = (xMax - xMin) * 0.5    -- 外挿用に ±50% 拡張
-      gMin   = xMin - ext
-      gMax   = xMax + ext
-      grid   = [ gMin + i * (gMax - gMin) / 99 | i <- [0..99] ]
-      qsAt p s =
-        let n = length s
-        in s !! min (n-1) (max 0 (floor (p * fromIntegral n) :: Int))
-      atX x =
-        let s = sortAsc (zipWith (\a b -> a + b * x) alphas betas)
-        in (qsAt 0.5 s, qsAt 0.025 s, qsAt 0.975 s)
-      preds = [ atX x | x <- grid ]
-      (mid, lo, hi) = unzip3 preds
-  in AR.SmoothData grid mid lo hi True
-
-writeARHBM :: DXD.DataFrame -> [Double] -> [Double] -> MCMCcore.Chain -> IO ()
-writeARHBM df xs ys chain = do
-  let aMean = maybe 0 id (MCMCcore.posteriorMean "alpha" chain)
-      bMean = maybe 0 id (MCMCcore.posteriorMean "beta"  chain)
-      fitted = [aMean + bMean * x | x <- xs]
-      resid  = zipWith (-) ys fitted
-      yBar   = sum ys / fromIntegral (length ys)
-      tss    = sum [(y - yBar) ^ (2 :: Int) | y <- ys]
-      rss    = sum [r ^ (2 :: Int) | r <- resid]
-      r2     = if tss < 1e-12 then 0 else 1 - rss / tss
-      smoothAR = makeHBMSmoothAR xs chain
-      fs = AR.FitSummary
-             { AR.fsModelType   = "HBM (NUTS)"
-             , AR.fsFormula     = "y ~ α + β·x"
-             , AR.fsCoeffs      = [("α", aMean), ("β", bMean)]
-             , AR.fsR2          = r2
-             , AR.fsR2Label     = "R²"
-             , AR.fsFitted      = fitted
-             , AR.fsResiduals   = resid
-             , AR.fsLinkName    = "Normal (identity)"
-             , AR.fsXColDegs    = [("x", 1)]
-             , AR.fsSmoothData  = Just ("x", smoothAR)
-             , AR.fsModelSelect = Nothing
-             }
-      hs = AR.HBMRegSummary
-             { AR.hbmsFit         = fs
-             , AR.hbmsModelGraph  = HBM.buildModelGraph (hbmModel xs ys)
-             , AR.hbmsChain       = chain
-             , AR.hbmsParams      = ["alpha", "beta", "sigma"]
-             , AR.hbmsPosteriorRows = mkPosteriorRows chain
-             }
-      rcfg = AR.AnalysisReportConfig "HBM (AnalysisReport)"
-  AR.writeAnalysisReport "trash/cmp_hbm_AR.html" rcfg df ["x"] "y"
-    (AR.HBMFit hs) []
-  putStrLn "  AR: trash/cmp_hbm_AR.html"
-
-mkPosteriorRows :: MCMCcore.Chain
-                -> [(T.Text, Double, Double, Double, Double)]
-mkPosteriorRows chain =
-  [ (p,
-     maybe 0 id (MCMCcore.posteriorMean p chain),
-     maybe 0 id (MCMCcore.posteriorSD p chain),
-     maybe 0 id (MCMCcore.posteriorQuantile 0.025 p chain),
-     maybe 0 id (MCMCcore.posteriorQuantile 0.975 p chain))
-  | p <- ["alpha", "beta", "sigma"] ]
-
-writeRBHBM :: DXD.DataFrame -> [Double] -> [Double] -> MCMCcore.Chain -> IO ()
-writeRBHBM df xs ys chain = do
-  appendixSec <- RB.secAppendixFromMd "付録: モデルの原理"
-                   "docs/principles/hbm.ja.md"
-  let cfg   = RB.defaultReportConfig "HBM (ReportBuilder)"
-      mgDag = VMG.buildMermaid (HBM.buildModelGraph (hbmModel xs ys))
-      rep   = RI.HBMLinearReport
-                { RI.hbmrChain     = chain
-                , RI.hbmrXs        = xs
-                , RI.hbmrYs        = ys
-                , RI.hbmrAlphaName = "alpha"
-                , RI.hbmrBetaName  = "beta"
-                , RI.hbmrSigmaName = "sigma"
-                , RI.hbmrGraph     = Just mgDag
-                }
-      sections = RB.toReport cfg df ["x"] "y" rep ++ [appendixSec]
-  RB.renderReport "trash/cmp_hbm_RB.html" cfg sections
-  putStrLn "  RB: trash/cmp_hbm_RB.html (Reportable HBMLinearReport instance)"
diff --git a/demo/regression/GPDemo.hs b/demo/regression/GPDemo.hs
deleted file mode 100644
--- a/demo/regression/GPDemo.hs
+++ /dev/null
@@ -1,84 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | GP 回帰デモ + HTML レポート生成
---
--- sin(x) + 0.3*cos(3x) の真の関数から 30 点をサンプルしてノイズを加え、
--- RBF / Matérn 5/2 / Periodic の 3 種類のカーネルで GP 回帰を行い
--- 総合 HTML レポートを demo/gp_report.html に出力します。
-module Main where
-
-import Hanalyze.Model.GP
-import Hanalyze.Viz.GPReport
-import Hanalyze.Viz.Core (openInBrowser)
-import Text.Printf (printf)
-
--- 真の関数
-trueF :: Double -> Double
-trueF x = sin x + 0.3 * cos (3 * x)
-
--- 決定論的な疑似ノイズ（再現性確保）
-pseudoNoise :: Int -> Double -> Double
-pseudoNoise seed x = 0.25 * sin (fromIntegral seed * 2.3998 + x * 17.3)
-
-main :: IO ()
-main = do
-  putStrLn "============================================"
-  putStrLn " Gaussian Process Regression Demo"
-  putStrLn "============================================"
-  putStrLn ""
-
-  -- 訓練データ: [0, 2π] から 30 点
-  let n      = 30
-      trainX = [ fromIntegral i * (2 * pi) / fromIntegral (n - 1)
-               | i <- [0 .. n - 1 :: Int] ]
-      trainY = zipWith (\i x -> trueF x + pseudoNoise i x)
-                 [0 :: Int ..] trainX
-      trainData = zip trainX trainY
-
-  -- テストグリッド (200 点)
-  let m     = 200
-      testX = [ fromIntegral i * (2 * pi) / fromIntegral (m - 1)
-              | i <- [0 .. m - 1 :: Int] ]
-
-  -- データ統計から初期ハイパーパラメータを設定
-  let p0 = initParamsFromData trainX trainY
-  printf "Initial params: l=%.3f  sf=%.3f  sn=%.3f\n"
-    (gpLengthScale p0) (sqrt (gpSignalVar p0)) (sqrt (gpNoiseVar p0))
-  putStrLn ""
-
-  -- 各カーネルの最適化とフィット
-  putStrLn "Optimizing RBF..."
-  let optRBF = optimizeGP RBF trainX trainY p0
-  printf "  RBF:     l=%.3f  sf=%.3f  sn=%.4f  LML=%.2f\n"
-    (gpLengthScale optRBF) (sqrt (gpSignalVar optRBF))
-    (sqrt (gpNoiseVar optRBF))
-    (logMarginalLikelihood trainX trainY RBF optRBF)
-
-  putStrLn "Optimizing Matern52..."
-  let optM52 = optimizeGP Matern52 trainX trainY p0
-  printf "  Matern:  l=%.3f  sf=%.3f  sn=%.4f  LML=%.2f\n"
-    (gpLengthScale optM52) (sqrt (gpSignalVar optM52))
-    (sqrt (gpNoiseVar optM52))
-    (logMarginalLikelihood trainX trainY Matern52 optM52)
-
-  putStrLn "Optimizing Periodic..."
-  let p0Per  = p0 { gpPeriod = 2 * pi }
-      optPer = optimizeGP Periodic trainX trainY p0Per
-  printf "  Periodic: l=%.3f  sf=%.3f  sn=%.4f  p=%.3f  LML=%.2f\n"
-    (gpLengthScale optPer) (sqrt (gpSignalVar optPer))
-    (sqrt (gpNoiseVar optPer)) (gpPeriod optPer)
-    (logMarginalLikelihood trainX trainY Periodic optPer)
-  putStrLn ""
-
-  -- フィット結果をまとめる
-  let fits =
-        [ makeGPFit "RBF"       RBF      optRBF trainX trainY testX
-        , makeGPFit "Matern5/2" Matern52 optM52 trainX trainY testX
-        , makeGPFit "Periodic"  Periodic optPer trainX trainY testX
-        ]
-
-  -- レポート生成
-  let rptCfg = defaultGPReportConfig "GP Regression Report"
-  writeGPReport "demo/gp_report.html" rptCfg trainData fits
-  putStrLn "Saved: demo/gp_report.html"
-
-  openInBrowser "demo/gp_report.html"
diff --git a/demo/regression/KernelDemo.hs b/demo/regression/KernelDemo.hs
deleted file mode 100644
--- a/demo/regression/KernelDemo.hs
+++ /dev/null
@@ -1,143 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | カーネル回帰のデモ (Phase N2)。
---
--- 真の関数: y = sin(2πx) + 0.3 sin(6πx)
--- Spline と同じデータで、Nadaraya-Watson と Kernel Ridge を比較。
-module Main where
-
-import qualified Data.Vector as V
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-import qualified System.Random.MWC.Distributions as MWC
-
-import Hanalyze.Model.Kernel (Kernel (..), nwRegression, kernelRidge,
-                     predictKernelRidge, gridSearchBandwidth)
-import Hanalyze.Viz.Core (defaultConfig, OutputFormat (..), PlotConfig (..),
-                 writeSpec)
-import Graphics.Vega.VegaLite
-
-trueF :: Double -> Double
-trueF x = sin (2 * pi * x) + 0.3 * sin (6 * pi * x)
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  カーネル回帰デモ (Phase N2)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  gen <- createSystemRandom
-  let n = 80
-      xs = V.fromList [fromIntegral i / fromIntegral (n - 1)
-                       | i <- [0 .. n - 1]]
-      ysClean = V.map trueF xs
-  noise <- V.replicateM n (MWC.normal 0 0.15 gen)
-  let ys = V.zipWith (+) ysClean noise
-  printf "観測 n=%d, 真の関数 y = sin(2πx) + 0.3 sin(6πx) + N(0, 0.15)\n" n
-  putStrLn ""
-
-  -- Bandwidth 選定 (LOO-CV)
-  let hCandidates = [0.02, 0.03, 0.05, 0.08, 0.10, 0.15, 0.20]
-  let (bestH, _bestErr) = gridSearchBandwidth Gaussian xs ys hCandidates
-  printf "Bandwidth 選定 (LOO-CV, Gaussian カーネル):\n"
-  mapM_ (\h ->
-            let (_, err) = gridSearchBandwidth Gaussian xs ys [h]
-                tag :: String
-                tag = if h == bestH then "  ← best" else ""
-            in printf "  h=%.3f  RMSE_LOO=%.4f%s\n" h err tag)
-        hCandidates
-  putStrLn ""
-
-  -- 4 つのカーネルで NW 回帰
-  let xGrid = V.fromList [fromIntegral i * 0.001 | i <- [0 .. 1000 :: Int]]
-      yGrid = V.map trueF xGrid
-      rmse a b = sqrt (V.sum (V.zipWith (\u v -> (u - v)^(2::Int)) a b)
-                       / fromIntegral (V.length a))
-
-  putStrLn "[Nadaraya-Watson, h=best (LOO 選定)]"
-  let yNwGauss = nwRegression Gaussian     bestH xs ys xGrid
-      yNwEpa   = nwRegression Epanechnikov bestH xs ys xGrid
-      yNwTri   = nwRegression Triangular   bestH xs ys xGrid
-      yNwTC    = nwRegression TriCube      bestH xs ys xGrid
-  printf "  Gaussian:     RMSE = %.4f\n" (rmse yNwGauss yGrid)
-  printf "  Epanechnikov: RMSE = %.4f\n" (rmse yNwEpa   yGrid)
-  printf "  Triangular:   RMSE = %.4f\n" (rmse yNwTri   yGrid)
-  printf "  TriCube:      RMSE = %.4f\n" (rmse yNwTC    yGrid)
-  putStrLn ""
-
-  -- Kernel Ridge
-  putStrLn "[Kernel Ridge, h=best, λ 比較]"
-  let lambdas = [0.001, 0.01, 0.1, 1.0]
-  yKRs <- mapM
-    (\lam -> do
-        let fit  = kernelRidge Gaussian bestH lam xs ys
-            yKR  = predictKernelRidge fit xGrid
-        printf "  λ=%.3f:  RMSE = %.4f\n" lam (rmse yKR yGrid)
-        return yKR)
-    lambdas
-  putStrLn ""
-
-  -- 可視化: 真値 + NW(Gaussian) + Kernel Ridge(λ=0.01) + 観測
-  let yKRBest = yKRs !! 1   -- λ = 0.01
-  let cfg = (defaultConfig "Kernel regression — NW vs Kernel Ridge")
-              { plotWidth = 700, plotHeight = 350 }
-      vlSpec = toVegaLite
-        [ title (plotTitle cfg) []
-        , layer
-            [ asSpec   -- 真値 (灰破線)
-                [ dataFromColumns []
-                    . dataColumn "x" (Numbers (V.toList xGrid))
-                    . dataColumn "y" (Numbers (V.toList yGrid))
-                    $ []
-                , mark Line [MColor "#888888", MStrokeWidth 1.5,
-                             MStrokeDash [4, 4]]
-                , encoding
-                    . position X [PName "x", PmType Quantitative]
-                    . position Y [PName "y", PmType Quantitative]
-                    $ []
-                ]
-            , asSpec   -- NW (青)
-                [ dataFromColumns []
-                    . dataColumn "x" (Numbers (V.toList xGrid))
-                    . dataColumn "y" (Numbers (V.toList yNwGauss))
-                    $ []
-                , mark Line [MColor "#1F77B4", MStrokeWidth 2.0]
-                , encoding
-                    . position X [PName "x", PmType Quantitative]
-                    . position Y [PName "y", PmType Quantitative]
-                    $ []
-                ]
-            , asSpec   -- Kernel Ridge (オレンジ)
-                [ dataFromColumns []
-                    . dataColumn "x" (Numbers (V.toList xGrid))
-                    . dataColumn "y" (Numbers (V.toList yKRBest))
-                    $ []
-                , mark Line [MColor "#FF8C42", MStrokeWidth 2.5]
-                , encoding
-                    . position X [PName "x", PmType Quantitative]
-                    . position Y [PName "y", PmType Quantitative]
-                    $ []
-                ]
-            , asSpec   -- 観測点 (黒)
-                [ dataFromColumns []
-                    . dataColumn "x" (Numbers (V.toList xs))
-                    . dataColumn "y" (Numbers (V.toList ys))
-                    $ []
-                , mark Point [MOpacity 0.5, MSize 25, MColor "#222222"]
-                , encoding
-                    . position X [PName "x", PmType Quantitative]
-                    . position Y [PName "y", PmType Quantitative]
-                    $ []
-                ]
-            ]
-        , width  (plotWidth cfg)
-        , height (plotHeight cfg)
-        ]
-  writeSpec HTML "kernel.html" vlSpec
-  putStrLn "  → kernel.html"
-  putStrLn "    真値=灰破線, NW(Gaussian)=青, Kernel Ridge=オレンジ, 観測=黒点"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Nadaraya-Watson と Kernel Ridge の双方が動作"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/regression/MultiLMDemo.hs b/demo/regression/MultiLMDemo.hs
deleted file mode 100644
--- a/demo/regression/MultiLMDemo.hs
+++ /dev/null
@@ -1,119 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Phase T1: Multivariate LM のデモ。
---
--- 真の回帰: Y = XB + E、3 出力 (q=3) を 4 説明変数 (p=4 incl. intercept) で
--- 同時に推定。残差の共分散も確認する。
-module Main where
-
-import qualified Numeric.LinearAlgebra as LA
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-import qualified System.Random.MWC.Distributions as MWC
-
-import Hanalyze.Model.Core (FitResult (..))
-import Hanalyze.Model.MultiLM
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Phase T1: Multivariate Linear Regression"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  let n = 100 :: Int
-      p = 4   :: Int
-      q = 3   :: Int
-  -- 真の係数行列 B (4 × 3)
-  let bTrue = LA.fromLists
-        [ [ 2.0, -1.0,  0.5]    -- intercept
-        , [ 1.0,  0.5, -0.3]    -- x1
-        , [-0.5,  1.0,  0.8]    -- x2
-        , [ 0.3, -0.2,  0.4]    -- x3
-        ]
-  printf "真の B (%dx%d):\n" p q
-  printM bTrue
-  putStrLn ""
-
-  -- データ生成 (X, ノイズ Σ_true 付き Y)
-  gen <- createSystemRandom
-  -- X: 切片 1 + 3 説明変数
-  let x1 = [(fromIntegral i) / fromIntegral n | i <- [0 .. n - 1]]
-      x2 = [sin (fromIntegral i / 10) | i <- [0 .. n - 1]]
-      x3 = [(fromIntegral i `mod` 7 :: Int) `quot` 2 | i <- [0 .. n - 1]]
-      x3' = map fromIntegral x3
-      xMat = LA.fromColumns
-              [ LA.konst 1 n
-              , LA.fromList x1
-              , LA.fromList x2
-              , LA.fromList x3' ]
-
-  -- ノイズ E ~ MvN(0, Σ_true) で 3 出力に相関を入れる
-  let sigmaTrue = LA.fromLists
-        [ [0.5, 0.2, 0.0]
-        , [0.2, 0.4, 0.1]
-        , [0.0, 0.1, 0.3]
-        ]
-  -- E を生成 (Cholesky 経由)
-  let lChol = LA.tr (LA.chol (LA.trustSym sigmaTrue))
-  zsRows <- mapM (const (do
-                          z1 <- MWC.standard gen
-                          z2 <- MWC.standard gen
-                          z3 <- MWC.standard gen
-                          return (LA.fromList [z1, z2, z3])))
-                 [1 .. n]
-  let zMat = LA.fromRows zsRows
-      eMat = zMat LA.<> LA.tr lChol
-      yMat = (xMat LA.<> bTrue) + eMat
-
-  printf "観測 Y (%dx%d), X (%dx%d) を生成 (真の Σ で相関ノイズ)\n" n q n p
-  putStrLn ""
-
-  -- フィット
-  let mf = fitMultiLM xMat yMat
-  printf "推定 B̂ (%dx%d):\n" p q
-  printM (coefficients (mfFit mf))
-  putStrLn ""
-
-  -- 真値との誤差
-  let bDiff = coefficients (mfFit mf) - bTrue
-      maxDev = LA.maxElement (LA.cmap abs bDiff)
-  printf "B̂ - B 最大絶対誤差: %.4f (n=%d で十分小さいはず)\n" maxDev n
-  putStrLn ""
-
-  -- R² (列ごと)
-  printf "列ごとの R²: %s\n"
-         (show (map (\v -> (fromIntegral (round (v * 1e4) :: Int) / 1e4) :: Double)
-                    (LA.toList (rSquared (mfFit mf)))))
-  putStrLn ""
-
-  -- 残差共分散の比較
-  putStrLn "推定 Σ̂ (residual covariance):"
-  printM (mfResidCov mf)
-  putStrLn ""
-  putStrLn "真の Σ:"
-  printM sigmaTrue
-  putStrLn ""
-  putStrLn "推定 残差相関行列:"
-  printM (mfResidCor mf)
-  putStrLn ""
-
-  -- 予測テスト
-  let xNew = LA.fromLists
-        [ [1, 0.5, 0.0, 1.0]
-        , [1, 0.8, 0.5, 2.0] ]
-      yPred = predictMultiLM mf xNew
-  printf "新規 2 観測の予測:\n"
-  printM yPred
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ MultiLM が動作: B̂ ≈ B、Σ̂ も真値に近い"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-
-  where
-    printM :: LA.Matrix Double -> IO ()
-    printM m = mapM_ (\row -> do
-                        putStr "  "
-                        mapM_ (printf "%+8.3f  ") (LA.toList row)
-                        putStrLn "")
-                     (LA.toRows m)
diff --git a/demo/regression/MultivariateDemo.hs b/demo/regression/MultivariateDemo.hs
deleted file mode 100644
--- a/demo/regression/MultivariateDemo.hs
+++ /dev/null
@@ -1,87 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Phase T3-T5: RRR / PLS / CCA のデモ。
-module Main where
-
-import qualified Numeric.LinearAlgebra as LA
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-import qualified System.Random.MWC.Distributions as MWC
-
-import Hanalyze.Model.Multivariate
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Phase T3-T5: RRR / PLS / CCA"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  -- データ: 真の B が rank 1 (= 1 つの latent factor で説明可能)
-  -- u (p×1): "directions" of X が response に効く
-  -- v (q×1): "loadings" on Y
-  let n = 100 :: Int
-      p = 5  :: Int
-      q = 3  :: Int
-  let uTrue = LA.asColumn (LA.fromList [1.0, -0.5, 0.3, 0.2, -0.1])  -- p × 1
-      vTrue = LA.asColumn (LA.fromList [2.0, 1.0, -0.5])             -- q × 1
-      bTrue = uTrue LA.<> LA.tr vTrue                                -- p × q (rank 1)
-
-  printf "真の B (rank 1, %dx%d):\n" p q
-  printM bTrue
-  putStrLn ""
-
-  gen <- createSystemRandom
-  -- X ~ N(0, I)
-  xRows <- mapM (const (mapM (const (MWC.standard gen)) [1 .. p])) [1 .. n]
-  let xMat = LA.fromLists xRows
-  -- Y = X B + noise
-  noiseRows <- mapM (const (mapM (const (MWC.normal 0 0.3 gen)) [1 .. q])) [1 .. n]
-  let nMat = LA.fromLists noiseRows
-      yMat = (xMat LA.<> bTrue) + nMat
-
-  -- ── RRR ──
-  putStrLn "[1] Reduced Rank Regression (rank=1)"
-  let rrr = reducedRankRegression 1 xMat yMat
-  printf "  推定 B̂ (rank %d):\n" (rrrRank rrr)
-  printM (rrrBeta rrr)
-  let bDiff = rrrBeta rrr - bTrue
-      maxErr = LA.maxElement (LA.cmap abs bDiff)
-  printf "  B̂ - B 最大誤差: %.4f\n" maxErr
-  putStrLn ""
-
-  -- 比較: 通常 OLS (rank 制約なし)
-  let bOLS = xMat LA.<\> yMat
-  printf "  比較: OLS B̂ (rank %d):\n" (LA.rank bOLS)
-  printM bOLS
-  putStrLn ""
-
-  -- ── PLS ──
-  putStrLn "[2] PLS Regression (k=2 成分)"
-  let plsFit = pls 2 xMat yMat
-  printf "  推定 B̂ (PLS k=2):\n"
-  printM (plsBeta plsFit)
-  let bDiffPLS = plsBeta plsFit - bTrue
-      maxErrPLS = LA.maxElement (LA.cmap abs bDiffPLS)
-  printf "  B̂ - B 最大誤差 (PLS): %.4f\n" maxErrPLS
-  putStrLn ""
-
-  -- ── CCA ──
-  putStrLn "[3] CCA"
-  let ccaFit = cca xMat yMat
-      corrs = LA.toList (ccaCorr ccaFit)
-  printf "  Canonical correlations: %s\n"
-         (show (map (\v -> fromIntegral (round (v * 1000) :: Int) / 1000 :: Double) corrs))
-  printf "  最大相関 (= rank 1 構造を反映): %.4f\n" (head corrs)
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ RRR / PLS / CCA すべて動作"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-
-  where
-    printM :: LA.Matrix Double -> IO ()
-    printM m = mapM_ (\row -> do
-                        putStr "    "
-                        mapM_ (printf "%+8.3f  ") (LA.toList row)
-                        putStrLn "")
-                     (LA.toRows m)
diff --git a/demo/regression/RFFDemo.hs b/demo/regression/RFFDemo.hs
deleted file mode 100644
--- a/demo/regression/RFFDemo.hs
+++ /dev/null
@@ -1,121 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Random Fourier Features (RFF) のデモ。
---
--- - 真の関数から N=200 点を生成
--- - 厳密 GP (O(n³)) と RFF GP (O(n D + D³)) を比較 (固定ハイパラ)
--- - D = 50 / 100 / 200 で RMSE と実行時間を計測
--- - RFF が n が大きいときにほぼ同精度・高速であることを確認
---
--- 注: optimizeGP の最適化は時間がかかるため、デモでは固定ハイパラを使用。
--- 実用では initParamsFromData → optimizeGP で先にカーネルを最適化してから
--- そのパラメータで RFF を構成する。
-module Main where
-
-import qualified Numeric.LinearAlgebra as LA
-import qualified System.Random.MWC as MWC
-import Control.Exception (evaluate)
-import Hanalyze.Model.GP        as GP
-import Hanalyze.Model.RFF       as RFF
-import Data.Time.Clock (getCurrentTime, diffUTCTime)
-import Text.Printf     (printf)
-
--- 真の関数
-trueF :: Double -> Double
-trueF x = sin (1.5 * x) + 0.3 * cos (3.0 * x)
-
--- 決定論的疑似ノイズ
-pseudoNoise :: Int -> Double -> Double
-pseudoNoise seed x = 0.15 * sin (fromIntegral seed * 2.3998 + x * 17.3)
-
-main :: IO ()
-main = do
-  putStrLn "================================================"
-  putStrLn " Random Fourier Features (RFF) Demo"
-  putStrLn "================================================"
-  putStrLn ""
-
-  -- 訓練データ: N=1500 点 (厳密 GP の O(n³) が体感できるサイズ)
-  let n      = 1500
-      trainX = [ fromIntegral i * (2 * pi) / fromIntegral (n - 1)
-               | i <- [0 .. n - 1 :: Int] ]
-      trainY = zipWith (\i x -> trueF x + pseudoNoise i x)
-                 [0 :: Int ..] trainX
-
-      -- テスト点 (200 点)
-      m     = 200
-      testX = [ 0.5 + fromIntegral i * (2 * pi - 1) / fromIntegral (m - 1)
-              | i <- [0 .. m - 1 :: Int] ]
-      testY = map trueF testX
-
-      -- 固定ハイパラ (公平な比較のため)
-      ell = 0.6 :: Double
-      sf  = 1.0 :: Double
-      sn  = 0.15 :: Double
-      sigF2 = sf * sf
-      noiseV = sn * sn
-
-  printf "Training samples: %d\n" n
-  printf "Test samples:     %d\n" m
-  printf "Fixed hyperparams: l=%.2f  sigma_f^2=%.2f  noise_var=%.4f\n"
-         ell sigF2 noiseV
-  putStrLn ""
-
-  -- ================================================
-  -- 1. 厳密 GP (Hanalyze.Model.GP, RBF)
-  -- ================================================
-  putStrLn "--- Exact GP (RBF, Cholesky O(n^3)) ---"
-  t0 <- getCurrentTime
-  let paramsX = GPParams { gpLengthScale  = ell
-                         , gpSignalVar    = sigF2
-                         , gpNoiseVar     = noiseV
-                         , gpPeriod       = 1.0
-                         , gpLengthScales = Nothing
-                         }
-      modelX  = GPModel RBF paramsX
-      resX    = fitGP modelX trainX trainY testX
-  _ <- evaluate (LA.sumElements (LA.fromList (gpMean resX)))
-  t1 <- getCurrentTime
-  let exactRMSE = rmse testY (gpMean resX)
-      exactTime = diffUTCTime t1 t0
-  printf "  RMSE (vs true f): %.4f\n" exactRMSE
-  printf "  Time:             %.3fs\n" (realToFrac exactTime :: Double)
-  putStrLn ""
-
-  -- ================================================
-  -- 2. RFF GP, D = 50, 100, 200
-  -- ================================================
-  putStrLn "--- RFF GP (RBF, D ∈ {50, 100, 200}) ---"
-
-  gen <- MWC.createSystemRandom
-
-  mapM_ (\d -> do
-    t2 <- getCurrentTime
-    feats   <- RFF.sampleRFFRBF d ell sf gen
-    let fit  = RFF.rffGP feats trainX trainY sn
-        pred_ = RFF.predictRFFGP fit testX
-    _ <- evaluate (sum (map fst pred_))
-    t3 <- getCurrentTime
-    let rffRMSE = rmse testY (map fst pred_)
-        rffTime = diffUTCTime t3 t2
-        speedup = realToFrac exactTime / realToFrac rffTime :: Double
-    printf "  D=%-3d  RMSE=%.4f  time=%.3fs  speedup=%.1fx\n"
-           d rffRMSE (realToFrac rffTime :: Double) speedup
-    ) [50, 100, 200]
-
-  putStrLn ""
-  putStrLn "--- RFF Ridge regression (no predictive variance, D=200) ---"
-  feats   <- RFF.sampleRFFRBF 200 ell sf gen
-  let lam  = 0.01
-      ridg = RFF.rffRidge feats trainX trainY lam
-      yhat = RFF.predictRFFRidge ridg testX
-      ridgeRMSE = rmse testY yhat
-  printf "  D=200, lambda=%.3f  RMSE=%.4f\n" lam ridgeRMSE
-  putStrLn ""
-  putStrLn "Done."
-
--- 平均二乗誤差の平方根
-rmse :: [Double] -> [Double] -> Double
-rmse a b =
-  let n = length a
-      sse = sum [ (x - y) ^ (2 :: Int) | (x, y) <- zip a b ]
-  in sqrt (sse / fromIntegral n)
diff --git a/demo/regression/RegularizedDemo.hs b/demo/regression/RegularizedDemo.hs
deleted file mode 100644
--- a/demo/regression/RegularizedDemo.hs
+++ /dev/null
@@ -1,112 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | 正則化回帰デモ (Phase Q)。
---
--- 真の β = [3, -2, 0, 0, 1.5, 0, 0, 0, 0, 0]  (10 列、5 つだけ非ゼロ)
--- p=10 列、n=50 観測、相関ある特徴量を含む高次元設定。
--- OLS / Ridge / Lasso / Elastic Net を比較。
-module Main where
-
-import qualified Data.Vector as V
-import qualified Numeric.LinearAlgebra as LA
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-import qualified System.Random.MWC.Distributions as MWC
-
-import Hanalyze.Model.Regularized (Penalty (..), RegFit (..),
-                          fitRegularized, standardize)
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  正則化回帰デモ (Phase Q) — Ridge / Lasso / ElasticNet"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  let n = 50
-      p = 10
-      betaTrue = [3.0, -2.0, 0.0, 0.0, 1.5, 0.0, 0.0, 0.0, 0.0, 0.0]
-  printf "設定: n=%d, p=%d\n" n p
-  printf "真の β = %s\n" (show betaTrue)
-  printf "  非ゼロ: 3 個 (列 1, 2, 5)\n"
-  putStrLn ""
-
-  -- データ生成
-  gen <- createSystemRandom
-  rows <- mapM (const (V.replicateM p (MWC.standard gen))) [1 .. n :: Int]
-  let xMat = LA.fromLists [V.toList r | r <- rows]
-      bV   = LA.fromList betaTrue
-  noise <- LA.fromList <$> mapM (const (MWC.normal 0 0.5 gen)) [1 .. n]
-  let yV  = (xMat LA.#> bV) + noise
-
-  -- 標準化
-  let (xStd, _means, sds) = standardize xMat
-  printf "X 列 sd の範囲: [%.3f, %.3f]\n"
-         (V.minimum sds) (V.maximum sds)
-  putStrLn ""
-
-  -- 4 モデルを fit
-  let fits =
-        [ ("OLS              ", fitRegularized NoPen          xStd yV)
-        , ("Ridge λ=0.1      ", fitRegularized (L2 0.1)       xStd yV)
-        , ("Ridge λ=1.0      ", fitRegularized (L2 1.0)       xStd yV)
-        , ("Lasso λ=0.05     ", fitRegularized (L1 0.05)      xStd yV)
-        , ("Lasso λ=0.20     ", fitRegularized (L1 0.20)      xStd yV)
-        , ("ElasticNet (.1,.1)", fitRegularized (ElasticNet 0.1 0.1) xStd yV)
-        ]
-
-  putStrLn "[1] 各モデルの係数 (標準化空間)"
-  printf "  %-18s | R²   | nonZero | iters\n" ("Model" :: String)
-  putStrLn (replicate 60 '-')
-  mapM_ (\(name, fit) ->
-            printf "  %s | %.4f | %7d | %5d\n"
-                   (name :: String)
-                   (rfR2 fit)
-                   (rfNonZero fit)
-                   (rfIters fit))
-        fits
-  putStrLn ""
-
-  putStrLn "[2] 推定 β を真値と比較 (列ごと)"
-  printf "  %-2s %s\n"
-         ("j" :: String)
-         (concat ["%-8s" | _ <- fits] :: String)
-  printf "      %s\n"
-         (concat [printf "%-8s" (take 7 name) :: String
-                 | (name, _) <- fits])
-  putStrLn (replicate 70 '-')
-  mapM_ (\j ->
-            do
-              printf "  %2d (%5.2f)" j (betaTrue !! j)
-              mapM_ (\(_, fit) ->
-                        printf " %+7.3f"
-                               ((LA.toList (rfBeta fit)) !! j))
-                    fits
-              putStrLn "")
-        [0 .. p - 1 :: Int]
-  putStrLn ""
-
-  -- 評価: 真値からの距離
-  putStrLn "[3] β 推定誤差 ||β̂ − β_true||₂ と sparsity"
-  printf "  %-18s | β 誤差 | sparsity (= 推定 0 の数)\n" ("Model" :: String)
-  putStrLn (replicate 60 '-')
-  -- β を unstandardize で元スケールに戻す
-  let bTrueV = LA.fromList betaTrue
-  mapM_ (\(name, fit) ->
-            do
-              -- 標準化 X で fit した β を元の x スケールに戻す:
-              -- β_orig_j = β_std_j / sd_j
-              let bStd = rfBeta fit
-                  bOrig = LA.fromList
-                    [ (bStd `LA.atIndex` j) / (sds V.! j)
-                    | j <- [0 .. p - 1] ]
-                  err   = LA.norm_2 (bOrig - bTrueV)
-                  zeros = length [v | v <- LA.toList bStd, abs v <= 1e-8]
-              printf "  %s | %6.3f | %5d / %d\n"
-                     (name :: String) err zeros p)
-        fits
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ Lasso が真の sparse 構造を回復"
-  putStrLn "    Ridge は非ゼロを縮小、Elastic Net は中間"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/regression/RobustGPDemo.hs b/demo/regression/RobustGPDemo.hs
deleted file mode 100644
--- a/demo/regression/RobustGPDemo.hs
+++ /dev/null
@@ -1,101 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | ロバスト GP のデモ。
---
--- 真の関数 sin(x) + 0.3 cos(3x) からデータを生成し、3 点を **大きな外れ値**
--- (+5σ レベル) に置き換える。次の 3 モデルで RMSE を比較:
---
--- 1. 通常 GP (Gaussian 観測)
--- 2. ロバスト GP w/ Cauchy(γ=0.5) — 重い裾、外れ値に強い
--- 3. ロバスト GP w/ StudentT(ν=4, σ=0.5) — Cauchy より軽い裾
-module Main where
-
-import Hanalyze.Model.GP        (Kernel (..), GPParams (..), GPModel (..), fitGP, gpMean)
-import Hanalyze.Model.GPRobust  (RobustLikelihood (..), RobustGPFit (..),
-                        fitGPRobust, predictGPRobust)
-import Text.Printf     (printf)
-
-trueF :: Double -> Double
-trueF x = sin x + 0.3 * cos (3 * x)
-
-pseudoNoise :: Int -> Double -> Double
-pseudoNoise seed x = 0.1 * sin (fromIntegral seed * 2.3998 + x * 17.3)
-
-main :: IO ()
-main = do
-  putStrLn "=================================="
-  putStrLn " Robust GP Demo (StudentT / Cauchy)"
-  putStrLn "=================================="
-  putStrLn ""
-
-  -- 訓練データ: 50 点
-  let n      = 50
-      trainX = [ fromIntegral i * (2 * pi) / fromIntegral (n - 1)
-               | i <- [0 .. n - 1 :: Int] ]
-      cleanY = zipWith (\i x -> trueF x + pseudoNoise i x)
-                 [0 :: Int ..] trainX
-      -- 3 点を外れ値に置換 (index 10, 25, 40)
-      trainY = [ if i `elem` [10, 25, 40]
-                   then y + 4.0          -- +4σ レベルの外れ値
-                   else y
-               | (i, y) <- zip [0 :: Int ..] cleanY ]
-
-      -- テスト点
-      m     = 100
-      testX = [ fromIntegral i * (2 * pi) / fromIntegral (m - 1)
-              | i <- [0 .. m - 1 :: Int] ]
-      testY = map trueF testX
-
-      -- ハイパラ (ノイズ含めて固定)
-      hp = GPParams { gpLengthScale  = 0.6
-                    , gpSignalVar    = 1.0
-                    , gpNoiseVar     = 0.05
-                    , gpPeriod       = 1.0
-                    , gpLengthScales = Nothing
-                    }
-
-  printf "Training: %d points (3 outliers at index 10, 25, 40 with +4 offset)\n" n
-  printf "Test:     %d points (clean true f)\n" m
-  printf "Hyperparams (fixed): l=%.2f sigma_f^2=%.2f noise=%.4f\n"
-         (gpLengthScale hp) (gpSignalVar hp) (gpNoiseVar hp)
-  putStrLn ""
-
-  -- 1. 通常 GP (Gaussian)
-  putStrLn "--- 1. Gaussian GP (Hanalyze.Model.GP) ---"
-  let gpRes = fitGP (GPModel RBF hp) trainX trainY testX
-      gaussRMSE = rmse testY (gpMean gpRes)
-  printf "  RMSE (vs true f): %.4f\n" gaussRMSE
-  putStrLn ""
-
-  -- 2. Robust GP w/ Cauchy
-  putStrLn "--- 2. Robust GP w/ Cauchy(gamma=0.5) ---"
-  let cauchyFit  = fitGPRobust RBF hp (RCauchy 0.5) trainX trainY
-      cauchyPred = predictGPRobust cauchyFit testX
-      cauchyRMSE = rmse testY (map fst cauchyPred)
-  printf "  IRLS converged in %d iterations\n" (rgpIters cauchyFit)
-  printf "  RMSE (vs true f): %.4f\n" cauchyRMSE
-  printf "  Improvement over Gaussian: %.1f%%\n"
-         (100 * (gaussRMSE - cauchyRMSE) / gaussRMSE)
-  putStrLn ""
-
-  -- 3. Robust GP w/ StudentT
-  putStrLn "--- 3. Robust GP w/ StudentT(nu=4, sigma=0.5) ---"
-  let stFit  = fitGPRobust RBF hp (RStudentT 4 0.5) trainX trainY
-      stPred = predictGPRobust stFit testX
-      stRMSE = rmse testY (map fst stPred)
-  printf "  IRLS converged in %d iterations\n" (rgpIters stFit)
-  printf "  RMSE (vs true f): %.4f\n" stRMSE
-  printf "  Improvement over Gaussian: %.1f%%\n"
-         (100 * (gaussRMSE - stRMSE) / gaussRMSE)
-  putStrLn ""
-
-  putStrLn "Done."
-  putStrLn ""
-  putStrLn "Cauchy is most robust (heaviest tails, lowest RMSE)."
-  putStrLn "StudentT(nu=4) is intermediate."
-  putStrLn "Gaussian is most distorted by the 3 outliers."
-
-rmse :: [Double] -> [Double] -> Double
-rmse a b =
-  let n = length a
-      sse = sum [ (x - y) ^ (2 :: Int) | (x, y) <- zip a b ]
-  in sqrt (sse / fromIntegral n)
diff --git a/demo/regression/SplineDemo.hs b/demo/regression/SplineDemo.hs
deleted file mode 100644
--- a/demo/regression/SplineDemo.hs
+++ /dev/null
@@ -1,132 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Spline 回帰のデモ (Phase N1)。
---
--- 真の関数: y = sin(2πx) + 0.3 sin(6πx)
--- これを n=80 サンプル + ノイズで観測し、B-spline (k=3) と
--- 自然立方スプラインで fit、結果を比較。
-module Main where
-
-import qualified Data.Vector as V
-import qualified Numeric.LinearAlgebra as LA
-import Text.Printf (printf)
-import System.Random.MWC (createSystemRandom)
-import qualified System.Random.MWC.Distributions as MWC
-
-import Hanalyze.Model.Spline (SplineKind (..), fitSpline, predictSpline,
-                     SplineFit (..), equalSpacedKnots)
-import Hanalyze.Model.Core (rSquared1)
-import Hanalyze.Viz.Core (defaultConfig, OutputFormat (..), PlotConfig (..),
-                 writeSpec)
-import Graphics.Vega.VegaLite
-
--- 真の関数
-trueF :: Double -> Double
-trueF x = sin (2 * pi * x) + 0.3 * sin (6 * pi * x)
-
-main :: IO ()
-main = do
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  Spline 回帰デモ (Phase N1)"
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn ""
-
-  gen <- createSystemRandom
-  let n = 80
-      xs = V.fromList [fromIntegral i / fromIntegral (n - 1)
-                       | i <- [0 .. n - 1]]
-  let ysClean = V.map trueF xs
-  noise <- V.replicateM n (MWC.normal 0 0.15 gen)
-  let ys = V.zipWith (+) ysClean noise
-  printf "観測 n=%d, 真の関数 y = sin(2πx) + 0.3 sin(6πx) + N(0, 0.15)\n" n
-  putStrLn ""
-
-  let knots = equalSpacedKnots 8 0 1
-  printf "ノット (8): %s\n" (show knots)
-  putStrLn ""
-
-  let bsFit = fitSpline (BSpline 3) knots xs ys
-      bsCoef = LA.toList (sfBeta bsFit)
-      bsR2 = rSquared1 (sfResult bsFit)
-  printf "[B-spline cubic, k=3]  係数次元 = %d, R² = %.4f\n"
-         (length bsCoef) bsR2
-
-  let ncFit = fitSpline NaturalCubic knots xs ys
-      ncCoef = LA.toList (sfBeta ncFit)
-      ncR2 = rSquared1 (sfResult ncFit)
-  printf "[Natural cubic]        係数次元 = %d, R² = %.4f\n"
-         (length ncCoef) ncR2
-  putStrLn ""
-
-  -- グリッドで予測 → 真値との RMSE
-  let xGrid = V.fromList [fromIntegral i * 0.001 | i <- [0 .. 1000]]
-      yGrid = V.map trueF xGrid
-      yBs   = predictSpline bsFit xGrid
-      yNc   = predictSpline ncFit xGrid
-      rmse a b = sqrt (V.sum (V.zipWith (\u v -> (u - v)^(2::Int)) a b)
-                       / fromIntegral (V.length a))
-  printf "  RMSE (B-spline, vs 真値) = %.4f\n" (rmse yBs yGrid)
-  printf "  RMSE (Natural,  vs 真値) = %.4f\n" (rmse yNc yGrid)
-  putStrLn ""
-
-  let cfg = (defaultConfig "Spline regression — B-spline vs Natural cubic")
-              { plotWidth = 700, plotHeight = 350 }
-      vlSpec = toVegaLite
-        [ title (plotTitle cfg) []
-        , layer
-            [ asSpec
-                [ dataFromColumns []
-                    . dataColumn "x" (Numbers (V.toList xGrid))
-                    . dataColumn "y" (Numbers (V.toList yGrid))
-                    $ []
-                , mark Line [MColor "#888888", MStrokeWidth 1.5,
-                             MStrokeDash [4, 4]]
-                , encoding
-                    . position X [PName "x", PmType Quantitative]
-                    . position Y [PName "y", PmType Quantitative]
-                    $ []
-                ]
-            , asSpec
-                [ dataFromColumns []
-                    . dataColumn "x" (Numbers (V.toList xGrid))
-                    . dataColumn "y" (Numbers (V.toList yBs))
-                    $ []
-                , mark Line [MColor "#1F77B4", MStrokeWidth 2.5]
-                , encoding
-                    . position X [PName "x", PmType Quantitative]
-                    . position Y [PName "y", PmType Quantitative]
-                    $ []
-                ]
-            , asSpec
-                [ dataFromColumns []
-                    . dataColumn "x" (Numbers (V.toList xGrid))
-                    . dataColumn "y" (Numbers (V.toList yNc))
-                    $ []
-                , mark Line [MColor "#FF8C42", MStrokeWidth 2.5]
-                , encoding
-                    . position X [PName "x", PmType Quantitative]
-                    . position Y [PName "y", PmType Quantitative]
-                    $ []
-                ]
-            , asSpec
-                [ dataFromColumns []
-                    . dataColumn "x" (Numbers (V.toList xs))
-                    . dataColumn "y" (Numbers (V.toList ys))
-                    $ []
-                , mark Point [MOpacity 0.5, MSize 25, MColor "#222222"]
-                , encoding
-                    . position X [PName "x", PmType Quantitative]
-                    . position Y [PName "y", PmType Quantitative]
-                    $ []
-                ]
-            ]
-        , width  (plotWidth cfg)
-        , height (plotHeight cfg)
-        ]
-  writeSpec HTML "spline.html" vlSpec
-  putStrLn "  → spline.html"
-  putStrLn "    真値=灰破線, B-spline=青, Natural=オレンジ, 観測=黒点"
-  putStrLn ""
-
-  putStrLn "═══════════════════════════════════════════════════════════════"
-  putStrLn "  ✓ B-spline / Natural cubic spline で非線形 fit"
-  putStrLn "═══════════════════════════════════════════════════════════════"
diff --git a/demo/visualization/BarDemo.hs b/demo/visualization/BarDemo.hs
deleted file mode 100644
--- a/demo/visualization/BarDemo.hs
+++ /dev/null
@@ -1,55 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Hanalyze.Viz.Bar と PNG/SVG 出力のデモ
-module Main where
-
-import Hanalyze.Viz.Core (defaultConfig, OutputFormat (..), writeSpec)
-import Hanalyze.Viz.Bar
-
--- ---------------------------------------------------------------------------
--- Main
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  let cfg = defaultConfig "Bar Demo"
-
-  -- ── 1. 縦棒グラフ → HTML ────────────────────────────────────────────
-  let spec1 = barChart cfg "Month" "Sales"
-                ["Jan","Feb","Mar","Apr","May","Jun"]
-                [120, 95, 140, 108, 155, 130]
-  writeSpec HTML "bar_vertical.html" spec1
-  putStrLn "bar_vertical.html を生成"
-
-  -- ── 2. 水平棒グラフ → HTML ──────────────────────────────────────────
-  let spec2 = barChartH cfg "Country" "GDP (trillion USD)"
-                ["Japan","Germany","USA","France","Canada"]
-                [4.2, 4.1, 25.5, 2.8, 2.1]
-  writeSpec HTML "bar_horizontal.html" spec2
-  putStrLn "bar_horizontal.html を生成"
-
-  -- ── 3. 積み上げ棒グラフ → HTML ──────────────────────────────────────
-  let quarters = concatMap (replicate 3) ["Q1","Q2","Q3","Q4"]
-      revenue  = [100,80,60, 120,90,70, 115,85,65, 130,100,80]
-      products = concat (replicate 4 ["Product A","Product B","Product C"])
-      spec3    = stackedBar cfg "Quarter" "Revenue" "Product"
-                   quarters revenue products
-  writeSpec HTML "bar_stacked.html" spec3
-  putStrLn "bar_stacked.html を生成"
-
-  -- ── 4. グループ別棒グラフ → HTML ────────────────────────────────────
-  let spec4 = groupedBar cfg "Method" "ESS" "Case"
-                ["MH","HMC","NUTS","MH","HMC","NUTS"]
-                [120, 900, 1800, 80, 1200, 1900]
-                ["Easy","Easy","Easy","Hard","Hard","Hard"]
-  writeSpec HTML "bar_grouped.html" spec4
-  putStrLn "bar_grouped.html を生成"
-
-  -- ── 5. PNG 出力テスト ────────────────────────────────────────────────
-  writeSpec PNG "bar_vertical.png" spec1
-  putStrLn "bar_vertical.png を生成 (vl-convert)"
-
-  -- ── 6. SVG 出力テスト ────────────────────────────────────────────────
-  writeSpec SVG "bar_vertical.svg" spec1
-  putStrLn "bar_vertical.svg を生成 (vl-convert)"
-
-  putStrLn "\n完了"
diff --git a/demo/visualization/NewSectionsDemo.hs b/demo/visualization/NewSectionsDemo.hs
deleted file mode 100644
--- a/demo/visualization/NewSectionsDemo.hs
+++ /dev/null
@@ -1,207 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Cycle 1 と Cycle 9 で追加した計 7 つの新セクション
--- (`secComparisonTable` / `secForestPlot` / `secFeatureImportance` / `secPPC`
---  + `secCalibration` / `sec3DScatter` / `secHeatmap`)
--- を 1 つのレポートで端から端まで使うショーケース。
---
--- 動作:
---   1. data/regression/test_lm.csv を読込
---   2. LM / GAM / RF (Random Forest) でフィット
---   3. 各モデルの RMSE / R² を 'secComparisonTable' で比較 (最良行ハイライト)
---   4. LM の β₀, β₁ について漸近 95% CI を 'secForestPlot' で可視化
---   5. RF の `featureImportance` を 'secFeatureImportance' で表示
---   6. LM の予測分布から 30 個の posterior-predictive 風サンプルを生成し
---      'secPPC' で観測値と重ね描き
---
--- 出力: trash/new_sections_demo.html
-module Main where
-
-import qualified Data.Vector as V
-import qualified Numeric.LinearAlgebra as LA
-import System.Random.MWC (createSystemRandom, GenIO)
-import qualified System.Random.MWC as MWC
-import Text.Printf (printf)
-import qualified Data.Text as T
-import Control.Monad (replicateM)
-
-import Hanalyze.DataIO.CSV          (loadAuto)
-import Hanalyze.DataIO.Convert      (getDoubleVec)
-import qualified Hanalyze.Model.LM  as LM
-import qualified Hanalyze.Model.GAM as GAM
-import qualified Hanalyze.Model.RandomForest as RF
-import Hanalyze.Model.Core          (coeffList, fittedList, residualsV, rSquared1)
-
-import qualified Hanalyze.Viz.ReportBuilder as RB
-
--- ---------------------------------------------------------------------------
--- ヘルパ
--- ---------------------------------------------------------------------------
-
-rmseOf :: [Double] -> [Double] -> Double
-rmseOf ys yh =
-  let n = length ys
-      r = zipWith (-) ys yh
-  in sqrt (sum [ x * x | x <- r ] / fromIntegral (max 1 n))
-
-r2Of :: [Double] -> [Double] -> Double
-r2Of ys yh =
-  let yBar = sum ys / fromIntegral (max 1 (length ys))
-      tss  = sum [ (y - yBar) ^ (2 :: Int) | y <- ys ]
-      rss  = sum [ (y - h)    ^ (2 :: Int) | (y, h) <- zip ys yh ]
-  in if tss < 1e-12 then 0 else 1 - rss / tss
-
--- | 平均 0、SD σ のガウス乱数 (Box-Muller)。
-gaussian :: Double -> GenIO -> IO Double
-gaussian sigma gen = do
-  u1 <- MWC.uniform gen
-  u2 <- MWC.uniform gen
-  let z = sqrt (-2 * log (max 1e-12 u1)) * cos (2 * pi * u2)
-  return (sigma * z)
-
-quickSort :: Ord a => [a] -> [a]
-quickSort [] = []
-quickSort (p:rs) = quickSort [x | x <- rs, x <= p]
-                ++ [p]
-                ++ quickSort [x | x <- rs, x > p]
-
--- ---------------------------------------------------------------------------
--- メイン
--- ---------------------------------------------------------------------------
-
-main :: IO ()
-main = do
-  putStrLn "============================================================"
-  putStrLn " New Sections Demo"
-  putStrLn " (secComparisonTable / secForestPlot /"
-  putStrLn "  secFeatureImportance / secPPC)"
-  putStrLn "============================================================"
-
-  Right df <- loadAuto "data/regression/test_lm.csv"
-  let Just xVec = getDoubleVec "x" df
-      Just yVec = getDoubleVec "y" df
-      xs = V.toList xVec
-      ys = V.toList yVec
-      n  = length xs
-
-  -- LM フィット
-  let xMat = LA.fromColumns [LA.konst 1 n, LA.fromList xs]
-      yLA  = LA.fromList ys
-      lmFit = LM.fitLMVec xMat yLA
-      lmYhat = fittedList lmFit
-      lmRMSE = rmseOf ys lmYhat
-      lmR2   = rSquared1 lmFit
-      lmBeta = coeffList lmFit
-      lmResid = LA.toList (residualsV lmFit)
-      sigmaHat = sqrt (sum [ r * r | r <- lmResid ]
-                       / fromIntegral (max 1 (n - 2)))
-      -- (XᵀX)⁻¹ で漸近 SE を計算
-      xtx    = LA.tr xMat LA.<> xMat
-      xtxInv = LA.inv xtx
-      diagXtxInv = LA.toList (LA.takeDiag xtxInv)
-      seBeta = [ sigmaHat * sqrt v | v <- diagXtxInv ]
-
-  -- GAM フィット
-  let gamFit = GAM.fitGAM 3 5 0.01 [xVec] yVec
-      gamYhat = LA.toList (GAM.gamYHat gamFit)
-      gamRMSE = rmseOf ys gamYhat
-      gamR2_  = GAM.gamR2 gamFit
-
-  -- RF フィット
-  gen <- createSystemRandom
-  let rows = [[x] | x <- xs]
-  rf <- RF.fitRF RF.defaultRFConfig rows ys gen
-  let rfYhat = [ RF.predictRF rf row | row <- rows ]
-      rfRMSE = rmseOf ys rfYhat
-      rfR2   = r2Of ys rfYhat
-      rfImport = V.toList (RF.featureImportance rf)
-
-  printf "  LM:   RMSE = %.4f, R² = %.4f\n" lmRMSE lmR2
-  printf "  GAM:  RMSE = %.4f, R² = %.4f\n" gamRMSE gamR2_
-  printf "  RF:   RMSE = %.4f, R² = %.4f\n" rfRMSE rfR2
-
-  -- 4 モデル比較行 + 最良 (lowest RMSE) 行のインデックス
-  let cmpHeaders = ["モデル", "RMSE", "R²"]
-      cmpRows =
-        [ ["LM",  T.pack (printf "%.4f" lmRMSE),  T.pack (printf "%.4f" lmR2)]
-        , ["GAM", T.pack (printf "%.4f" gamRMSE), T.pack (printf "%.4f" gamR2_)]
-        , ["RF",  T.pack (printf "%.4f" rfRMSE),  T.pack (printf "%.4f" rfR2)]
-        ]
-      bestIdx =
-        let rmses = [lmRMSE, gamRMSE, rfRMSE]
-            mn = minimum rmses
-        in length (takeWhile (/= mn) rmses)
-
-  -- Forest plot: LM の β₀, β₁ について 95% CI = mean ± 1.96 · SE
-  let forestRows =
-        [ ("β₀ (intercept)",
-            head lmBeta - 1.96 * head seBeta,
-            head lmBeta,
-            head lmBeta + 1.96 * head seBeta)
-        , ("β₁ (x)",
-            (lmBeta !! 1) - 1.96 * (seBeta !! 1),
-            lmBeta !! 1,
-            (lmBeta !! 1) + 1.96 * (seBeta !! 1))
-        ]
-
-  -- Feature importance: 1 特徴 (x) のみ
-  let importPairs = zip ["x"] rfImport
-
-  -- Posterior Predictive Check: LM 予測分布から 30 replicate 生成
-  -- y_rep_i ~ Normal(β₀ + β₁ x_i, σ̂)
-  reps <- replicateM 30 $ do
-    eps <- mapM (\_ -> gaussian sigmaHat gen) xs
-    return (zipWith (+) lmYhat eps)
-
-  -- Calibration: LM yhat を sigmoid で 0..1 に圧縮 → 予測確率、観測 = (y > median) の二値
-  let medY = let s = quickSort ys in s !! (length s `div` 2)
-      pPred = [ 1 / (1 + exp (-(h - medY))) | h <- lmYhat ]
-      yBin  = [ if y > medY then 1 else 0 | y <- ys ]
-
-  -- 3D scatter: (x, yhat, residual)
-  let zs3d = lmResid
-
-  -- Heatmap: 3 モデルの (RMSE, R², 1-R²) を 3×3 メトリック行列として表示
-  let heatRows  = ["LM", "GAM", "RF"]
-      heatCols  = ["RMSE", "R²", "1−R²"]
-      heatVals  =
-        [ [lmRMSE,  lmR2,   1 - lmR2]
-        , [gamRMSE, gamR2_, 1 - gamR2_]
-        , [rfRMSE,  rfR2,   1 - rfR2]
-        ]
-
-  -- レポート組立
-  let cfg = RB.defaultReportConfig
-              "新セクション 7 種ショーケース (Comparison / Forest / Importance / PPC / Calibration / 3D / Heatmap)"
-      sections =
-        [ RB.secMarkdown "概要"
-            (T.unlines
-              [ "Cycle 1 + Cycle 9 で `Hanalyze.Viz.ReportBuilder` に追加した計 7 つのセクションを"
-              , "1 つのレポートで使うデモ。"
-              , ""
-              , "データ: `data/regression/test_lm.csv` (50 行、x, y 二列)。"
-              , "LM / GAM / RandomForest の 3 モデルをフィットして RMSE/R² を比較し、"
-              , "LM の係数 95% CI を Forest plot、RF の特徴量重要度をバーで表示、"
-              , "LM の予測分布からの replicate を観測と重ね描きで表示する。"
-              , "さらに Calibration plot / 3D scatter / Heatmap を順に追加。"
-              ])
-        , RB.secComparisonTable
-            "モデル比較 (RMSE 最小行をハイライト)"
-            cmpHeaders cmpRows (Just bestIdx)
-        , RB.secForestPlot "LM 係数の漸近 95% CI" forestRows
-        , RB.secFeatureImportance "Random Forest 特徴量重要度" importPairs
-        , RB.secPPC "Posterior Predictive Check (LM 予測分布、30 replicate)"
-            ys reps
-        , RB.secCalibration
-            "Calibration plot (sigmoid(yhat - median y) vs (y > median))"
-            pPred (map fromIntegral yBin)
-        , RB.sec3DScatter
-            "3D scatter (擬似: x / yhat / 残差を色エンコード)"
-            "x" "yhat" "residual" xs lmYhat zs3d
-        , RB.secHeatmap
-            "モデル × メトリック ヒートマップ (値の色で大小表現)"
-            heatCols heatRows heatVals
-        ]
-
-  RB.renderReport "trash/new_sections_demo.html" cfg sections
-  putStrLn ""
-  putStrLn "Report: trash/new_sections_demo.html"
diff --git a/hanalyze.cabal b/hanalyze.cabal
--- a/hanalyze.cabal
+++ b/hanalyze.cabal
@@ -1,2535 +1,303 @@
 cabal-version: 3.0
 name:          hanalyze
-version:       0.2.0.0
-synopsis:      A general-purpose statistical analysis, optimization and visualization toolkit
-description:
-    @hanalyze@ is a self-contained Haskell toolkit for classical regression
-    (LM, GLM, GLMM, splines, kernels, GP, RFF), Bayesian modeling
-    (HBM DSL with MH, HMC, NUTS, Gibbs, ADVI), design of experiments
-    (full/fractional factorial, RSM, D-optimal, orthogonal arrays, Taguchi),
-    optimization (Nelder-Mead, L-BFGS, DE, CMA-ES, NSGA-II, Bayesian
-    optimization, augmented Lagrangian), and Vega-Lite-based visualization
-    with HTML / PNG / SVG output.
-    .
-    All algorithms are implemented natively in Haskell — no R / Stan / Python
-    bridges. Data interchange uses the @dataframe@ package as a first-class
-    citizen.
-    .
-    A unified @hanalyze@ command-line interface exposes the most common
-    workflows (@regress@, @info@, @hist@, @doe@, @taguchi@, @ridge@,
-    @kernel@, @spline@, @multireg@, @clean@, @melt@, @regrid@, ...).
-homepage:      https://github.com/frenzieddoll/hanalyze
-bug-reports:   https://github.com/frenzieddoll/hanalyze/issues
-license:       BSD-3-Clause
-license-file:  LICENSE
-author:        Toshiaki Honda
-maintainer:    frenzieddoll@gmail.com
-copyright:     2026 Aelysce Project (Toshiaki Honda)
-category:      Math, Statistics, Numeric, Machine Learning
-build-type:    Simple
-tested-with:   GHC == 9.6.7
-extra-doc-files:
-    README.md
-    CHANGELOG.md
-
-extra-source-files:
-    data/dirty/*.csv
-    data/distributions/*.csv
-    data/io/*.csv
-    data/readme/*.csv
-    data/regression/*.csv
-
-source-repository head
-  type:     git
-  location: https://github.com/frenzieddoll/hanalyze.git
-
-flag plot-integration
-  description: hgg 連携 (Hanalyze.Plot = toPlot/Plottable) を有効化する。
-               on にすると hgg-core/-svg に依存する (= sibling repo 必須)。
-               既定 off で upstream hanalyze は plot 非依存・standalone build を維持。
-  default:     False
-  manual:      True
-
-flag demos
-  description: デモ/例 executable 群 (*-demo / *-smoke / hbm-example 等) を build する。
-               既定 off で default build は library + hanalyze のみ (高速)。
-               有効化: cabal build -f demos / cabal run -f demos <name>。
-  default:     False
-  manual:      True
-
-flag benches
-  description: ベンチ executable 群 (bench-* / *-bench-demo / bench-data-gen) を build する。
-               既定 off で bench の重い依存 (tasty-bench 等) も default build から外す。
-               有効化: cabal build -f benches / cabal run -f benches <name>。
-  default:     False
-  manual:      True
-
-common warnings
-  ghc-options: -Wall -Wcompat -Widentities -Wredundant-constraints
-
-common opt
-  ghc-options: -O2 -funbox-strict-fields
-
--- demo/bench executable をフラグ配下に置くゲート (import: …, demo-gate で適用)。
--- フラグ off のとき buildable: False = default build から外れる。
-common demo-gate
-  if !flag(demos)
-    buildable: False
-
-common bench-gate
-  if !flag(benches)
-    buildable: False
-
-library
-  import:           warnings, opt
-  hs-source-dirs:   src
-  default-language: GHC2021
-  exposed-modules:
-    Hanalyze
-    Hanalyze.Data.ColumnSource
-    Hanalyze.Data.Factor
-    Hanalyze.Data.Strings
-    Hanalyze.Data.Transform
-    Hanalyze.Data.Wrangle
-    Hanalyze.DataIO.CSV
-    Hanalyze.DataIO.Preprocess
-    Hanalyze.DataIO.External
-    Hanalyze.DataIO.Convert
-    Hanalyze.DataIO.Log
-    Hanalyze.DataIO.Health
-    Hanalyze.DataIO.Sniff
-    Hanalyze.DataIO.Clean
-    Hanalyze.DataIO.Reshape
-    Hanalyze.Viz.Core
-    Hanalyze.Viz.PlotConfig
-    Hanalyze.Viz.PlotData
-    Hanalyze.Viz.PlotData.DataFrame
-    Hanalyze.Viz.Scatter
-    Hanalyze.Viz.Histogram
-    Hanalyze.Viz.Bar
-    Hanalyze.Model.Core
-    Hanalyze.Model.Wrappers
-    Hanalyze.Diagnostics
-    Hanalyze.Fit
-    Hanalyze.Model.LM
-    Hanalyze.Model.LM.Diagnostics
-    Hanalyze.Model.Formula
-    Hanalyze.Model.Formula.Frame
-    Hanalyze.Model.Formula.Design
-    Hanalyze.Model.Formula.RFormula
-    Hanalyze.Model.Formula.Nonlinear
-    Hanalyze.Model.Formula.Mixed
-    Hanalyze.Model.GLM
-    Hanalyze.Model.GLMM
-    Hanalyze.Model.Spline
-    Hanalyze.Model.Kernel
-    Hanalyze.Model.KernelRegression
-    Hanalyze.Model.Regularized
-    Hanalyze.Model.RFF
-    Hanalyze.Model.GPRobust
-    Hanalyze.Model.DAG
-    Hanalyze.Model.LiNGAM.Direct
-    Hanalyze.Model.LiNGAM.Bootstrap
-    Hanalyze.Model.LiNGAM.Pairwise
-    Hanalyze.Model.LiNGAM.ICA
-    Hanalyze.Model.LiNGAM.VAR
-    Hanalyze.Model.LiNGAM.MultiGroup
-    Hanalyze.Model.LiNGAM.Parce
-    Hanalyze.Math.ICA
-    Hanalyze.Math.Hungarian
-    Hanalyze.Math.HSIC
-    Hanalyze.Model.Quantile
-    Hanalyze.Model.GAM
-    Hanalyze.Model.RandomForest
-    Hanalyze.Model.MultiLM
-    Hanalyze.Model.Multivariate
-    Hanalyze.Model.MultiGP
-    Hanalyze.Model.MultiOutput
-    Hanalyze.Model.PCA
-    Hanalyze.Model.MDS
-    Hanalyze.Model.Cluster
-    Hanalyze.Model.DecisionTree
-    Hanalyze.Model.PartialDependence
-    Hanalyze.Model.TimeSeries
-    Hanalyze.Model.Survival
-    Hanalyze.Model.Weibull
-    Hanalyze.Model.Reliability
-    Hanalyze.Model.PLS
-    Hanalyze.Model.Discriminant
-    Hanalyze.Model.HierarchicalCluster
-    Hanalyze.Model.AFT
-    Hanalyze.Model.RandomForestClassifier
-    Hanalyze.Model.FitYByX
-    Hanalyze.Model.StateSpace
-    Hanalyze.Model.NeuralNetwork
-    Hanalyze.Design.GaugeRR
-    Hanalyze.Design.Diagnostics
-    Hanalyze.Design.SpaceFilling
-    Hanalyze.Design.DSD
-    Hanalyze.Design.Mixture
-    Hanalyze.Design.Sequential
-    Hanalyze.Design.Factorial
-    Hanalyze.Design.Block
-    Hanalyze.Design.Mixed
-    Hanalyze.Design.Anova
-    Hanalyze.Design.Power
-    Hanalyze.Design.Quality
-    Hanalyze.Design.RSM
-    Hanalyze.Design.Workflow
-    Hanalyze.Design.Optimal
-    Hanalyze.Design.Constraint
-    Hanalyze.Design.Custom.Factor
-    Hanalyze.Design.Custom.Model
-    Hanalyze.Design.Custom.Constraint
-    Hanalyze.Design.Custom.Coordinate
-    Hanalyze.Design.Custom.Compare
-    Hanalyze.Design.Custom.Power
-    Hanalyze.Design.Custom.Augment
-    Hanalyze.Design.Custom.SplitPlot
-    Hanalyze.Design.Custom.Structured
-    Hanalyze.Design.Custom.Bayesian
-    Hanalyze.Design.Custom.RegionMoment
-    Hanalyze.MCMC.SMC
-    Hanalyze.Stat.BridgeSampling
-    Hanalyze.Stat.BayesFactor
-    Hanalyze.Stat.BayesianModelAveraging
-    Hanalyze.Stat.Causal.PropensityScore
-    Hanalyze.Stat.Causal.IPW
-    Hanalyze.Stat.Causal.DoublyRobust
-    Hanalyze.Stat.Causal.CATE
-    Hanalyze.Model.RegularizedAdvanced
-    Hanalyze.Model.Robust
-    Hanalyze.Stat.CorrelationNetwork
-    Hanalyze.Model.LatentClassAnalysis
-    Hanalyze.Model.FDA
-    Hanalyze.Model.GradientBoosting
-    Hanalyze.Model.SVM
-    Hanalyze.Stat.MDS
-    Hanalyze.Model.KNN
-    Hanalyze.Model.NaiveBayes
-    Hanalyze.Model.GARCH
-    Hanalyze.Model.VAR
-    Hanalyze.Model.CompetingRisks
-    Hanalyze.Model.ReliabilityBlockDiagram
-    Hanalyze.Design.MultiRSM
-    Hanalyze.Design.Orthogonal
-    Hanalyze.Design.Taguchi
-    Hanalyze.Optim.Desirability
-    Hanalyze.Model.HBM
-    Hanalyze.Model.HBM.Ast
-    Hanalyze.Model.HBM.Distribution
-    Hanalyze.Model.HBM.Eval
-    Hanalyze.Model.HBM.Gradient
-    Hanalyze.Model.HBM.IR
-    Hanalyze.Model.HBM.Interp
-    Hanalyze.Model.HBM.Model
-    Hanalyze.Model.HBM.Sampling
-    Hanalyze.Model.HBM.Track
-    Hanalyze.Model.HBM.Util
-    Hanalyze.Model.HBM.VecAD
-    Hanalyze.MCMC.Core
-    Hanalyze.MCMC.MH
-    Hanalyze.MCMC.HMC
-    Hanalyze.MCMC.NUTS
-    Hanalyze.MCMC.Progress
-    Hanalyze.MCMC.BayesianTest
-    Hanalyze.MCMC.Gibbs
-    Hanalyze.MCMC.Slice
-    Hanalyze.Stat.Descriptive
-    Hanalyze.Stat.Distribution
-    Hanalyze.Stat.Standardize
-    Hanalyze.Stat.NumberFormat
-    Hanalyze.Stat.MCMC
-    Hanalyze.Stat.ModelSelect
-    Hanalyze.Stat.AD
-    Hanalyze.Stat.VI
-    Hanalyze.Stat.PosteriorPredictive
-    Hanalyze.Stat.Summary
-    Hanalyze.Stat.Interpolate
-    Hanalyze.Stat.AdaptiveGrid
-    Hanalyze.Stat.KernelDist
-    Hanalyze.Stat.Cholesky
-    Hanalyze.Stat.QuasiRandom
-    Hanalyze.Stat.Test
-    Hanalyze.Stat.ClassMetrics
-    Hanalyze.Stat.CV
-    Hanalyze.Stat.MultipleTesting
-    Hanalyze.Stat.Bootstrap
-    Hanalyze.Stat.Effect
-    Hanalyze.Stat.Interpret
-    Hanalyze.Stat.SPC
-    Hanalyze.Stat.GroupComparison
-    Hanalyze.Optim.Adam
-    Hanalyze.Optim.GradAscent
-    Hanalyze.Optim.Numeric
-    Hanalyze.Optim.Common
-    Hanalyze.Optim.NelderMead
-    Hanalyze.Optim.LBFGS
-    Hanalyze.Optim.LineSearch
-    Hanalyze.Optim.DifferentialEvolution
-    Hanalyze.Optim.CMAES
-    Hanalyze.Optim.CMAESFull
-    Hanalyze.Optim.SimulatedAnnealing
-    Hanalyze.Optim.ParticleSwarm
-    Hanalyze.Optim.Constrained
-    Hanalyze.Optim.NSGA
-    Hanalyze.Optim.Pareto
-    Hanalyze.Optim.Acquisition
-    Hanalyze.Optim.BayesOpt
-    Hanalyze.Viz.MCMC
-    Hanalyze.Viz.ModelGraph
-    Hanalyze.Viz.ModelGraphDot
-    Hanalyze.Viz.Report
-    Hanalyze.Model.GP
-    Hanalyze.Viz.GP
-    Hanalyze.Viz.GPReport
-    Hanalyze.Viz.Assets
-    Hanalyze.Viz.AnalysisReport
-    Hanalyze.Viz.Pareto
-    Hanalyze.Viz.Taguchi
-    Hanalyze.Viz.ReportBuilder
-    Hanalyze.Viz.ReportInstances
-  build-depends:
-      base                 >= 4.14 && < 5
-    , array                >= 0.5  && < 0.6
-    , async                >= 2.2  && < 2.3
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , filepath             >= 1.4  && < 1.6
-    , hmatrix              >= 0.20 && < 0.22
-    , hvega                >= 0.12 && < 0.13
-    , mwc-random           >= 0.15 && < 0.16
-    , primitive            >= 0.7  && < 0.10
-    , deepseq              >= 1.4  && < 1.6
-    , parallel             >= 3.2  && < 3.3
-    , process              >= 1.6  && < 1.8
-    , statistics           >= 0.16 && < 0.17
-    , text                 >= 1.2  && < 2.2
-    , aeson                >= 2.0  && < 2.3
-    , directory            >= 1.3  && < 1.4
-    , temporary            >= 1.3  && < 1.4
-    , unordered-containers >= 0.2  && < 0.3
-    , ad                   >= 4.4  && < 4.6
-    -- Phase 92 B3: logDensityRD (AD 定数正規化項の畳み込み) の Reifies 制約用
-    -- (ad の依存 closure 内・新規 install なし)
-    , reflection           >= 2.1  && < 2.2
-    , vector               >= 0.12 && < 0.14
-    , dataframe-core        ^>= 1.1
-    , dataframe-operations  >= 1.1.1 && < 1.2
-    , dataframe-csv         ^>= 1.0.2
-    , dataframe-json        ^>= 1.0
-    , dataframe-parquet     ^>= 1.1
-    , deepseq              >= 1.4  && < 1.6
-    , massiv               >= 1.0  && < 1.1
-    , parallel             >= 3.2  && < 3.3
-    , vector-algorithms    >= 0.9  && < 0.10
-    , megaparsec           >= 9.0  && < 9.7
-    , parser-combinators   >= 1.3  && < 1.4
-    , unicode-transforms   >= 0.4  && < 0.5
-    , regex-tdfa           >= 1.3  && < 1.4
-    , regex-base           >= 0.94 && < 0.95
-
-  -- hgg 連携 (flag plot-integration 配下・upstream 非 portable)。
-  -- 既定 off では Hanalyze.Plot を build せず plot 依存も持たない (standalone 維持)。
-  if flag(plot-integration)
-    exposed-modules:
-        Hanalyze.Plot
-        Hanalyze.Plot.Core
-        Hanalyze.Plot.Linear
-        Hanalyze.Plot.Smooth
-        Hanalyze.Plot.Robust
-        Hanalyze.Plot.Bayes
-        Hanalyze.Plot.ML
-        Hanalyze.Plot.Wrappers
-    build-depends:
-        hgg-core
-      , hgg-svg
-      , hgg-3d
-      , hgg-custom
-
--- NOTE (public sync 2026-07-18): hanalyze-plot-test / phase104-probe-prof /
--- plot-integration-demo は fork 側の demo-plot/ test-plot/ experiments/ 配下
--- を参照するが、 これらのディレクトリは同期対象外 (private plot 連携リポジトリ
--- 側の開発物)。 plot-integration flag は残すが、 上記 3 stanza は hs-source-dirs
--- が存在しないため省いている。
-
--- Phase 82: DOE demo — RSM 予測精度 vs 実験数 (必要実験数の見極め)。
---   hgg で RMSE vs n の折れ線 (noise sd 参照線つき) を SVG 出力。
-executable doe-rsm-samplesize-demo
-  import:           warnings
-  main-is:          RSMSampleSizeDemo.hs
-  hs-source-dirs:   demo/doe
-  default-language: GHC2021
-  if flag(plot-integration)
-    build-depends:
-        base
-      , text
-      , vector
-      , hmatrix
-      , directory
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-svg
-  else
-    buildable: False
-
--- Phase 83: 逐次DOE デモ (半導体インプラ最適化・30因子スクリーニング→試作RSM)。
-executable doe-implant-sequential-demo
-  import:           warnings
-  main-is:          ImplantSequentialDemo.hs
-  hs-source-dirs:   demo/doe
-  default-language: GHC2021
-  if flag(plot-integration)
-    build-depends:
-        base
-      , text
-      , vector
-      , hmatrix
-      , directory
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-svg
-  else
-    buildable: False
-
-executable hanalyze
-  import:           warnings, opt
-  main-is:          Main.hs
-  hs-source-dirs:   app
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , vector     >= 0.12 && < 0.14
-    , hmatrix    >= 0.20 && < 0.22
-    , mwc-random >= 0.15 && < 0.16
-    , containers >= 0.6  && < 0.8
-    , filepath   >= 1.4  && < 1.6
-    , hvega      >= 0.12 && < 0.13
-    , dataframe-core        ^>= 1.1
-    , dataframe-operations  >= 1.1.1 && < 1.2
-    , dataframe-csv         ^>= 1.0.2
-    , time       >= 1.11 && < 1.13
-
-executable glmm-demo
-  import:           warnings, opt, demo-gate
-  main-is:          Demo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base     >= 4.14 && < 5
-    , hanalyze
-    , vector   >= 0.12 && < 0.14
-    , hmatrix  >= 0.20 && < 0.22
-    , text     >= 1.2  && < 2.2
-    , dataframe-core        ^>= 1.1
-
-executable hbm-example
-  import:           warnings, opt, demo-gate
-  main-is:          HBMExample.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable phase37-a0-verify
-  import:           warnings, opt, demo-gate
-  main-is:          Phase37A0VerifyDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable plate-notation-demo
-  import:           warnings, opt, demo-gate
-  main-is:          PlateNotationDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , directory  >= 1.3  && < 1.4
-
-executable test-hmc-nuts
-  import:           warnings, opt, demo-gate
-  main-is:          TestHMCNUTS.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable bench-mcmc
-  import:           warnings, opt, bench-gate
-  main-is:          BenchMCMC.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-    , time       >= 1.9  && < 1.15
-
-executable vi-demo
-  import:           warnings, opt, demo-gate
-  main-is:          VIDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-    , time       >= 1.9  && < 1.15
-
-executable gibbs-demo
-  import:           warnings, opt, demo-gate
-  main-is:          GibbsDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-    , time       >= 1.9  && < 1.15
-
-executable regrid-bench-demo
-  import:           warnings, opt, bench-gate
-  main-is:          RegridBenchDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , dataframe-core        ^>= 1.1
-    , mwc-random >= 0.15 && < 0.16
-    , text       >= 1.2  && < 2.2
-
-executable bar-demo
-  import:           warnings, opt, demo-gate
-  main-is:          BarDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-
-executable clinical-trial
-  import:           warnings, opt, demo-gate
-  main-is:          ClinicalTrial.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable gp-demo
-  import:           warnings, opt, demo-gate
-  main-is:          GPDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base     >= 4.14 && < 5
-    , hanalyze
-
-executable preprocess-demo
-  import:           warnings, opt, demo-gate
-  main-is:          PreprocessDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , vector     >= 0.12 && < 0.14
-    , dataframe-core        ^>= 1.1
-    , dataframe-operations  >= 1.1.1 && < 1.2
-
-executable dirty-data-demo
-  import:           warnings, opt, demo-gate
-  main-is:          DirtyDataDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , dataframe-core        ^>= 1.1
-    , dataframe-operations  >= 1.1.1 && < 1.2
-
-executable external-io-demo
-  import:           warnings, opt, demo-gate
-  main-is:          ExternalIODemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , vector     >= 0.12 && < 0.14
-    , dataframe-core        ^>= 1.1
-    , dataframe-operations  >= 1.1.1 && < 1.2
-
-executable analysis-compare-demo
-  import:           warnings, opt, demo-gate
-  main-is:          AnalysisCompareDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , vector     >= 0.12 && < 0.14
-    , hmatrix    >= 0.20 && < 0.22
-    , mwc-random >= 0.15 && < 0.16
-    , containers >= 0.6  && < 0.8
-    , dataframe-core        ^>= 1.1
-    , dataframe-operations  >= 1.1.1 && < 1.2
-
-executable new-sections-demo
-  import:           warnings, opt, demo-gate
-  main-is:          NewSectionsDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , vector     >= 0.12 && < 0.14
-    , hmatrix    >= 0.20 && < 0.22
-    , mwc-random >= 0.15 && < 0.16
-
-executable robust-gp-demo
-  import:           warnings, opt, demo-gate
-  main-is:          RobustGPDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-
-executable rff-demo
-  import:           warnings, opt, demo-gate
-  main-is:          RFFDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , hmatrix    >= 0.20 && < 0.21
-    , vector     >= 0.12 && < 0.14
-    , mwc-random >= 0.15 && < 0.16
-    , time       >= 1.9  && < 1.13
-
-executable gibbs-hbm-demo
-  import:           warnings, opt, demo-gate
-  main-is:          GibbsHBMDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable newdistribs-demo
-  import:           warnings, opt, demo-gate
-  main-is:          NewDistribsDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable regularized-demo
-  import:           warnings, opt, demo-gate
-  main-is:          RegularizedDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , vector     >= 0.12 && < 0.14
-    , hmatrix    >= 0.20 && < 0.22
-    , mwc-random >= 0.15 && < 0.16
-
-executable optimaldoe-demo
-  import:           warnings, opt, demo-gate
-  main-is:          OptimalDOEDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-
-executable pareto-smoke
-  import:           warnings, opt, demo-gate
-  main-is:          ParetoSmokeDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-
-executable materials-moo-demo
-  import:           warnings, opt, demo-gate
-  main-is:          MaterialsMOODemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , vector     >= 0.12 && < 0.14
-    , hmatrix    >= 0.20 && < 0.22
-    , mwc-random >= 0.15 && < 0.16
-
-executable bayesopt-demo
-  import:           warnings, opt, demo-gate
-  main-is:          BayesOptDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , mwc-random >= 0.15 && < 0.16
-
-executable multirsm-demo
-  import:           warnings, opt, demo-gate
-  main-is:          MultiRSMDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , hmatrix    >= 0.20 && < 0.22
-
-executable multivariate-demo
-  import:           warnings, opt, demo-gate
-  main-is:          MultivariateDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , hmatrix    >= 0.20 && < 0.22
-    , mwc-random >= 0.15 && < 0.16
-
-executable multilm-demo
-  import:           warnings, opt, demo-gate
-  main-is:          MultiLMDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , hmatrix    >= 0.20 && < 0.22
-    , mwc-random >= 0.15 && < 0.16
-
-executable nsga-demo
-  import:           warnings, opt, demo-gate
-  main-is:          NSGADemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , mwc-random >= 0.15 && < 0.16
-    , text       >= 1.2  && < 2.2
-    , vector     >= 0.13 && < 0.14
-
-executable nsga-smoke
-  import:           warnings, opt, demo-gate
-  main-is:          NSGASmokeDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , mwc-random >= 0.15 && < 0.16
-
-executable rsm-demo
-  import:           warnings, opt, demo-gate
-  main-is:          RSMDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , hmatrix    >= 0.20 && < 0.22
-    , mwc-random >= 0.15 && < 0.16
-
-executable doe-demo
-  import:           warnings, opt, demo-gate
-  main-is:          DOEDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-
-executable cis-implant-workflow-demo
-  import:           warnings, opt, demo-gate
-  main-is:          CISImplantWorkflowDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base            >= 4.14 && < 5
-    , hanalyze
-    , text            >= 1.2  && < 2.2
-    , hmatrix         >= 0.20
-    , vector          >= 0.12 && < 0.14
-    , directory       >= 1.3
-
-executable kernel-demo
-  import:           warnings, opt, demo-gate
-  main-is:          KernelDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , vector     >= 0.12 && < 0.14
-    , hvega      >= 0.12 && < 0.13
-    , mwc-random >= 0.15 && < 0.16
-
-executable spline-demo
-  import:           warnings, opt, demo-gate
-  main-is:          SplineDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , vector     >= 0.12 && < 0.14
-    , hmatrix    >= 0.20 && < 0.22
-    , hvega      >= 0.12 && < 0.13
-    , mwc-random >= 0.15 && < 0.16
-
-executable integrated-demo
-  import:           warnings, opt, demo-gate
-  main-is:          IntegratedDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable slice-demo
-  import:           warnings, opt, demo-gate
-  main-is:          SliceDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable ar1-demo
-  import:           warnings, opt, demo-gate
-  main-is:          AR1Demo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable lkj3d-demo
-  import:           warnings, opt, demo-gate
-  main-is:          LKJ3DDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable lkj-demo
-  import:           warnings, opt, demo-gate
-  main-is:          LKJDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable zeroinflated-demo
-  import:           warnings, opt, demo-gate
-  main-is:          ZeroInflatedDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable multinomial-demo
-  import:           warnings, opt, demo-gate
-  main-is:          MultinomialDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable negbinom-demo
-  import:           warnings, opt, demo-gate
-  main-is:          NegBinomDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable mvnormal-latent-demo
-  import:           warnings, opt, demo-gate
-  main-is:          MvNormalLatentDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable setdata-demo
-  import:           warnings, opt, demo-gate
-  main-is:          SetDataDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable dirichlet-demo
-  import:           warnings, opt, demo-gate
-  main-is:          DirichletDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable noncentered-demo
-  import:           warnings, opt, demo-gate
-  main-is:          NonCenteredDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable deterministic-demo
-  import:           warnings, opt, demo-gate
-  main-is:          DeterministicDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable summary-demo
-  import:           warnings, opt, demo-gate
-  main-is:          SummaryDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable pymc-status-demo
-  import:           warnings, opt, demo-gate
-  main-is:          PyMCStatusDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-
-executable energy-demo
-  import:           warnings, opt, demo-gate
-  main-is:          EnergyDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable mvnormal-demo
-  import:           warnings, opt, demo-gate
-  main-is:          MvNormalDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable cdf-test
-  import:           warnings, opt, demo-gate
-  main-is:          CDFTestDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-
-executable trunc-censor-demo
-  import:           warnings, opt, demo-gate
-  main-is:          TruncCensorDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable mixture-demo
-  import:           warnings, opt, demo-gate
-  main-is:          MixtureDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable potential-demo
-  import:           warnings, opt, demo-gate
-  main-is:          PotentialDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable potential-multiout-demo
-  import:           warnings, opt, demo-gate
-  main-is:          PotentialMultiOut.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , vector     >= 0.12 && < 0.14
-    , hmatrix    >= 0.20 && < 0.22
-
-executable potential-multikr-demo
-  import:           warnings, opt, demo-gate
-  main-is:          PotentialMultiKR.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , vector     >= 0.12 && < 0.14
-    , hmatrix    >= 0.20 && < 0.22
-
-executable single-opt-bench-demo
-  import:           warnings, opt, bench-gate
-  main-is:          SingleOptBench.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , mwc-random >= 0.15 && < 0.16
-    , hvega      >= 0.12 && < 0.13
-
-executable forest-compare
-  import:           warnings, opt, demo-gate
-  main-is:          ForestCompareDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable ppc-demo
-  import:           warnings, opt, demo-gate
-  main-is:          PPCDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable discrete-obs-demo
-  import:           warnings, opt, demo-gate
-  main-is:          DiscreteObsDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable new-distrib-demo
-  import:           warnings, opt, demo-gate
-  main-is:          NewDistribDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-
-executable hbm-random-slope
-  import:           warnings, opt, demo-gate
-  main-is:          HBMRandomSlopeDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-    , vector     >= 0.12 && < 0.14
-    , dataframe-core        ^>= 1.1
-
-executable simpson-paradox
-  import:           warnings, opt, demo-gate
-  main-is:          SimpsonParadoxDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-    , vector     >= 0.12 && < 0.14
-    , hmatrix    >= 0.20 && < 0.22
-    , dataframe-core        ^>= 1.1
-
-executable hbm-regression
-  import:           warnings, opt, demo-gate
-  main-is:          HBMRegressionDemo.hs
-  hs-source-dirs:   demo demo/regression demo/doe-optim demo/bayesian demo/visualization demo/io
-  default-language: GHC2021
-  build-depends:
-      base       >= 4.14 && < 5
-    , hanalyze
-    , text       >= 1.2  && < 2.2
-    , containers >= 0.6  && < 0.8
-    , mwc-random >= 0.15 && < 0.16
-    , vector     >= 0.12 && < 0.14
-    , dataframe-core        ^>= 1.1
-
--- Bench data generator: produces deterministic CSVs that both the Haskell
--- benchmarks and the Python comparison scripts read. (See bench/README.md.)
-executable bench-data-gen
-  import:           warnings, opt, bench-gate
-  main-is:          BenchDataGen.hs
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , directory            >= 1.3  && < 1.4
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , vector               >= 0.12 && < 0.14
-
--- Phase 47 A5: Formula DSL の Haskell 参照値生成器 (statsmodels/scipy 突合用)。
-executable formula-ref-gen
-  import:           warnings, opt, demo-gate
-  main-is:          BenchFormulaRef.hs
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , hanalyze
-    , dataframe-core        ^>= 1.1
-    , hmatrix              >= 0.20 && < 0.22
-    , text                 >= 1.2  && < 2.2
-    , vector               >= 0.12 && < 0.14
-
--- Bayesian optimization bench (B5): Branin / Hartmann6.
-executable bench-bo
-  import:           warnings, opt, bench-gate
-  main-is:          BenchBO.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Phase 1-7 (Spotfire/JMP gap) features の Python/R 比較ベンチ。
--- 共通入力 CSV を bench/data/ に生成 + Haskell 側時間と精度を計測。
-executable bench-phase17
-  import:           warnings, opt, bench-gate
-  main-is:          BenchPhase17.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , hanalyze
-    , directory            >= 1.3  && < 1.4
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Phase 27: JMP 同等性検証ベンチ。 文献例題 + JMP 公式 example の参照値と
--- hanalyze 実装出力を golden CSV で比較する。 deterministic seed 固定。
-executable bench-custom-design
-  import:           warnings, opt, bench-gate
-  main-is:          BenchCustomDesign.hs
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , hanalyze
-    , directory            >= 1.3  && < 1.4
-    , hmatrix              >= 0.20 && < 0.22
-    , text                 >= 1.2  && < 2.2
-    , vector               >= 0.12 && < 0.14
-
-executable bench-tier12
-  import:           warnings, opt, bench-gate
-  main-is:          BenchTier12.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , hanalyze
-    , directory            >= 1.3  && < 1.4
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Standalone bench: hmatrix vs massiv on pairwise squared distance
--- (F4 evaluation, F5 multi-core Par evaluation).
-executable bench-massiv
-  import:           warnings, opt, bench-gate
-  main-is:          BenchMassiv.hs
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  ghc-options:      -threaded -rtsopts -with-rtsopts=-N
-  build-depends:
-      base                 >= 4.14 && < 5
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , massiv               >= 1.0  && < 1.1
-    , time                 >= 1.9  && < 1.13
-    , deepseq              >= 1.4  && < 1.6
-
--- Multi-objective optimization bench (B4): NSGA-II on ZDT/DTLZ.
-executable bench-mo
-  import:           warnings, opt, bench-gate
-  main-is:          BenchMO.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Standalone investigation: P37 Cheng-BB Beta sampler vs 2-Gamma + division.
-executable bench-beta-isolate
-  import:           warnings, opt, bench-gate
-  main-is:          BenchBetaIsolate.hs
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , hanalyze
-    , mwc-random           >= 0.15 && < 0.16
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-
--- Standalone investigation: P40 Bootstrap resampling hot-path
--- (uniformVector batch + GEMV row-sum vs naive index-loop).
-executable bench-bootstrap-isolate
-  import:           warnings, opt, bench-gate
-  main-is:          BenchBootstrapIsolate.hs
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-
--- Single-objective optimization bench (B3).
-executable bench-optim
-  import:           warnings, opt, bench-gate
-  main-is:          BenchOptim.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- OOM regression bench (Phase 11b): RFF.medianPairwiseDist + rbfKernelMat.
-executable bench-rff-oom
-  import:           warnings, opt, bench-gate
-  main-is:          BenchRFFOOM.hs
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  build-depends:
-      base                 >= 4.14 && < 5
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , time                 >= 1.9  && < 1.13
-
-executable bench-mem-vi
-  import:           warnings, opt, bench-gate
-  main-is:          BenchMemVI.hs
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  build-depends:
-      base                 >= 4.14 && < 5
-    , hanalyze
-    , containers           >= 0.6  && < 0.8
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , mwc-random           >= 0.15 && < 0.16
-
-executable bench-mem-aggregate
-  import:           warnings, opt, bench-gate
-  main-is:          BenchMemAggregate.hs
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  build-depends:
-      base                 >= 4.14 && < 5
-    , hanalyze
-    , dataframe-core        ^>= 1.1
-    , dataframe-operations  >= 1.1.1 && < 1.2
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.13 && < 0.14
-
-executable bench-mem-nsga2
-  import:           warnings, opt, bench-gate
-  main-is:          BenchMemNSGA.hs
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  build-depends:
-      base                 >= 4.14 && < 5
-    , hanalyze
-    , time                 >= 1.9  && < 1.13
-    , mwc-random           >= 0.15 && < 0.16
-
-executable bench-mem-bo
-  import:           warnings, opt, bench-gate
-  main-is:          BenchMemBO.hs
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  build-depends:
-      base                 >= 4.14 && < 5
-    , hanalyze
-    , time                 >= 1.9  && < 1.13
-    , mwc-random           >= 0.15 && < 0.16
-
-executable bench-mem-mcmc
-  import:           warnings, opt, bench-gate
-  main-is:          BenchMemMCMC.hs
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  build-depends:
-      base                 >= 4.14 && < 5
-    , hanalyze
-    , containers           >= 0.6  && < 0.8
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , mwc-random           >= 0.15 && < 0.16
-
--- Kernel / GP bench (B2): KR / NW / RFF / GP / GPRobust.
-executable bench-kernel
-  import:           warnings, opt, bench-gate
-  main-is:          BenchKernel.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- ML bench (B6): PCA / KMeans / DecisionTree / RandomForest.
-executable bench-ml
-  import:           warnings, opt, bench-gate
-  main-is:          BenchML.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Survival / TimeSeries bench (B8): ARIMA / Cox / KM / Quantile / GAM / Spline.
-executable bench-survts
-  import:           warnings, opt, bench-gate
-  main-is:          BenchSurvTS.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , random               >= 1.2  && < 1.4
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- MCMC diagnostic (B10b): explore why hanalyze NUTS ESS is poor.
-executable bench-mcmc-diag
-  import:           warnings, opt, bench-gate
-  main-is:          BenchMCMCDiag.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- MCMC bench (B7): HMC / NUTS on hierarchical normal model.
-executable bench-mcmc-b7
-  import:           warnings, opt, bench-gate
-  main-is:          BenchMCMCB7.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- HBM サンプラ性能スケーリング (NUTS vs PyMC・iter 掃き)。
-executable bench-hbm-scaling
-  import:           warnings, opt, bench-gate
-  main-is:          BenchHBMScaling.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Phase 54.4a: ハイブリッド gradADU (vec-tape ObserveLM) per-call 計測。
-executable bench-hbm-54a
-  import:           warnings, opt, bench-gate
-  main-is:          BenchHBM54a.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Phase 53: HBM 勾配ボトルネック診断 (forward vs reverse AD)。
-executable bench-hbm-profile
-  import:           warnings, opt, bench-gate
-  main-is:          BenchHBMProfile.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Phase 85.1: radon 相関モデルの gradVecIR per-eval 内訳プロファイル。
-executable bench-hbm-vecir-prof
-  import:           warnings, opt, bench-gate
-  main-is:          BenchHBMVecIRProf.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , ad                   >= 4.4  && < 4.6
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Phase 85.3a: vecIR 融合方式の feasibility spike (synthetic chain)。
-executable bench-hbm-fuse-spike
-  import:           warnings, opt, bench-gate
-  main-is:          BenchHBMFuseSpike.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Phase 85.5: M2 単独 NUTS の A/B 計測 (+RTS -s で alloc/GC 突合・rtsopts 付)。
-executable bench-m2-iso
-  import:           warnings, opt, bench-gate
-  main-is:          BenchM2Iso.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Phase 85.6a: warmup 固定費の内訳プロファイル (radon)。
-executable bench-warmup-prof
-  import:           warnings, opt, bench-gate
-  main-is:          BenchWarmupProf.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Phase 89: posteriordb 横断ベンチマーク・モデル別実行体 (1 モデル = 1 exe)。
--- hgg (dashboardFullOf の PNG 出力) を使うため plot-integration
--- flag 配下 (`cabal build --project-file=cabal.project.plot`)。
-executable posteriordb-glm-poisson
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/01-glm-poisson, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-dogs
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/02-dogs, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-garch11
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/03-garch11, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-eight-schools
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/09-eight-schools, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-gp-regr
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/07-gp-regr, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-low-dim-gauss-mix
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/04-low-dim-gauss-mix, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , containers
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-mh
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/05-mh, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-irt-2pl
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/06-irt-2pl, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-rats
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/10-rats, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-seeds
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/11-seeds, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-ark
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/12-ark, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-bym2
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/13-traffic-accident-nyc, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , containers
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-hmm
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/14-hmm-example, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-dugongs
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/15-dugongs, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-lda
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/16-lda, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-nes
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/17-nes, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-loss-curves
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/18-loss-curves, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-surgical
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/19-surgical, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-bones
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/20-bones, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-radon
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/21-radon, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
-executable posteriordb-arma
-  import:           warnings
-  main-is:          Model.hs
-  other-modules:    Common
-  hs-source-dirs:   bench/posteriordb/22-arma, bench/posteriordb
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  if flag(plot-integration)
-    build-depends:
-        base
-      , aeson
-      , text
-      , vector
-      , hanalyze
-      , hgg-core
-      , hgg-frame
-      , hgg-rasterific
-      , time
-      , deepseq
-  else
-    buildable: False
-
--- Phase 53 追加調査: NUTS コストセンタ・プロファイル用 (+RTS -p)。
-executable prof-nuts
-  import:           warnings, opt, bench-gate
-  main-is:          ProfNUTS.hs
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  ghc-options:      -rtsopts
-  build-depends:
-      base                 >= 4.14 && < 5
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , vector               >= 0.12 && < 0.14
-
--- Phase 55.3: heteroscedastic σ 式モデルの per-call 勾配 A/B (IR 吸収 vs 旧 fallback)。
--- Phase 56.6: 観測分布ごとの per-call 勾配 A/B (bench-hbm-het の一般化)。
-executable bench-hbm-dist
-  import:           warnings, opt, bench-gate
-  main-is:          BenchHBMDist.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , ad                   >= 4.4  && < 4.6
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
-executable bench-hbm-het
-  import:           warnings, opt, bench-gate
-  main-is:          BenchHBMHet.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , ad                   >= 4.4  && < 4.6
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Phase 54.11 spike: 非線形 μ の vec-tape (手組みベクトル式 IR) ゲート判定。
-executable bench-hbm-vecir
-  import:           warnings, opt, bench-gate
-  main-is:          BenchHBMVecIRSpike.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , ad                   >= 4.4  && < 4.6
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Phase 53 追加調査: AD モード比較 (forward/reverse/reverse.double/kahn)。
-executable bench-hbm-admodes
-  import:           warnings, opt, bench-gate
-  main-is:          BenchHBMADModes.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , ad                   >= 4.4  && < 4.6
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Phase 54.0 feasibility spike: 観測尤度ベクトル化 × Reverse.Double 勾配保存/per-grad 計測。
-executable bench-hbm-vecspike
-  import:           warnings, opt, bench-gate
-  main-is:          BenchHBMVecSpike.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , ad                   >= 4.4  && < 4.6
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Phase 54 専用ベクトル化 AD feasibility spike: ad vs 手書きベクトル化解析勾配。
-executable bench-hbm-vecad
-  import:           warnings, opt, bench-gate
-  main-is:          BenchHBMVecADSpike.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , ad                   >= 4.4  && < 4.6
-    , array                >= 0.5  && < 0.6
-    , backprop             >= 0.2  && < 0.3
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- B7 残: Gibbs / ADVI / WAIC.
-executable bench-mcmc-extras
-  import:           warnings, opt, bench-gate
-  main-is:          BenchMCMCExtras.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , containers           >= 0.6  && < 0.8
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- B13 Regrid: regridLong on a jagged long-form fixture.
-executable bench-regrid
-  import:           warnings, opt, bench-gate
-  main-is:          BenchRegrid.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , dataframe-operations  >= 1.1.1 && < 1.2
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- B12 Multi-output: MultiLM / MultiGP.
-executable bench-multi-output
-  import:           warnings, opt, bench-gate
-  main-is:          BenchMultiOutput.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- B10 Stat util: Bootstrap / t-test / KS / MW / BH / Halton / AUC / k-fold.
-executable bench-stat-util
-  import:           warnings, opt, bench-gate
-  main-is:          BenchStatUtil.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- B9 Optim+: Constrained / Adam / CMAESFull.
-executable bench-optim-plus
-  import:           warnings, opt, bench-gate
-  main-is:          BenchOptimPlus.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , mwc-random           >= 0.15 && < 0.16
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- B8 残: Holt-Winters / GAM / Spline 補間.
-executable bench-ts-extras
-  import:           warnings, opt, bench-gate
-  main-is:          BenchTSExtras.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
--- Regression bench (B1): LM / GLM / GLMM / Ridge / Lasso / ElasticNet.
-executable bench-regression
-  import:           warnings, opt, bench-gate
-  main-is:          BenchRegression.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
-executable bench-profile
-  import:           warnings, opt, bench-gate
-  main-is:          BenchProfile.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  ghc-options:      -rtsopts "-with-rtsopts=-T"
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , deepseq              >= 1.4  && < 1.6
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-
-executable bench-tasty
-  import:           warnings, opt, bench-gate
-  main-is:          BenchTasty.hs
-  other-modules:    BenchUtil
-  hs-source-dirs:   bench/haskell
-  default-language: GHC2021
-  build-depends:
-      base                 >= 4.14 && < 5
-    , bytestring           >= 0.11 && < 0.13
-    , cassava              >= 0.5  && < 0.6
-    , hanalyze
-    , hmatrix              >= 0.20 && < 0.22
-    , tasty-bench          >= 0.3  && < 0.5
-    , tasty                >= 1.4  && < 1.6
-    , text                 >= 1.2  && < 2.2
-    , time                 >= 1.9  && < 1.13
-    , vector               >= 0.12 && < 0.14
-
-test-suite hanalyze-test
-  import:           warnings, opt
-  type:             exitcode-stdio-1.0
-  main-is:          Spec.hs
-  hs-source-dirs:   test
-  default-language: GHC2021
-  -- WorkflowSpec の一部診断テスト (tracesOf / MultiVarModel 事後予測帯) は plot 連携層
-  -- 依存ゆえ CPP (#ifdef PLOT_INTEGRATION) で囲む。 flag on のときだけ define し compile。
-  if flag(plot-integration)
-    cpp-options: -DPLOT_INTEGRATION
+version:       0.2.0.1
+synopsis:      A general-purpose statistical analysis, optimization and visualization toolkit
+description:
+    @hanalyze@ is a self-contained Haskell toolkit for classical regression
+    (LM, GLM, GLMM, splines, kernels, GP, RFF), Bayesian modeling
+    (HBM DSL with MH, HMC, NUTS, Gibbs, ADVI), design of experiments
+    (full/fractional factorial, RSM, D-optimal, orthogonal arrays, Taguchi),
+    optimization (Nelder-Mead, L-BFGS, DE, CMA-ES, NSGA-II, Bayesian
+    optimization, augmented Lagrangian), and Vega-Lite-based visualization
+    with HTML / PNG / SVG output.
+    .
+    All algorithms are implemented natively in Haskell — no R / Stan / Python
+    bridges. Data interchange uses the @dataframe@ package as a first-class
+    citizen.
+    .
+    This is the umbrella package: it re-exports all 200 modules of the six
+    split layers (core, frame, bayes, models, design, viz) under their
+    original names, so depending on this one package is enough and downstream
+    imports never have to change. It also implements four modules of its own
+    that cut across the layers -- the quickstart front end
+    @Hanalyze@, the unified fit operator @(|->)@ in
+    @Hanalyze.Fit@, coefficient diagnostics in
+    @Hanalyze.Diagnostics@, and the plotting wrappers in
+    @Hanalyze.Model.Wrappers@.
+    .
+    The unified @hanalyze@ command-line interface (@regress@, @info@, @hist@,
+    @doe@, @taguchi@, @ridge@, @kernel@, @spline@, @multireg@, @clean@,
+    @melt@, @regrid@, ...) ships separately in the hanalyze-cli
+    package. See README.md for the layer map and a usage example.
+homepage:      https://github.com/frenzieddoll/hanalyze
+bug-reports:   https://github.com/frenzieddoll/hanalyze/issues
+license:       BSD-3-Clause
+license-file:  LICENSE
+author:        Toshiaki Honda
+maintainer:    frenzieddoll@gmail.com
+copyright:     2026 Aelysce Project (Toshiaki Honda)
+category:      Math, Statistics, Numeric, Machine Learning
+build-type:    Simple
+tested-with:   GHC == 9.6.7
+extra-doc-files:
+    CHANGELOG.md
+extra-source-files:
+    README.md
+    README.ja.md
+
+-- Phase 106.8: README / data/*.csv は repo root の workspace 資産に据え置き
+-- (package 外 path は cabal の extra-*-files に書けない)。 fork umbrella は
+-- Hackage 非公開のため sdist 完全性は upstream hanalyze (モノリス) 側で担保。
+-- Phase 109.2 (2026-07-24): demo/bench exe 112 stanza は hanalyze-demos へ
+-- 移行済 (106.8 妥協の解消)。 umbrella は library + test のみ。
+
+source-repository head
+  type:     git
+  location: https://github.com/frenzieddoll/hanalyze.git
+
+common warnings
+  ghc-options: -Wall -Wcompat -Widentities -Wredundant-constraints
+
+common opt
+  ghc-options: -O2 -funbox-strict-fields
+
+library
+  import:           warnings, opt
+  hs-source-dirs:   src
+  default-language: GHC2021
+  exposed-modules:
+    Hanalyze
+    Hanalyze.Diagnostics
+    Hanalyze.Fit
+    Hanalyze.Model.Wrappers
+  -- Phase 106: 分割 7 package (repo 直下 flat 配置) の全 module を現行名のまま再輸出。
+  -- 下流 (test / bench / demo / streaming bridge / canvas backend) の import は不変。
+  reexported-modules:
+      Hanalyze.Data.ColumnSource,
+      Hanalyze.Data.Factor,
+      Hanalyze.Data.Strings,
+      Hanalyze.Data.Transform,
+      Hanalyze.Data.Wrangle,
+      Hanalyze.DataIO.CSV,
+      Hanalyze.DataIO.Clean,
+      Hanalyze.DataIO.Convert,
+      Hanalyze.DataIO.External,
+      Hanalyze.DataIO.Health,
+      Hanalyze.DataIO.Log,
+      Hanalyze.DataIO.Preprocess,
+      Hanalyze.DataIO.Reshape,
+      Hanalyze.DataIO.Sniff,
+      Hanalyze.Design.Anova,
+      Hanalyze.Design.Block,
+      Hanalyze.Design.Constraint,
+      Hanalyze.Design.Custom.Augment,
+      Hanalyze.Design.Custom.Bayesian,
+      Hanalyze.Design.Custom.Compare,
+      Hanalyze.Design.Custom.Constraint,
+      Hanalyze.Design.Custom.Coordinate,
+      Hanalyze.Design.Custom.Factor,
+      Hanalyze.Design.Custom.Model,
+      Hanalyze.Design.Custom.Power,
+      Hanalyze.Design.Custom.RegionMoment,
+      Hanalyze.Design.Custom.SplitPlot,
+      Hanalyze.Design.Custom.Structured,
+      Hanalyze.Design.DSD,
+      Hanalyze.Design.Diagnostics,
+      Hanalyze.Design.Factorial,
+      Hanalyze.Design.GaugeRR,
+      Hanalyze.Design.Mixed,
+      Hanalyze.Design.Mixture,
+      Hanalyze.Design.MultiRSM,
+      Hanalyze.Design.Optimal,
+      Hanalyze.Design.Orthogonal,
+      Hanalyze.Design.Power,
+      Hanalyze.Design.Quality,
+      Hanalyze.Design.RSM,
+      Hanalyze.Design.Sequential,
+      Hanalyze.Design.SpaceFilling,
+      Hanalyze.Design.Taguchi,
+      Hanalyze.Design.Workflow,
+      Hanalyze.MCMC.BayesianTest,
+      Hanalyze.MCMC.Core,
+      Hanalyze.MCMC.Gibbs,
+      Hanalyze.MCMC.HMC,
+      Hanalyze.MCMC.MH,
+      Hanalyze.MCMC.NUTS,
+      Hanalyze.MCMC.Progress,
+      Hanalyze.MCMC.SMC,
+      Hanalyze.MCMC.Slice,
+      Hanalyze.Math.HSIC,
+      Hanalyze.Math.Hungarian,
+      Hanalyze.Math.ICA,
+      Hanalyze.Model.AFT,
+      Hanalyze.Model.Cluster,
+      Hanalyze.Model.CompetingRisks,
+      Hanalyze.Model.Core,
+      Hanalyze.Model.DAG,
+      Hanalyze.Model.DecisionTree,
+      Hanalyze.Model.Discriminant,
+      Hanalyze.Model.FDA,
+      Hanalyze.Model.FitYByX,
+      Hanalyze.Model.Formula,
+      Hanalyze.Model.Formula.Design,
+      Hanalyze.Model.Formula.Frame,
+      Hanalyze.Model.Formula.Mixed,
+      Hanalyze.Model.Formula.Nonlinear,
+      Hanalyze.Model.Formula.RFormula,
+      Hanalyze.Model.GAM,
+      Hanalyze.Model.GARCH,
+      Hanalyze.Model.GLM,
+      Hanalyze.Model.GLMM,
+      Hanalyze.Model.GP,
+      Hanalyze.Model.GPRobust,
+      Hanalyze.Model.GradientBoosting,
+      Hanalyze.Model.HBM,
+      Hanalyze.Model.HBM.Ast,
+      Hanalyze.Model.HBM.Distribution,
+      Hanalyze.Model.HBM.Eval,
+      Hanalyze.Model.HBM.Gradient,
+      Hanalyze.Model.HBM.IR,
+      Hanalyze.Model.HBM.Interp,
+      Hanalyze.Model.HBM.Model,
+      Hanalyze.Model.HBM.Sampling,
+      Hanalyze.Model.HBM.Track,
+      Hanalyze.Model.HBM.Util,
+      Hanalyze.Model.HBM.VecAD,
+      Hanalyze.Model.HierarchicalCluster,
+      Hanalyze.Model.KNN,
+      Hanalyze.Model.Kernel,
+      Hanalyze.Model.KernelRegression,
+      Hanalyze.Model.LM,
+      Hanalyze.Model.LM.Diagnostics,
+      Hanalyze.Model.LatentClassAnalysis,
+      Hanalyze.Model.LiNGAM.Bootstrap,
+      Hanalyze.Model.LiNGAM.Direct,
+      Hanalyze.Model.LiNGAM.ICA,
+      Hanalyze.Model.LiNGAM.MultiGroup,
+      Hanalyze.Model.LiNGAM.Pairwise,
+      Hanalyze.Model.LiNGAM.Parce,
+      Hanalyze.Model.LiNGAM.VAR,
+      Hanalyze.Model.MDS,
+      Hanalyze.Model.MultiGP,
+      Hanalyze.Model.MultiLM,
+      Hanalyze.Model.MultiOutput,
+      Hanalyze.Model.Multivariate,
+      Hanalyze.Model.NaiveBayes,
+      Hanalyze.Model.NeuralNetwork,
+      Hanalyze.Model.PCA,
+      Hanalyze.Model.PLS,
+      Hanalyze.Model.PartialDependence,
+      Hanalyze.Model.Quantile,
+      Hanalyze.Model.RFF,
+      Hanalyze.Model.RandomForest,
+      Hanalyze.Model.RandomForestClassifier,
+      Hanalyze.Model.Regularized,
+      Hanalyze.Model.RegularizedAdvanced,
+      Hanalyze.Model.Reliability,
+      Hanalyze.Model.ReliabilityBlockDiagram,
+      Hanalyze.Model.Robust,
+      Hanalyze.Model.SVM,
+      Hanalyze.Model.Spline,
+      Hanalyze.Model.StateSpace,
+      Hanalyze.Model.Survival,
+      Hanalyze.Model.TimeSeries,
+      Hanalyze.Model.VAR,
+      Hanalyze.Model.Weibull,
+      Hanalyze.Optim.Acquisition,
+      Hanalyze.Optim.Adam,
+      Hanalyze.Optim.BayesOpt,
+      Hanalyze.Optim.CMAES,
+      Hanalyze.Optim.CMAESFull,
+      Hanalyze.Optim.Common,
+      Hanalyze.Optim.Constrained,
+      Hanalyze.Optim.Desirability,
+      Hanalyze.Optim.DifferentialEvolution,
+      Hanalyze.Optim.GradAscent,
+      Hanalyze.Optim.LBFGS,
+      Hanalyze.Optim.LineSearch,
+      Hanalyze.Optim.NSGA,
+      Hanalyze.Optim.NelderMead,
+      Hanalyze.Optim.Numeric,
+      Hanalyze.Optim.Pareto,
+      Hanalyze.Optim.ParticleSwarm,
+      Hanalyze.Optim.SimulatedAnnealing,
+      Hanalyze.Stat.AD,
+      Hanalyze.Stat.AdaptiveGrid,
+      Hanalyze.Stat.BayesFactor,
+      Hanalyze.Stat.BayesianModelAveraging,
+      Hanalyze.Stat.Bootstrap,
+      Hanalyze.Stat.BridgeSampling,
+      Hanalyze.Stat.CV,
+      Hanalyze.Stat.Causal.CATE,
+      Hanalyze.Stat.Causal.DoublyRobust,
+      Hanalyze.Stat.Causal.IPW,
+      Hanalyze.Stat.Causal.PropensityScore,
+      Hanalyze.Stat.Cholesky,
+      Hanalyze.Stat.ClassMetrics,
+      Hanalyze.Stat.CorrelationNetwork,
+      Hanalyze.Stat.Descriptive,
+      Hanalyze.Stat.Distribution,
+      Hanalyze.Stat.Effect,
+      Hanalyze.Stat.GroupComparison,
+      Hanalyze.Stat.Interpolate,
+      Hanalyze.Stat.Interpret,
+      Hanalyze.Stat.KernelDist,
+      Hanalyze.Stat.MCMC,
+      Hanalyze.Stat.MDS,
+      Hanalyze.Stat.ModelSelect,
+      Hanalyze.Stat.MultipleTesting,
+      Hanalyze.Stat.NumberFormat,
+      Hanalyze.Stat.PosteriorPredictive,
+      Hanalyze.Stat.QuasiRandom,
+      Hanalyze.Stat.SPC,
+      Hanalyze.Stat.Standardize,
+      Hanalyze.Stat.Summary,
+      Hanalyze.Stat.Test,
+      Hanalyze.Stat.VI,
+      Hanalyze.Viz.AnalysisReport,
+      Hanalyze.Viz.Assets,
+      Hanalyze.Viz.Bar,
+      Hanalyze.Viz.Core,
+      Hanalyze.Viz.GP,
+      Hanalyze.Viz.GPReport,
+      Hanalyze.Viz.Histogram,
+      Hanalyze.Viz.MCMC,
+      Hanalyze.Viz.ModelGraph,
+      Hanalyze.Viz.ModelGraphDot,
+      Hanalyze.Viz.Pareto,
+      Hanalyze.Viz.PlotConfig,
+      Hanalyze.Viz.PlotData,
+      Hanalyze.Viz.PlotData.DataFrame,
+      Hanalyze.Viz.Report,
+      Hanalyze.Viz.ReportBuilder,
+      Hanalyze.Viz.ReportInstances,
+      Hanalyze.Viz.Scatter,
+      Hanalyze.Viz.Taguchi
+  build-depends:
+      base                 >= 4.14 && < 5
+    , containers           >= 0.6  && < 0.8
+    , hmatrix              >= 0.20 && < 0.22
+    , mwc-random           >= 0.15 && < 0.16
+    , statistics           >= 0.16 && < 0.17
+    , text                 >= 1.2  && < 2.2
+    , vector               >= 0.12 && < 0.14
+    , dataframe-core        ^>= 1.1
+    -- Phase 106: 分割 package (層構成 = specification/phases/phase-106-package-split.md)
+    , hanalyze-core   == 0.2.0.1
+    , hanalyze-frame  == 0.2.0.1
+    , hanalyze-bayes  == 0.2.0.1
+    , hanalyze-models == 0.2.0.1
+    , hanalyze-design == 0.2.0.1
+    , hanalyze-viz    == 0.2.0.1
+
+test-suite hanalyze-test
+  import:           warnings, opt
+  type:             exitcode-stdio-1.0
+  main-is:          Spec.hs
+  hs-source-dirs:   test
+  default-language: GHC2021
+  -- WorkflowSpec の plot 連携診断テスト (tracesOf / MultiVarModel 事後予測帯) は
+  -- Phase 106.4 で hanalyze-plot-test (hanalyze-plot) へ移行済。
   build-depends:
       base       >= 4.14 && < 5
     , hanalyze
diff --git a/src/Hanalyze.hs b/src/Hanalyze.hs
--- a/src/Hanalyze.hs
+++ b/src/Hanalyze.hs
@@ -4,17 +4,35 @@
 -- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
 -- License     : BSD-3-Clause
 --
--- Hanalyze の quickstart 出入口 (umbrella module)。
+-- [日本語]: Hanalyze の quickstart 出入口 (umbrella module)。
 --
 -- 最初に触れる中核 (モデル fit・基本統計・可視化・CSV I/O) を 1 つの
 -- @import Hanalyze@ で揃える窓口。 個別機能は各サブモジュール
 -- (@Hanalyze.Model.*@ / @Hanalyze.Stat.*@ / @Hanalyze.Viz.*@) を直接 import する。
 --
--- 方針 (Phase 46 / plot Phase 15 §A5):
---   * ここは **plot 非依存・portable** (re-export のみ、 flag 不要)。
---     plot 連携 (@toPlot@ / @Plottable@) は @flag plot-integration@ 配下の
---     @Hanalyze.Plot@ に分離してあり、 本 umbrella には含めない。
+-- 方針:
+--   * ここは __plot 非依存・portable__ (re-export のみ、 別パッケージ不要)。
+--     plot 連携 (@toPlot@ / @Plottable@) は別パッケージ @hanalyze-plot@
+--     の @Hanalyze.Plot@ (@cabal build --project-file=cabal.project.plot@
+--     で build) に分離してあり、 本 umbrella には含めない。
 --   * Formula DSL は本 Phase 対象外 (ロードマップ B 段)。
+--
+-- [English]: Quickstart entry point for Hanalyze (umbrella module).
+--
+-- Gathers the core pieces you'll touch first (model fitting, basic
+-- statistics, visualization, CSV I\/O) behind a single @import
+-- Hanalyze@. Individual features can be imported directly from their
+-- submodules (@Hanalyze.Model.*@ \/ @Hanalyze.Stat.*@ \/
+-- @Hanalyze.Viz.*@).
+--
+-- Policy:
+--   * This module is __plot-independent and portable__ (re-exports only, no
+--     separate package required). Plot integration (@toPlot@ \/ @Plottable@)
+--     is kept separate in the @hanalyze-plot@ package's
+--     @Hanalyze.Plot@ (built via @cabal build
+--     --project-file=cabal.project.plot@) and is not included in this
+--     umbrella.
+--   * The Formula DSL is out of scope for this Phase (roadmap stage B).
 module Hanalyze
   ( -- * モデル fit の共有核 / 能力別 protocol
     module Hanalyze.Model.Core
diff --git a/src/Hanalyze/Data/ColumnSource.hs b/src/Hanalyze/Data/ColumnSource.hs
deleted file mode 100644
--- a/src/Hanalyze/Data/ColumnSource.hs
+++ /dev/null
@@ -1,77 +0,0 @@
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Data.ColumnSource
--- Description : 列名 → 数値列を引ける「データ源」の最小抽象型クラス (plot 非依存)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- 列名 → 数値列 を引ける「データ源」 の最小抽象。
---
--- モデル学習の入口 (Phase 51 の @df |-> spec@) を、 データ表現
--- (@[(Text,[Double])]@ / @Map Text [Double]@ / Hackage @DataFrame@ /
--- plot @ColData@) から疎結合にするための型クラス。 数値列の取得と
--- 列名列挙の 2 メソッドのみを持ち、 factor/NA の解釈は上位
--- (Phase 47 formula 経路) に委ねる。
---
--- このモジュールは **plot 非依存 (portable)**。 plot 専用の
--- @[(Text, ColData)]@ instance は flag @plot-integration@ 配下
--- (@Hanalyze.Plot@) に隔離する。
-module Hanalyze.Data.ColumnSource
-  ( ColumnSource (..)
-  ) where
-
-import           Data.Map.Strict (Map)
-import qualified Data.Map.Strict as Map
-import           Data.Text       (Text)
-import qualified Data.Vector     as V
-import qualified DataFrame.Internal.Column    as DX
-import qualified DataFrame.Internal.DataFrame  as DX
-
-import           Hanalyze.DataIO.Convert (getDoubleVec)
-
--- ===========================================================================
--- 型クラス
--- ===========================================================================
-
--- | 列名で数値列を引けるデータ源。
---
--- * 'lookupCol' は **数値列**のみを返す (factor 列は formula 経路が
---   contrast 展開するため、 ここでは数値列の素取得に限る)。
--- * 'columnNames' は欠落検出 (要求列が無い) のための全列名列挙。
-class ColumnSource d where
-  -- | 列名 → 数値列 (無ければ 'Nothing')。
-  lookupCol   :: Text -> d -> Maybe [Double]
-  -- | 全列名。
-  columnNames :: d -> [Text]
-  -- | データ源全体を Hackage @DataFrame@ に変換 (formula 経路 = Phase 47 の
-  --   @MissingPolicy@\/contrast\/応答列判定で ModelFrame に変換するため)。
-  --
-  --   既定は **数値列のみから再構築** (assoc\/Map など数値源で正しい)。
-  --   'DX.DataFrame' instance は 'id' で上書きし factor\/NA を温存する
-  --   (formula 多変量の canonical 経路)。
-  toFrame :: d -> DX.DataFrame
-  toFrame d = DX.fromNamedColumns
-    [ (n, DX.fromList vs)
-    | n <- columnNames d, Just vs <- [lookupCol n d] ]
-
--- ===========================================================================
--- core instance (portable)
--- ===========================================================================
-
--- | HBM の既存入力 (列名 assoc) と同型。
-instance ColumnSource [(Text, [Double])] where
-  lookupCol n = lookup n
-  columnNames = map fst
-
--- | 'Map' 版。
-instance ColumnSource (Map Text [Double]) where
-  lookupCol   = Map.lookup
-  columnNames = Map.keys
-
--- | Hackage @dataframe@ (analyze formula 経路と同じ df)。
---   数値変換は 'getDoubleVec' に委譲 (formula 経路と同じ判定)。
-instance ColumnSource DX.DataFrame where
-  lookupCol n df = V.toList <$> getDoubleVec n df
-  columnNames    = DX.columnNames
-  toFrame        = id   -- factor/NA を温存 (formula 経路の canonical)
diff --git a/src/Hanalyze/Data/Factor.hs b/src/Hanalyze/Data/Factor.hs
deleted file mode 100644
--- a/src/Hanalyze/Data/Factor.hs
+++ /dev/null
@@ -1,346 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
--- |
--- Module      : Hanalyze.Data.Factor
--- Description : forcats 流の因子 (factor) 型と水準操作 (fct_* 相当)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- forcats 流の因子 (factor) 型操作 (Phase 28 Ch16 "Factors")。
---
--- R の `factor` を **水準 (levels) の順序付きリスト + 各観測の水準コード**として
--- 表す 'Factor' 型と、 forcats の `fct_*` 相当を純粋関数として公開する
--- ('Data.Strings' / 'Data.Transform' と同列の `Data/` 純粋抽象)。
---
--- === なぜ専用型か
--- ただの @[Text]@ と違い factor は (1) 水準の**意味的順序** (アルファベット順とは別)、
--- (2) データに現れない水準の保持、 (3) 整数コード化、 を持つ。 forcats の `fct_*` は
--- この水準順序や中身を操作する関数群で、 順序概念のない @[Text]@ には無い。
---
--- === HBM の Column.Factor との違い
--- 'Hanalyze.Model.HBM' の内部 @Column = Numeric | Factor@ は NUTS に渡す
--- 観測列の内部表現。 本 'Factor' は **データ整形ドメイン**の公開型で責務が別 (独立実装)。
---
--- === コード規約
--- 'facCodes' は **0 始まり** (@facLevels !! code@ で復元)。 欠損 (R の @\<NA\>@) は
--- コード @-1@ で表す ('naCode')。
-module Hanalyze.Data.Factor
-  ( -- * 型
-    Factor (..)
-  , naCode
-    -- * 生成 (factor / fct / ordered)
-  , factor
-  , factorWith
-  , fct
-  , ordered
-    -- * 参照 (levels / as.character / count)
-  , levels
-  , isOrdered
-  , asTexts
-  , asTextsMaybe
-  , fctCount
-    -- * 順序操作 (16.4 forcats fct_reorder 系)
-  , fctReorder
-  , fctRelevel
-  , fctReorder2
-  , fctInfreq
-  , fctRev
-    -- * 水準操作 (16.5 forcats fct_recode / fct_collapse / fct_lump 系)
-  , fctRecode
-  , fctCollapse
-  , fctLumpN
-  , fctLumpMin
-  , fctLumpProp
-  , fctLumpLowfreq
-  ) where
-
-import           Data.List (foldl', sort, sortBy, elemIndex)
-import           Data.Maybe (mapMaybe)
-import           Data.Ord (comparing, Down (..))
-import qualified Data.Map.Strict as M
-import           Data.Text (Text)
-import qualified Data.Vector as V
-import qualified Data.Vector.Unboxed as VU
-
--- === 型 =====================================================================
-
--- | 因子。 水準ラベル + 各観測の水準コード (0 始まり・NA は 'naCode')。
-data Factor = Factor
-  { facLevels  :: [Text]        -- ^ 水準ラベル (定義順)
-  , facCodes   :: VU.Vector Int -- ^ 各観測の水準コード (0 始まり・NA = 'naCode')
-  , facOrdered :: Bool          -- ^ 順序付き因子か (R `ordered()`)
-  } deriving (Eq, Show)
-
--- | 欠損コード (R の @NA_integer_@ 相当)。
-naCode :: Int
-naCode = -1
-
--- === 生成 ===================================================================
-
--- | @factor xs@ : R の `factor()` 既定。 水準 = 値の **ソート済 unique**。
-factor :: [Text] -> Factor
-factor xs = factorWith (sortUnique xs) xs
-
--- | @factorWith lvls xs@ : 水準を明示。 @lvls@ に無い値は NA ('naCode')。
-factorWith :: [Text] -> [Text] -> Factor
-factorWith lvls xs = Factor lvls (VU.fromList (map enc xs)) False
-  where
-    idx   = M.fromList (zip lvls [0 ..])
-    enc x = M.findWithDefault naCode x idx
-
--- | @fct xs@ : forcats の `fct()`。 水準 = 値の **出現順 unique** (factor() と違い sort しない)。
-fct :: [Text] -> Factor
-fct xs = factorWith (nubKeepOrder xs) xs
-
--- | @ordered lvls xs@ : 順序付き因子 (R `ordered()`)。 水準間に @<@ 順序を持つ。
-ordered :: [Text] -> [Text] -> Factor
-ordered lvls xs = (factorWith lvls xs) { facOrdered = True }
-
--- === 参照 ===================================================================
-
--- | 水準ラベル (定義順)。
-levels :: Factor -> [Text]
-levels = facLevels
-
--- | 順序付き因子か。
-isOrdered :: Factor -> Bool
-isOrdered = facOrdered
-
--- | @as.character()@ 相当。 各観測をラベルへ。 NA は @""@。
-asTexts :: Factor -> [Text]
-asTexts f = map (maybe "" id) (asTextsMaybe f)
-
--- | NA を 'Nothing' で残す版。
-asTextsMaybe :: Factor -> [Maybe Text]
-asTextsMaybe f = map lab (VU.toList (facCodes f))
-  where
-    v = V.fromList (facLevels f)
-    lab c | c < 0 || c >= V.length v = Nothing
-          | otherwise                = Just (v V.! c)
-
--- | dplyr `count()` 相当。 **水準順**に (ラベル, 出現数)。 0 件水準も含む。 NA は除外。
-fctCount :: Factor -> [(Text, Int)]
-fctCount f = [ (lv, M.findWithDefault 0 i cmap) | (lv, i) <- zip (facLevels f) [0 ..] ]
-  where
-    cmap = foldl' (\m c -> if c < 0 then m else M.insertWith (+) c 1 m)
-                  M.empty (VU.toList (facCodes f))
-
--- === 順序操作 (16.4) ========================================================
---
--- いずれも **facLevels の順序を入れ替え、 facCodes を旧 code → 新 code に
--- 再マッピング**して実装する ('applyLevelOrder')。 観測の中身 (どの水準か) は
--- 不変で、 水準の**並び順**だけが変わる (forcats `fct_reorder` 系と同義)。
-
--- | forcats `fct_reorder(f, x, .fun)`。 各水準に属する @x@ 値へ集約関数 @fun@
--- (R4DS の既定は @median@) を適用し、 その値の**昇順**に水準を並べ替える。
--- @x@ 中の @NaN@ は NA とみなし集約前に除去。 値が無い水準は末尾。
-fctReorder :: ([Double] -> Double) -> Factor -> [Double] -> Factor
-fctReorder fun f xs = applyLevelOrder order f
-  where
-    n     = length (facLevels f)
-    grp   = groupByCode n (facCodes f) xs
-    key i = case filter (not . isNaN) (grp V.! i) of
-              [] -> 1 / 0          -- 値なし → +Inf で末尾へ
-              vs -> fun vs
-    order = sortBy (comparing key) [0 .. n - 1]
-
--- | forcats `fct_relevel(f, ...)`。 指定した水準を (指定順で) **先頭**へ移し、
--- 残りは元の相対順序を保つ。 存在しない水準名は無視する。
-fctRelevel :: [Text] -> Factor -> Factor
-fctRelevel front f = applyLevelOrder order f
-  where
-    lvs      = facLevels f
-    frontIx  = mapMaybe (`elemIndex` lvs) front
-    rest     = [ i | i <- [0 .. length lvs - 1], i `notElem` frontIx ]
-    order    = frontIx ++ rest
-
--- | forcats `fct_reorder2(f, x, y)` (既定 @last2@)。 各水準で **最大の @x@ に
--- 対応する @y@** を取り、 その値の**降順**に水準を並べ替える (凡例順を線の
--- 右端の高さに合わせる用途)。 @x@ 同値は後着 (入力順で後ろ) を採用。
-fctReorder2 :: Factor -> [Double] -> [Double] -> Factor
-fctReorder2 f xs ys = applyLevelOrder order f
-  where
-    n     = length (facLevels f)
-    grp   = groupPairs n (facCodes f) xs ys
-    key i = case grp V.! i of
-              [] -> -1 / 0         -- 値なし → -Inf で降順末尾へ
-              ps -> snd (foldl1 (\a b -> if fst b >= fst a then b else a) ps)
-    order = sortBy (comparing (Down . key)) [0 .. n - 1]
-
--- | forcats `fct_infreq(f)`。 出現**頻度の降順**に水準を並べ替える。
--- 同頻度は元の水準順を保つ (安定)。 NA は計数対象外。
-fctInfreq :: Factor -> Factor
-fctInfreq f = applyLevelOrder order f
-  where
-    n      = length (facLevels f)
-    counts = [ M.findWithDefault 0 i cmap | i <- [0 .. n - 1] ] :: [Int]
-    cmap   = foldl' (\m c -> if c < 0 then m else M.insertWith (+) c 1 m)
-                    M.empty (VU.toList (facCodes f))
-    order  = sortBy (comparing (Down . (countArr V.!))) [0 .. n - 1]
-    countArr = V.fromList counts
-
--- | forcats `fct_rev(f)`。 水準を**逆順**にする。
-fctRev :: Factor -> Factor
-fctRev f = applyLevelOrder (reverse [0 .. length (facLevels f) - 1]) f
-
--- === 水準操作 (16.5) ========================================================
---
--- 水準**ラベルそのもの**を付け替える/併合する操作。 ラベルを付け替えた結果
--- 同名になった水準は 1 つに畳む ('relabelMerge')。 lump 系は余りを @"Other"@ に
--- まとめ、 forcats と同じく @"Other"@ を**末尾水準**に置く。
-
--- | 余り水準のまとめ先ラベル (forcats `other_level` 既定)。
-otherLevel :: Text
-otherLevel = "Other"
-
--- | forcats `fct_recode(f, new = "old", ...)`。 水準ラベルを改名する。
--- 引数は @(新, 旧)@ の対。 複数の旧を同じ新に向ければ**併合**される。
--- 言及されない水準はそのまま。 水準の相対順序は保つ。
-fctRecode :: [(Text, Text)] -> Factor -> Factor
-fctRecode pairs f = relabelMerge newLabels f
-  where
-    m         = M.fromList [ (old, new) | (new, old) <- pairs ]
-    newLabels = [ M.findWithDefault lv lv m | lv <- facLevels f ]
-
--- | forcats `fct_collapse(f, new = c("o1","o2"), ...)`。 複数水準を 1 つへ併合。
--- 引数は @(新, [旧...])@。 言及されない水準はそのまま (`other_level` は非対応)。
-fctCollapse :: [(Text, [Text])] -> Factor -> Factor
-fctCollapse groups f = relabelMerge newLabels f
-  where
-    m         = M.fromList [ (old, new) | (new, olds) <- groups, old <- olds ]
-    newLabels = [ M.findWithDefault lv lv m | lv <- facLevels f ]
-
--- | forcats `fct_lump_n(f, n)`。 頻度上位 @n@ 水準を残し、 他を @"Other"@ へ。
--- @n@ 負なら頻度**下位** @|n|@ を残す。 同頻度のタイは両方残す (上位 n 側)。
-fctLumpN :: Int -> Factor -> Factor
-fctLumpN n f
-  | m == 0    = f
-  | n == 0    = lumpBy (const True) f
-  | n > 0     = lumpBy (\i -> cnts !! i < thrTop) f
-  | otherwise = lumpBy (\i -> cnts !! i > thrBot) f
-  where
-    cnts   = levelCounts f
-    m      = length cnts
-    thrTop | n >= m    = minimum cnts                      -- 全水準保持
-           | otherwise = sortBy (comparing Down) cnts !! (n - 1)
-    k      = negate n
-    thrBot | k >= m    = maximum cnts                      -- 全水準保持
-           | otherwise = sort cnts !! (k - 1)
-
--- | forcats `fct_lump_min(f, min)`。 出現回数 @< min@ の水準を @"Other"@ へ (strict)。
-fctLumpMin :: Int -> Factor -> Factor
-fctLumpMin k f = lumpBy (\i -> cnts !! i < k) f
-  where cnts = levelCounts f
-
--- | forcats `fct_lump_prop(f, prop)`。 出現割合 @< prop@ の水準を @"Other"@ へ。
-fctLumpProp :: Double -> Factor -> Factor
-fctLumpProp p f = lumpBy (\i -> fromIntegral (cnts !! i) < p * fromIntegral total) f
-  where
-    cnts  = levelCounts f
-    total = sum cnts
-
--- | forcats `fct_lump_lowfreq(f)`。 低頻度水準を、 @"Other"@ が**最小水準のまま**で
--- いられる範囲で併合する。 頻度降順に並べ、 ある水準が「それより低頻度な水準の
--- 合計」 を上回った時点で、 以降を @"Other"@ へまとめる (forcats `lump_cutoff` 同型)。
-fctLumpLowfreq :: Factor -> Factor
-fctLumpLowfreq f
-  | m == 0    = f
-  | otherwise = lumpBy (\i -> (cnts !! i, i) `elem` lumpedKeys) f
-  where
-    cnts       = levelCounts f
-    m          = length cnts
-    -- (頻度, 水準index) を頻度降順 (タイは index 昇順) に
-    descKeys   = sortBy (comparing (\(c, i) -> (Down c, i))) (zip cnts [0 ..])
-    cut        = cutoff (map fst descKeys)          -- 保持する個数 (cut..末尾を lump)
-    lumpedKeys = drop cut descKeys
-
--- | forcats `lump_cutoff`: 降順 count 列で、 「自分 > 残り (より低頻度) の合計」 と
--- なる最初の位置 @i@ を見つけ、 @i+1@ (= 0 始まりで @i@ 番目まで保持) を返す。
--- 該当無しなら全保持 (= 長さ)。
-cutoff :: [Int] -> Int
-cutoff xs = go 0 (sum xs) xs
-  where
-    go i _    []       = i
-    go i left (c : cs)
-      | c > left - c   = i + 1
-      | otherwise      = go (i + 1) (left - c) cs
-
--- === 内部 helper ============================================================
-
--- | 水準ラベルを @newLabels@ (旧水準 index 順・長さ = 旧水準数) に付け替え、
--- 同名になった水準を 1 つに畳む。 新水準順は新ラベルの**初出順**。 NA は不変。
-relabelMerge :: [Text] -> Factor -> Factor
-relabelMerge newLabels f =
-  Factor newLevels (VU.map remap (facCodes f)) (facOrdered f)
-  where
-    newLevels = nubKeepOrder newLabels
-    pos       = M.fromList (zip newLevels [0 ..])
-    o2nVec    = V.fromList [ pos M.! lab | lab <- newLabels ]
-    remap c | c < 0 || c >= V.length o2nVec = c
-            | otherwise                     = o2nVec V.! c
-
--- | 指定水準を**末尾**へ移す (存在しなければ不変)。 'applyLevelOrder' を再利用。
-moveLevelToEnd :: Text -> Factor -> Factor
-moveLevelToEnd lv f = case elemIndex lv (facLevels f) of
-  Nothing -> f
-  Just i  -> applyLevelOrder ([ j | j <- [0 .. n - 1], j /= i ] ++ [i]) f
-  where n = length (facLevels f)
-
--- | 水準 index で lump 判定し、 余りを 'otherLevel' へ併合 (末尾に置く)。
--- lump 対象が無ければ不変。
-lumpBy :: (Int -> Bool) -> Factor -> Factor
-lumpBy isLump f
-  | not (or lumpFlags) = f
-  | otherwise          = moveLevelToEnd otherLevel (relabelMerge newLabels f)
-  where
-    lumpFlags = map isLump [0 .. length (facLevels f) - 1]
-    newLabels = [ if isLump i then otherLevel else lv
-                | (i, lv) <- zip [0 ..] (facLevels f) ]
-
--- | 各水準 (定義順) の出現回数。 NA は除外。
-levelCounts :: Factor -> [Int]
-levelCounts f = [ M.findWithDefault 0 i cmap | i <- [0 .. length (facLevels f) - 1] ]
-  where
-    cmap = foldl' (\m c -> if c < 0 then m else M.insertWith (+) c 1 m)
-                  M.empty (VU.toList (facCodes f))
-
--- | @applyLevelOrder order f@ : @order@ は新しい並びを表す**旧 index のリスト**
--- (@order !! p@ = 新位置 @p@ に来る旧水準 index)。 facLevels を並べ替え、
--- facCodes を旧 code → 新 code に再マップする。 NA ('naCode') はそのまま。
-applyLevelOrder :: [Int] -> Factor -> Factor
-applyLevelOrder order f = f
-  { facLevels = map (lvs V.!) order
-  , facCodes  = VU.map remap (facCodes f)
-  }
-  where
-    lvs   = V.fromList (facLevels f)
-    n     = length (facLevels f)
-    -- 旧 index → 新 index
-    o2n   = VU.replicate n (-1) VU.// [ (old, new) | (new, old) <- zip [0 ..] order ]
-    remap c | c < 0 || c >= n = c
-            | otherwise       = o2n VU.! c
-
--- | 水準コードで @x@ 値をグループ化 (長さ n・入力順保持・NA/範囲外は除外)。
-groupByCode :: Int -> VU.Vector Int -> [Double] -> V.Vector [Double]
-groupByCode n codes xs =
-  V.map reverse $ V.accum (flip (:)) (V.replicate n [])
-    [ (c, x) | (c, x) <- zip (VU.toList codes) xs, c >= 0, c < n ]
-
--- | 水準コードで @(x, y)@ 対をグループ化 (長さ n・入力順保持・NA/範囲外は除外)。
-groupPairs :: Int -> VU.Vector Int -> [Double] -> [Double] -> V.Vector [(Double, Double)]
-groupPairs n codes xs ys =
-  V.map reverse $ V.accum (flip (:)) (V.replicate n [])
-    [ (c, (x, y)) | (c, x, y) <- zip3 (VU.toList codes) xs ys, c >= 0, c < n ]
-
--- | ソート済 unique (Map のキー集合 = 昇順 unique)。
-sortUnique :: [Text] -> [Text]
-sortUnique = M.keys . M.fromList . map (\x -> (x, ()))
-
--- | 出現順を保った unique。
-nubKeepOrder :: [Text] -> [Text]
-nubKeepOrder = go M.empty
-  where
-    go _ [] = []
-    go seen (x : xs)
-      | x `M.member` seen = go seen xs
-      | otherwise         = x : go (M.insert x () seen) xs
diff --git a/src/Hanalyze/Data/Strings.hs b/src/Hanalyze/Data/Strings.hs
deleted file mode 100644
--- a/src/Hanalyze/Data/Strings.hs
+++ /dev/null
@@ -1,582 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
--- |
--- Module      : Hanalyze.Data.Strings
--- Description : stringr 流の Text 純粋操作 (str_* 相当・行/列展開含む)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- stringr 流の文字列操作 (Phase 28 Ch14 "Strings")。
---
--- R4DS Ch14 で扱う `str_*` 関数を **純粋な `Text` 操作**として公開する
--- ('Data.Transform' と同列の `Data/` 純粋抽象)。 DataFrame 行/列展開を伴う
--- `separate_*` は別途 (本モジュール下部・要 DataFrame)。
---
--- === recycling / NA
--- `str_c` 相当は tidyverse の **recycling 規則** (長さ 1 か n) に従う。 NA 伝播は
--- 'Maybe' 版 ('strCMaybe') で表す (R の `NA` は `Nothing`)。
---
--- === locale
--- 'strToUpper' / 'strSort' は **既定 locale** (Unicode コードポイント順・en 相当)。
--- R4DS §14.6.3 の locale 依存 (Czech の "ch"・Turkish の dotless i 等) は ICU が要るため
--- 本モジュールでは扱わず、 tutorial 側で「概念のみ」 honest に注記する。
-module Hanalyze.Data.Strings
-  ( -- * 長さ / 部分取り出し (str_length / str_sub)
-    strLength
-  , strSub
-    -- * 連結 (str_c / str_flatten / str_glue)
-  , strC
-  , strCMaybe
-  , strFlatten
-  , strGlue
-    -- * 大文字化 / ソート (str_to_upper / str_sort)
-  , strToUpper
-  , strSort
-    -- * 文字比較 / encoding (str_equal / charToRaw・§14.6)
-  , strEqual
-  , charToRaw
-    -- * 行展開 (separate_longer・DataFrame)
-  , separateLongerDelim
-  , separateLongerPosition
-    -- * 列分割 (separate_wider・DataFrame)
-  , TooFew (..)
-  , TooMany (..)
-  , separateWiderDelim
-  , separateWiderDelimWith
-  , separateWiderPosition
-  , separateWiderPositionWith
-    -- * 正規表現 (§15 Regular expressions・regex-tdfa)
-  , strDetect
-  , strDetectWith
-  , strCount
-  , strSubset
-  , strWhich
-  , strExtract
-  , strExtractAll
-  , strMatch
-  , strReplace
-  , strReplaceAll
-  , strRemove
-  , strRemoveAll
-  , strSplit
-  , strLocate
-  , strEscape
-  , separateWiderRegex
-  ) where
-
-import           Data.Array  (elems)
-import           Data.List   (sort, transpose)
-import           Data.Maybe  (isJust)
-import           Data.Word   (Word8)
-import qualified Data.ByteString as BS
-import           Data.Text   (Text)
-import qualified Data.Text   as T
-import qualified Data.Text.Encoding as TE
-import           Data.Text.Normalize (NormalizationMode (NFC), normalize)
-import qualified Data.Vector as V
-import qualified Data.Vector.Unboxed as VU
-import qualified DataFrame.Internal.Column    as DF
-import qualified DataFrame.Internal.DataFrame  as DF
-import qualified DataFrame.Operations.Subset   as DF
-import qualified DataFrame.Internal.Column  as DFC
-import qualified DataFrame.Operations.Subset as DFS (rowsAtIndices)
-import           Text.Regex.TDFA            (Regex, CompOption (..), ExecOption (..))
-import qualified Text.Regex.TDFA            as RE
-
-import           Hanalyze.DataIO.Convert (getMaybeTextVec)
-
--- ===========================================================================
--- 長さ / 部分取り出し
--- ===========================================================================
-
--- | 文字数 (= stringr @str_length@・@T.length@)。 コードポイント単位。
-strLength :: Text -> Int
-strLength = T.length
-
--- | 部分文字列 (= @str_sub(string, start, end)@)。 **1 始まり・両端含む**。
---   負の index は末尾から (@-1@ = 最終文字)。 範囲外は内側にクリップ。
---   例: @strSub 1 3 "Apple" == "App"@・@strSub (-3) (-1) "Apple" == "ple"@。
-strSub :: Int -> Int -> Text -> Text
-strSub start end t =
-  let n = T.length t
-      norm i | i < 0     = n + i + 1   -- -1 → n (1 始まり)
-             | otherwise = i
-      s = max 1 (norm start)
-      e = min n (norm end)
-  in if s > e then "" else T.take (e - s + 1) (T.drop (s - 1) t)
-
--- ===========================================================================
--- 連結
--- ===========================================================================
-
--- | ベクトル連結 (= @str_c(...)@)。 各列を **recycling 規則** (長さ 1 か n) で
---   揃え、 行ごとに連結する。 リテラルは長さ 1 の列 (@["x"]@) として渡す。
---   例: @strC [["Hello "], names, ["!"]]@。 NA 伝播版は 'strCMaybe'。
-strC :: [[Text]] -> [Text]
-strC [] = []
-strC cols = map T.concat (recycleCols cols)
-
--- | 'strC' の NA 伝播版 (= @str_c@ の R 既定)。 行内に 'Nothing' があれば結果も
---   'Nothing' (R の `NA` 伝播)。 リテラルは @[Just "x"]@。
-strCMaybe :: [[Maybe Text]] -> [Maybe Text]
-strCMaybe [] = []
-strCMaybe cols =
-  [ if any (== Nothing) row then Nothing else Just (T.concat [x | Just x <- row])
-  | row <- recycleCols cols ]
-
--- | 文字ベクトル→単一文字列 (= @str_flatten(x, collapse)@)。 @T.intercalate@。
---   例: @strFlatten ", " ["a","b","c"] == "a, b, c"@。
-strFlatten :: Text -> [Text] -> Text
-strFlatten = T.intercalate
-
--- | テンプレート補間 (= @str_glue@)。 @"{key}"@ を @env@ の列で置換 (recycling)。
---   例: @strGlue "Hello {name}!" [("name", names)]@。 未知 key は error。
-strGlue :: Text -> [(Text, [Text])] -> [Text]
-strGlue tmpl env =
-  let n    = maximum (1 : map (length . snd) env)
-      segs = parseGlue tmpl
-      val key i = case lookup key env of
-        Just col
-          | length col == 1 -> head col
-          | i < length col  -> col !! i
-          | otherwise       -> error "strGlue: 列長が不揃い (recycling 不能)"
-        Nothing -> error ("strGlue: 未知の {" ++ T.unpack key ++ "}")
-  in [ T.concat [ either id (\k -> val k i) s | s <- segs ] | i <- [0 .. n - 1] ]
-
--- ===========================================================================
--- 大文字化 / ソート
--- ===========================================================================
-
--- | 大文字化 (= @str_to_upper@・既定 locale)。 @T.toUpper@。
-strToUpper :: Text -> Text
-strToUpper = T.toUpper
-
--- | 昇順ソート (= @str_sort@・既定 locale = Unicode コードポイント順)。
-strSort :: [Text] -> [Text]
-strSort = sort
-
--- ===========================================================================
--- 文字比較 / encoding (§14.6 Non-English Text)
--- ===========================================================================
-
--- | 見た目が同じ文字の等価判定 (= @str_equal@・§14.6.2)。 アクセント付き文字は
---   合成済 (@"\xfc"@ = ü) と 基底+結合 (@"u\x308"@) で**符号列が違っても見た目は同じ**。
---   両者を **NFC 正規化**してから比較するため等価になる
---   (例: @strEqual "\xfc" "u\x308" == True@・素の @==@ では False)。
-strEqual :: Text -> Text -> Bool
-strEqual a b = normalize NFC a == normalize NFC b
-
--- | 文字列の **UTF-8 バイト列** (= R @charToRaw@・§14.6.1)。 各バイトを 'Word8' で
---   返す (R は 16 進表示)。 例: @charToRaw "Hadley" == [0x48,0x61,0x64,0x6c,0x65,0x79]@。
-charToRaw :: Text -> [Word8]
-charToRaw = BS.unpack . TE.encodeUtf8
-
--- ===========================================================================
--- 行展開 (separate_longer・DataFrame)
--- ===========================================================================
---
--- @separate_longer_*@ は 1 行を **複数行**に展開する (tidyr §14.4.1)。 対象列の
--- 各セルを分割し、 piece 数だけその行を複製 (他列はそのまま複製) して、 対象列を
--- flatten した piece で差し替える。 NA (Nothing) は分割せず 1 行のまま保持。
-
--- | 区切り文字で行展開 (= @separate_longer_delim(df, col, delim)@)。
---   例: 列 @x@ の @"a,b,c"@ → 3 行 (@"a"@/@"b"@/@"c"@)、 他列は複製。
-separateLongerDelim :: Text -> Text -> DF.DataFrame -> DF.DataFrame
-separateLongerDelim col delim =
-  separateLongerWith col $ \mv -> case mv of
-    Nothing -> [Nothing]
-    Just t  -> map Just (T.splitOn delim t)
-
--- | 固定幅で行展開 (= @separate_longer_position(df, col, width)@)。
---   各セルを先頭から @width@ 文字ずつの塊に分割して行展開する。
---   例: @width=1@ で @"131"@ → 3 行 (@"1"@/@"3"@/@"1"@)。
-separateLongerPosition :: Text -> Int -> DF.DataFrame -> DF.DataFrame
-separateLongerPosition col width
-  | width <= 0 = error "separateLongerPosition: width は正でなければならない"
-  | otherwise  =
-      separateLongerWith col $ \mv -> case mv of
-        Nothing -> [Nothing]
-        Just t  | T.null t  -> [Just ""]            -- 空セルは 1 空行を保持
-                | otherwise -> map Just (T.chunksOf width t)
-
--- | 行展開の核: 対象列を Text 読みし、 各行を @split@ で pieces 化。 piece 数で
---   全列を 'rowsAtIndices' 複製し、 対象列を flatten した piece に差し替える。
-separateLongerWith
-  :: Text -> (Maybe Text -> [Maybe Text]) -> DF.DataFrame -> DF.DataFrame
-separateLongerWith col split df =
-  case getMaybeTextVec col df of
-    Nothing  -> error ("separateLonger: 列 " ++ T.unpack col
-                        ++ " が見つからない、 または Text 列でない")
-    Just vec ->
-      let piecesPerRow = map split (V.toList vec)                 -- [[Maybe Text]]
-          idxs = concat [ replicate (length ps) i
-                        | (i, ps) <- zip [0 ..] piecesPerRow ]
-          flat = concat piecesPerRow
-          expanded = DFS.rowsAtIndices (VU.fromList idxs) df
-      in DF.insertColumn col (buildTextCol flat) expanded
-
--- | @[Maybe Text]@ → Column。 全要素 Just なら素の Text 列、 NA 混在なら Maybe 列。
-buildTextCol :: [Maybe Text] -> DFC.Column
-buildTextCol xs
-  | all isJust xs = DF.fromList [ t | Just t <- xs ]
-  | otherwise     = DF.fromList xs
-
--- ===========================================================================
--- 列分割 (separate_wider・DataFrame)
--- ===========================================================================
---
--- @separate_wider_*@ は 1 セルを **複数列**に分割する (tidyr §14.4.2・行数不変)。
--- piece 数と新列名の数が合わないときの方針を 'TooFew' / 'TooMany' で指定する
--- (§14.4.3 の @too_few@ / @too_many@)。
-
--- | piece が **足りない**ときの方針 (= @too_few@)。
-data TooFew
-  = AlignStart    -- ^ 不足分を右側に NA で埋める (= @"align_start"@)。
-  | AlignEnd      -- ^ 不足分を左側に NA で埋める (= @"align_end"@)。
-  | TooFewError   -- ^ 不足があれば error (= @"error"@・既定)。
-  | TooFewDebug   -- ^ align_start で埋めつつ診断列を付与 (= @"debug"@)。
-  deriving (Eq, Show)
-
--- | piece が **多すぎる**ときの方針 (= @too_many@)。
-data TooMany
-  = DropExtra     -- ^ 余剰 piece を捨てる (= @"drop"@)。
-  | MergeExtra    -- ^ 余剰を最終列に区切り文字で再結合 (= @"merge"@)。
-  | TooManyError  -- ^ 余剰があれば error (= @"error"@・既定)。
-  | TooManyDebug  -- ^ drop で埋めつつ診断列 (余剰を remainder) を付与 (= @"debug"@)。
-  deriving (Eq, Show)
-
--- | 区切り文字で列分割 (= @separate_wider_delim(df, col, delim, names)@・厳密)。
---   piece 数と @names@ 数が不一致なら error。 @names@ の 'Nothing' はその piece を
---   捨てる (= R の @NA@・§14.4.2)。
-separateWiderDelim
-  :: Text -> Text -> [Maybe Text] -> DF.DataFrame -> DF.DataFrame
-separateWiderDelim col delim names =
-  separateWiderDelimWith col delim names TooFewError TooManyError
-
--- | 'separateWiderDelim' の方針指定版 (§14.4.3 の @too_few@ / @too_many@)。
-separateWiderDelimWith
-  :: Text -> Text -> [Maybe Text] -> TooFew -> TooMany -> DF.DataFrame -> DF.DataFrame
-separateWiderDelimWith col delim names tf tm =
-  separateWiderImpl col names tf tm (T.splitOn delim) (T.intercalate delim)
-
--- | 固定幅で列分割 (= @separate_wider_position(df, col, widths)@・厳密)。
---   @widths@ = @[(列名, 文字数)]@。 文字列長が総幅と一致しなければ error。
-separateWiderPosition
-  :: Text -> [(Text, Int)] -> DF.DataFrame -> DF.DataFrame
-separateWiderPosition col widths =
-  separateWiderPositionWith col widths TooFewError TooManyError
-
--- | 'separateWiderPosition' の方針指定版。 文字列が総幅より短ければ @too_few@、
---   長ければ余り (remainder) を @too_many@ で処理する。
-separateWiderPositionWith
-  :: Text -> [(Text, Int)] -> TooFew -> TooMany -> DF.DataFrame -> DF.DataFrame
-separateWiderPositionWith col widths tf tm df =
-  let names = map (Just . fst) widths
-      -- 幅順に切り出す。 文字列が尽きたら打ち切り (→ piece 不足 = too_few)。
-      -- 総幅を超える余りは最後に「余剰 piece」として 1 個付ける (→ too_many)。
-      chop t =
-        let go [] rest = ([], rest)
-            go (w : ws) rest
-              | T.null rest = ([], rest)
-              | otherwise   = let (h, r)   = T.splitAt w rest
-                                  (hs, r') = go ws r
-                              in (h : hs, r')
-            (pieces, remainder) = go (map snd widths) t
-        in if T.null remainder then pieces else pieces ++ [remainder]
-  in separateWiderImpl col names tf tm chop T.concat df
-
--- | 列分割の核実装。 @split@ で各セルを piece 化し、 'TooFew' / 'TooMany' で
---   ちょうど @length names@ スロットに整える。 'TooFewDebug' / 'TooManyDebug' の
---   とき診断列 @{col}_ok@ / @{col}_pieces@ / @{col}_remainder@ を付ける。
-separateWiderImpl
-  :: Text -> [Maybe Text] -> TooFew -> TooMany
-  -> (Text -> [Text]) -> ([Text] -> Text)
-  -> DF.DataFrame -> DF.DataFrame
-separateWiderImpl col names tf tm split rejoin df =
-  case getMaybeTextVec col df of
-    Nothing  -> error ("separateWider: 列 " ++ T.unpack col
-                        ++ " が見つからない、 または Text 列でない")
-    Just vec ->
-      let cells    = V.toList vec
-          rows     = map (resolveRow . fmap split) cells
-          -- スロット行列を列向きに転置 (各新列 = 全行のその位置)。
-          slotCols = transpose [ s | (s, _, _) <- rows ]            -- [[Maybe Text]] (列×行)
-          oks      = [ ok | (_, ok, _) <- rows ]
-          rems     = [ r  | (_, _,  r) <- rows ]
-          pieceCnt = map (maybe 0 (length . split)) cells
-          debug    = tf == TooFewDebug || tm == TooManyDebug
-          -- names と slotCols を突き合わせ、 Just name の列だけ採用。
-          namedCols = [ (nm, buildTextCol scol)
-                      | (Just nm, scol) <- zip names slotCols ]
-          diagCols  = [ (col <> "_ok",        DF.fromList oks)
-                      , (col <> "_pieces",    DF.fromList pieceCnt)
-                      , (col <> "_remainder", DF.fromList rems) ]
-          base      = DF.exclude [col] df
-          insertAll = foldl (\d (nm, c) -> DF.insertColumn nm c d)
-          withNamed = insertAll base namedCols
-      in if debug then insertAll withNamed diagCols else withNamed
-  where
-    n0 = length names
-    -- 1 行を (スロット [Maybe Text]・ok・remainder) に解決。
-    -- remainder は診断列用で、 余剰なし行は "" (R の @{col}_remainder@ 準拠)。
-    resolveRow :: Maybe [Text] -> ([Maybe Text], Bool, Text)
-    resolveRow Nothing       = (replicate n0 Nothing, True, "")  -- NA 入力は全 NA
-    resolveRow (Just pieces) =
-      let k = length pieces
-      in if k == n0
-           then (map Just pieces, True, "")
-           else if k < n0
-             then case tf of
-               TooFewError -> error ("separateWider: piece が不足 (" ++ show k
-                                      ++ " < " ++ show n0 ++ ") col=" ++ T.unpack col)
-               AlignEnd    -> (replicate (n0 - k) Nothing ++ map Just pieces, False, "")
-               _           -> (map Just pieces ++ replicate (n0 - k) Nothing, False, "")
-                              -- AlignStart / TooFewDebug
-             else case tm of   -- k > n0
-               TooManyError -> error ("separateWider: piece が過多 (" ++ show k
-                                       ++ " > " ++ show n0 ++ ") col=" ++ T.unpack col)
-               MergeExtra   ->
-                 let (keep, extra) = splitAt (n0 - 1) pieces
-                 in (map Just keep ++ [Just (rejoin extra)], False, "")
-               _            ->  -- DropExtra / TooManyDebug
-                 let (keep, extra) = splitAt n0 pieces
-                 in (map Just keep, False, rejoin extra)
-
--- ===========================================================================
--- 内部
--- ===========================================================================
-
--- | 列群を recycling 規則 (長さ 1 か n) で n 行に揃え、 行ごとの列リストに転置。
-recycleCols :: [[a]] -> [[a]]
-recycleCols cols =
-  let n = maximum (map length cols)
-      recy c
-        | length c == n = c
-        | length c == 1 = replicate n (head c)
-        | otherwise     = error "str_c/glue: 列長は 1 か n でなければならない (recycling)"
-  in if n == 0 then [] else transpose (map recy cols)
-
--- | @"a {x} b {y}"@ → @[Left "a ", Right "x", Left " b ", Right "y"]@。
---   @{{@ / @}}@ はリテラルの @{@ / @}@ にエスケープ (glue 同様)。
-parseGlue :: Text -> [Either Text Text]
-parseGlue = go
-  where
-    go t
-      | T.null t  = []
-      | otherwise =
-          let (lit, rest) = T.break (== '{') t
-          in case T.uncons rest of
-               Nothing -> prependLit lit []
-               Just ('{', r1)
-                 | Just ('{', r2) <- T.uncons r1 ->   -- "{{" → リテラル '{'
-                     prependLit (lit <> "{") (go r2)
-                 | otherwise ->
-                     let (key, r2) = T.break (== '}') r1
-                     in case T.uncons r2 of
-                          Just ('}', r3) ->
-                            prependLit lit (Right (T.strip key) : go r3)
-                          _ -> error "strGlue: 閉じない { がある"
-               _ -> [Left lit]
-
-    -- リテラル segment では @}}@ を @}@ に畳む (glue の閉じ波括弧エスケープ。
-    -- @{{@ → @{@ は break 側で処理済み)。
-    prependLit l0 xs = let l = T.replace "}}" "}" l0 in [Left l | not (T.null l)] ++ xs
-
--- ===========================================================================
--- 正規表現 (§15 Regular expressions・regex-tdfa POSIX ERE)
--- ===========================================================================
---
--- regex-tdfa は **POSIX ERE** ゆえ PCRE ショートハンド @\\d@ @\\s@ @\\w@ を解さない
--- (実測 2026-06-19)。 本モジュールは R(stringr) 流のパターンをそのまま使えるよう、
--- @\\d \\D \\s \\S \\w \\W@ を 'translateShorthand' で **POSIX クラスに変換**してから
--- tdfa に渡す。 単語境界 @\\b@ は tdfa が直接対応。 ★後方参照 @\\1@ は POSIX に無く
--- **非対応** (tutorial 側で「概念のみ」 honest 注記)。
---
--- 引数順は **pattern 先・string 後** (stringr は string 先だが、 Haskell では
--- @map (strDetect pat) xs@ / @filter (strDetect pat) xs@ と部分適用しやすいため)。
-
--- | PCRE ショートハンド (@\\d \\D \\s \\S \\w \\W@) を POSIX クラスに変換する。
---   文字クラス @[...]@ の内外で展開形が違う (外: @[[:digit:]]@・内: @[:digit:]@)。
---   @\\\\@ (literal backslash) や他のエスケープ (@\\.@ @\\b@ @\\1@ 等) はそのまま通す。
-translateShorthand :: Text -> Text
-translateShorthand = T.pack . go False . T.unpack
-  where
-    go _     []            = []
-    go inCls ('\\':c:rest)
-      | Just body <- lookup c shorthands = wrap inCls body ++ go inCls rest
-      | otherwise                        = '\\' : c : go inCls rest   -- \. \\ \b \1 等はそのまま
-    go _     ('[':rest)    = '[' : go True  rest
-    go _     (']':rest)    = ']' : go False rest
-    go inCls (c:rest)      = c   : go inCls rest
-
-    shorthands =
-      [ ('d', "[:digit:]"),  ('D', "^[:digit:]")
-      , ('s', "[:space:]"),  ('S', "^[:space:]")
-      , ('w', "[:alnum:]_"), ('W', "^[:alnum:]_") ]
-    -- クラス外は @[ ... ]@ で囲む (否定 ^... はクラス否定 [^...] に)。 クラス内は
-    -- 中身だけ ([:digit:] 等)。 クラス内の否定 (\D 等) は POSIX で表現不能ゆえ近似 (caret 落とし)。
-    wrap True  body          = stripCaret body
-    wrap False ('^':body)    = "[^" ++ body ++ "]"
-    wrap False body          = "[" ++ body ++ "]"
-    stripCaret ('^':b) = b
-    stripCaret b       = b
-
--- | パターン (ショートハンド変換済) を tdfa 'Regex' に compile。
---   @ci@ = ignore_case (§15.5 の @regex(ignore_case = TRUE)@)。
---   @^@ @$@ は **文字列全体**の先頭/末尾 (R 既定・single line = multiline False)。
-mkRegex :: Bool -> Text -> Regex
-mkRegex ci pat =
-  RE.makeRegexOpts comp RE.defaultExecOpt (T.unpack (translateShorthand pat))
-  where
-    comp = RE.defaultCompOpt { caseSensitive = not ci, multiline = False }
-
--- | マッチ配列 (whole + groups) を @[(text, offset, len)]@ に。 offset<0 = 不参加グループ。
-matchElems :: RE.MatchText String -> [(String, Int, Int)]
-matchElems arr = [ (g, o, l) | (g, (o, l)) <- elems arr ]
-
--- | パターンにマッチするか (= @str_detect(string, pattern)@・§15.3.1)。
-strDetect :: Text -> Text -> Bool
-strDetect = strDetectWith False
-
--- | 'strDetect' の ignore_case 指定版 (§15.5)。 @strDetectWith True pat s@ で大小無視。
-strDetectWith :: Bool -> Text -> Text -> Bool
-strDetectWith ci pat s = RE.matchTest (mkRegex ci pat) (T.unpack s)
-
--- | マッチ回数 (= @str_count(string, pattern)@・§15.3.2)。
-strCount :: Text -> Text -> Int
-strCount pat s = RE.matchCount (mkRegex False pat) (T.unpack s)
-
--- | マッチした要素だけ残す (= @str_subset(x, pattern)@)。
-strSubset :: Text -> [Text] -> [Text]
-strSubset pat = filter (strDetect pat)
-
--- | マッチした要素の位置 (= @str_which@・**1 始まり**)。
-strWhich :: Text -> [Text] -> [Int]
-strWhich pat xs = [ i | (i, x) <- zip [1 ..] xs, strDetect pat x ]
-
--- | 最初のマッチを取り出す (= @str_extract(string, pattern)@)。 無マッチは 'Nothing'。
-strExtract :: Text -> Text -> Maybe Text
-strExtract pat s =
-  case RE.matchOnceText (mkRegex False pat) (T.unpack s) of
-    Just (_, arr, _) | ((m, _, _) : _) <- matchElems arr -> Just (T.pack m)
-    _                                                    -> Nothing
-
--- | すべてのマッチを取り出す (= @str_extract_all@)。
-strExtractAll :: Text -> Text -> [Text]
-strExtractAll pat s =
-  [ T.pack m
-  | arr <- RE.matchAllText (mkRegex False pat) (T.unpack s)
-  , ((m, _, _) : _) <- [matchElems arr] ]
-
--- | 最初のマッチの **whole + capture groups** (= @str_match@)。 不参加グループ = 'Nothing'。
---   先頭が whole match、 以降が @()@ グループ。 無マッチは @[]@。
-strMatch :: Text -> Text -> [Maybe Text]
-strMatch pat s =
-  case RE.matchOnceText (mkRegex False pat) (T.unpack s) of
-    Just (_, arr, _) -> [ if o < 0 then Nothing else Just (T.pack g)
-                        | (g, o, _) <- matchElems arr ]
-    Nothing          -> []
-
--- | 最初のマッチを置換 (= @str_replace(string, pattern, replacement)@・§15.3.3)。
---   replacement 内の @\\1@..@\\9@ は capture group 参照、 @\\\\@ はリテラル @\\@。
-strReplace :: Text -> Text -> Text -> Text
-strReplace = replaceImpl False
-
--- | すべてのマッチを置換 (= @str_replace_all@)。
-strReplaceAll :: Text -> Text -> Text -> Text
-strReplaceAll = replaceImpl True
-
-replaceImpl :: Bool -> Text -> Text -> Text -> Text
-replaceImpl global pat rep s =
-  let rx      = mkRegex False pat
-      str     = T.unpack s
-      ms      = (if global then id else take 1) (RE.matchAllText rx str)
-      repl    = T.unpack rep
-      go cur [] = drop cur str
-      go cur (arr : rest) =
-        case matchElems arr of
-          gs@((_, o, l) : _) ->
-            take (o - cur) (drop cur str) ++ expandRep repl gs ++ go (o + l) rest
-          [] -> go cur rest
-  in T.pack (go 0 ms)
-
--- | replacement の @\\n@ を group n に展開 (@\\0@=whole・@\\\\@=リテラル @\\@)。
-expandRep :: String -> [(String, Int, Int)] -> String
-expandRep r groups = ex r
-  where
-    ex []                = []
-    ex ('\\' : d : rest)
-      | d >= '0' && d <= '9' = grp (fromEnum d - fromEnum '0') ++ ex rest
-      | d == '\\'            = '\\' : ex rest
-      | otherwise           = d : ex rest
-    ex (c : rest)          = c : ex rest
-    grp i = case drop i groups of
-              ((g, o, _) : _) | o >= 0 -> g
-              _                        -> ""
-
--- | 最初のマッチを削除 (= @str_remove@ = @str_replace(., pattern, "")@)。
-strRemove :: Text -> Text -> Text
-strRemove pat = strReplace pat ""
-
--- | すべてのマッチを削除 (= @str_remove_all@)。
-strRemoveAll :: Text -> Text -> Text
-strRemoveAll pat = strReplaceAll pat ""
-
--- | パターンで分割 (= @str_split(string, pattern)@)。 マッチ部分を区切りとして除く。
-strSplit :: Text -> Text -> [Text]
-strSplit pat s =
-  let rx  = mkRegex False pat
-      str = T.unpack s
-      ms  = RE.matchAllText rx str
-      go cur []          = [drop cur str]
-      go cur (arr : rest) =
-        case matchElems arr of
-          ((_, o, l) : _) -> take (o - cur) (drop cur str) : go (o + l) rest
-          []              -> go cur rest
-  in map T.pack (go 0 ms)
-
--- | 最初のマッチの位置 @(start, end)@ (= @str_locate@・**1 始まり・両端含む**)。 無マッチ = 'Nothing'。
-strLocate :: Text -> Text -> Maybe (Int, Int)
-strLocate pat s =
-  case RE.matchOnceText (mkRegex False pat) (T.unpack s) of
-    Just (_, arr, _) | ((_, o, l) : _) <- matchElems arr, o >= 0 -> Just (o + 1, o + l)
-    _                                                            -> Nothing
-
--- | 正規表現メタ文字をエスケープ (= @str_escape@・§15.6・リテラル文字列からパターンを作る用)。
-strEscape :: Text -> Text
-strEscape = T.concatMap esc
-  where
-    esc c | c `elem` metas = T.pack ['\\', c]
-          | otherwise      = T.singleton c
-    metas = ".^$|()[]{}*+?\\" :: String
-
--- | 名前付きグループで列に分割 (= @separate_wider_regex(df, col, patterns)@・§15.3.4)。
---   @specs@ = @[(Just 列名 | Nothing, 部分パターン)]@。 各部分パターンを順に capture group 化し、
---   セルを文字列全体マッチ (@^...$@) して各 group を対応列へ。 'Nothing' の group は捨てる
---   (= R の名無し)。 ★各部分パターンは **内部に capturing group を持たない前提**
---   (持つと group index がずれる・R4DS の例は単純パターンのみ)。
-separateWiderRegex :: Text -> [(Maybe Text, Text)] -> DF.DataFrame -> DF.DataFrame
-separateWiderRegex col specs df =
-  case getMaybeTextVec col df of
-    Nothing  -> error ("separateWiderRegex: 列 " ++ T.unpack col
-                        ++ " が見つからない、 または Text 列でない")
-    Just vec ->
-      let pat   = "^" <> T.concat [ "(" <> p <> ")" | (_, p) <- specs ] <> "$"
-          rx    = mkRegex False pat
-          names = map fst specs
-          nslot = length specs
-          rowGroups mv = case mv of
-            Nothing -> replicate nslot Nothing
-            Just t  -> case RE.matchOnceText rx (T.unpack t) of
-              Just (_, arr, _) ->
-                let grps = drop 1 (matchElems arr)   -- whole match を除く
-                in take nslot
-                     ([ if o < 0 then Nothing else Just (T.pack g) | (g, o, _) <- grps ]
-                       ++ repeat Nothing)
-              Nothing -> error ("separateWiderRegex: パターン不一致 col=" ++ T.unpack col
-                                 ++ " value=" ++ show t)
-          rows     = map rowGroups (V.toList vec)
-          slotCols = transpose rows
-          named    = [ (nm, buildTextCol scol) | (Just nm, scol) <- zip names slotCols ]
-          base     = DF.exclude [col] df
-      in foldl (\d (nm, c) -> DF.insertColumn nm c d) base named
diff --git a/src/Hanalyze/Data/Transform.hs b/src/Hanalyze/Data/Transform.hs
deleted file mode 100644
--- a/src/Hanalyze/Data/Transform.hs
+++ /dev/null
@@ -1,169 +0,0 @@
-{-# LANGUAGE ScopedTypeVariables #-}
--- |
--- Module      : Hanalyze.Data.Transform
--- Description : dplyr 流の順位・オフセット・累積・区間化を純粋な [a] -> [b] として提供
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- dplyr 流の順序付き/窓関数的なベクトル変換 (Phase 66)。
---
--- R4DS Ch13 "Numbers" で扱う順位・オフセット・累積・区間化・連続識別子を、
--- **純粋な `[a] -> [b]`** として公開する。 統計 ('Stat.Descriptive') でも IO/DataFrame
--- ('DataIO') でもなく、 純粋データ抽象の `Data/` 名前空間に置く ('Data.ColumnSource'
--- の隣)。 DataFrame 直結解析 API (Phase 67) の @mutate@ もこれを呼ぶ。
---
--- === NA
--- 順位関数は NA を含むベクトル用に @*NA@ 変種 (@[Maybe a] -> [Maybe b]@・dplyr の
--- @na.last="keep"@ と同じく Nothing は Nothing を保ち、 残りを順位付け) を併設する。
---
--- === desc
--- 降順順位は別関数を設けず 'Data.Ord.Down' を被せる (@minRank (map Down xs)@・
--- NA 付きは @minRankNA (map (fmap Down) xs)@)。
-module Hanalyze.Data.Transform
-  ( -- * 順位 (dplyr ranking)
-    minRank, denseRank, rowNumber
-  , percentRank, cumeDist
-    -- * 順位 (NA 保持変種)
-  , minRankNA, denseRankNA, rowNumberNA
-  , percentRankNA, cumeDistNA
-    -- * オフセット
-  , lag, lead
-    -- * 累積
-  , cumsum, cumprod, cummin, cummax, cummean
-    -- * 区間化
-  , cut, cutLabels
-    -- * 連続識別子
-  , consecutiveId
-  ) where
-
-import           Data.List       (sortOn)
-import qualified Data.Map.Strict as M
-import qualified Data.Set        as S
-
--- ===========================================================================
--- 順位
--- ===========================================================================
-
--- | 最小順位法 (dplyr @min_rank@・tie は最小を共有し次を飛ばす: 1,2,2,4)。
---   各値の順位 = 1 + (厳密に小さい要素数)。
-minRank :: Ord a => [a] -> [Int]
-minRank xs = map ((rm M.!) ) xs
-  where
-    sorted = sortOn id xs
-    -- 値 → 最初の出現 index (= 厳密に小さい要素数)。1 始まりにして格納。
-    rm = M.fromListWith min [ (v, i + 1) | (i, v) <- zip [0 ..] sorted ]
-
--- | 密順位法 (dplyr @dense_rank@・tie で番号を飛ばさない: 1,2,2,3)。
---   各値の順位 = その値以下の **相異なる値の個数**。
-denseRank :: Ord a => [a] -> [Int]
-denseRank xs = map (dm M.!) xs
-  where
-    distinct = S.toAscList (S.fromList xs)
-    dm = M.fromList (zip distinct [1 :: Int ..])
-
--- | 行番号 (dplyr @row_number@・tie も出現順で一意: 1,2,3,4)。
-rowNumber :: Ord a => [a] -> [Int]
-rowNumber xs = map (rm M.!) [0 .. n - 1]
-  where
-    n     = length xs
-    order = map fst (sortOn snd (zip [0 :: Int ..] xs))  -- (origIdx, value) を value, origIdx 昇順
-    rm    = M.fromList (zip order [1 :: Int ..])
-
--- | パーセント順位 (dplyr @percent_rank@ = (minRank - 1)/(n - 1))。
-percentRank :: Ord a => [a] -> [Double]
-percentRank xs
-  | n <= 1    = map (const 0) xs
-  | otherwise = [ fromIntegral (r - 1) / fromIntegral (n - 1) | r <- minRank xs ]
-  where n = length xs
-
--- | 累積分布 (dplyr @cume_dist@ = (≤x の個数)/n)。
-cumeDist :: Ord a => [a] -> [Double]
-cumeDist xs = map (\x -> fromIntegral (cm M.! x) / fromIntegral n) xs
-  where
-    n      = length xs
-    sorted = sortOn id xs
-    -- 値 → その値の最後の出現 index + 1 (= ≤ その値の個数)。
-    cm = M.fromListWith max [ (v, i + 1) | (i, v) <- zip [0 ..] sorted ]
-
--- --- NA 保持変種 -----------------------------------------------------------
-
--- | 非 NA だけを @f@ で順位付けし、 NA (Nothing) は Nothing のまま位置を保つ。
-onJusts :: forall a b. ([a] -> [b]) -> [Maybe a] -> [Maybe b]
-onJusts f xs =
-  let idxVals = [ (i, a) | (i, Just a) <- zip [0 ..] xs ]
-      ranked  = f (map snd idxVals)
-      m       = M.fromList (zip (map fst idxVals) ranked)
-  in [ M.lookup i m | i <- [0 .. length xs - 1] ]
-
-minRankNA     :: Ord a => [Maybe a] -> [Maybe Int]
-minRankNA      = onJusts minRank
-denseRankNA   :: Ord a => [Maybe a] -> [Maybe Int]
-denseRankNA    = onJusts denseRank
-rowNumberNA   :: Ord a => [Maybe a] -> [Maybe Int]
-rowNumberNA    = onJusts rowNumber
-percentRankNA :: Ord a => [Maybe a] -> [Maybe Double]
-percentRankNA  = onJusts percentRank
-cumeDistNA    :: Ord a => [Maybe a] -> [Maybe Double]
-cumeDistNA     = onJusts cumeDist
-
--- ===========================================================================
--- オフセット
--- ===========================================================================
-
--- | @lag n d xs@: 各値を n 個後ろへずらし、 先頭 n 個を default @d@ で埋める
---   (dplyr @lag(x, n, default)@・既定 R は NA)。 入力と同長。
-lag :: Int -> a -> [a] -> [a]
-lag n d xs = take (length xs) (replicate n d ++ xs)
-
--- | @lead n d xs@: 各値を n 個前へずらし、 末尾 n 個を default @d@ で埋める。
-lead :: Int -> a -> [a] -> [a]
-lead n d xs = take (length xs) (drop n xs ++ repeat d)
-
--- ===========================================================================
--- 累積
--- ===========================================================================
-
-cumsum  :: Num a => [a] -> [a]
-cumsum   = scanl1 (+)
-cumprod :: Num a => [a] -> [a]
-cumprod  = scanl1 (*)
-cummin  :: Ord a => [a] -> [a]
-cummin   = scanl1 min
-cummax  :: Ord a => [a] -> [a]
-cummax   = scanl1 max
-
--- | 累積平均 (dplyr @cummean@・i 番目 = 先頭 i 個の平均)。
-cummean :: [Double] -> [Double]
-cummean xs = zipWith (\s i -> s / fromIntegral i) (scanl1 (+) xs) [1 :: Int ..]
-
--- ===========================================================================
--- 区間化 (base R cut・既定 right = TRUE → (a, b])
--- ===========================================================================
-
--- | @cut breaks xs@: 各値が属する bin の index (1 始まり) を返す。 境界は昇順前提・
---   区間は @(lo, hi]@ (right=TRUE)・範囲外は Nothing (= R の NA)。
-cut :: [Double] -> [Double] -> [Maybe Int]
-cut breaks = map binOf
-  where
-    intervals = zip [1 :: Int ..] (zip breaks (drop 1 breaks))
-    binOf x = case [ i | (i, (lo, hi)) <- intervals, x > lo, x <= hi ] of
-                (i : _) -> Just i
-                []      -> Nothing
-
--- | ラベル付き 'cut' (@labels@ は @breaks - 1@ 個)。
-cutLabels :: [b] -> [Double] -> [Double] -> [Maybe b]
-cutLabels labels breaks = map (fmap (labels !!) . fmap (subtract 1)) . cut breaks
-
--- ===========================================================================
--- 連続識別子 (dplyr consecutive_id・値が変わるたび +1)
--- ===========================================================================
-
-consecutiveId :: Eq a => [a] -> [Int]
-consecutiveId = go Nothing 0
-  where
-    go _ _ [] = []
-    go prev cur (y : ys) =
-      let cur' = case prev of
-                   Just p | p == y -> cur
-                   _               -> cur + 1
-      in cur' : go (Just y) cur' ys
diff --git a/src/Hanalyze/Data/Wrangle.hs b/src/Hanalyze/Data/Wrangle.hs
deleted file mode 100644
--- a/src/Hanalyze/Data/Wrangle.hs
+++ /dev/null
@@ -1,255 +0,0 @@
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE FlexibleInstances   #-}
-{-# LANGUAGE OverloadedStrings   #-}
--- |
--- Module      : Hanalyze.Data.Wrangle
--- Description : DataFrame 直結の dplyr 風 summarise/mutate/groupBy 動詞
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- DataFrame 直結の解析動詞 (Phase 67)。
---
--- hgg の @df |>> layer (scatter "x" "y")@ と対称に、 DataFrame を直接
--- データ源として dplyr の @summarise@ / @mutate@ / @group_by@ 相当を
--- 「列名参照 + パイプライン + DataFrame in/out」 で書ける薄層。
---
--- 数値ロジックは 'Hanalyze.Stat.Descriptive' (Phase 65) と
--- 'Hanalyze.Data.Transform' (Phase 66) に委譲し、 本モジュールは
--- **列名解決 + 群化 + DataFrame 組み立て + NA 処理**のみを担う。
---
--- @
--- import DataFrame.Operators ((|>))
--- df |> summarise [ "mean" =: meanOf "dep_delay", "q95" =: quantileOf 0.95 "dep_delay", "n" =: nOf ]
--- df |> mutate    [ "z" =: zscoreOf "x", "rank" =: minRankOf "dep_delay" ]
--- df |> groupBy ["year","month","day"] |> summarise [ "mean" =: meanOf "dep_delay" ]
--- @
---
--- 集約子は既定で **NA 除去 (na.rm=TRUE 相当)**。 数値列のみ対象 (factor は群キー
--- としてのみ扱う)。 群の並びは dplyr 同様キー昇順。
---
--- v1 の制限 (判断申し送り): 入力は Hackage @DataFrame@ 限定 ('ColumnSource' 全多相化は
--- 後続)。 grouped @mutate@ は未対応 (ungrouped のみ)。
-module Hanalyze.Data.Wrangle
-  ( -- * 動詞
-    summarise, Summarisable
-  , mutate
-  , groupBy, Grouped
-    -- * 命名
-  , (=:)
-    -- * 集約子 (列名 → スカラ)
-  , Agg
-  , meanOf, medianOf, sdOf, varOf, iqrOf, quantileOf, sumOf, minOf, maxOf
-  , nOf, nDistinctOf
-    -- * 列式 (mutate・列名 → 列)
-  , ColExpr
-  , zscoreOf, minRankOf, denseRankOf, lagOf, leadOf, cumsumOf
-  ) where
-
-import           Data.Maybe               (catMaybes, isJust)
-import           Data.List                (nub, transpose)
-import qualified Data.Map.Strict          as M
-import qualified Data.Vector              as V
-import           Data.Text                (Text)
-import qualified DataFrame.Internal.Column    as DF
-import qualified DataFrame.Internal.DataFrame  as DF
-import qualified DataFrame.Operations.Core     as DF
-import qualified DataFrame.Internal.Column as DFC
-
-import           Hanalyze.DataIO.Convert    (getMaybeTextVec)
-import           Hanalyze.DataIO.Preprocess (readMaybeDoubleColumn)
-import qualified Hanalyze.Stat.Descriptive  as D
-import qualified Hanalyze.Data.Transform    as T
-
--- ===========================================================================
--- 内部表現
--- ===========================================================================
-
--- | 1 群分のデータ (行数 + 数値列の name→値・NA 保持・行整列)。
-data Group = Group
-  { gSize :: !Int
-  , gNum  :: !(M.Map Text [Maybe Double])
-  }
-
--- | 集約結果のセル (数値列 or 整数列)。
-data Cell = CD !Double | CI !Int
-
--- | 群キーの 1 要素 (数値 or 文字列)。Ord は KNum<KTxt・KNum は数値順。
-data KeyVal = KNum !Double | KTxt !Text deriving (Eq, Ord)
-
--- | 集約子: 群 → セル。
-newtype Agg = Agg (Group -> Cell)
-
--- | 列式: 群 → 新しい列 (NA 保持)。
-newtype ColExpr = ColExpr (Group -> [Maybe Double])
-
--- | 結果列名 + 操作 を結ぶ。
-(=:) :: Text -> a -> (Text, a)
-(=:) = (,)
-infixr 0 =:
-
--- ===========================================================================
--- 集約子 (na.rm = TRUE 既定)
--- ===========================================================================
-
-col1 :: Text -> ([Double] -> Double) -> Agg
-col1 c f = Agg (\g -> CD (f (catMaybes (M.findWithDefault [] c (gNum g)))))
-
-meanOf, medianOf, sdOf, varOf, iqrOf, sumOf, minOf, maxOf :: Text -> Agg
-meanOf   c = col1 c D.meanL
-medianOf c = col1 c D.medianL
-sdOf     c = col1 c D.sdL
-varOf    c = col1 c D.varianceL
-iqrOf    c = col1 c D.iqrL
-sumOf    c = col1 c sum
-minOf    c = col1 c minimum
-maxOf    c = col1 c maximum
-
-quantileOf :: Double -> Text -> Agg
-quantileOf p c = col1 c (D.quantileL p)
-
--- | 行数 (= R @n()@)。
-nOf :: Agg
-nOf = Agg (CI . gSize)
-
--- | 相異なる非 NA 値の個数 (= R @n_distinct()@)。
-nDistinctOf :: Text -> Agg
-nDistinctOf c = Agg (\g -> CI (length (nub (catMaybes (M.findWithDefault [] c (gNum g))))))
-
--- ===========================================================================
--- 列式 (mutate)
--- ===========================================================================
-
-colE :: Text -> ([Maybe Double] -> [Maybe Double]) -> ColExpr
-colE c f = ColExpr (\g -> f (M.findWithDefault [] c (gNum g)))
-
--- | Z スコア (x - mean) / sd。 NA は NA のまま。
-zscoreOf :: Text -> ColExpr
-zscoreOf c = colE c $ \xs ->
-  let ys = catMaybes xs; m = D.meanL ys; s = D.sdL ys
-  in map (fmap (\x -> (x - m) / s)) xs
-
--- | 最小順位 (dplyr min_rank・NA 保持)。
-minRankOf :: Text -> ColExpr
-minRankOf c = colE c (map (fmap fromIntegral) . T.minRankNA)
-
--- | 密順位 (dplyr dense_rank・NA 保持)。
-denseRankOf :: Text -> ColExpr
-denseRankOf c = colE c (map (fmap fromIntegral) . T.denseRankNA)
-
--- | n 個ラグ (先頭を NA 埋め)。
-lagOf :: Int -> Text -> ColExpr
-lagOf n c = colE c (T.lag n Nothing)
-
--- | n 個リード (末尾を NA 埋め)。
-leadOf :: Int -> Text -> ColExpr
-leadOf n c = colE c (T.lead n Nothing)
-
--- | 累積和 (NA は以降へ伝播 = R cumsum)。
-cumsumOf :: Text -> ColExpr
-cumsumOf c = colE c scan
-  where scan []       = []
-        scan (x : xs) = scanl (\acc y -> (+) <$> acc <*> y) x xs
-
--- ===========================================================================
--- 列抽出・群化
--- ===========================================================================
-
-nrows :: DF.DataFrame -> Int
-nrows = fst . DF.dimensions
-
--- | 数値列をすべて NA 保持・行整列で取り出す。
-numColumns :: DF.DataFrame -> [(Text, V.Vector (Maybe Double))]
-numColumns df =
-  [ (n, V.fromList ms) | n <- DF.columnNames df, Just ms <- [readMaybeDoubleColumn n df] ]
-
--- | 群キー列を 'KeyVal' 列として取り出す (数値優先・無理なら Text)。
-keyColumn :: Text -> DF.DataFrame -> [KeyVal]
-keyColumn name df =
-  -- 数値読みが「全 NA」 なら実体は Text 列ゆえ Text 抽出に倒す
-  -- (readMaybeDoubleColumn は Text 列を Just [Nothing..] で返す罠の回避)。
-  case readMaybeDoubleColumn name df of
-    Just ms | any isJust ms -> map (maybe (KNum (0/0)) KNum) ms
-    _ -> case getMaybeTextVec name df of
-      Just v  -> map (maybe (KTxt "NA") KTxt) (V.toList v)
-      Nothing -> replicate (nrows df) (KTxt "NA")
-
--- | 各行の群キー tuple。
-rowKeys :: [Text] -> DF.DataFrame -> [[KeyVal]]
-rowKeys keys df = transpose (map (`keyColumn` df) keys)
-
--- | キー昇順の群 (キー tuple, 行 index 群)。
-groupRows :: [Text] -> DF.DataFrame -> [([KeyVal], [Int])]
-groupRows keys df =
-  M.toAscList (M.fromListWith (flip (++)) (zip (rowKeys keys df) (map (: []) [0 ..])))
-
-mkGroup :: [(Text, V.Vector (Maybe Double))] -> [Int] -> Group
-mkGroup ncols idxs = Group
-  { gSize = length idxs
-  , gNum  = M.fromList [ (n, [ v V.! i | i <- idxs ]) | (n, v) <- ncols ]
-  }
-
--- ===========================================================================
--- セル / キー列 → DataFrame Column
--- ===========================================================================
-
-buildCol :: [Cell] -> DFC.Column
-buildCol cells
-  | all isCI cells = DF.fromList [ i | CI i <- cells ]
-  | otherwise      = DF.fromList (map toD cells)
-  where
-    isCI (CI _) = True
-    isCI _      = False
-    toD (CD x)  = x
-    toD (CI i)  = fromIntegral i
-
-buildKeyCol :: [KeyVal] -> DFC.Column
-buildKeyCol ks
-  | all isTxt ks                  = DF.fromList [ t | KTxt t <- ks ]
-  | all isWholeNum ks             = DF.fromList [ round d :: Int | KNum d <- ks ]
-  | otherwise                     = DF.fromList [ d | KNum d <- ks ]
-  where
-    isTxt (KTxt _) = True
-    isTxt _        = False
-    isWholeNum (KNum d) = not (isNaN d) && d == fromIntegral (round d :: Int)
-    isWholeNum _        = False
-
--- ===========================================================================
--- 動詞
--- ===========================================================================
-
--- | 群化された DataFrame (キー列名を保持)。
-data Grouped = Grouped ![Text] !DF.DataFrame
-
--- | dplyr @group_by@。
-groupBy :: [Text] -> DF.DataFrame -> Grouped
-groupBy = Grouped
-
--- | @summarise@ は DataFrame (= 1 群) でも 'Grouped' でも使える。
-class Summarisable g where
-  summarise :: [(Text, Agg)] -> g -> DF.DataFrame
-
-instance Summarisable DF.DataFrame where
-  summarise aggs df =
-    let g = Group { gSize = nrows df
-                  , gNum  = M.fromList (map (fmap V.toList) (numColumns df)) }
-    in DF.fromNamedColumns [ (rn, buildCol [runA a g]) | (rn, a) <- aggs ]
-
-instance Summarisable Grouped where
-  summarise aggs (Grouped keys df) =
-    let ncols = numColumns df
-        grps  = [ (kt, mkGroup ncols idxs) | (kt, idxs) <- groupRows keys df ]
-        keyCols = [ (kn, buildKeyCol [ kt !! j | (kt, _) <- grps ])
-                  | (j, kn) <- zip [0 ..] keys ]
-        resCols = [ (rn, buildCol [ runA a g | (_, g) <- grps ])
-                  | (rn, a) <- aggs ]
-    in DF.fromNamedColumns (keyCols ++ resCols)
-
-runA :: Agg -> Group -> Cell
-runA (Agg f) = f
-
--- | dplyr @mutate@ (ungrouped・元列を温存して新列を右端に足す)。
-mutate :: [(Text, ColExpr)] -> DF.DataFrame -> DF.DataFrame
-mutate exprs df =
-  let g = Group { gSize = nrows df
-                , gNum  = M.fromList (map (fmap V.toList) (numColumns df)) }
-  in foldl (\d (nm, ColExpr f) -> DF.insertVector nm (V.fromList (f g)) d) df exprs
diff --git a/src/Hanalyze/DataIO/CSV.hs b/src/Hanalyze/DataIO/CSV.hs
deleted file mode 100644
--- a/src/Hanalyze/DataIO/CSV.hs
+++ /dev/null
@@ -1,409 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.DataIO.CSV
--- Description : CSV / TSV / SSV を Hackage dataframe の DataFrame として読み込むローダ群
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- CSV / TSV / SSV loaders that return Hackage @dataframe@'s
--- 'DataFrame.Internal.DataFrame.DataFrame' directly.
---
---   * CSV / TSV — delegated to Hackage's 'DX.readCsv' / 'DX.readTsv'
---     (improved type inference, missing-bitmap support).
---   * SSV       — Hackage has no dedicated loader, so we read with
---     @cassava@ and assemble columns via 'DX.fromList' /
---     'DX.insertColumn'.
-module Hanalyze.DataIO.CSV
-  ( loadCSV
-  , loadTSV
-  , loadSSV
-  , loadAuto
-    -- * Safe loaders (return Either + LogReport)
-  , loadCsvSafe
-  , loadTsvSafe
-  , loadSsvSafe
-  , loadAutoSafe
-    -- * Loader options (Phase A4)
-  , LoadOpts (..)
-  , defaultLoadOpts
-  , loadAutoSafeWith
-  , ParseError
-  ) where
-
-import qualified DataFrame.Internal.Column    as DX
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified DataFrame.IO.CSV              as DX
-import qualified DataFrame.IO.CSV             as DXIO
-import qualified DataFrame.Internal.DataFrame as DXD
-
-import Control.Exception (SomeException, try, evaluate)
-import qualified Data.ByteString      as BS
-import qualified Data.ByteString.Lazy as BL
-import Data.Char (ord)
-import Data.List (isSuffixOf)
-import Data.Csv (NamedRecord, Header, defaultDecodeOptions, DecodeOptions(..), decodeByNameWith)
-import qualified Data.HashMap.Strict as HM
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Data.Text.Encoding as TE
-import qualified Data.Vector as V
-import System.IO.Error (tryIOError)
-import Text.Read (readMaybe)
-
-import Hanalyze.DataIO.Log (Loaded, LogReport, mkInfo, hasWarnings, logReport, entries)
-import Hanalyze.DataIO.Health (inspectWithPreview)
-import qualified Hanalyze.DataIO.Sniff as Sniff
-import qualified System.IO.Temp as Tmp
-import System.IO (hClose)
-
--- | Parse-error message (a plain 'String').
-type ParseError = String
-
--- ---------------------------------------------------------------------------
--- CSV / TSV: Hackage に直接委譲
--- ---------------------------------------------------------------------------
-
--- | Load a CSV file via Hackage's @readCsv@.
-loadCSV :: FilePath -> IO (Either ParseError DXD.DataFrame)
-loadCSV = loadHackage DX.readCsv
-
--- | Load a TSV file via Hackage's @readTsv@.
-loadTSV :: FilePath -> IO (Either ParseError DXD.DataFrame)
-loadTSV = loadHackage DX.readTsv
-
-loadHackage :: (FilePath -> IO DXD.DataFrame)
-            -> FilePath -> IO (Either ParseError DXD.DataFrame)
-loadHackage reader path = do
-  r <- try (reader path) :: IO (Either SomeException DXD.DataFrame)
-  return $ case r of
-    Left  e  -> Left ("CSV/TSV loader failed: " ++ show e)
-    Right df -> Right df
-
--- ---------------------------------------------------------------------------
--- SSV: cassava で読み、Hackage 'DataFrame' に詰め替える
--- ---------------------------------------------------------------------------
-
--- | Load a space-separated value file via @cassava@; the result is
--- repackaged into a Hackage 'DXD.DataFrame'.
-loadSSV :: FilePath -> IO (Either ParseError DXD.DataFrame)
-loadSSV path = do
-  content <- BL.readFile path
-  let opts = defaultDecodeOptions { decDelimiter = fromIntegral (ord ' ') }
-  case decodeByNameWith opts content of
-    Left err          -> return (Left err)
-    Right (hdr, rows) -> return (Right (toHackageDF hdr rows))
-
-toHackageDF :: Header -> V.Vector NamedRecord -> DXD.DataFrame
-toHackageDF hdr rows =
-  foldl insert DX.empty
-    [ (TE.decodeUtf8 key, classifyCells key rows) | key <- V.toList hdr ]
-  where
-    insert df (name, col) = DX.insertColumn name col df
-
--- | 列の値を全て読み 'Double' として parse できれば数値列、そうでなければ Text 列。
-classifyCells :: BS.ByteString -> V.Vector NamedRecord -> DX.Column
-classifyCells key rows =
-  let cells = V.map (TE.decodeUtf8 . HM.lookupDefault "" key) rows
-      texts = V.toList cells
-  in case mapM (readMaybe . T.unpack) texts of
-       Just nums -> DX.fromList (nums :: [Double])
-       Nothing   -> DX.fromList (texts :: [Text])
-
--- ---------------------------------------------------------------------------
--- 拡張子による自動振り分け
--- ---------------------------------------------------------------------------
-
--- | Auto-dispatch by file extension: @.tsv@ → @loadTSV@, @.ssv@ →
--- 'loadSSV', otherwise 'loadCSV'.
-loadAuto :: FilePath -> IO (Either ParseError DXD.DataFrame)
-loadAuto path
-  | ".tsv" `isSuffixOf` path = loadTSV path
-  | ".ssv" `isSuffixOf` path = loadSSV path
-  | otherwise                = loadCSV path
-
--- ---------------------------------------------------------------------------
--- Safe loaders (Phase A2)
---
--- 空ファイル / ヘッダのみ / Hackage の internal 'error' を全て 'Either' に
--- 押し込め、call stack を端末に出さないようにする。'Loaded' で副次的な
--- ログを返せる (現状はパス情報のみ、A3 で W コードが付き始める)。
--- ---------------------------------------------------------------------------
-
--- | ファイルを行ベクトルで先読みして、空 / ヘッダのみを検出する。
--- Right に返るのは「行リスト (改行で split, 空行は除く)」。
-preflight :: FilePath -> IO (Either ParseError [BS.ByteString])
-preflight path = do
-  e <- tryIOError (BS.readFile path)
-  case e of
-    Left ioe  -> return (Left ("Cannot read file: " ++ show ioe))
-    Right bs0 ->
-      let bs   = stripBOM bs0
-          rows = filter (not . BS.null) (BS.split (fromIntegral (ord '\n')) bs)
-          rs   = map stripCR rows
-      in case rs of
-           []      -> return (Left "Empty file (no rows).")
-           [_only] -> return (Left "File has only a header row (no data).")
-           _       -> return (Right rs)
-
-stripCR :: BS.ByteString -> BS.ByteString
-stripCR bs
-  | BS.null bs                  = bs
-  | BS.last bs == fromIntegral (ord '\r') = BS.init bs
-  | otherwise                   = bs
-
--- | UTF-8 BOM (EF BB BF) を取り除く。
-stripBOM :: BS.ByteString -> BS.ByteString
-stripBOM bs
-  | BS.length bs >= 3
-  , BS.index bs 0 == 0xEF
-  , BS.index bs 1 == 0xBB
-  , BS.index bs 2 == 0xBF = BS.drop 3 bs
-  | otherwise             = bs
-
--- | Hackage @readCsv@ / @readTsv@ を例外捕捉付きで呼ぶ。
-runHackageSafe
-  :: (FilePath -> IO DXD.DataFrame)
-  -> FilePath
-  -> IO (Either ParseError DXD.DataFrame)
-runHackageSafe reader path = do
-  r <- try (reader path >>= evaluate) :: IO (Either SomeException DXD.DataFrame)
-  return $ case r of
-    Right df -> Right df
-    Left  e  -> Left (cleanError (show e))
-
--- | call-stack 行を取り除き、ユーザに見せる 1 行メッセージに整形する。
-cleanError :: String -> String
-cleanError = takeWhile (/= '\n')
-
--- | Safe CSV loader. Returns 'Left' on empty input, header-only files,
--- or Hackage internal errors instead of bubbling them up as exceptions.
-loadCsvSafe :: FilePath -> IO (Either ParseError (Loaded DXD.DataFrame))
-loadCsvSafe = loadHackageSafe DX.readCsv
-
--- | Safe TSV loader (TSV analogue of 'loadCsvSafe').
-loadTsvSafe :: FilePath -> IO (Either ParseError (Loaded DXD.DataFrame))
-loadTsvSafe = loadHackageSafe DX.readTsv
-
-loadHackageSafe
-  :: (FilePath -> IO DXD.DataFrame) -> FilePath
-  -> IO (Either ParseError (Loaded DXD.DataFrame))
-loadHackageSafe reader path = do
-  pre <- preflight path
-  case pre of
-    Left e   -> return (Left e)
-    Right rs -> do
-      r <- runHackageSafe reader path
-      return $ case r of
-        Left  e  -> Left e
-        Right df -> Right (df, inspectWithPreview (previewBytes rs) df)
-
--- | Safe SSV loader (SSV analogue of 'loadCsvSafe').
-loadSsvSafe :: FilePath -> IO (Either ParseError (Loaded DXD.DataFrame))
-loadSsvSafe path = do
-  pre <- preflight path
-  case pre of
-    Left e  -> return (Left e)
-    Right rs -> do
-      r <- loadSSV path
-      return $ case r of
-        Left  e  -> Left e
-        Right df -> Right (df, inspectWithPreview (previewBytes rs) df)
-
--- | 先頭 8 KB 程度を健全性検査のプレビュー用に切り出す。
-previewBytes :: [BS.ByteString] -> BS.ByteString
-previewBytes rs =
-  let joined = BS.intercalate "\n" rs
-  in BS.take 8192 joined
-
--- | Auto-dispatch safe loader: picks 'loadCsvSafe' / 'loadTsvSafe' /
--- 'loadSsvSafe' from the file extension.
-loadAutoSafe :: FilePath -> IO (Either ParseError (Loaded DXD.DataFrame))
-loadAutoSafe path
-  | ".tsv" `isSuffixOf` path = loadTsvSafe path
-  | ".ssv" `isSuffixOf` path = loadSsvSafe path
-  | otherwise                = loadCsvSafe path
-
--- ---------------------------------------------------------------------------
--- Phase A4: ロードオプション
--- ---------------------------------------------------------------------------
-
--- | Loading options that can be supplied from the CLI.
-data LoadOpts = LoadOpts
-  { loSkip     :: !Int            -- ^ Skip the first @N@ rows.
-  , loComment  :: !(Maybe Char)   -- ^ Skip rows starting with this character (e.g. @\'#\'@).
-  , loNoHeader :: !Bool           -- ^ Treat the file as header-less and generate @col0, col1, …@.
-  , loStrict   :: !Bool           -- ^ Short-circuit to 'Left' if the
-                                  --   @LogReport@ contains a @Warn@ entry.
-  , loSniff    :: !Bool           -- ^ Enable auto-inference (default 'True').
-  , loDelim    :: !(Maybe Char)   -- ^ Override the delimiter ('Nothing'
-                                  --   uses the file extension and sniff result).
-  } deriving (Eq, Show)
-
--- | Default loading options: no skip, no comment char, header expected,
--- non-strict, sniff enabled, no delimiter override.
-defaultLoadOpts :: LoadOpts
-defaultLoadOpts = LoadOpts 0 Nothing False False True Nothing
-
--- | Run 'loadAutoSafe' with the given @LoadOpts@. When @skip@,
--- @comment@ and @noHeader@ are all unset the file is read directly;
--- otherwise the request is realized by writing to a temporary file
--- 前処理結果を書き出してから読む。
---
--- 'loSniff' が True (デフォルト) のときは、ユーザ未指定の項目に限り
--- 'Hanalyze.DataIO.Sniff.sniffBytes' の結果で自動補完する:
---
--- * 'loSkip == 0' なら sniff の skip 値で上書き
--- * 'loComment == Nothing' なら sniff のコメント文字で上書き
--- * 'loNoHeader == False' で sniff が「ヘッダ無し」を強く示唆したら上書き
---
--- 自動推論で値が変わったときは I013 (Info コード) として LogReport に残す。
-loadAutoSafeWith
-  :: LoadOpts -> FilePath
-  -> IO (Either ParseError (Loaded DXD.DataFrame))
-loadAutoSafeWith opts0 path = do
-  -- Sniff: 必要なら冒頭バイト列を読んでオプションを補完する
-  (opts, sniffLog) <- if loSniff opts0
-    then do
-      eRaw <- try (BS.readFile path) :: IO (Either SomeException BS.ByteString)
-      case eRaw of
-        Left _    -> return (opts0, mempty)
-        Right raw -> return (applySniff opts0 (Sniff.sniffBytes (BS.take 8192 raw)))
-    else return (opts0, mempty)
-  if needRewrite opts
-    then withRewritten opts path (\p extra -> go opts p (sniffLog <> extra))
-    else go opts path sniffLog
-  where
-    go effOpts p extraLog = do
-      -- delimiter 指定があれば Hackage の readCsvWithOpts を使う
-      r <- case loDelim effOpts of
-        Nothing -> loadAutoSafe p
-        Just c  -> loadCsvWithDelim c p
-      case r of
-        Left  e        -> return (Left e)
-        Right (df, lg) ->
-          let lg' = extraLog <> lg
-          in if loStrict opts0 && hasWarnings lg'
-               then return $ Left
-                      ("strict: 警告が発生しました ("
-                         <> show (length (entries lg'))
-                         <> " 件)。--strict を外すか、--skip / --comment / --no-header / --no-sniff で対処してください。")
-               else return (Right (df, lg'))
-
--- | 指定 delimiter で CSV を読み、loadAutoSafe 同等の Loaded を返す。
-loadCsvWithDelim
-  :: Char -> FilePath -> IO (Either ParseError (Loaded DXD.DataFrame))
-loadCsvWithDelim c path = do
-  pre <- preflight path
-  case pre of
-    Left e  -> return (Left e)
-    Right rs -> do
-      let opts = DXIO.defaultReadOptions { DXIO.columnSeparator = c }
-      r <- try (DXIO.readCsvWithOpts opts path >>= evaluate)
-             :: IO (Either SomeException DXD.DataFrame)
-      return $ case r of
-        Left  e  -> Left (cleanError (show e))
-        Right df -> Right (df, inspectWithPreview (previewBytes rs) df)
-
--- | sniff 結果を @LoadOpts@ に反映する。ユーザ指定がある項目 (>0 / Just /
--- True) は尊重し、未指定のところだけ書き換える。書き換えた項目は
--- I013 ログに残す。
-applySniff :: LoadOpts -> Sniff.Sniff -> (LoadOpts, LogReport)
-applySniff o s =
-  let (skip', noteSkip)   =
-        if loSkip o == 0 && Sniff.sfSkip s > 0
-          then (Sniff.sfSkip s,
-                Just $ "先頭 " <> tShow (Sniff.sfSkip s) <> " 行を skip (sniff)")
-          else (loSkip o, Nothing)
-      (comm', noteComm)  =
-        case (loComment o, Sniff.sfCommentChar s) of
-          (Nothing, Just c) -> (Just c,
-                                Just $ "コメント文字 '" <> T.singleton c <> "' を採用 (sniff)")
-          _                 -> (loComment o, Nothing)
-      (nohd', noteHdr)   =
-        if not (loNoHeader o) && not (Sniff.sfHasHeader s)
-          then (True,
-                Just "ヘッダ無しと推論 (sniff): col0... を生成")
-          else (loNoHeader o, Nothing)
-      (delim', noteDelim) =
-        case (loDelim o, Sniff.sfDelim s) of
-          (Nothing, c) | c /= ',' ->
-            (Just c, Just $ "delimiter '" <> T.singleton c <> "' を採用 (sniff)")
-          _                       -> (loDelim o, Nothing)
-      lg = mconcat
-             [ logReport (mkInfo "I013" m Nothing)
-             | Just m <- [noteSkip, noteComm, noteHdr, noteDelim]
-             ]
-  in (o { loSkip = skip', loComment = comm', loNoHeader = nohd'
-        , loDelim = delim' }, lg)
-
-needRewrite :: LoadOpts -> Bool
-needRewrite o = loSkip o > 0
-             || loComment o /= Nothing
-             || loNoHeader o
-
--- | 前処理 (skip / comment / no-header) を施した一時ファイルを作って
--- アクションに渡す。withSystemTempFile で自動クリーンアップ。
-withRewritten
-  :: LoadOpts -> FilePath
-  -> (FilePath -> LogReport -> IO (Either ParseError (Loaded DXD.DataFrame)))
-  -> IO (Either ParseError (Loaded DXD.DataFrame))
-withRewritten opts path act = do
-  raw <- BS.readFile path
-  let (rewritten, plog) = rewriteContent opts raw
-  Tmp.withSystemTempFile "ha-rewrite-.csv" $ \tmp h -> do
-    BS.hPut h rewritten
-    hClose h
-    act tmp plog
-
--- | LoadOpts に従ってバイト列を変換し、変換ログを返す。
-rewriteContent :: LoadOpts -> BS.ByteString -> (BS.ByteString, LogReport)
-rewriteContent opts bs0 =
-  let nl       = fromIntegral (ord '\n')
-      bs       = stripBOM bs0
-      rawLines = BS.split nl bs
-      lines0   = map stripCR rawLines
-      (afterSkip, skippedNote) =
-        if loSkip opts > 0
-          then ( drop (loSkip opts) lines0
-               , logReport (mkInfo "I010"
-                   ("先頭 " <> tShow (loSkip opts) <> " 行を skip しました。")
-                   Nothing))
-          else (lines0, mempty)
-      (afterComment, commentNote) = case loComment opts of
-        Just ch ->
-          let chBy = fromIntegral (ord ch)
-              isC l = case BS.uncons (BS.dropWhile (== fromIntegral (ord ' ')) l) of
-                        Just (c, _) -> c == chBy
-                        Nothing     -> False
-              kept   = filter (not . isC) afterSkip
-              dropped = length afterSkip - length kept
-          in if dropped > 0
-               then ( kept
-                    , logReport (mkInfo "I011"
-                        ("コメント文字 '" <> T.singleton ch
-                            <> "' で始まる行を " <> tShow dropped <> " 件 skip しました。")
-                        Nothing))
-               else (kept, mempty)
-        Nothing -> (afterSkip, mempty)
-      (afterHeader, headerNote) =
-        if loNoHeader opts
-          then case dropWhile BS.null afterComment of
-                 [] -> (afterComment, mempty)
-                 (firstRow:_) ->
-                   let nCols = length (BS.split (fromIntegral (ord ',')) firstRow)
-                       hdr   = BS.intercalate ","
-                                 [ TE.encodeUtf8 (T.pack ("col" ++ show i))
-                                 | i <- [0 .. nCols - 1] ]
-                       lg = logReport (mkInfo "I012"
-                              ("--no-header: ヘッダ "
-                                 <> tShow nCols
-                                 <> " 列 (col0...) を生成しました。")
-                              Nothing)
-                   in (hdr : afterComment, lg)
-          else (afterComment, mempty)
-      out      = BS.intercalate (BS.singleton nl) afterHeader
-      logTotal = skippedNote <> commentNote <> headerNote
-  in (out, logTotal)
-
-tShow :: Show a => a -> Text
-tShow = T.pack . show
diff --git a/src/Hanalyze/DataIO/Clean.hs b/src/Hanalyze/DataIO/Clean.hs
deleted file mode 100644
--- a/src/Hanalyze/DataIO/Clean.hs
+++ /dev/null
@@ -1,285 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeApplications #-}
--- |
--- Module      : Hanalyze.DataIO.Clean
--- Description : Health check の警告を数値化ルールへ変換する列単位クリーニング DSL
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Column-level cleaning DSL.
---
--- Health checks ('Hanalyze.DataIO.Health') only emit warnings for columns
--- containing currency symbols, thousands separators, units, or alternate
--- decimal points. This module turns each warning into an explicit rule
--- ('ColumnRule') that converts the column into a numeric column.
---
--- Design notes:
---
---   * Each rule has the shape "extract a Text column → transform →
---     write back into the DataFrame", and always returns a transformation
---     log (@LogReport@).
---   * Cells that fail to convert are stored as 'Nothing' (the null
---     bitmap). The number of failures is recorded as I100-series Info
---     codes in the log.
---   * 'cleanPipeline' applies multiple rules in sequence.
--- * Phase B の自動推論との二段構え: sniff で読み込みは通るがセル値が
---   text のままになる #08 / #16 を、Clean で数値化して回帰可能にする。
---
--- 主要ルール
---
--- * 'StripUnits'      末尾の英字を取り除いて Double 化 (\"12.3kg\" → 12.3)
--- * 'ParseCurrency'   通貨記号 / 桁区切り (@$@/@¥@/@€@/@,@) を除去して数値化
--- * 'ParseDecimalEU'  decimal separator が ',' (EU style) のセルを Double 化
--- * 'TrimText'        前後の空白を除く
--- * 'CoerceNumeric'   上記 3 種を順に試して最初に成功した変換を採用
--- * @DedupeColumns@   重複列名に @_2@ などのサフィックスを付ける
--- * @FillBlankNames@  空列名を @col0@ 等で埋める
-module Hanalyze.DataIO.Clean
-  ( -- * 型
-    ColumnRule (..)
-    -- * Single-rule operators
-  , applyRule
-  , stripUnitsCol
-  , parseCurrencyCol
-  , parseDecimalEUCol
-  , trimTextCol
-  , coerceNumericCol
-    -- * Pipeline
-  , cleanPipeline
-    -- * DataFrame-level operations
-  , dedupeColumns
-  , fillBlankNames
-  ) where
-
-import qualified DataFrame.Internal.Column    as DX
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified DataFrame.Operations.Core     as DX
-import qualified DataFrame.Internal.DataFrame as DXD
-import qualified DataFrame.Internal.Column    as DXC
-
-import Data.Char (isAlpha, isDigit)
-import qualified Data.Map.Strict      as Map
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import Text.Read (readMaybe)
-
-import Hanalyze.DataIO.Convert (getMaybeTextVec)
-import Hanalyze.DataIO.Log     (LogReport, mkInfo, mkWarn, logReport, noLog)
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
--- | A single column-cleaning rule.
-data ColumnRule
-  = StripUnits     -- ^ Strip trailing alphabetic suffix and parse
-                   --   (@\"12.3kg\" → 12.3@).
-  | ParseCurrency  -- ^ Parse currency-like strings such as @\"$1,234.56\"@
-                   --   or @\"¥10,000\"@ into 'Double'.
-  | ParseDecimalEU -- ^ Decimal point as @\",\"@ (@\"3,14\" → 3.14@).
-  | TrimText       -- ^ Strip surrounding whitespace; column stays as 'Text'.
-  | CoerceNumeric  -- ^ Try @StripUnits@, then @ParseCurrency@, then
-                   --   @ParseDecimalEU@ in that order.
-  deriving (Eq, Show)
-
--- ---------------------------------------------------------------------------
--- 個別ルール (列名指定)
--- ---------------------------------------------------------------------------
-
--- | Apply a single 'ColumnRule' to a single named column.
-applyRule :: ColumnRule -> Text -> DXD.DataFrame -> (DXD.DataFrame, LogReport)
-applyRule r name df = case r of
-  StripUnits     -> stripUnitsCol     name df
-  ParseCurrency  -> parseCurrencyCol  name df
-  ParseDecimalEU -> parseDecimalEUCol name df
-  TrimText       -> trimTextCol       name df
-  CoerceNumeric  -> coerceNumericCol  name df
-
--- | Drop a trailing alphabetic unit suffix and parse the prefix as
--- 'Double' (e.g. @\"12.3kg\"@, @\"11.5cm\"@).
-stripUnitsCol :: Text -> DXD.DataFrame -> (DXD.DataFrame, LogReport)
-stripUnitsCol = liftCellRule "I100" "stripUnits" $ \t ->
-  let s = T.strip t
-      (digits, rest) = T.span (\c -> isDigit c || c == '.' || c == '-') s
-      suffix = T.takeWhile isAlpha rest
-  in if T.null digits
-       then Nothing
-       else if T.null suffix
-              then readMaybe (T.unpack digits)
-              else readMaybe (T.unpack digits)
-
--- | Parse a currency-formatted string (with currency symbol and
--- thousands separators) into 'Double': @\"$1,234.56\" → 1234.56@.
-parseCurrencyCol :: Text -> DXD.DataFrame -> (DXD.DataFrame, LogReport)
-parseCurrencyCol = liftCellRule "I101" "parseCurrency" $ \t ->
-  let s1 = T.strip t
-      s2 = T.dropWhile (`elem` ("$¥€£" :: String)) s1
-      s3 = T.replace "," "" s2
-  in readMaybe (T.unpack s3)
-
--- | EU-style decimal separator @\",\"@: @\"3,14\" → 3.14@.
-parseDecimalEUCol :: Text -> DXD.DataFrame -> (DXD.DataFrame, LogReport)
-parseDecimalEUCol = liftCellRule "I102" "parseDecimalEU" $ \t ->
-  let s = T.replace "," "." (T.strip t)
-  in readMaybe (T.unpack s)
-
--- | Strip surrounding whitespace and write back as a 'Text' column.
-trimTextCol :: Text -> DXD.DataFrame -> (DXD.DataFrame, LogReport)
-trimTextCol name df = case getMaybeTextVec name df of
-  Nothing -> (df, logReport (mkWarn "I103W"
-                  ("trimText: 列 '" <> name <> "' を text として取り出せません。")
-                  Nothing))
-  Just v  ->
-    let trimmed = V.map (fmap T.strip) v
-        out     = V.toList trimmed
-    in ( DX.insertColumn name (DX.fromList (out :: [Maybe Text])) df
-       , logReport (mkInfo "I103" ("trimText 適用: 列 '" <> name <> "'") Nothing))
-
--- | Catch-all numeric coercion: try @StripUnits@, then
--- @ParseCurrency@, then @ParseDecimalEU@ in order. The first successful
--- conversion wins; a cell that fails every rule is stored as a null
--- (via the null bitmap).
-coerceNumericCol :: Text -> DXD.DataFrame -> (DXD.DataFrame, LogReport)
-coerceNumericCol = liftCellRule "I104" "coerceNumeric" $ \t ->
-  let candidates =
-        [ \s -> let s' = T.strip s
-                in readMaybe (T.unpack s') :: Maybe Double
-        , \s -> -- StripUnits 風
-            let s' = T.strip s
-                (digits, _) = T.span (\c -> isDigit c || c == '.' || c == '-') s'
-            in if T.null digits then Nothing
-               else readMaybe (T.unpack digits)
-        , \s -> -- ParseCurrency 風
-            let s1 = T.strip s
-                s2 = T.dropWhile (`elem` ("$¥€£" :: String)) s1
-                s3 = T.replace "," "" s2
-            in readMaybe (T.unpack s3)
-        , \s -> -- ParseDecimalEU 風
-            let s' = T.replace "," "." (T.strip s)
-            in readMaybe (T.unpack s')
-        ]
-      tryAll [] = Nothing
-      tryAll (f : fs) = case f t of
-        Just x  -> Just x
-        Nothing -> tryAll fs
-  in tryAll candidates
-
--- ---------------------------------------------------------------------------
--- 共通ヘルパ: text → Maybe Double 変換を 1 列に適用
--- ---------------------------------------------------------------------------
-
--- | Helper: apply an arbitrary @text → 'Maybe Double'@ converter to a
--- single column. If the column cannot be read as Text, the DataFrame
--- is returned unchanged with a warning log entry.
-liftCellRule
-  :: Text                           -- ^ Info code.
-  -> Text                           -- ^ Rule name (for the log).
-  -> (Text -> Maybe Double)         -- ^ Cell converter.
-  -> Text                           -- ^ 列名
-  -> DXD.DataFrame
-  -> (DXD.DataFrame, LogReport)
-liftCellRule code rule fn name df = case getMaybeTextVec name df of
-  Nothing ->
-    ( df
-    , logReport (mkWarn (code <> "W")
-        (rule <> ": 列 '" <> name <> "' を text として取り出せません。")
-        (Just "数値列に対しては不要かもしれません。"))
-    )
-  Just v  ->
-    let raw       = V.toList v
-            -- raw :: [Maybe Text]
-        processed = [ mt >>= (\t -> if isMissing t then Nothing else fn t)
-                    | mt <- raw ]
-        nIn  = length raw
-        nOk  = length [ () | Just _ <- processed ]
-        nMis = length [ () | Just t <- raw, isMissing t ]
-        df'  = DX.insertColumn name
-                   (DX.fromList (processed :: [Maybe Double])) df
-        msg  = rule <> " 適用: 列 '" <> name <> "' "
-                  <> tShow nOk <> "/" <> tShow nIn <> " 成功"
-                  <> (if nMis > 0 then " (NA " <> tShow nMis <> ")" else "")
-        lg   = logReport (mkInfo code msg Nothing)
-        warnLog = if nOk * 2 < nIn  -- 半数未満しか成功していない場合
-                    then logReport (mkWarn (code <> "L")
-                            (rule <> ": 列 '" <> name <> "' は変換成功率が低いです (" <> tShow nOk <> "/" <> tShow nIn <> ")")
-                            (Just "別ルールを試すか、データを確認してください。"))
-                    else noLog
-    in (df', lg <> warnLog)
-
-isMissing :: Text -> Bool
-isMissing t = T.null (T.strip t)
-
-tShow :: Show a => a -> Text
-tShow = T.pack . show
-
--- ---------------------------------------------------------------------------
--- パイプライン
--- ---------------------------------------------------------------------------
-
--- | Apply several rules in order, concatenating the per-rule logs.
-cleanPipeline
-  :: [(Text, ColumnRule)]
-  -> DXD.DataFrame
-  -> (DXD.DataFrame, LogReport)
-cleanPipeline []           df = (df, noLog)
-cleanPipeline ((n, r):rs) df0 =
-  let (df1, lg1) = applyRule r n df0
-      (df2, lg2) = cleanPipeline rs df1
-  in (df2, lg1 <> lg2)
-
--- ---------------------------------------------------------------------------
--- DataFrame レベル操作
--- ---------------------------------------------------------------------------
-
--- | Disambiguate duplicate column names by appending @_2@, @_3@, ...
--- (Hackage の DataFrame は重複列を後勝ちでマージするため、ロード前に
--- 行いたい場合は CSV テキスト側で。本関数はロード後の DataFrame に対して
--- 行う suffix 付与で、新しい DataFrame を返す。)
-dedupeColumns :: DXD.DataFrame -> (DXD.DataFrame, LogReport)
-dedupeColumns df =
-  let names = DX.columnNames df
-      go acc [] = reverse acc
-      go acc (x:xs) =
-        let used = Map.fromListWith (+) [(y, 1 :: Int) | y <- acc]
-            n0   = Map.findWithDefault 0 x used
-        in if n0 == 0 then go (x:acc) xs
-                      else go ((x <> "_" <> tShow (n0 + 1)):acc) xs
-      newNames = go [] names
-      changed  = [ (a, b) | (a, b) <- zip names newNames, a /= b ]
-  in if null changed
-       then (df, noLog)
-       else
-         let df' = foldl rename df (zip names newNames)
-             rename d (old, new)
-               | old == new = d
-               | otherwise  = DX.rename old new d
-         in ( df'
-            , logReport (mkInfo "I105"
-                ("重複列名に suffix を付与: "
-                   <> T.intercalate ", "
-                       [ a <> " → " <> b | (a, b) <- changed ])
-                Nothing))
-
--- | 空列名を col0 / col1 / ... で埋める。
-fillBlankNames :: DXD.DataFrame -> (DXD.DataFrame, LogReport)
-fillBlankNames df =
-  let names = DX.columnNames df
-      replaceBlank i n
-        | T.null (T.strip n) = "col" <> tShow i
-        | otherwise          = n
-      newNames = zipWith replaceBlank [0 :: Int ..] names
-      changed  = [ (a, b) | (a, b) <- zip names newNames, a /= b ]
-  in if null changed
-       then (df, noLog)
-       else
-         let df' = foldl rn df (zip names newNames)
-             rn d (old, new) | old == new = d | otherwise = DX.rename old new d
-         in ( df'
-            , logReport (mkInfo "I106"
-                ("空列名を埋めました: " <> tShow (length changed) <> " 列")
-                Nothing))
-
--- 未使用 warning 抑止
-_unused :: DXC.Column -> ()
-_unused _ = ()
diff --git a/src/Hanalyze/DataIO/Convert.hs b/src/Hanalyze/DataIO/Convert.hs
deleted file mode 100644
--- a/src/Hanalyze/DataIO/Convert.hs
+++ /dev/null
@@ -1,79 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeApplications #-}
-{-# LANGUAGE ScopedTypeVariables #-}
--- |
--- Module      : Hanalyze.DataIO.Convert
--- Description : Hackage dataframe から数値/Text 列を安全に Vector へ抽出する変換層
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Safe extraction of numeric / Text vectors from a Hackage @dataframe@
--- ('DXD.DataFrame'). Used widely across @Model.*@ and @Viz.*@.
---
---   * 'getDoubleVec' — normalize Double / Int / Integer / Maybe Double /
---     Maybe Int / Maybe Integer / Text columns to @V.Vector Double@. Text values are parsed; if any
---     missing slot is present (null bitmap or NA string), returns
---     'Nothing' so model fits cannot crash on missing data.
---   * 'getTextVec'   — extract a Text column. Returns 'Nothing' on type
---     mismatch.
-module Hanalyze.DataIO.Convert
-  ( getDoubleVec
-  , getTextVec
-  , getMaybeTextVec
-  ) where
-
-import qualified DataFrame.Operators           as DX
-import qualified DataFrame.Operations.Core     as DX
-import qualified DataFrame.Internal.Column    as DXC
-import qualified DataFrame.Internal.DataFrame as DXD
-
-import Control.DeepSeq (NFData, force)
-import Control.Exception (SomeException, try, evaluate)
-import Data.Text (Text)
-import qualified Data.Vector as V
-import System.IO.Unsafe (unsafePerformIO)
-
-import Hanalyze.DataIO.Preprocess (readMaybeDoubleColumn)
-
--- | Extract a numeric column as 'V.Vector Double'. Returns 'Nothing'
--- when any cell is missing or fails to parse.
-getDoubleVec :: Text -> DXD.DataFrame -> Maybe (V.Vector Double)
-getDoubleVec name df = do
-  xs <- readMaybeDoubleColumn name df
-  vs <- sequence xs
-  return (V.fromList vs)
-
--- | Extract a Text column as 'V.Vector Text'. Returns 'Nothing' if any
--- slot has its null bit set, guaranteeing the result holds only proper
--- strings.
-getTextVec :: Text -> DXD.DataFrame -> Maybe (V.Vector Text)
-getTextVec name df = case tryColumnAsList @Text name df of
-  Just xs -> Just (V.fromList xs)
-  Nothing -> Nothing
-
--- | Extract a Text column as 'V.Vector (Maybe Text)'. Null cells become
--- 'Nothing' instead of failing. Useful for inspecting columns where
--- 'getTextVec' would return 'Nothing' (e.g. for @info@ display).
-getMaybeTextVec :: Text -> DXD.DataFrame -> Maybe (V.Vector (Maybe Text))
-getMaybeTextVec name df =
-  case tryColumnAsList @(Maybe Text) name df of
-    Just xs -> Just (V.fromList xs)
-    Nothing -> case tryColumnAsList @Text name df of
-      Just xs -> Just (V.fromList (map Just xs))
-      Nothing -> Nothing
-
--- | 'DX.columnAsList' を例外セーフに呼び出す。型不一致 / null 要素アクセス
--- (Hackage が内部で 'error "fromMaybeVec: Nothing slot"' を投げるケース等)
--- でも 'Nothing' を返す。
---
--- 重要: 'evaluate' は WHNF までしか評価しないので、リスト要素に潜む 'error'
--- が逃げてくる。'force' を挟んで NF まで詰めてから捕捉する。
-tryColumnAsList
-  :: forall a. (DXC.Columnable a, NFData a)
-  => Text -> DXD.DataFrame -> Maybe [a]
-tryColumnAsList name df = unsafePerformIO $ do
-  r <- try (evaluate (force (DX.columnAsList (DX.col @a name) df)))
-         :: IO (Either SomeException [a])
-  return $ case r of
-    Right xs -> Just xs
-    Left _   -> Nothing
diff --git a/src/Hanalyze/DataIO/External.hs b/src/Hanalyze/DataIO/External.hs
deleted file mode 100644
--- a/src/Hanalyze/DataIO/External.hs
+++ /dev/null
@@ -1,38 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.DataIO.External
--- Description : Hackage dataframe 経由の Parquet / JSON 外部フォーマットローダ
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- External data-format loaders (Parquet / JSON) via the Hackage
--- @dataframe@ library.
---
--- Returns Hackage's 'DataFrame.Internal.DataFrame.DataFrame' directly.
--- For CSV and TSV use 'Hanalyze.DataIO.CSV.loadCSV' / @loadTSV@ instead.
-module Hanalyze.DataIO.External
-  ( loadParquet
-  , loadJSON
-  ) where
-
-import qualified DataFrame.IO.Parquet          as DX
-import qualified DataFrame.Internal.DataFrame as DXD
-import qualified DataFrame.IO.JSON            as DXJ
-
-import Control.Exception (SomeException, try)
-
--- | Load an Apache Parquet file (columnar, supports compression).
-loadParquet :: FilePath -> IO (Either String DXD.DataFrame)
-loadParquet = loadRaw DX.readParquet
-
--- | Load a JSON file in records-of-objects format.
-loadJSON :: FilePath -> IO (Either String DXD.DataFrame)
-loadJSON = loadRaw DXJ.readJSON
-
-loadRaw :: (FilePath -> IO DXD.DataFrame)
-        -> FilePath -> IO (Either String DXD.DataFrame)
-loadRaw reader path = do
-  result <- try (reader path) :: IO (Either SomeException DXD.DataFrame)
-  return $ case result of
-    Left  e  -> Left ("External loader failed: " ++ show e)
-    Right df -> Right df
diff --git a/src/Hanalyze/DataIO/Health.hs b/src/Hanalyze/DataIO/Health.hs
deleted file mode 100644
--- a/src/Hanalyze/DataIO/Health.hs
+++ /dev/null
@@ -1,405 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.DataIO.Health
--- Description : 読み込み済み DataFrame の疑わしいパターンを警告コード (W001〜W008) として検出
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- DataFrame health check. Surfaces the "looks suspicious" patterns that
--- can hide in a successfully-loaded DataFrame, as warning codes.
---
--- Codes detected:
---
---   * @W001@ — header is suspect (all column names parse as numbers).
---   * @W003@ — ragged: per-column lengths differ (Hackage normally pads,
---     but we double-check).
---   * @W004@ — duplicate / empty / surrounding-whitespace column names.
---   * @W005@ — delimiter mismatch: single-column DataFrame whose values
---     contain another delimiter candidate.
---   * @W006@ — heterogeneous mix of NA strings.
---   * @W007@ — unit suffix inferred (most cells in a Text column match
---     @^\\d+\\.?\\d*[a-zA-Z]+$@).
---   * @W008@ — currency or thousand-separator suspect.
---
--- Auxiliary checks that need a raw-byte preview are in
--- 'inspectWithPreview'.
--- それ以外は 'inspectDataFrame' で DataFrame だけから判定可能。
---
--- 利用シナリオ:
---
--- @
--- (df, lg0) <- loadAutoSafe path
--- let lg = lg0 <> inspectDataFrame df
--- printLogReport lg
--- @
-module Hanalyze.DataIO.Health
-  ( inspectDataFrame
-  , inspectWithPreview
-  , detectHeaderless
-  , detectDuplicateBlankNames
-  , detectMixedNAStrings
-  , detectUnitSuffix
-  , detectThousandsCurrency
-  , detectDelimiterMismatch
-  , detectCommentLines
-  , detectRagged
-  ) where
-
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified DataFrame.Operations.Core     as DX
-import qualified DataFrame.Internal.Column    as DXC
-import qualified DataFrame.Internal.DataFrame as DXD
-
-import qualified Data.ByteString      as BS
-import qualified Data.Map.Strict      as Map
-import Data.Char (isDigit, isAlpha, ord)
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import Text.Read (readMaybe)
-
-import Hanalyze.DataIO.Log (LogEntry, LogReport, mkWarn, logReport, noLog)
-import Hanalyze.DataIO.Convert (getMaybeTextVec)
-import Hanalyze.DataIO.Preprocess (isNAString)
-
--- ---------------------------------------------------------------------------
--- 公開エントリポイント
--- ---------------------------------------------------------------------------
-
--- | Aggregate every W-code that can be checked from the DataFrame
--- alone (without the source bytes).
-inspectDataFrame :: DXD.DataFrame -> LogReport
-inspectDataFrame df = mconcat
-  [ detectHeaderless df
-  , detectDuplicateBlankNames df
-  , detectRagged df
-  , detectMixedNAStrings df
-  , detectUnitSuffix df
-  , detectThousandsCurrency df
-  ]
-
--- | DataFrame plus a leading raw-byte preview, used for the W-codes
--- that need both inputs (e.g. W005 delimiter
--- ミスマッチ / W004 ヘッダ行レベルの重複) も合わせて返す。
-inspectWithPreview :: BS.ByteString -> DXD.DataFrame -> LogReport
-inspectWithPreview preview df = mconcat
-  [ inspectDataFrame df
-  , detectDelimiterMismatch preview df
-  , detectRawHeaderIssues preview df
-  , detectCommentLines preview
-  ]
-
--- ---------------------------------------------------------------------------
--- W002 コメント行 (#/!/// 等で始まる先頭行)
--- ---------------------------------------------------------------------------
-
-detectCommentLines :: BS.ByteString -> LogReport
-detectCommentLines preview =
-  let ls = take 8 (BS.split (fromIntegral (ord '\n')) preview)
-      isComment l =
-        case BS.uncons (BS.dropWhile (\c -> c == fromIntegral (ord ' ')
-                                          || c == fromIntegral (ord '\t')) l) of
-          Just (c, _) -> c `elem` map (fromIntegral . ord) (['#', '!'] :: String)
-          Nothing     -> False
-      n = length (filter isComment ls)
-  in if n > 0
-       then logReport
-              (mkWarn "W002"
-                 ("先頭付近に "
-                    <> T.pack (show n)
-                    <> " 件のコメント風行 (# / ! 始まり) を検出。")
-                 (Just "--skip N でコメント行数を読み飛ばすか、--comment '#' を指定してください。"))
-       else noLog
-
--- | 原本ヘッダ行 (先頭行) を見て、列数 / 重複 / 空セルが DataFrame と
--- 食い違っていないかをチェックする。Hackage は読込時に重複列を後勝ちで
--- 黙ってマージするため、ここで原本側を走査して気付く必要がある。
-detectRawHeaderIssues :: BS.ByteString -> DXD.DataFrame -> LogReport
-detectRawHeaderIssues preview df =
-  case takeFirstLine preview of
-    Nothing -> noLog
-    Just hdrLine ->
-      let -- まずは comma 区切りで素朴に分割 (TSV/SSV では別 delimiter だが、
-          -- W005 で別途検出されるので OK)
-          rawCells = T.splitOn "," (decodeAscii hdrLine)
-          rawTrim  = map T.strip rawCells
-          dups     = findDups rawTrim
-          blanks   = filter T.null rawTrim
-          dfCols   = DX.columnNames df
-          missing  = length rawTrim - length dfCols
-      in mconcat
-           [ if null dups then noLog
-               else logReport
-                      (mkWarn "W004"
-                         ("原本ヘッダに重複列名: "
-                            <> T.intercalate ", " dups
-                            <> " — 後勝ちでマージされ、データの一部が消失している恐れがあります。")
-                         (Just "重複を解消した CSV を渡すか、コピー前の原本を確認してください。"))
-           , if null blanks then noLog
-               else logReport
-                      (mkWarn "W004"
-                         ("原本ヘッダに空セルが "
-                            <> T.pack (show (length blanks))
-                            <> " 件。匿名列として扱われます。")
-                         (Just "ヘッダ行のフォーマットを見直してください。"))
-           , if missing > 0 && not (null dfCols)
-               then logReport
-                      (mkWarn "W004"
-                         ("原本ヘッダ列数 (" <> T.pack (show (length rawTrim))
-                            <> ") と DataFrame 列数 (" <> T.pack (show (length dfCols))
-                            <> ") が不一致 — 列がマージ/欠落している可能性。")
-                         (Just "列名のフォーマットを確認してください。"))
-               else noLog
-           ]
-
-takeFirstLine :: BS.ByteString -> Maybe BS.ByteString
-takeFirstLine bs =
-  case BS.split (fromIntegral (ord '\n')) bs of
-    (l:_) | not (BS.null l) -> Just l
-    _                       -> Nothing
-
-decodeAscii :: BS.ByteString -> Text
-decodeAscii = T.pack . map (toEnum . fromIntegral) . BS.unpack
-
-findDups :: Ord a => [a] -> [a]
-findDups xs =
-  let cnt = Map.fromListWith (+) [(x, 1 :: Int) | x <- xs]
-  in [ x | (x, k) <- Map.toList cnt, k > 1 ]
-
--- ---------------------------------------------------------------------------
--- W001 ヘッダ無し疑い
--- ---------------------------------------------------------------------------
-
--- | 全列名が Double として parse できるなら、先頭行が data 行だった可能性が高い。
-detectHeaderless :: DXD.DataFrame -> LogReport
-detectHeaderless df =
-  let names = DX.columnNames df
-      allNumeric = not (null names)
-                && all (\n -> case readMaybe (T.unpack n) :: Maybe Double of
-                                Just _  -> True
-                                Nothing -> False) names
-  in if allNumeric
-       then logReport
-              (mkWarn "W001"
-                 ("列名が全て数値です: "
-                    <> T.intercalate ", " names
-                    <> " — ヘッダ行が無いファイルの可能性。")
-                 (Just "ヘッダ無しなら --no-header を指定してください。"))
-       else noLog
-
--- ---------------------------------------------------------------------------
--- W003 ragged (列ごとに非 null セル数が大きく異なる)
--- ---------------------------------------------------------------------------
-
--- | DataFrame の各列について、null 以外のセル数を求め、最大と最小の差が
--- 全行数の 1/3 を超えていたら警告。Hackage は ragged 行を null bitmap で
--- 補うため、この差で間接的に検出できる。
-detectRagged :: DXD.DataFrame -> LogReport
-detectRagged df =
-  let names    = DX.columnNames df
-      (nrows, _) = DX.dimensions df
-      -- 列内の null bitmap を直接走査して非 null セル数を求める。
-      -- これにより数値 / Text を問わず使える。
-      nonNullN n = case DXD.getColumn n df of
-        Nothing -> nrows
-        Just c  ->
-          let len = DXC.columnLength c
-          in length [ () | i <- [0 .. len - 1]
-                         , not (DXC.columnElemIsNull c i) ]
-      counts = [ (n, nonNullN n) | n <- names ]
-  in case counts of
-       [] -> noLog
-       _  ->
-         let mx = maximum (map snd counts)
-             mn = minimum (map snd counts)
-             gap = mx - mn
-             worst = [ n | (n, k) <- counts, k == mn ]
-         in if nrows >= 6 && gap > 0 && gap * 3 >= nrows
-              then logReport
-                     (mkWarn "W003"
-                        ("列ごとの非 null セル数に乖離: "
-                           <> T.pack (show mn) <> "..." <> T.pack (show mx)
-                           <> " (差 " <> T.pack (show gap) <> "); "
-                           <> "短い列: " <> T.intercalate ", " worst)
-                        (Just "ragged な行 (列数が揃っていない) の可能性。CSV を整形してください。"))
-              else noLog
-
--- ---------------------------------------------------------------------------
--- W004 重複 / 空 / 前後空白の列名
--- ---------------------------------------------------------------------------
-
-detectDuplicateBlankNames :: DXD.DataFrame -> LogReport
-detectDuplicateBlankNames df =
-  let names = DX.columnNames df
-      blanks = [ n | n <- names, T.null (T.strip n) ]
-      trimmedDiffer = [ n | n <- names, n /= T.strip n ]
-      grouped = Map.fromListWith (+) [(n, 1 :: Int) | n <- names]
-      dups = [ n | (n, k) <- Map.toList grouped, k > 1 ]
-      mk code msg hint = logReport (mkWarn code msg hint)
-  in mconcat
-       [ if null blanks then noLog
-           else mk "W004"
-                  ("空または空白のみの列名が "
-                     <> T.pack (show (length blanks))
-                     <> " 件あります。")
-                  (Just "ヘッダ行に空セルがある可能性。--skip N で読み飛ばすか、--no-header をお試しください。")
-       , if null trimmedDiffer then noLog
-           else mk "W004"
-                  ("前後に空白を持つ列名: "
-                     <> T.intercalate ", " (map (T.pack . show) trimmedDiffer))
-                  (Just "Hanalyze.DataIO.Preprocess.renameColumn でリネームできます。")
-       , if null dups then noLog
-           else mk "W004"
-                  ("重複した列名: "
-                     <> T.intercalate ", " dups
-                     <> " — 後勝ちで一方が消失している恐れがあります。")
-                  (Just "事前に列名を変更するか、CSV を見直してください。")
-       ]
-
--- ---------------------------------------------------------------------------
--- W006 NA 文字列の多型混在
--- ---------------------------------------------------------------------------
-
--- | NA とみなしうる広めの文字列セット。'isNAString' (defaultNAStrings) に
--- 加えて単独の @-@ / @--@ / @.@ も対象にする (検出限定の判定であり、
--- 既存の補完 API の挙動は変えない)。
-isNALike :: Text -> Bool
-isNALike t =
-  isNAString t
-  || (let s = T.strip t in s `elem` ["-", "--", ".", "—"])
-
--- | 1 列の中に異なる NA 表現が 2 種以上混じっていたら警告。
--- DataFrame の null bitmap (= 既に欠損として処理されたセル) と、文字列上に
--- 残っている NA-like トークンを別カウントとして扱う。
-detectMixedNAStrings :: DXD.DataFrame -> LogReport
-detectMixedNAStrings df = mconcat
-  [ checkColumn n
-  | n <- DX.columnNames df
-  ]
-  where
-    checkColumn n = case getMaybeTextVec n df of
-      Nothing -> noLog
-      Just v  ->
-        let cells = V.toList v
-            -- "<null>" を 1 つの形として扱う
-            tokens = [ case mx of
-                         Nothing -> "<null>"
-                         Just x  -> T.toLower (T.strip x)
-                     | mx <- cells
-                     , case mx of
-                         Nothing -> True
-                         Just x  -> isNALike x
-                     ]
-            naSet = Map.fromListWith (+) [ (k, 1 :: Int) | k <- tokens ]
-        in if Map.size naSet >= 2
-             then logReport
-                    (mkWarn "W006"
-                       ("列 " <> T.pack (show n)
-                          <> " に NA 表現が複数種類混在: "
-                          <> T.intercalate ", "
-                              [ k <> "(" <> T.pack (show v') <> ")"
-                              | (k, v') <- Map.toList naSet ])
-                       (Just "Hanalyze.DataIO.Preprocess.imputeMean / dropMissingRows で正規化できます。"))
-             else noLog
-
--- ---------------------------------------------------------------------------
--- W007 単位混入
--- ---------------------------------------------------------------------------
-
--- | text 列で「数字 + 英字サフィックス」のセルが過半なら、単位付きの数値とみなす。
-detectUnitSuffix :: DXD.DataFrame -> LogReport
-detectUnitSuffix df = mconcat
-  [ checkColumn n | n <- DX.columnNames df ]
-  where
-    checkColumn n = case getMaybeTextVec n df of
-      Nothing -> noLog
-      Just v  ->
-        let xs = [ x | Just x <- V.toList v, not (isNAString x) ]
-            n0 = length xs
-            hits = length (filter looksLikeUnitNumber xs)
-        in if n0 >= 2 && hits * 2 >= n0
-             then logReport
-                    (mkWarn "W007"
-                       ("列 " <> T.pack (show n)
-                          <> " は単位付きの数値が混入している可能性 ("
-                          <> T.pack (show hits) <> "/"
-                          <> T.pack (show n0) <> " セル)。")
-                       (Just "Phase C で stripUnits を実装予定。当面は手動で数値化してください。"))
-             else noLog
-
--- | "12.3kg" / "11cm" 等のパターン判定。
-looksLikeUnitNumber :: Text -> Bool
-looksLikeUnitNumber t =
-  let s = T.strip t
-      (digits, rest) = T.span (\c -> isDigit c || c == '.' || c == '-') s
-      suffix = T.takeWhile isAlpha rest
-  in not (T.null digits)
-     && not (T.null suffix)
-     && T.length suffix <= 4
-     && case readMaybe (T.unpack digits) :: Maybe Double of
-          Just _  -> True
-          Nothing -> False
-
--- ---------------------------------------------------------------------------
--- W008 通貨 / 桁区切り
--- ---------------------------------------------------------------------------
-
--- | "$1,234.56" / "1,234" / "¥10,000" 等のパターンを検出。
-detectThousandsCurrency :: DXD.DataFrame -> LogReport
-detectThousandsCurrency df = mconcat
-  [ checkColumn n | n <- DX.columnNames df ]
-  where
-    checkColumn n = case getMaybeTextVec n df of
-      Nothing -> noLog
-      Just v  ->
-        let xs = [ x | Just x <- V.toList v, not (isNAString x) ]
-            n0 = length xs
-            hits = length (filter looksLikeThousands xs)
-        in if n0 >= 2 && hits * 2 >= n0
-             then logReport
-                    (mkWarn "W008"
-                       ("列 " <> T.pack (show n)
-                          <> " に通貨記号 / 桁区切りつき数値の可能性 ("
-                          <> T.pack (show hits) <> "/"
-                          <> T.pack (show n0) <> " セル)。")
-                       (Just "Phase C で parseCurrency を実装予定。"))
-             else noLog
-
-looksLikeThousands :: Text -> Bool
-looksLikeThousands t0 =
-  let t1 = T.strip t0
-      t2 = T.dropWhile (`elem` ("$¥€£" :: String)) t1
-      hasComma = T.any (== ',') t2
-      onlyMoney = T.all (\c -> isDigit c || c == ',' || c == '.' || c == '-') t2
-  in hasComma && onlyMoney
-
--- ---------------------------------------------------------------------------
--- W005 delimiter ミスマッチ
--- ---------------------------------------------------------------------------
-
--- | DataFrame が 1 列だけで、その値に @;@ / @\t@ / @|@ が頻出するなら delimiter
--- 判定がずれた可能性が高い。preview として渡された生バイト列も確認材料にする。
-detectDelimiterMismatch :: BS.ByteString -> DXD.DataFrame -> LogReport
-detectDelimiterMismatch preview df =
-  let nCols = length (DX.columnNames df)
-      candidates = [(';', "セミコロン"), ('\t', "タブ"), ('|', "縦棒")]
-      counts =
-        [ (c, n, ja)
-        | (c, ja) <- candidates
-        , let n = BS.count (fromIntegral (ord c)) preview
-        , n > 0
-        ]
-      heavy = [ (c, n, ja) | (c, n, ja) <- counts, n >= 2 ]
-  in if nCols == 1 && not (null heavy)
-       then logReport
-              (mkWarn "W005"
-                 ("DataFrame が 1 列のみで、生データに "
-                    <> T.intercalate "/" [ ja <> "(" <> T.pack (show n) <> ")"
-                                         | (_,n,ja) <- heavy ]
-                    <> " が含まれます。delimiter が違う可能性。")
-                 (Just "--delim ';'/'\\t'/'|' を試してください。"))
-       else noLog
-
--- 未使用ワーニングを抑える (将来 LogEntry を直接構築する箇所で使う)
-_unused :: LogEntry
-_unused = mkWarn "" "" Nothing
diff --git a/src/Hanalyze/DataIO/Log.hs b/src/Hanalyze/DataIO/Log.hs
deleted file mode 100644
--- a/src/Hanalyze/DataIO/Log.hs
+++ /dev/null
@@ -1,165 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.DataIO.Log
--- Description : データローダ / 前処理が共有する構造化警告・情報メッセージ (LogEntry/LogReport)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Structured warning / informational messaging shared by data loaders
--- and preprocessing.
---
---   * 'LogEntry'        — a single message (severity / code / body / hint).
---   * @LogReport@       — a 'Monoid' wrapper around @[LogEntry]@.
---   * 'Loaded'          — the @(value, log)@ pair returned by every loader.
---   * 'printLogReport'  — stdout pretty printer.
---   * @logEntriesAsHtml@ — adapter for 'Hanalyze.Viz.ReportBuilder'.
---
--- 利用シナリオ:
---
--- @
--- (df, lg) <- loadCsvSafe path  -- :: IO (Either ParseError (Loaded DataFrame))
--- printLogReport lg              -- 警告を端末に出す
--- when (isStrict opts && hasErrors lg) $ exitFailure
--- @
-module Hanalyze.DataIO.Log
-  ( -- * 型
-    Severity (..)
-  , LogEntry (..)
-  , LogReport
-  , Loaded
-    -- * Construction
-  , mkInfo
-  , mkWarn
-  , mkErr
-  , addEntry
-  , logReport
-  , noLog
-    -- * Aggregation
-  , entries
-  , hasErrors
-  , hasWarnings
-  , severityCount
-    -- * Output
-  , printLogReport
-  , prettyEntry
-  ) where
-
-import Data.Text (Text)
-import qualified Data.Text    as T
-import qualified Data.Text.IO as TIO
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
--- | Message severity.
-data Severity = Info | Warn | Err
-  deriving (Eq, Ord, Show)
-
--- | A single log entry.
---
--- 'lgCode' is a stable identifier of the form @W001@ / @E002@ used for
--- grepping output and writing tests against the log.
-data LogEntry = LogEntry
-  { lgSev  :: !Severity
-  , lgCode :: !Text
-  , lgMsg  :: !Text
-  , lgHint :: !(Maybe Text)
-  } deriving (Eq, Show)
-
--- | A 'Monoid' list-wrapper of 'LogEntry'.
-newtype LogReport = LogReport { entries :: [LogEntry] }
-  deriving (Eq, Show)
-
-instance Semigroup LogReport where
-  LogReport a <> LogReport b = LogReport (a ++ b)
-
-instance Monoid LogReport where
-  mempty = LogReport []
-
--- | A value paired with its log. Loaders and cleaners return this shape.
-type Loaded a = (a, LogReport)
-
--- ---------------------------------------------------------------------------
--- 構築
--- ---------------------------------------------------------------------------
-
--- | Build an 'Info' entry from @(code, message, optional hint)@.
-mkInfo :: Text -> Text -> Maybe Text -> LogEntry
-mkInfo c m h = LogEntry Info c m h
-
--- | Build a @Warn@ entry.
-mkWarn :: Text -> Text -> Maybe Text -> LogEntry
-mkWarn c m h = LogEntry Warn c m h
-
--- | Build an 'Err' entry.
-mkErr :: Text -> Text -> Maybe Text -> LogEntry
-mkErr c m h = LogEntry Err c m h
-
--- | Append an entry to the end of a report.
-addEntry :: LogEntry -> LogReport -> LogReport
-addEntry e (LogReport xs) = LogReport (xs ++ [e])
-
--- | Make a @LogReport@ that contains a single entry.
-logReport :: LogEntry -> LogReport
-logReport e = LogReport [e]
-
--- | The empty log (alias for 'mempty').
-noLog :: LogReport
-noLog = mempty
-
--- ---------------------------------------------------------------------------
--- 集約
--- ---------------------------------------------------------------------------
-
--- | True if the report contains any 'Err' entries.
---
--- >>> hasErrors noLog
--- False
--- >>> hasErrors (logReport (mkErr "E001" "boom" Nothing))
--- True
-hasErrors :: LogReport -> Bool
-hasErrors (LogReport xs) = any ((== Err) . lgSev) xs
-
--- | True if the report contains any @Warn@ entries.
-hasWarnings :: LogReport -> Bool
-hasWarnings (LogReport xs) = any ((== Warn) . lgSev) xs
-
--- | Number of entries with the given severity.
-severityCount :: Severity -> LogReport -> Int
-severityCount s (LogReport xs) = length (filter ((== s) . lgSev) xs)
-
--- ---------------------------------------------------------------------------
--- 出力
--- ---------------------------------------------------------------------------
-
--- | Pretty-print a single 'LogEntry' (severity tag + code + message,
--- and optionally the hint on a second line).
-prettyEntry :: LogEntry -> Text
-prettyEntry e =
-  let prefix = case lgSev e of
-        Info -> "[INFO]  "
-        Warn -> "[WARN]  "
-        Err  -> "[ERROR] "
-      hint = case lgHint e of
-        Nothing -> ""
-        Just h  -> "\n        ヒント: " <> h
-  in prefix <> lgCode e <> ": " <> lgMsg e <> hint
-
--- | Print the log to stdout. Empty logs print nothing.
-printLogReport :: LogReport -> IO ()
-printLogReport (LogReport []) = return ()
-printLogReport (LogReport xs) = do
-  let nW = length (filter ((== Warn) . lgSev) xs)
-      nE = length (filter ((== Err)  . lgSev) xs)
-      nI = length (filter ((== Info) . lgSev) xs)
-      summary = T.concat
-        [ "(" , T.pack (show (length xs)), " entries"
-        , if nE > 0 then ", " <> T.pack (show nE) <> " error"   else ""
-        , if nW > 0 then ", " <> T.pack (show nW) <> " warning" else ""
-        , if nI > 0 then ", " <> T.pack (show nI) <> " info"    else ""
-        , ")"
-        ]
-  TIO.putStrLn ("--- DataIO log " <> summary <> " ---")
-  mapM_ (TIO.putStrLn . prettyEntry) xs
-  TIO.putStrLn "----------------------"
diff --git a/src/Hanalyze/DataIO/Preprocess.hs b/src/Hanalyze/DataIO/Preprocess.hs
deleted file mode 100644
--- a/src/Hanalyze/DataIO/Preprocess.hs
+++ /dev/null
@@ -1,819 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeApplications #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE AllowAmbiguousTypes #-}
--- |
--- Module      : Hanalyze.DataIO.Preprocess
--- Description : Hackage dataframe 上の前処理ヘルパ (欠損検出/除去/補完・列選択・派生列)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Data-preprocessing helpers built on Hackage's @dataframe@.
---
--- All operations consume and produce 'DXD.DataFrame'.
---
---   * Missing-value detection, removal, and imputation
---     (mean / median / constant).
--- - 列の選択 / 削除 / リネーム
--- - 行のフィルタリング
--- - 派生列の計算 (mapNumeric / deriveNumeric / deriveText)
--- - Text 列を数値化 (NA 除去 + parse)
---
--- すべて純粋に新しい 'DXD.DataFrame' を返す。
-module Hanalyze.DataIO.Preprocess
-  ( -- * 値・行の表現
-    Value (..)
-  , DataRow
-  , isVMissing
-    -- * NA detection
-  , isNAString
-  , defaultNAStrings
-    -- * Column select / drop / rename
-  , selectColumns
-  , dropColumns
-  , renameColumn
-    -- * Missing-value handling
-  , countMissing
-  , dropMissingRows
-  , imputeConstant
-  , imputeMean
-  , imputeMedian
-  , parseNumericColumn
-  , readMaybeDoubleColumn
-    -- * Row filters
-  , rowsOf
-  , filterRows
-  , filterRowsByNumeric
-    -- * Derived columns
-  , mapNumeric
-  , deriveNumeric
-  , deriveText
-  , replaceColumn
-  , addColumn
-    -- * groupBy and aggregate
-  , groupByAggregate
-  , groupByMean
-  , groupBySum
-  , groupByMin
-  , groupByMax
-  , groupByMedian
-  , groupByCount
-    -- * Wide ↔ long transformation (melt)
-  , meltLonger
-    -- * Long-form regrid (resample jagged data onto a common grid)
-  , ZBoundsMode (..)
-  , RegridOpts (..)
-  , defaultRegridOpts
-  , RegridResult (..)
-  , PerIdStat (..)
-  , regridLong
-  ) where
-
-import qualified DataFrame.Internal.Column    as DX
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified DataFrame.Operators           as DX
-import qualified DataFrame.Operations.Core     as DX
-import qualified DataFrame.Operations.Subset   as DX
-import qualified DataFrame.Internal.Column    as DXC
-import qualified DataFrame.Internal.DataFrame as DXD
-import qualified DataFrame.Internal.Types     as DXT
-
-import Control.DeepSeq (NFData, force)
-import Control.Exception (SomeException, try, evaluate)
-import Data.List (foldl', sort)
-import qualified Data.List
-import qualified Data.Ord
-import qualified Data.Map.Strict as Map
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import System.IO.Unsafe (unsafePerformIO)
-import Text.Read (readMaybe)
-import qualified Hanalyze.Stat.Interpolate
-import qualified Hanalyze.Stat.AdaptiveGrid
-
--- ---------------------------------------------------------------------------
--- 値 / 行の表現 (deriveNumeric/deriveText 用の述語インタフェース)
--- ---------------------------------------------------------------------------
-
--- | A typed cell value used by 'deriveNumeric' / 'deriveText'-style
--- predicates. Missing values become 'VMissing'.
-data Value = VNum Double | VText Text | VMissing
-  deriving (Show, Eq)
-
--- | True for 'VMissing'; useful inside row predicates.
-isVMissing :: Value -> Bool
-isVMissing VMissing = True
-isVMissing _        = False
-
--- | A single row keyed by column name.
-type DataRow = Map.Map Text Value
-
--- ---------------------------------------------------------------------------
--- NA 検出 (Text レベル)
--- ---------------------------------------------------------------------------
-
--- | Strings recognised as missing values (case-sensitive on the trimmed
--- text): @\"\"@, @\"NA\"@, @\"N/A\"@, @\"n/a\"@, @\"null\"@, @\"NULL\"@,
--- @\"NaN\"@, @\"nan\"@, @\"?\"@.
-defaultNAStrings :: [Text]
-defaultNAStrings = ["", "NA", "N/A", "n/a", "null", "NULL", "NaN", "nan", "?"]
-
--- | True when the trimmed input text is in 'defaultNAStrings'.
-isNAString :: Text -> Bool
-isNAString t = T.strip t `elem` defaultNAStrings
-
--- ---------------------------------------------------------------------------
--- 列の選択 / 削除 / リネーム
--- ---------------------------------------------------------------------------
-
--- | Keep only the named columns (silently ignoring names that are not
--- present).
-selectColumns :: [Text] -> DXD.DataFrame -> DXD.DataFrame
-selectColumns names df =
-  let present = filter (`elem` DX.columnNames df) names
-  in DX.select present df
-
--- | Drop the named columns (silently ignoring names that are not present).
-dropColumns :: [Text] -> DXD.DataFrame -> DXD.DataFrame
-dropColumns names df =
-  let present = filter (`elem` DX.columnNames df) names
-  in DX.exclude present df
-
--- | Rename @old@ to @new@. No-op if @old@ is missing.
-renameColumn :: Text -> Text -> DXD.DataFrame -> DXD.DataFrame
-renameColumn old new df
-  | old `elem` DX.columnNames df = DX.rename old new df
-  | otherwise                    = df
-
--- ---------------------------------------------------------------------------
--- 内部: 列値の安全な取得 (型不一致時 Nothing)
--- ---------------------------------------------------------------------------
-
--- | Length of a named column (0 if absent).
-colLength :: Text -> DXD.DataFrame -> Int
-colLength name df = case DXD.getColumn name df of
-  Just c  -> DXC.columnLength c
-  Nothing -> 0
-
--- | Is the @i@-th cell null? Returns 'True' for missing columns.
-isNullAt :: Text -> Int -> DXD.DataFrame -> Bool
-isNullAt name i df = case DXD.getColumn name df of
-  Just c  -> DXC.columnElemIsNull c i
-  Nothing -> True
-
--- | 列を @[a]@ として安全に取り出す。型不一致や例外 (Hackage が
--- @error "fromMaybeVec: Nothing slot"@ 等を投げるケース) も 'Nothing' で吸収。
--- 'force' でリスト要素まで NF にしてから捕捉する。
-tryColumnAsList
-  :: forall a. (DXC.Columnable a, NFData a)
-  => Text -> DXD.DataFrame -> Maybe [a]
-tryColumnAsList name df = unsafePerformIO $ do
-  r <- try (evaluate (force (DX.columnAsList (DX.col @a name) df)))
-         :: IO (Either SomeException [a])
-  return $ case r of
-    Right xs -> Just xs
-    Left _   -> Nothing
-
--- ---------------------------------------------------------------------------
--- 欠損値処理
--- ---------------------------------------------------------------------------
-
--- | Per-column missing count. Columns without a null bitmap contribute
--- 0; columns with a bitmap contribute their null count. Text columns
--- additionally count cells whose value is in 'defaultNAStrings' (for
--- CSV-source compatibility).
-countMissing :: DXD.DataFrame -> [(Text, Int)]
-countMissing df =
-  [ (n, countOne n) | n <- DX.columnNames df ]
-  where
-    countOne n =
-      let len    = colLength n df
-          nulls  = length [ () | i <- [0 .. len - 1], isNullAt n i df ]
-          texts  = case tryColumnAsList @Text n df of
-                     Just xs -> length (filter isNAString xs)
-                     Nothing -> 0
-      in nulls + texts
-
--- | Drop rows where any of the listed columns is null. NA strings in
--- Text columns are also treated as missing.
---
--- Phase 11b (2026-05-14): cache per-column Text @Vector@ once instead
--- of calling @tryColumnAsList@ + @xs !! i@ inside the inner row loop.
--- The previous version was O(rows² × cols); the cached version is
--- O(rows × cols).
-dropMissingRows :: [Text] -> DXD.DataFrame -> DXD.DataFrame
-dropMissingRows targets df =
-  let cols = targets
-      n    = if null cols then 0 else maximum (map (`colLength` df) cols)
-      -- One pass per column: Maybe (Vector Text) of NA-eligible entries.
-      textCache :: [(Text, Maybe (V.Vector Text))]
-      textCache =
-        [ (c, fmap V.fromList (tryColumnAsList @Text c df))
-        | c <- cols ]
-      isTextNAVec mv i = case mv of
-        Just v  -> i < V.length v && isNAString (V.unsafeIndex v i)
-        Nothing -> False
-      rowMissing i =
-        any (\(c, mv) -> isNullAt c i df || isTextNAVec mv i) textCache
-      keep = [ i | i <- [0 .. n - 1], not (rowMissing i) ]
-  in selectRows keep df
-
--- | インデックス集合で全列を縦スライス。
-selectRows :: [Int] -> DXD.DataFrame -> DXD.DataFrame
-selectRows idxs df = foldr ins DX.empty (DX.columnNames df)
-  where
-    ins name acc =
-      case sliceColumn name df idxs of
-        Just c  -> DX.insertColumn name c acc
-        Nothing -> acc
-
--- | 列を indices で取り出して新しい Column を作る。
--- BoxedColumn / UnboxedColumn のどちらでも columnAsList 経由で安全に処理する。
-sliceColumn :: Text -> DXD.DataFrame -> [Int] -> Maybe DX.Column
-sliceColumn name df idxs = case DXD.getColumn name df of
-  Nothing -> Nothing
-  Just _  ->
-    -- 型を順に試す。Maybe Double → Double → Maybe Int → Int → Text の順。
-    tryAs @(Maybe Double)
-      (tryAs @Double
-        (tryAs @(Maybe Int)
-          (tryAs @Int
-            (tryAs @Text Nothing))))
-  where
-    -- Phase 11b (2026-05-14): convert the column to a @Vector@ once and
-    -- use 'unsafeIndex'. The previous @xs !! i@ in a list-comprehension
-    -- was O(i) per index, so 'sliceColumn' on n indices was O(n²).
-    tryAs
-      :: forall a. (DXC.Columnable a, NFData a,
-                    DXC.ColumnifyRep (DXT.KindOf a) a)
-      => Maybe DX.Column -> Maybe DX.Column
-    tryAs fallback = case tryColumnAsList @a name df of
-      Just xs ->
-        let v   = V.fromList xs
-            len = V.length v
-        in Just (DX.fromList
-                   [ V.unsafeIndex v i | i <- idxs, i < len ])
-      Nothing -> fallback
-
--- | Impute missing values with a constant and homogenize to a 'Double'
--- column.
-imputeConstant :: Text -> Double -> DXD.DataFrame -> Maybe DXD.DataFrame
-imputeConstant name fill df = case readMaybeDoubleColumn name df of
-  Nothing -> Nothing
-  Just xs ->
-    let filled = map (maybe fill id) xs
-    in Just (DX.insertColumn name (DX.fromList filled) df)
-
--- | Impute missing values with the mean of the present cells. Returns
--- 'Nothing' when the column has no non-missing cells.
-imputeMean :: Text -> DXD.DataFrame -> Maybe DXD.DataFrame
-imputeMean name df = case readMaybeDoubleColumn name df of
-  Nothing -> Nothing
-  Just xs ->
-    let nums = [ x | Just x <- xs ]
-    in if null nums
-         then Nothing
-         else
-           let m = sum nums / fromIntegral (length nums)
-           in imputeConstant name m df
-
--- | Impute missing values with the median. Returns 'Nothing' when the
--- column has no non-missing cells.
-imputeMedian :: Text -> DXD.DataFrame -> Maybe DXD.DataFrame
-imputeMedian name df = case readMaybeDoubleColumn name df of
-  Nothing -> Nothing
-  Just xs ->
-    let s = sort [ x | Just x <- xs ]
-    in if null s
-         then Nothing
-         else imputeConstant name (s !! (length s `div` 2)) df
-
--- | Read any of Text / Double / Maybe Double / Int / Maybe Int as
--- @[Maybe Double]@.
--- に正規化して取り出す。Text 列の NA 文字列・parse 失敗は Nothing として扱う。
---
--- 注意: Hackage 'DX.columnAsList' は @Maybe a@ 列に対して @col @a@ を要求しても
--- 例外を投げず、null セルを 0 などのデフォルト値で埋めて返す。そのため null は
--- 必ず @isNullAt@ (= columnElemIsNull) で別途マスクする。
-readMaybeDoubleColumn :: Text -> DXD.DataFrame -> Maybe [Maybe Double]
-readMaybeDoubleColumn name df = fmap (maskNulls . zip [0..]) raw
-  where
-    maskNulls = map (\(i, x) -> if isNullAt name i df then Nothing else x)
-    raw =
-      case tryColumnAsList @(Maybe Double) name df of
-        Just xs -> Just xs
-        Nothing -> case tryColumnAsList @(Maybe Int) name df of
-          Just xs -> Just (map (fmap fromIntegral) xs)
-          Nothing -> case tryColumnAsList @Double name df of
-            Just xs -> Just (map Just xs)
-            Nothing -> case tryColumnAsList @Int name df of
-              Just xs -> Just (map (Just . fromIntegral) xs)
-              -- Phase 60.3: Integer 列の黙殺根治 ([[numericcols-integer-silent-drop]])。
-              -- 判定列に Integer / Maybe Integer が無く、 df |-> hbm の group 列が
-              -- Integer 型だと dataNamed が黙って空になっていた。
-              Nothing -> case tryColumnAsList @Integer name df of
-                Just xs -> Just (map (Just . fromIntegral) xs)
-                Nothing -> case tryColumnAsList @(Maybe Integer) name df of
-                  Just xs -> Just (map (fmap fromIntegral) xs)
-                  Nothing -> case tryColumnAsList @Text name df of
-                    Just xs -> Just
-                      [ if isNAString t
-                          then Nothing
-                          else readMaybe (T.unpack t)
-                      | t <- xs ]
-                    Nothing -> Nothing
-
--- | Convert a Text column into a Double column. Returns 'Nothing' if
--- any cell is missing or fails to parse.
-parseNumericColumn :: Text -> DXD.DataFrame -> Maybe DXD.DataFrame
-parseNumericColumn name df =
-  case tryColumnAsList @Double name df of
-    Just _  -> Just df
-    Nothing -> case tryColumnAsList @Text name df of
-      Nothing -> Nothing
-      Just xs -> do
-        ds <- mapM (readMaybe . T.unpack) xs
-        return (DX.insertColumn name (DX.fromList (ds :: [Double])) df)
-
--- ---------------------------------------------------------------------------
--- 行フィルタ (DataRow ベース、レガシー API)
--- ---------------------------------------------------------------------------
-
--- | Expand a DataFrame into a list of 'DataRow'. NA strings become
--- 'VMissing'.
-rowsOf :: DXD.DataFrame -> [DataRow]
-rowsOf df =
-  let cols = DX.columnNames df
-      n    = if null cols then 0 else maximum (map (`colLength` df) cols)
-  in [ Map.fromList [ (c, cellAt c i) | c <- cols ] | i <- [0 .. n - 1] ]
-  where
-    cellAt c i
-      | isNullAt c i df = VMissing
-      | otherwise = case readMaybeDoubleColumn c df of
-          Just xs | i < length xs ->
-            case xs !! i of
-              Just d  -> VNum d
-              Nothing ->
-                case tryColumnAsList @Text c df of
-                  Just ts | i < length ts ->
-                    let t = ts !! i
-                    in if isNAString t then VMissing else VText t
-                  _ -> VMissing
-          _ -> case tryColumnAsList @Text c df of
-                 Just ts | i < length ts ->
-                   let t = ts !! i
-                   in if isNAString t then VMissing else VText t
-                 _ -> VMissing
-
--- | Keep only the rows for which the predicate evaluates to 'True'.
-filterRows :: (DataRow -> Bool) -> DXD.DataFrame -> DXD.DataFrame
-filterRows p df =
-  let keep = [ i | (i, r) <- zip [0..] (rowsOf df), p r ]
-  in selectRows keep df
-
--- | Keep only the rows for which a numeric column satisfies the
--- predicate.
-filterRowsByNumeric :: Text -> (Double -> Bool) -> DXD.DataFrame -> DXD.DataFrame
-filterRowsByNumeric name p df =
-  case readMaybeDoubleColumn name df of
-    Nothing -> df
-    Just xs ->
-      let keep = [ i | (i, Just x) <- zip [0..] xs, p x ]
-      in selectRows keep df
-
--- ---------------------------------------------------------------------------
--- 派生列
--- ---------------------------------------------------------------------------
-
--- | Apply @f@ element-wise to a numeric column. The column is left
--- unchanged when its type is not @Double@.
-mapNumeric :: Text -> (Double -> Double) -> DXD.DataFrame -> DXD.DataFrame
-mapNumeric name f df = case tryColumnAsList @Double name df of
-  Just xs -> DX.insertColumn name (DX.fromList (map f xs)) df
-  Nothing -> df
-
--- | Derive a new numeric column from each row.
-deriveNumeric :: Text -> (DataRow -> Double) -> DXD.DataFrame -> DXD.DataFrame
-deriveNumeric newName f df =
-  let vals = map f (rowsOf df)
-  in DX.insertColumn newName (DX.fromList (vals :: [Double])) df
-
--- | Derive a new text column from each row.
-deriveText :: Text -> (DataRow -> Text) -> DXD.DataFrame -> DXD.DataFrame
-deriveText newName f df =
-  let vals = map f (rowsOf df)
-  in DX.insertColumn newName (DX.fromList (vals :: [Text])) df
-
--- | Replace or insert a column (Hackage's 'DX.insertColumn' replaces an
--- existing column).
-replaceColumn :: Text -> DX.Column -> DXD.DataFrame -> DXD.DataFrame
-replaceColumn = DX.insertColumn
-
--- | Append a new column (or replace if the name already exists).
-addColumn :: Text -> DX.Column -> DXD.DataFrame -> DXD.DataFrame
-addColumn = DX.insertColumn
-
--- ---------------------------------------------------------------------------
--- groupBy / aggregate
--- ---------------------------------------------------------------------------
-
--- | Aggregate a numeric column with the given function, grouped by a
--- text key column.
--- カスタム集約 (任意の @[Double] -> Double@) を扱うため、Hackage の
--- @groupBy + aggregate@ ではなく独自バケット実装。決まった集約は
--- 'groupByMean' 等を経由した方が高速。
-groupByAggregate
-  :: Text                          -- ^ グループ列
-  -> Text                          -- ^ 集約対象列
-  -> ([Double] -> Double)          -- ^ 集約関数
-  -> DXD.DataFrame
-  -> Maybe DXD.DataFrame
-groupByAggregate gCol nCol agg df =
-  case (tryColumnAsList @Text gCol df, readMaybeDoubleColumn nCol df) of
-    (Just gs, Just nsM) ->
-      let pairs = [ (g, x) | (g, Just x) <- zip gs nsM ]
-          buckets = collectInOrder pairs
-          groups   = map fst buckets
-          aggVals  = map (agg . snd) buckets
-      in Just $
-           DX.insertColumn nCol (DX.fromList (aggVals :: [Double])) $
-           DX.insertColumn gCol (DX.fromList (groups  :: [Text]))
-             DX.empty
-    _ -> Nothing
-
--- | 順序保持の group→[value] 蓄積。
---
--- Phase Q3 (2026-05-14): 旧実装は @foldl@ + @lookup@ + @vs ++ [v]@ の三重で
--- O(n²) (n=50000 で 1.2 s / 10.4 GB alloc を観測)。Map で初出順 index と
--- 累積値を保持し、最後に index 順に並べる O(n log n) 実装に置換。
--- 蓄積は @v :@ で先頭 cons → 最後に @reverse@ するため per-element O(1)。
-collectInOrder :: Ord k => [(k, v)] -> [(k, [v])]
-collectInOrder kvs =
-  let go (!nextIdx, !mp) (k, v) =
-        case Map.lookup k mp of
-          Just (i, rev)  -> (nextIdx, Map.insert k (i, v : rev) mp)
-          Nothing        -> (nextIdx + 1, Map.insert k (nextIdx, [v]) mp)
-      (_, finalMp) = foldl' go (0 :: Int, Map.empty) kvs
-      bucketsByIdx = Data.List.sortBy
-                       (Data.Ord.comparing (fst . snd))
-                       (Map.toList finalMp)
-  in [ (k, reverse rev) | (k, (_, rev)) <- bucketsByIdx ]
-
--- | Group-by aggregation with the per-group mean.
-groupByMean   :: Text -> Text -> DXD.DataFrame -> Maybe DXD.DataFrame
-groupByMean g n = groupByAggregate g n meanD
-
--- | Group-by aggregation with the per-group sum.
-groupBySum    :: Text -> Text -> DXD.DataFrame -> Maybe DXD.DataFrame
-groupBySum g n = groupByAggregate g n sum
-
--- | Group-by aggregation with the per-group minimum.
-groupByMin    :: Text -> Text -> DXD.DataFrame -> Maybe DXD.DataFrame
-groupByMin g n = groupByAggregate g n minimum
-
--- | Group-by aggregation with the per-group maximum.
-groupByMax    :: Text -> Text -> DXD.DataFrame -> Maybe DXD.DataFrame
-groupByMax g n = groupByAggregate g n maximum
-
--- | Group-by aggregation with the per-group median.
-groupByMedian :: Text -> Text -> DXD.DataFrame -> Maybe DXD.DataFrame
-groupByMedian g n = groupByAggregate g n medianD
-
--- | Per-group row count. The output column is named @\"count\"@.
-groupByCount :: Text -> DXD.DataFrame -> Maybe DXD.DataFrame
-groupByCount gCol df = case tryColumnAsList @Text gCol df of
-  Nothing -> Nothing
-  Just gs ->
-    let buckets = collectInOrder [ (g, ()) | g <- gs ]
-        keys    = map fst buckets
-        counts  = map (fromIntegral . length . snd) buckets
-    in Just $
-         DX.insertColumn "count" (DX.fromList (counts :: [Double])) $
-         DX.insertColumn gCol    (DX.fromList (keys   :: [Text]))
-           DX.empty
-
-meanD :: [Double] -> Double
-meanD [] = 0
-meanD xs = sum xs / fromIntegral (length xs)
-
-medianD :: [Double] -> Double
-medianD [] = 0
-medianD xs = let s = sort xs in s !! (length s `div` 2)
-
--- ---------------------------------------------------------------------------
--- Wide → Long 変形 (melt / pivot_longer)
--- ---------------------------------------------------------------------------
-
--- | Wide-form の DataFrame を long-form に展開する (R/pandas の pivot_longer
--- / melt 相当)。
---
--- @meltLonger idCols valueCols varName valueName parseVarAsDouble df@:
---
--- * @idCols@         そのまま残す (繰返しコピー) 列。
--- * @valueCols@      縦方向に展開する列。これらの列名が新しい @varName@ 列の値になる。
--- * @varName@        新しい variable 列の名前 (例: \"t\")。
--- * @valueName@      新しい value 列の名前 (例: \"y\")。
--- * @parseVarAsDouble@
---                    True なら variable 列の中身 (= 元 wide 列名) を Double として
---                    parse して数値列に。Parse 失敗時は Text 列のまま。
---
--- 元セルが NA (null bitmap or NA 文字列) の行は出力から除外される。
---
--- 例:
---
--- @
--- name x1 1   2   3       --      name x1 t y
--- a    1  10  20  -    →          a    1  1 10
--- b    2  -   30  60              a    1  2 20
---                                 b    2  2 30
---                                 b    2  3 60
--- @
-meltLonger
-  :: [Text]      -- ^ id 列 (そのまま残す)
-  -> [Text]      -- ^ wide 列 (縦展開する)
-  -> Text        -- ^ 新しい variable 列名
-  -> Text        -- ^ 新しい value 列名
-  -> Bool        -- ^ True: variable 列を Double に parse
-  -> DXD.DataFrame
-  -> DXD.DataFrame
-meltLonger idCols valueCols varName valueName parseVar df =
-  let nrows = fst (DX.dimensions df)
-      -- 各 id 列を [Maybe Text] / [Maybe Double] として取り出す
-      idTexts =
-        [ (n, idColAsText n) | n <- idCols ]
-      -- id 列を [Text] として取り出す: Maybe Text → Text → Maybe Double → Double → Maybe Int → Int の順に試行
-      idColAsText n =
-        case tryColumnAsList @(Maybe Text) n df of
-          Just xs -> Just (map (maybe "" id) xs)
-          Nothing -> case tryColumnAsList @Text n df of
-            Just xs -> Just xs
-            Nothing -> case tryColumnAsList @(Maybe Double) n df of
-              Just xs -> Just (map showMaybeD xs)
-              Nothing -> case tryColumnAsList @Double n df of
-                Just xs -> Just (map showD xs)
-                Nothing -> case tryColumnAsList @(Maybe Int) n df of
-                  Just xs -> Just (map showMaybeI xs)
-                  Nothing -> case tryColumnAsList @Int n df of
-                    Just xs -> Just (map (T.pack . show) xs)
-                    Nothing -> Nothing
-      showMaybeD Nothing  = ""
-      showMaybeD (Just d) = showD d
-      showD d
-        | d == fromInteger (round d) = T.pack (show (round d :: Integer))
-        | otherwise                   = T.pack (show d)
-      showMaybeI Nothing  = ""
-      showMaybeI (Just i) = T.pack (show i)
-      -- valueCols のセル値を [Maybe Double] で取り出す
-      valData =
-        [ (n, valueAsMaybeDouble n df, varValue n)
-        | n <- valueCols ]
-      varValue n
-        | parseVar  = case readMaybe (T.unpack n) :: Maybe Double of
-                        Just d  -> Right d
-                        Nothing -> Left n
-        | otherwise = Left n
-      -- 全 (id行 × value列) ペアから NA でない物だけ残す
-      indices = [(i, j) | i <- [0 .. nrows - 1], j <- [0 .. length valueCols - 1]]
-      keep = [ (i, j, v)
-             | (i, j) <- indices
-             , let (_, vs, _) = valData !! j
-             , Just v <- [vs !! i]
-             ]
-      -- id 列を keep 行数ぶん展開
-      mkIdCol (n, mxs) =
-        let xs = case mxs of
-                   Just xs0 -> xs0
-                   Nothing  -> replicate nrows ""
-            ys = [ xs !! i | (i, _, _) <- keep ]
-        in (n, ys)
-      -- variable 列
-      varValues = [ thd ((valData !! j)) | (_, j, _) <- keep ]
-        where thd (_,_,c) = c
-      -- value 列 (Double)
-      valValues = [ v | (_, _, v) <- keep ]
-      idColsOut = map mkIdCol idTexts
-      df0       = foldl insertText DX.empty idColsOut
-      df1       = case (parseVar, sequence (map varEither varValues)) of
-                    (True, Just ds) ->
-                      DX.insertColumn varName (DX.fromList (ds :: [Double])) df0
-                    _               ->
-                      let texts = map (either id (T.pack . show)) varValues
-                      in DX.insertColumn varName (DX.fromList texts) df0
-      df2       = DX.insertColumn valueName (DX.fromList (valValues :: [Double])) df1
-  in df2
-  where
-    insertText d (n, xs) = DX.insertColumn n (DX.fromList (xs :: [Text])) d
-    varEither (Right d) = Just d
-    varEither (Left  _) = Nothing
-    showCell Nothing  = ""
-    showCell (Just t) = t
-
--- | 列を 'Maybe Double' のリストとして取り出すヘルパ (内部用)。
--- 数値 / Maybe Double / Int / Maybe Int / Text 列のいずれでも対応。
-valueAsMaybeDouble :: Text -> DXD.DataFrame -> [Maybe Double]
-valueAsMaybeDouble name df = case readMaybeDoubleColumn name df of
-  Just xs -> xs
-  Nothing -> replicate (fst (DX.dimensions df)) Nothing
-
--- ---------------------------------------------------------------------------
--- Long-form regrid (Phase G3): 歯抜けの long-form データを共通 grid に揃える
--- ---------------------------------------------------------------------------
-
--- | 共通 z 範囲の決定方式。
-data ZBoundsMode
-  = ZIntersection  -- ^ 全 id で観測がある区間: (max_id min_z, min_id max_z) — 外挿なし
-  | ZUnion         -- ^ 全 id をカバー: (min_id min_z, max_id max_z) — 外挿あり
-  deriving (Show, Eq)
-
--- | 'regridLong' の設定。
-data RegridOpts = RegridOpts
-  { roInterp     :: !Hanalyze.Stat.Interpolate.InterpKind
-  , roGridKind   :: !Hanalyze.Stat.AdaptiveGrid.GridKind
-  , roN          :: !Int
-  , roZBoundsMode :: !ZBoundsMode
-  , roCoarseN    :: !Int     -- ^ adaptive 用粗 grid サイズ (default 200)
-  , roEpsRatio   :: !Double  -- ^ adaptive 用平坦部最低密度比 (default 0.05)
-  } deriving (Show, Eq)
-
--- | 推奨デフォルト (PCHIP / Adaptive / N=30 / Intersection / coarse=200 / ε=0.05)。
-defaultRegridOpts :: RegridOpts
-defaultRegridOpts = RegridOpts
-  { roInterp      = Hanalyze.Stat.Interpolate.PCHIP
-  , roGridKind    = Hanalyze.Stat.AdaptiveGrid.Adaptive
-  , roN           = 30
-  , roZBoundsMode = ZIntersection
-  , roCoarseN     = 200
-  , roEpsRatio    = 0.05
-  }
-
--- | id ごとの統計 (G4 のレポートで使用)。
-data PerIdStat = PerIdStat
-  { piId          :: !Text
-  , piNObserved   :: !Int        -- ^ 元観測点数
-  , piZMin        :: !Double     -- ^ 観測 z 最小
-  , piZMax        :: !Double     -- ^ 観測 z 最大
-  , piExtrapBelow :: !Double     -- ^ 共通 grid zmin が観測 zmin より小さい量 (>0 なら外挿)
-  , piExtrapAbove :: !Double     -- ^ 共通 grid zmax が観測 zmax より大きい量 (>0 なら外挿)
-  , piResidualMax :: !Double     -- ^ 補間関数を観測 z に再投入したときの最大残差
-  } deriving (Show, Eq)
-
--- | regridLong の戻り値。data + レポート用統計。
-data RegridResult = RegridResult
-  { rrDataFrame   :: !DXD.DataFrame
-  , rrZGrid       :: ![Double]
-  , rrZMin        :: !Double
-  , rrZMax        :: !Double
-  , rrPerIdStats  :: ![PerIdStat]
-  , rrIds         :: ![Text]
-  , rrPerIdInterp :: ![(Text, [(Double, Double)], Double -> Double)]
-                       -- ^ id ごとに (id, 元観測点, 補間関数)。レポートのオーバーレイ用
-  , rrDensity     :: ![(Double, Double)]   -- ^ adaptive 時の (z, density) ペア (空: uniform 時)
-  }
-
--- | 歯抜けの long-form @[idCol, zCol, yCol]@ を共通 grid に揃える。
---
--- 1. idCol で groupBy → id ごとに (z, y) ペア取得 (NA は除外)
--- 2. ZBoundsMode に従って共通 (zmin, zmax) を決定
--- 3. 'Hanalyze.Stat.AdaptiveGrid.makeGrid' で N 点 grid を生成
--- 4. 各 id を 'Hanalyze.Stat.Interpolate.interp1d' で補間し grid 上で評価
--- 5. id × grid の long-form DataFrame を返す
---
--- 観測点が < 2 の id は補間できないため除外され、レポートに記録される。
-regridLong
-  :: Text          -- ^ id 列名
-  -> Text          -- ^ z 列名
-  -> Text          -- ^ y 列名
-  -> RegridOpts
-  -> DXD.DataFrame
-  -> RegridResult
-regridLong idCol zCol yCol opts df =
-  let -- 列を取り出す
-      ids   = case tryColumnAsList @Text idCol df of
-                Just xs -> xs
-                Nothing -> case tryColumnAsList @(Maybe Text) idCol df of
-                  Just xs -> map (maybe "" id) xs
-                  Nothing -> case tryColumnAsList @Double idCol df of
-                    Just xs -> map (T.pack . show) xs
-                    Nothing -> case tryColumnAsList @Int idCol df of
-                      Just xs -> map (T.pack . show) xs
-                      Nothing -> []
-      zs    = valueAsMaybeDouble zCol df
-      ys    = valueAsMaybeDouble yCol df
-      -- (id, [(z, y)]) にグループ化、NA 行は除外
-      triples = [ (i, z, y)
-                | (i, mz, my) <- zip3 ids zs ys
-                , Just z <- [mz]
-                , Just y <- [my] ]
-      grouped =
-        let m = foldl (\acc (i, z, y) -> Map.insertWith (++) i [(z, y)] acc)
-                      Map.empty triples
-        in [ (i, sortBy (Data.Ord.comparing fst) pts)
-           | (i, pts) <- Map.toList m
-           , length pts >= 2 ]
-      idsKept = map fst grouped
-      perIdPts = map snd grouped
-      -- z 範囲
-      ranges = [ (minimum (map fst pts), maximum (map fst pts)) | pts <- perIdPts ]
-      (zmin, zmax) = case roZBoundsMode opts of
-        ZIntersection ->
-          if null ranges
-            then (0, 1)
-            else (maximum (map fst ranges), minimum (map snd ranges))
-        ZUnion        ->
-          if null ranges
-            then (0, 1)
-            else (minimum (map fst ranges), maximum (map snd ranges))
-      -- 共通 grid
-      gridSpec = Hanalyze.Stat.AdaptiveGrid.GridSpec
-        { Hanalyze.Stat.AdaptiveGrid.gsKind       = roGridKind opts
-        , Hanalyze.Stat.AdaptiveGrid.gsN          = roN opts
-        , Hanalyze.Stat.AdaptiveGrid.gsInterpKind = roInterp opts
-        , Hanalyze.Stat.AdaptiveGrid.gsCoarseN    = roCoarseN opts
-        , Hanalyze.Stat.AdaptiveGrid.gsEpsRatio   = roEpsRatio opts
-        }
-      grid = Hanalyze.Stat.AdaptiveGrid.makeGrid perIdPts (zmin, zmax) gridSpec
-      -- id ごとに補間関数 + grid 上の y を評価
-      interpFns = [ (i, pts, Hanalyze.Stat.Interpolate.interp1d (roInterp opts) pts)
-                  | (i, pts) <- grouped ]
-      perIdY    = [ map f grid | (_, _, f) <- interpFns ]
-      -- 統計
-      stats = [ let zMn = fst rg
-                    zMx = snd rg
-                    extL = max 0 (zMn - zmin)
-                    extU = max 0 (zmax - zMx)
-                    residMax = if null pts then 0
-                               else maximum [ abs (f z - y) | (z, y) <- pts ]
-                in PerIdStat
-                     { piId          = i
-                     , piNObserved   = length pts
-                     , piZMin        = zMn
-                     , piZMax        = zMx
-                     , piExtrapBelow = extL
-                     , piExtrapAbove = extU
-                     , piResidualMax = residMax
-                     }
-              | ((i, pts, f), rg) <- zip interpFns ranges
-              ]
-      -- 出力 long DataFrame: 行数 = nIds × len grid
-      n = length grid
-      idsOut    = concat [ replicate n i | i <- idsKept ]
-      zsOut     = concat (replicate (length idsKept) grid)
-      ysOut     = concat perIdY
-      dfOut     = DX.insertColumn yCol  (DX.fromList ysOut)
-                $ DX.insertColumn zCol  (DX.fromList zsOut)
-                $ DX.insertColumn idCol (DX.fromList idsOut)
-                $ DX.empty
-      -- adaptive density (レポート用): coarse grid 上の (z, density)
-      density = case roGridKind opts of
-        Hanalyze.Stat.AdaptiveGrid.Uniform  -> []
-        Hanalyze.Stat.AdaptiveGrid.Adaptive -> computeDensity perIdPts (roInterp opts)
-                                                    (roCoarseN opts) zmin zmax
-  in RegridResult
-       { rrDataFrame   = dfOut
-       , rrZGrid       = grid
-       , rrZMin        = zmin
-       , rrZMax        = zmax
-       , rrPerIdStats  = stats
-       , rrIds         = idsKept
-       , rrPerIdInterp = interpFns
-       , rrDensity     = density
-       }
-  where
-    sortBy = Data.List.sortBy
-
--- | 内部: adaptive レポート用の (z, max_id |dy/dz|) 列を再計算 (G4 の R3 で表示)。
-computeDensity
-  :: [[(Double, Double)]] -> Hanalyze.Stat.Interpolate.InterpKind -> Int
-  -> Double -> Double
-  -> [(Double, Double)]
-computeDensity perIdPts kind coarseN zmin zmax =
-  let coarse = Hanalyze.Stat.AdaptiveGrid.uniformGrid coarseN zmin zmax
-      ysPerId = [ map (Hanalyze.Stat.Interpolate.interp1d kind pts) coarse
-                | pts <- perIdPts, length pts >= 2 ]
-      slopeAbsLocal zs ys =
-        let n = length zs
-            zarr = zs
-            yarr = ys
-        in [ if n < 2 then 0
-             else if i == 0 then abs ((yarr !! 1 - yarr !! 0) /
-                                      (zarr !! 1 - zarr !! 0))
-             else if i == n - 1 then abs ((yarr !! (n-1) - yarr !! (n-2)) /
-                                          (zarr !! (n-1) - zarr !! (n-2)))
-             else abs ((yarr !! (i+1) - yarr !! (i-1)) /
-                       (zarr !! (i+1) - zarr !! (i-1)))
-           | i <- [0 .. n-1] ]
-      slopes = map (slopeAbsLocal coarse) ysPerId
-      peak = if null slopes
-               then replicate coarseN 0
-               else [ maximum [ s !! i | s <- slopes ] | i <- [0 .. coarseN - 1] ]
-  in zip coarse peak
-
--- 既存モジュールでの import 追加
--- (tryColumnAsList などは元々 import 済み、Map.insertWith / Data.List.sortBy /
---  Data.Ord.comparing も import 済み)
-
--- 補助 import (修飾名で参照するため)
-{-# NOINLINE _placeholderRegridImports #-}
-_placeholderRegridImports :: ()
-_placeholderRegridImports = ()
diff --git a/src/Hanalyze/DataIO/Reshape.hs b/src/Hanalyze/DataIO/Reshape.hs
deleted file mode 100644
--- a/src/Hanalyze/DataIO/Reshape.hs
+++ /dev/null
@@ -1,256 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.DataIO.Reshape
--- Description : Hackage dataframe に無い reshape 操作 (pivotWider・oneHot・lag/lead・rolling)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Data-frame reshaping helpers that are missing in Hackage
--- @dataframe@:
---
---   * 'pivotWider' — long → wide reshape (inverse of @meltLonger@).
---   * 'oneHot' — one-hot encoding of a categorical column.
---   * @lag@ / @lead@ — shift a numeric column for time-series feature
---     engineering.
---   * 'rollingMean' / 'rollingSum' — fixed-window rolling stats.
---
--- For @join@, @sortBy@, @meltLonger@ etc., use the upstream
--- @DataFrame@ API directly — those are first-class there.
-module Hanalyze.DataIO.Reshape
-  ( pivotWider
-  , oneHot
-  , lagColumn
-  , leadColumn
-  , rollingMean
-  , rollingSum
-  , rollingApply
-  ) where
-
-import qualified Data.Text             as T
-import qualified Data.Vector           as V
-import qualified DataFrame.Internal.Column    as DX
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified DataFrame.Operations.Subset   as DX
-import qualified DataFrame.Internal.DataFrame as DXD
-import qualified Hanalyze.DataIO.Convert        as Conv
-import           Data.Maybe            (fromMaybe)
-import qualified Data.Set              as Set
-
--- ---------------------------------------------------------------------------
--- Pivot wider
--- ---------------------------------------------------------------------------
-
--- | Reshape a long-form DataFrame into wide form. Inverse of
--- @meltLonger@.
---
--- Given:
---
---   * a DataFrame with rows like @(id, name, value)@,
---   * @namesFrom@ = the column whose distinct values become new
---     column names,
---   * @valuesFrom@ = the column holding the values to spread,
---   * @idCols@ = identifier columns kept as the row key.
---
--- Produces a DataFrame where each unique value of @namesFrom@ becomes
--- a new column. Missing combinations are filled with NaN (as Double).
---
--- Example: long-form @[(1, "x", 10), (1, "y", 20), (2, "x", 30)]@ →
--- wide-form @[(1, 10, 20), (2, 30, NaN)]@ with columns
--- @[id, x, y]@.
-pivotWider
-  :: [T.Text]            -- ^ Identifier columns.
-  -> T.Text              -- ^ Column with new column names (@namesFrom@).
-  -> T.Text              -- ^ Column with values to spread (@valuesFrom@).
-  -> DXD.DataFrame
-  -> DXD.DataFrame
-pivotWider idCols namesFrom valuesFrom df =
-  let nameVec    = fromMaybe (error ("pivotWider: column '"
-                                      ++ T.unpack namesFrom
-                                      ++ "' not found"))
-                     (Conv.getTextVec namesFrom df)
-      valueVec   = fromMaybe (error ("pivotWider: column '"
-                                      ++ T.unpack valuesFrom
-                                      ++ "' not found"))
-                     (Conv.getDoubleVec valuesFrom df)
-      n          = V.length nameVec
-      -- Distinct names (preserves order of first appearance).
-      distinct   = orderedUnique (V.toList nameVec)
-      -- Get id-column values per row as a tuple key.
-      idColVecs  = [ fromMaybe (error ("pivotWider: id col '"
-                                        ++ T.unpack c ++ "' not found"))
-                       (Conv.getTextVec c df
-                          `mappendMaybe`
-                        fmap (V.map (T.pack . show))
-                          (Conv.getDoubleVec c df))
-                   | c <- idCols ]
-      -- Group rows by id-key.
-      keyOf i    = [vec V.! i | vec <- idColVecs]
-      keys       = orderedUnique [keyOf i | i <- [0..n-1]]
-      -- For each (key, name) compute the value (NaN if missing).
-      lookup1 key name =
-        let matching = [ V.unsafeIndex valueVec i
-                       | i <- [0..n-1]
-                       , keyOf i == key
-                       , V.unsafeIndex nameVec i == name
-                       ]
-        in case matching of
-             []    -> 0/0  -- NaN
-             (v:_) -> v
-      -- Build wide DataFrame.
-      keyToTexts k = k                      -- already [Text]
-      idCols' = [ (c, V.fromList [V.unsafeIndex (idColVecs !! ci) i
-                                  | i <- rowIndices])
-                | (ci, c) <- zip [0..] idCols ]
-      rowIndices = [ head [i | i <- [0..n-1], keyOf i == k] | k <- keys ]
-      _ = idCols'
-      _ = keyToTexts
-      -- Wide columns.
-      wideCols   = [ (name,
-                      V.fromList [lookup1 k name | k <- keys])
-                   | name <- distinct ]
-      -- Build via DX.fromList so the dataframe knows column types.
-      idColData  = [ (c, DX.fromList (V.toList (V.fromList
-                                                  [V.unsafeIndex (idColVecs !! ci) i
-                                                  | i <- rowIndices])))
-                   | (ci, c) <- zip [0..] idCols ]
-      wideColData = [ (name,
-                       DX.fromList (V.toList vs))
-                    | (name, vs) <- wideCols ]
-  in DX.fromNamedColumns (idColData ++ wideColData)
-
--- | Append the second 'Maybe' as a fallback if the first is Nothing.
-mappendMaybe :: Maybe a -> Maybe a -> Maybe a
-mappendMaybe (Just x) _ = Just x
-mappendMaybe Nothing y  = y
-
--- ---------------------------------------------------------------------------
--- One-hot encoding
--- ---------------------------------------------------------------------------
-
--- | One-hot encode a categorical text column. Returns a DataFrame
--- with the original column dropped and one new 0/1 indicator column
--- per category (named "@<col>_<category>@").
---
--- @dropFirst@ controls whether to omit the first category (= drop
--- redundant column for use in regression to avoid multicollinearity).
-oneHot
-  :: Bool             -- ^ Drop first category?
-  -> T.Text           -- ^ Categorical column name.
-  -> DXD.DataFrame
-  -> DXD.DataFrame
-oneHot dropFirst colName df =
-  let vec      = fromMaybe (error ("oneHot: column '"
-                                    ++ T.unpack colName ++ "' not found"))
-                   (Conv.getTextVec colName df)
-      n        = V.length vec
-      cats     = orderedUnique (V.toList vec)
-      keep     = if dropFirst then drop 1 cats else cats
-      indicator c =
-        DX.fromList [ if V.unsafeIndex vec i == c then (1 :: Double)
-                                                  else 0
-                    | i <- [0..n-1] ]
-      newCols  = [(colName <> "_" <> c, indicator c) | c <- keep]
-      withoutOrig = DX.exclude [colName] df
-  in foldr (\(name, col) d -> DX.insertColumn name col d)
-           withoutOrig newCols
-
--- ---------------------------------------------------------------------------
--- Lag / Lead
--- ---------------------------------------------------------------------------
-
--- | Shift a numeric column @k@ positions forward (lag). The first @k@
--- entries become NaN. Useful for time-series feature engineering.
-lagColumn
-  :: Int              -- ^ k (positive).
-  -> T.Text           -- ^ Source column.
-  -> T.Text           -- ^ Output column name.
-  -> DXD.DataFrame
-  -> DXD.DataFrame
-lagColumn k src out df =
-  let vec = fromMaybe (error ("lagColumn: column '"
-                               ++ T.unpack src ++ "' not found"))
-              (Conv.getDoubleVec src df)
-      n   = V.length vec
-      shifted = V.fromList
-        [ if i < k then 0/0
-            else V.unsafeIndex vec (i - k)
-        | i <- [0..n-1] ]
-  in DX.insertColumn out (DX.fromList (V.toList shifted)) df
-
--- | Shift a numeric column @k@ positions backward (lead). The last
--- @k@ entries become NaN.
-leadColumn
-  :: Int
-  -> T.Text
-  -> T.Text
-  -> DXD.DataFrame
-  -> DXD.DataFrame
-leadColumn k src out df =
-  let vec = fromMaybe (error ("leadColumn: column '"
-                               ++ T.unpack src ++ "' not found"))
-              (Conv.getDoubleVec src df)
-      n   = V.length vec
-      shifted = V.fromList
-        [ if i + k >= n then 0/0
-            else V.unsafeIndex vec (i + k)
-        | i <- [0..n-1] ]
-  in DX.insertColumn out (DX.fromList (V.toList shifted)) df
-
--- ---------------------------------------------------------------------------
--- Rolling window
--- ---------------------------------------------------------------------------
-
--- | Rolling mean with a fixed window size. The first @(window-1)@
--- entries are NaN.
-rollingMean
-  :: Int              -- ^ Window size.
-  -> T.Text           -- ^ Source column.
-  -> T.Text           -- ^ Output column.
-  -> DXD.DataFrame
-  -> DXD.DataFrame
-rollingMean win src out =
-  rollingApply win mean src out
-  where
-    mean xs = sum xs / fromIntegral (length xs)
-
--- | Rolling sum with a fixed window size.
-rollingSum
-  :: Int
-  -> T.Text
-  -> T.Text
-  -> DXD.DataFrame
-  -> DXD.DataFrame
-rollingSum win = rollingApply win sum
-
--- | Apply an arbitrary aggregation @f :: [Double] -> Double@ over a
--- rolling window. The first @(window-1)@ entries become NaN.
-rollingApply
-  :: Int
-  -> ([Double] -> Double)
-  -> T.Text
-  -> T.Text
-  -> DXD.DataFrame
-  -> DXD.DataFrame
-rollingApply win f src out df =
-  let vec = fromMaybe (error ("rollingApply: column '"
-                               ++ T.unpack src ++ "' not found"))
-              (Conv.getDoubleVec src df)
-      n   = V.length vec
-      results = V.fromList
-        [ if i + 1 < win then 0/0
-            else f [V.unsafeIndex vec (i - win + 1 + j) | j <- [0..win-1]]
-        | i <- [0..n-1] ]
-  in DX.insertColumn out (DX.fromList (V.toList results)) df
-
--- ---------------------------------------------------------------------------
--- Internal helpers
--- ---------------------------------------------------------------------------
-
--- | Distinct values, preserving order of first appearance.
-orderedUnique :: Ord a => [a] -> [a]
-orderedUnique = go Set.empty
-  where
-    go _    []     = []
-    go seen (x:xs)
-      | Set.member x seen = go seen xs
-      | otherwise         = x : go (Set.insert x seen) xs
diff --git a/src/Hanalyze/DataIO/Sniff.hs b/src/Hanalyze/DataIO/Sniff.hs
deleted file mode 100644
--- a/src/Hanalyze/DataIO/Sniff.hs
+++ /dev/null
@@ -1,228 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.DataIO.Sniff
--- Description : CSV の delimiter・comment・header 有無・NA 候補を先頭 8KB から推測する
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Auto-detect a CSV's delimiter, comment lines, presence of header,
--- and NA candidates by inspecting the first 8 KB. While @LoadOpts@ lets
--- the user state these explicitly, this module adds a layer that guesses
--- when nothing is specified.
---
--- Design notes:
---
---   * 8 KB is assumed to be enough to decide structure (we don't stream
---     huge files).
---   * Inference results live in a 'Sniff' record. Supporting evidence
---     (per-delimiter scores etc.) is recorded in 'sfNotes' and emitted
---     as Info codes through @LogReport@.
---   * Sniffing is best-effort and decoupled from the strict path: users
---     can disable it entirely with @--no-sniff@, or escalate any
---     mismatch to an error with @--strict@.
-module Hanalyze.DataIO.Sniff
-  ( -- * 型
-    Sniff (..)
-  , defaultSniff
-    -- * Inference
-  , sniffBytes
-  , sniffFile
-    -- * Per-check helpers (exposed for tests)
-  , detectDelimiter
-  , detectHasHeader
-  , detectSkip
-  , detectCommentChar
-  ) where
-
-import qualified Data.ByteString      as BS
-import qualified Data.ByteString.Char8 as BS8
-import Data.Char (ord, isDigit)
-import Data.List (sortBy, maximumBy)
-import Data.Ord  (comparing)
-import Data.Text (Text)
-import qualified Data.Text as T
-import Text.Read (readMaybe)
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
--- | Result of sniffing a file's structure.
-data Sniff = Sniff
-  { sfDelim       :: !Char         -- ^ Inferred delimiter
-                                   --   (@\",\" \";\" \"\\t\" \" \" \"|\"@).
-  , sfHasHeader   :: !Bool         -- ^ Does the file appear to have a
-                                   --   header row? When 'False', generate
-                                   --   @col0@-style names.
-  , sfSkip        :: !Int          -- ^ Number of leading rows to skip
-                                   --   (comments / metadata).
-  , sfCommentChar :: !(Maybe Char) -- ^ Comment-line prefix character, if
-                                   --   detected.
-  , sfNotes       :: ![Text]       -- ^ Human-readable notes on the
-                                   --   inference (used by @LogReport@).
-  } deriving (Eq, Show)
-
--- | Default sniff result: comma-delimited, header present, no skip, no
--- comment char.
-defaultSniff :: Sniff
-defaultSniff = Sniff
-  { sfDelim       = ','
-  , sfHasHeader   = True
-  , sfSkip        = 0
-  , sfCommentChar = Nothing
-  , sfNotes       = []
-  }
-
--- ---------------------------------------------------------------------------
--- 公開 API
--- ---------------------------------------------------------------------------
-
--- | Sniff a file by reading its first 8 KB.
-sniffFile :: FilePath -> IO Sniff
-sniffFile path = do
-  bs <- BS.readFile path
-  return (sniffBytes (BS.take 8192 bs))
-
--- | Sniff a byte buffer directly (for testing or non-file sources).
-sniffBytes :: BS.ByteString -> Sniff
-sniffBytes bs0 =
-  let bs       = stripBOM bs0
-      ls0      = filter (not . BS.null) (BS.split (fromIntegral (ord '\n')) bs)
-      ls       = map stripCR ls0
-      (skipN, mComment) = detectSkip ls
-      dataLines = drop skipN ls
-      delim    = detectDelimiter dataLines
-      hasHdr   = detectHasHeader delim dataLines
-      notes    = mconcat
-        [ ["delimiter = " <> renderDelim delim]
-        , ["header     = " <> if hasHdr then "yes" else "no"]
-        , [ "skip       = " <> T.pack (show skipN)
-          | skipN > 0 ]
-        , [ "comment    = '" <> T.singleton c <> "'"
-          | Just c <- [mComment] ]
-        ]
-  in Sniff
-       { sfDelim       = delim
-       , sfHasHeader   = hasHdr
-       , sfSkip        = skipN
-       , sfCommentChar = mComment
-       , sfNotes       = notes
-       }
-
-renderDelim :: Char -> Text
-renderDelim '\t' = "tab"
-renderDelim ' '  = "space"
-renderDelim c    = "'" <> T.singleton c <> "'"
-
--- ---------------------------------------------------------------------------
--- delimiter 推論
--- ---------------------------------------------------------------------------
-
--- | 候補 delimiter ('`,;\t|`') について各行での出現数を取り、
--- 「行ごとの分散が小さい」 + 「中央値の出現数が多い」を優先する。
--- そもそも空入力やシングル行の場合は ',' を返す。
-detectDelimiter :: [BS.ByteString] -> Char
-detectDelimiter [] = ','
-detectDelimiter ls =
-  let candidates = ',' : ';' : '\t' : '|' : []
-      score c =
-        let counts = map (BS.count (fromIntegral (ord c))) (take 20 ls)
-        in (median counts, varianceD counts)  -- (大→良, 小→良)
-      -- variance を最優先で昇順 (= 列数が安定している = 確実な delimiter)、
-      -- 次に median を降順 (出現数が多いほど良い)。
-      -- median 優先だと "1,5;2,5;3,0" のような文字列で comma が 3 出るために
-      -- 誤って comma が選ばれてしまう。
-      cmp a b =
-        let (ma, va) = score a
-            (mb, vb) = score b
-        in compare va vb <> compare mb ma
-      ranked = sortBy cmp candidates
-  in case ranked of
-       (c:_) | fst (score c) >= 1 -> c
-       _                           -> ','
-
-median :: [Int] -> Int
-median xs =
-  let s = sortBy compare xs
-      n = length s
-  in if n == 0 then 0 else s !! (n `div` 2)
-
--- | 整数除算で潰さない分散 (Double で計算)。
-varianceD :: [Int] -> Double
-varianceD xs =
-  let n = length xs
-      m = (fromIntegral (sum xs) :: Double) / fromIntegral (max 1 n)
-  in if n <= 1 then 0
-     else sum [ (fromIntegral x - m) ** 2 | x <- xs ] / fromIntegral (n - 1)
-
--- ---------------------------------------------------------------------------
--- ヘッダ有無の推論
--- ---------------------------------------------------------------------------
-
--- | 1 行目の各セルが全て numeric token なら「ヘッダ無し」と判断する。
--- それ以外 (text を含む) は「ヘッダ有り」。空入力は True を返す
--- (Hackage が空 CSV を弾くため、あとはそちら側で扱う)。
-detectHasHeader :: Char -> [BS.ByteString] -> Bool
-detectHasHeader _      []       = True
-detectHasHeader delim (l:_) =
-  let cells  = BS.split (fromIntegral (ord delim)) l
-      tokens = map (T.strip . decodeAscii) cells
-      isNum t = case readMaybe (T.unpack t) :: Maybe Double of
-                  Just _  -> True
-                  Nothing -> False
-  in not (all isNum tokens)
-  || null tokens
-  || all T.null tokens
-
--- ---------------------------------------------------------------------------
--- 先頭 skip / コメント文字の推論
--- ---------------------------------------------------------------------------
-
--- | 先頭から「コメント文字」で始まる行が連続する数を skip 候補とする。
--- コメント文字は @#@ / @!@ / @;@ / @\/\/@ のどれか。検出文字も返す。
-detectSkip :: [BS.ByteString] -> (Int, Maybe Char)
-detectSkip ls =
-  let candidates = ['#', '!']
-      n c = length (takeWhile (startsWith c) ls)
-      best = maximumBy (comparing (\c -> n c)) candidates
-      k    = n best
-  in if k > 0
-       then (k, Just best)
-       else (0, Nothing)
-
-startsWith :: Char -> BS.ByteString -> Bool
-startsWith c bs =
-  let bs' = BS.dropWhile (\b -> b == fromIntegral (ord ' ')
-                              || b == fromIntegral (ord '\t')) bs
-  in case BS.uncons bs' of
-       Just (h, _) -> h == fromIntegral (ord c)
-       Nothing     -> False
-
--- | 'detectSkip' の結果からコメント文字だけ取り出すラッパ。
-detectCommentChar :: [BS.ByteString] -> Maybe Char
-detectCommentChar = snd . detectSkip
-
--- ---------------------------------------------------------------------------
--- ユーティリティ
--- ---------------------------------------------------------------------------
-
-stripCR :: BS.ByteString -> BS.ByteString
-stripCR bs
-  | BS.null bs                              = bs
-  | BS.last bs == fromIntegral (ord '\r')   = BS.init bs
-  | otherwise                               = bs
-
-stripBOM :: BS.ByteString -> BS.ByteString
-stripBOM bs
-  | BS.length bs >= 3
-  , BS.index bs 0 == 0xEF
-  , BS.index bs 1 == 0xBB
-  , BS.index bs 2 == 0xBF = BS.drop 3 bs
-  | otherwise             = bs
-
-decodeAscii :: BS.ByteString -> Text
-decodeAscii = T.pack . BS8.unpack
-
--- 未使用ワーニング抑止
-_unusedRefs :: ([Char], Char -> Bool)
-_unusedRefs = ("?", isDigit)
diff --git a/src/Hanalyze/Design/Anova.hs b/src/Hanalyze/Design/Anova.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Anova.hs
+++ /dev/null
@@ -1,151 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.Anova
--- Description : 一元配置・二元配置 ANOVA/ANCOVA 表の算出 (F 値・p 値・η² 効果量)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- ANOVA / ANCOVA tables.
---
--- Computes one-way and two-way analysis of variance, reporting F values,
--- p values, and the @η²@ effect size.
-module Hanalyze.Design.Anova
-  ( AnovaRow (..)
-  , AnovaTable (..)
-  , oneWayAnova
-  , twoWayAnova
-  , printAnovaTable
-  ) where
-
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.List (groupBy, sort)
-import Data.Function (on)
-import Text.Printf (printf)
-import qualified Statistics.Distribution as SD
-import qualified Statistics.Distribution.FDistribution as FD
-
--- | One row of an ANOVA table.
-data AnovaRow = AnovaRow
-  { arSource :: Text             -- ^ Source label.
-  , arDF     :: Int              -- ^ Degrees of freedom.
-  , arSS     :: Double           -- ^ Sum of squares.
-  , arMS     :: Double           -- ^ Mean square (@SS / DF@).
-  , arF      :: Maybe Double     -- ^ F statistic ('Nothing' for total / error rows).
-  , arPVal   :: Maybe Double     -- ^ p-value.
-  , arEtaSq  :: Maybe Double     -- ^ Effect size @η² = SS_factor / SS_total@.
-  } deriving (Show)
-
--- | A complete ANOVA table.
-newtype AnovaTable = AnovaTable [AnovaRow] deriving (Show)
-
--- | One-way ANOVA. The arguments are the group label per data point and
--- the corresponding values.
-oneWayAnova :: [Text] -> [Double] -> AnovaTable
-oneWayAnova labels values =
-  let n         = length values
-      grandMean = sum values / fromIntegral n
-      groups    = groupBy ((==) `on` fst)
-                $ sort (zip labels values)
-      ssTotal   = sum [(v - grandMean)^(2::Int) | v <- values]
-      -- グループ間平方和 (Between)
-      ssBetween = sum
-        [ let xs   = map snd g
-              gm   = sum xs / fromIntegral (length xs)
-              k    = length xs
-          in fromIntegral k * (gm - grandMean)^(2::Int)
-        | g <- groups ]
-      ssWithin  = ssTotal - ssBetween
-      kGroups   = length groups
-      dfBetween = kGroups - 1
-      dfWithin  = n - kGroups
-      msBetween = ssBetween / fromIntegral dfBetween
-      msWithin  = ssWithin  / fromIntegral dfWithin
-      fStat     = msBetween / msWithin
-      pVal      = if dfWithin <= 0 || msWithin <= 0
-                    then 1
-                    else SD.complCumulative
-                          (FD.fDistribution dfBetween dfWithin) fStat
-      etaSq     = ssBetween / ssTotal
-  in AnovaTable
-      [ AnovaRow "Between" dfBetween ssBetween msBetween
-                 (Just fStat) (Just pVal) (Just etaSq)
-      , AnovaRow "Within"  dfWithin  ssWithin  msWithin
-                 Nothing Nothing Nothing
-      , AnovaRow "Total"   (n - 1)   ssTotal   (ssTotal / fromIntegral (n - 1))
-                 Nothing Nothing Nothing
-      ]
-
--- | Two-way ANOVA (no interaction term).
---
--- Each cell @(a, b)@ is assumed to hold exactly one observation, and
--- the data is assumed balanced (all cells have the same observation
--- count).
-twoWayAnova :: [Text]   -- ^ Factor A label per observation.
-            -> [Text]   -- ^ Factor B label per observation.
-            -> [Double] -- ^ Values.
-            -> AnovaTable
-twoWayAnova as bs values =
-  let n    = length values
-      gm   = sum values / fromIntegral n
-      ssT  = sum [(v - gm)^(2::Int) | v <- values]
-      -- 因子 A の主効果
-      aGroups = groupBy ((==) `on` fst) (sort (zip as values))
-      ssA  = sum
-        [ let vs = map snd g
-              m  = sum vs / fromIntegral (length vs)
-          in fromIntegral (length vs) * (m - gm)^(2::Int)
-        | g <- aGroups ]
-      -- 因子 B の主効果
-      bGroups = groupBy ((==) `on` fst) (sort (zip bs values))
-      ssB  = sum
-        [ let vs = map snd g
-              m  = sum vs / fromIntegral (length vs)
-          in fromIntegral (length vs) * (m - gm)^(2::Int)
-        | g <- bGroups ]
-      ssE  = ssT - ssA - ssB
-      a    = length aGroups
-      b    = length bGroups
-      dfA  = a - 1
-      dfB  = b - 1
-      dfE  = n - a - b + 1
-      msA  = ssA / fromIntegral dfA
-      msB  = ssB / fromIntegral dfB
-      msE  = if dfE > 0 then ssE / fromIntegral dfE else 1
-      fA   = msA / msE
-      fB   = msB / msE
-      pA   = if dfE <= 0 then 1
-               else SD.complCumulative (FD.fDistribution dfA dfE) fA
-      pB   = if dfE <= 0 then 1
-               else SD.complCumulative (FD.fDistribution dfB dfE) fB
-  in AnovaTable
-      [ AnovaRow "Factor A" dfA ssA msA (Just fA) (Just pA) (Just (ssA/ssT))
-      , AnovaRow "Factor B" dfB ssB msB (Just fB) (Just pB) (Just (ssB/ssT))
-      , AnovaRow "Error"    dfE ssE msE Nothing Nothing Nothing
-      , AnovaRow "Total"    (n - 1) ssT (ssT / fromIntegral (n - 1))
-                 Nothing Nothing Nothing
-      ]
-
--- | Pretty-print the table to stdout.
-printAnovaTable :: AnovaTable -> IO ()
-printAnovaTable (AnovaTable rows) = do
-  printf "%-12s %4s %12s %12s %10s %10s %8s\n"
-    ("Source" :: String) ("DF" :: String) ("SS" :: String) ("MS" :: String)
-    ("F" :: String) ("p-value" :: String) ("η²" :: String)
-  putStrLn (replicate 76 '-')
-  mapM_ printRow rows
-  where
-    printRow r = do
-      printf "%-12s %4d %12.4f %12.4f"
-             (T.unpack (arSource r)) (arDF r) (arSS r) (arMS r)
-      let fmtMaybe :: Double -> String
-          fmtMaybe v = printf "%10.4f" v
-      case arF r of
-        Just f  -> putStr (fmtMaybe f)
-        Nothing -> putStr (printf "%10s" ("--" :: String))
-      case arPVal r of
-        Just p  -> putStr (fmtMaybe p)
-        Nothing -> putStr (printf "%10s" ("--" :: String))
-      case arEtaSq r of
-        Just e  -> printf "%8.4f\n" e
-        Nothing -> printf "%8s\n" ("--" :: String)
diff --git a/src/Hanalyze/Design/Block.hs b/src/Hanalyze/Design/Block.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Block.hs
+++ /dev/null
@@ -1,79 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.Block
--- Description : ブロック計画 (ラテン方格・グレコラテン方格・乱塊法) の生成
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Block designs: Latin squares and randomized complete block designs.
---
---   * 'latinSquare'        — @n × n@ Latin square (efficient arrangement
---     of @n@ treatments).
---   * 'graecoLatinSquare'  — pair of orthogonal Latin squares.
---   * 'randomizedBlock'    — randomized block design (@b@ blocks × @t@
---     treatments).
---   * 'shuffleSeq'         — pseudo-random sequence shuffler (seed-driven
---     for reproducibility).
-module Hanalyze.Design.Block
-  ( latinSquare
-  , graecoLatinSquare
-  , randomizedBlock
-  , shuffleSeq
-  ) where
-
-import Data.List (foldl')
-
--- | Build an @n × n@ Latin square. Cell values are @1..n@.
---
--- 標準形 (cyclic shift):
---   row i, col j → ((i + j) mod n) + 1
-latinSquare :: Int -> [[Int]]
-latinSquare n
-  | n < 1     = []
-  | otherwise =
-      [ [((i + j) `mod` n) + 1 | j <- [0 .. n - 1]]
-      | i <- [0 .. n - 1] ]
-
--- | Graeco-Latin square (a pair of orthogonal Latin squares).
--- n が素数のとき構成可能 (n=6 は不可能)。
--- 戻り値は (n × n) のセルごとに (a, b) のペア (両方とも 1..n)。
---
--- 構成: (i + j) mod n と (i + 2j) mod n
-graecoLatinSquare :: Int -> Maybe [[(Int, Int)]]
-graecoLatinSquare n
-  | n < 3 || n == 6 = Nothing
-  | otherwise = Just
-      [ [ (((i + j)     `mod` n) + 1
-          , ((i + 2 * j) `mod` n) + 1)
-        | j <- [0 .. n - 1] ]
-      | i <- [0 .. n - 1] ]
-
--- | Randomized complete block design: @b@ blocks of @t@ treatments.
---
--- Within each block, treatments @1..t@ are placed in a randomized order.
--- The result @[[Int]]@ has one row per block; values inside a row are
--- the application order of treatment IDs.
-randomizedBlock :: Int             -- ^ Number of blocks @b@.
-                -> Int             -- ^ Number of treatments @t@.
-                -> Int             -- ^ Random seed.
-                -> [[Int]]
-randomizedBlock b t seed =
-  [ shuffleSeq (seed + i * 1000) [1 .. t] | i <- [0 .. b - 1] ]
-
--- | Fisher-Yates pseudo-random shuffle (seeded for reproducibility).
--- Uses a simple internal LCG (test-quality only, not cryptographically
--- strong).
-shuffleSeq :: Int -> [a] -> [a]
-shuffleSeq seed xs =
-  let n   = length xs
-      lcg s = (s * 1103515245 + 12345) `mod` (2 ^ (31 :: Int))
-      seeds = take n (drop 1 (iterate lcg seed))
-      -- (rand, original_index) でソート → 擬似シャッフル
-      paired = zip seeds xs
-      sorted = foldl' insert [] paired
-      insert acc p = mergeOne p acc
-      mergeOne (k, x) ((k', y) : rest)
-        | k < k' = (k, x) : (k', y) : rest
-        | otherwise = (k', y) : mergeOne (k, x) rest
-      mergeOne p [] = [p]
-  in map snd sorted
diff --git a/src/Hanalyze/Design/Constraint.hs b/src/Hanalyze/Design/Constraint.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Constraint.hs
+++ /dev/null
@@ -1,93 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.Constraint
--- Description : DoE 古典側の設計制約 (線形不等式・禁止行の組合せ) によるフィルタ / 検証
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- DoE 古典側の設計制約 (Phase 23-b)。
---
--- 候補集合ベースの 'Hanalyze.Design.Optimal' に渡す前のフィルタ用途、
--- および手動構築した設計行列の事後検証用途を想定。
---
--- ADT は最小 2 種:
---
---   * 'LinearConstraint' coeffs rel rhs — 線形不等式 / 等式
---     @sum_i (coeffs[i] * x[i]) `rel` rhs@
---   * 'ForbiddenCombination' values — 厳密に一致する row を禁止
---     (浮動小数比較は 'forbiddenTolerance' = 1e-9 で許容)
---
--- 条件付 (If-then) 制約は本モジュールでは扱わない (Custom Design spec 専有、
--- spec/hanalyze-doe-custom-design-spec.md §2.3 / §9 参照)。
---
--- spec: doe-spec v0.2 §2.8 / §3.12。
-module Hanalyze.Design.Constraint
-  ( ConstraintRel (..)
-  , DesignConstraint (..)
-  , checkRow
-  , checkDesign
-  , filterCandidates
-  , forbiddenTolerance
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
--- | 線形制約の関係子。
-data ConstraintRel = CLeq | CEq | CGeq
-  deriving (Eq, Show)
-
--- | 設計行列に対する制約。
-data DesignConstraint
-  = LinearConstraint     ![Double] !ConstraintRel !Double
-    -- ^ @sum_i (coeffs[i] * x[i]) `rel` rhs@。 coeffs の長さは row の
-    --   次元と一致する必要 ('checkRow' は不一致を即 False として弾く)
-  | ForbiddenCombination ![Double]
-    -- ^ row がこの値と (許容誤差 'forbiddenTolerance' で) 一致したら違反。
-  deriving (Eq, Show)
-
--- | 'ForbiddenCombination' の浮動小数比較に用いる許容誤差。
-forbiddenTolerance :: Double
-forbiddenTolerance = 1e-9
-
--- ===========================================================================
--- 公開 API
--- ===========================================================================
-
--- | 1 row が全制約を満たすか。 制約違反 (= 不可) なら 'False'。
-checkRow :: [DesignConstraint] -> [Double] -> Bool
-checkRow cs row = all (rowSatisfies row) cs
-
--- | 設計行列 (= 各 row が 1 試行) の制約違反 row index を返す。
--- row 数 0 のときは空 list。
-checkDesign :: [DesignConstraint] -> LA.Matrix Double -> [Int]
-checkDesign cs m =
-  let rows = LA.toLists m
-  in [ i | (i, r) <- zip [0 ..] rows, not (checkRow cs r) ]
-
--- | 候補集合から制約違反 row を除去する helper。 'Hanalyze.Design.Optimal'
--- の入力候補を作る前に挟む想定。 順序は保持。
-filterCandidates :: [DesignConstraint] -> [[Double]] -> [[Double]]
-filterCandidates cs = filter (checkRow cs)
-
--- ===========================================================================
--- 内部
--- ===========================================================================
-
--- | 1 row が単一制約を満たすか判定。
-rowSatisfies :: [Double] -> DesignConstraint -> Bool
-rowSatisfies row (LinearConstraint coeffs rel rhs)
-  | length coeffs /= length row = False
-  | otherwise =
-      let lhs = sum (zipWith (*) coeffs row)
-      in case rel of
-           CLeq -> lhs <= rhs + forbiddenTolerance
-           CEq  -> abs (lhs - rhs) <= forbiddenTolerance
-           CGeq -> lhs >= rhs - forbiddenTolerance
-rowSatisfies row (ForbiddenCombination vals)
-  | length vals /= length row = True   -- 次元不一致 = forbidden ではない
-  | otherwise =
-      not (and (zipWith (\a b -> abs (a - b) <= forbiddenTolerance) row vals))
diff --git a/src/Hanalyze/Design/Custom/Augment.hs b/src/Hanalyze/Design/Custom/Augment.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Custom/Augment.hs
+++ /dev/null
@@ -1,383 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns      #-}
--- |
--- Module      : Hanalyze.Design.Custom.Augment
--- Description : Custom Design の Augment 5 メニュー (Replicate/AddCenter/AddAxial/AddRuns/Foldover)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Custom Design の Augment 5 メニュー (Phase 25-6/7/8)。
---
--- spec: doe-custom-design-spec v0.1.1 §2.6 / §3。
--- 参考: JMP "Augment Design" platform。
---
--- ## 5 メニュー
---
---   * 'Replicate n'   : 既存 design を n 回複製
---   * 'AddCenter  n'  : 中心点 (全連続因子 = 0、 categorical は ref level) を n 行追加
---   * 'AddAxial   α'  : 1 因子だけを ±α、 他を 0 にした axial 点を全連続因子で追加
---                       (= 2 * #continuous-factors 行)
---   * 'AddRuns    n'  : 既存 augmentDesign (古典 Fedorov 交換) で N 行追加
---   * 'Foldover   k'  : 既存 design の sign-flipped 行を全部追加 (Full)、
---                       または指定因子のみ flip (Partial)
---
--- ## 制限 (Phase 25-8 暫定)
---
---   * 'cdsInitial' が 'Nothing' の場合は 'Left' (既存 design 必須)
---   * AddCenter / AddAxial は連続因子のみ。 categorical 列は ref index 0 を使う
---   * Foldover は 2 水準連続因子のみ正しく動作。 categorical はそのまま (flip しない)
---   * AddAxial は coded space ([-1, 1]) 想定、 raw range を考慮しない
-module Hanalyze.Design.Custom.Augment
-  ( AugmentMenu (..)
-  , FoldoverKind (..)
-  , AugmentMenuResult (..)
-  , augmentMenu
-  ) where
-
-import           Data.Text                (Text)
-import qualified Data.Text                as T
-import qualified Numeric.LinearAlgebra    as LA
-
-import           Hanalyze.Design.Custom.Factor
-import           Hanalyze.Design.Custom.Coordinate
-                   (CustomDesignSpec (..))
-import qualified Hanalyze.Design.Optimal  as Opt
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
-data AugmentMenu
-  = Replicate !Int
-  | AddCenter !Int
-  | AddAxial  !Double !Bool
-    -- ^ axial 点。 第 2 引数 @rawUnits@ が False のとき (= 既定) は coded
-    -- @[-1, 1]@ 空間で center 0 + ±α (NCoded モデル想定)。 True のとき raw
-    -- 単位で center (lo+hi)/2 ± α·(hi-lo)/2 (Phase 28-10 で追加)。 raw 形式の
-    -- 既存設計に直接 ±α coded 相当の axial 点を入れたいケースに使う
-  | AddRuns   !Int
-  | Foldover  !FoldoverKind
-  deriving (Show, Eq)
-
-data FoldoverKind
-  = FullFoldover
-  | PartialFoldover ![Text]  -- ^ flip する因子名のリスト
-  | CategoricalSwap ![(Text, [(Text, Text)])]
-    -- ^ Phase 28-7: categorical 因子の level swap mapping。 各エントリ
-    -- @(factor_name, [(old_level, new_level), ...])@ に対し、 既存設計の
-    -- 該当列の level を mapping で置換した行を追加。 連続因子の符号 flip は
-    -- 行わない (CategoricalSwap は categorical 専用)。 mapping に現れない
-    -- level はそのまま (自分自身に map)
-  deriving (Show, Eq)
-
-data AugmentMenuResult = AugmentMenuResult
-  { amrMatrix :: !(LA.Matrix Double)
-    -- ^ 増補後の design (existing + added)
-  , amrAdded  :: !Int
-    -- ^ 追加された行数
-  , amrMethod :: !Text
-    -- ^ "Replicate" / "AddCenter" / etc.
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- 公開 API
--- ---------------------------------------------------------------------------
-
-augmentMenu :: CustomDesignSpec -> AugmentMenu -> IO (Either Text AugmentMenuResult)
-augmentMenu spec menu =
-  case cdsInitial spec of
-    Nothing -> pure (Left (T.pack "augmentMenu: cdsInitial is required"))
-    Just existing ->
-      case menu of
-        Replicate k    -> pure (augmentReplicate existing k)
-        AddCenter k    -> pure (augmentAddCenter (cdsFactors spec) existing k)
-        AddAxial alpha rawUnits ->
-          pure (augmentAddAxial (cdsFactors spec) existing alpha rawUnits)
-        AddRuns k      -> pure (augmentAddRuns spec existing k)
-        Foldover kind  -> pure (augmentFoldover (cdsFactors spec) existing kind)
-
--- ---------------------------------------------------------------------------
--- Replicate
--- ---------------------------------------------------------------------------
-
-augmentReplicate :: LA.Matrix Double -> Int -> Either Text AugmentMenuResult
-augmentReplicate existing k
-  | k < 1 = Left (T.pack "Replicate: k must be >= 1")
-  | otherwise =
-      let !rows = LA.toRows existing
-          !reps = concat (replicate k rows)
-          !added = LA.fromRows reps
-          !full = LA.fromRows (rows ++ reps)
-      in Right AugmentMenuResult
-           { amrMatrix = full
-           , amrAdded  = LA.rows added
-           , amrMethod = T.pack "Replicate"
-           }
-
--- ---------------------------------------------------------------------------
--- AddCenter
--- ---------------------------------------------------------------------------
-
--- | 中心点: 連続因子は 0、 categorical は level index 0 (= reference)。
-augmentAddCenter
-  :: [Factor]
-  -> LA.Matrix Double
-  -> Int
-  -> Either Text AugmentMenuResult
-augmentAddCenter factors existing k
-  | k < 1 = Left (T.pack "AddCenter: k must be >= 1")
-  | LA.cols existing /= length factors =
-      Left (T.pack "AddCenter: existing column count ≠ #factors")
-  | otherwise =
-      let !centerRow = LA.fromList (map factorCenter factors)
-          !added = LA.fromRows (replicate k centerRow)
-          !full  = existing LA.=== added
-      in Right AugmentMenuResult
-           { amrMatrix = full
-           , amrAdded  = k
-           , amrMethod = T.pack "AddCenter"
-           }
-
-factorCenter :: Factor -> Double
-factorCenter f = case fKind f of
-  Continuous  _ _ -> 0
-  DiscreteNum xs  -> case xs of
-                       []      -> 0
-                       (h:_)   -> sum xs / fromIntegral (length xs)
-                         where _ = h
-  Mixture lo hi   -> (lo + hi) / 2
-  Categorical _   -> 0
-  Ordinal     _   -> 0
-
--- ---------------------------------------------------------------------------
--- AddAxial
--- ---------------------------------------------------------------------------
-
--- | axial / star 点: 各連続因子について、 その因子だけを +α / -α、
--- 他を 0 (中心) にした 2 点ずつを追加。
---
--- @rawUnits@ False: coded 空間 (center 0 + ±α) で生成。 NCoded モデルで
---   raw 行列が既に coded されている前提。
--- @rawUnits@ True: 因子の (lo, hi) を使って center (lo+hi)/2 + ±α·(hi-lo)/2
---   で生成 (raw 単位、 coded ±α 相当の位置)。 Continuous / DiscreteNum /
---   Mixture でそれぞれの range を解釈する。
-augmentAddAxial
-  :: [Factor]
-  -> LA.Matrix Double
-  -> Double
-  -> Bool
-  -> Either Text AugmentMenuResult
-augmentAddAxial factors existing alpha rawUnits
-  | alpha <= 0 = Left (T.pack "AddAxial: alpha must be > 0")
-  | LA.cols existing /= length factors =
-      Left (T.pack "AddAxial: existing column count ≠ #factors")
-  | otherwise =
-      let !contIxs =
-            [ i | (i, f) <- zip [0 ..] factors, factorIsContinuous f ]
-      in if null contIxs
-           then Left (T.pack "AddAxial: no continuous factors to augment")
-           else
-             let !centers = if rawUnits
-                              then map factorCenterRaw factors
-                              else map factorCenter factors
-                 axialOffset i = if rawUnits
-                                   then alpha * factorHalfRange (factors !! i)
-                                   else alpha
-                 !rows =
-                   [ LA.fromList
-                       [ if j == i then (centers !! j) + sgn * axialOffset i
-                                   else centers !! j
-                       | j <- [0 .. length factors - 1]
-                       ]
-                   | i <- contIxs, sgn <- [1, -1]
-                   ]
-                 !added = LA.fromRows rows
-                 !full  = existing LA.=== added
-             in Right AugmentMenuResult
-                  { amrMatrix = full
-                  , amrAdded  = length rows
-                  , amrMethod = T.pack "AddAxial"
-                  }
-
--- | 因子の半幅 = (hi - lo) / 2 (Continuous / DiscreteNum / Mixture)。
--- raw 単位 axial の scale factor として使う。 Categorical / Ordinal は意味を
--- 持たないため 0 を返す (caller 側 contIxs で除外済の想定)。
-factorHalfRange :: Factor -> Double
-factorHalfRange f = case fKind f of
-  Continuous  lo hi -> (hi - lo) / 2
-  DiscreteNum xs    -> case xs of
-                         [] -> 0
-                         _  -> (maximum xs - minimum xs) / 2
-  Mixture     lo hi -> (hi - lo) / 2
-  _                 -> 0
-
--- | 因子の raw 単位での中心 = (lo + hi) / 2 (Phase 28-10 AddAxial rawUnits=True 用)。
--- 'factorCenter' は coded 空間想定 (Continuous → 0)、 これは raw 空間。
-factorCenterRaw :: Factor -> Double
-factorCenterRaw f = case fKind f of
-  Continuous  lo hi -> (lo + hi) / 2
-  DiscreteNum xs    -> case xs of
-                         [] -> 0
-                         _  -> (maximum xs + minimum xs) / 2
-  Mixture     lo hi -> (lo + hi) / 2
-  _                 -> 0
-
--- ---------------------------------------------------------------------------
--- AddRuns (既存 augmentDesign を wrap)
--- ---------------------------------------------------------------------------
-
--- | AddRuns: 既存 'Hanalyze.Design.Optimal.augmentDesign' を使い、
--- 候補集合は連続因子は ±1 grid、 categorical は全 level の cartesian product。
--- 候補集合サイズが大きくなりすぎる場合 (例 2^20 等) は呼び出し側で nRuns を
--- 抑制すること。
-augmentAddRuns
-  :: CustomDesignSpec
-  -> LA.Matrix Double
-  -> Int
-  -> Either Text AugmentMenuResult
-augmentAddRuns spec existing k
-  | k < 1 = Left (T.pack "AddRuns: k must be >= 1")
-  | otherwise =
-      let factors  = cdsFactors spec
-          cands    = candidateRows factors
-          existRow = LA.toLists existing
-          seed     = case cdsSeed spec of Just s -> s; Nothing -> 0
-          arRes    = Opt.augmentDesign (cdsCriterion spec) existRow k cands seed
-      in if length (Opt.arNewRows arRes) /= k
-           then Left (T.pack
-             ("AddRuns: failed to add " <> show k <> " rows (candidates may be too few)"))
-           else
-             let !added = LA.fromLists (Opt.arNewRows arRes)
-                 !full  = existing LA.=== added
-             in Right AugmentMenuResult
-                  { amrMatrix = full
-                  , amrAdded  = k
-                  , amrMethod = T.pack "AddRuns"
-                  }
-
--- | 候補集合: 連続因子は ±1、 categorical / ordinal は全 level、 DiscreteNum は xs、
--- Mixture は [lo, hi] の 2 点 とする (簡略化、 Phase 25-7 で grid 拡張可)。
-candidateRows :: [Factor] -> [[Double]]
-candidateRows = cart . map factorCandidates
-  where
-    factorCandidates f = case fKind f of
-      Continuous _ _    -> [-1, 1]
-      DiscreteNum xs    -> xs
-      Mixture lo hi     -> [lo, hi]
-      Categorical xs    -> [fromIntegral i | i <- [0 .. length xs - 1]]
-      Ordinal     xs    -> [fromIntegral i | i <- [0 .. length xs - 1]]
-    cart :: [[Double]] -> [[Double]]
-    cart [] = [[]]
-    cart (xs:xss) =
-      [ x : ys | x <- xs, ys <- cart xss ]
-
--- ---------------------------------------------------------------------------
--- Foldover
--- ---------------------------------------------------------------------------
-
--- | Foldover: 既存 design の符号反転行を追加。
--- Full: 全因子の符号を flip。
--- Partial [names]: 指定因子のみ flip。
--- categorical 列は flip しない (符号の概念が無い)。
-augmentFoldover
-  :: [Factor]
-  -> LA.Matrix Double
-  -> FoldoverKind
-  -> Either Text AugmentMenuResult
-augmentFoldover factors existing kind
-  | LA.cols existing /= length factors =
-      Left (T.pack "Foldover: existing column count ≠ #factors")
-  | otherwise = case kind of
-      CategoricalSwap entries -> applyCatSwap factors existing entries
-      _ ->
-        let !names = map fName factors
-            !flipIdxs = case kind of
-              FullFoldover -> [ i | (i, f) <- zip [0 ..] factors, factorIsContinuous f ]
-              PartialFoldover ns ->
-                [ i | (i, f) <- zip [0 ..] factors
-                , factorIsContinuous f, fName f `elem` ns
-                ]
-              CategoricalSwap _ -> []  -- 上で処理済 (到達不可)
-        in if null flipIdxs && case kind of { FullFoldover -> False; _ -> True }
-             then Left (T.pack "Foldover: no factors to flip (check factor names)")
-             else
-               let !nE = LA.rows existing
-                   !p  = LA.cols existing
-                   !rows = LA.toLists existing
-                   !flipped =
-                     [ [ if j `elem` flipIdxs then negate (r !! j) else r !! j
-                       | j <- [0 .. p - 1] ]
-                     | r <- rows ]
-                   !added = LA.fromLists flipped
-                   !full  = existing LA.=== added
-                   _ = names  -- 未使用警告対策
-               in Right AugmentMenuResult
-                    { amrMatrix = full
-                    , amrAdded  = nE
-                    , amrMethod = case kind of
-                        FullFoldover     -> T.pack "Foldover/Full"
-                        PartialFoldover _ -> T.pack "Foldover/Partial"
-                        CategoricalSwap _ -> T.pack "Foldover/CatSwap"  -- 到達不可
-                    }
-
--- | Phase 28-7: categorical level swap foldover。 各エントリ
--- @(factor_name, [(old, new), ...])@ について、 該当列の level index 値を
--- old → new mapping で置換する (raw 値は level index Double として保持)。
-applyCatSwap
-  :: [Factor]
-  -> LA.Matrix Double
-  -> [(Text, [(Text, Text)])]
-  -> Either Text AugmentMenuResult
-applyCatSwap factors existing entries
-  | null entries = Left (T.pack "Foldover/CatSwap: empty mapping list")
-  | otherwise = do
-      perCol <- traverse (resolveSwap factors) entries
-      let nE   = LA.rows existing
-          p    = LA.cols existing
-          rows = LA.toLists existing
-          swapAt j v = case lookup j perCol of
-            Nothing -> v
-            Just m  -> case lookup (round v :: Int) m of
-              Just newIx -> fromIntegral newIx
-              Nothing    -> v
-          newRows = [ [ swapAt j (r !! j) | j <- [0 .. p - 1] ] | r <- rows ]
-          added = LA.fromLists newRows
-          full  = existing LA.=== added
-      pure AugmentMenuResult
-        { amrMatrix = full
-        , amrAdded  = nE
-        , amrMethod = T.pack "Foldover/CatSwap"
-        }
-
--- | factor 名 + level 名 mapping を、 列 index + level index mapping に解決。
-resolveSwap
-  :: [Factor]
-  -> (Text, [(Text, Text)])
-  -> Either Text (Int, [(Int, Int)])
-resolveSwap factors (fn, pairs) =
-  case lookupWithIdx fn factors of
-    Nothing -> Left (T.pack ("Foldover/CatSwap: factor not found: " <> T.unpack fn))
-    Just (i, f) -> case fKind f of
-      Categorical xs -> Right (i, mkPairs xs)
-      Ordinal     xs -> Right (i, mkPairs xs)
-      _ -> Left (T.pack
-            ("Foldover/CatSwap: factor " <> T.unpack fn <> " is not categorical/ordinal"))
-  where
-    mkPairs xs = [ (idx old, idx new) | (old, new) <- pairs
-                                      , idx old >= 0, idx new >= 0 ]
-      where
-        idx t = case lookup t (zip (map fst (zip xs [(0::Int)..])) [0..]) of
-          Just k  -> k
-          Nothing -> case elemIxOf t xs of Just k -> k; Nothing -> -1
-    elemIxOf t = go 0
-      where
-        go _ [] = Nothing
-        go k (x:xs') | t == x = Just k
-                     | otherwise = go (k + 1) xs'
-    lookupWithIdx :: Text -> [Factor] -> Maybe (Int, Factor)
-    lookupWithIdx n fs = go 0 fs
-      where
-        go _ [] = Nothing
-        go k (g:gs)
-          | fName g == n = Just (k, g)
-          | otherwise    = go (k + 1) gs
diff --git a/src/Hanalyze/Design/Custom/Bayesian.hs b/src/Hanalyze/Design/Custom/Bayesian.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Custom/Bayesian.hs
+++ /dev/null
@@ -1,151 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.Custom.Bayesian
--- Description : Bayesian D-optimality (DuMouchel-Jones 1994) の事前精度行列ヘルパ
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Bayesian D-optimality (DuMouchel-Jones 1994) のヘルパ (Phase 26)。
---
--- spec: doe-custom-design-spec v0.1.1 §2.7。
--- 参考: DuMouchel & Jones (1994) "A Simple Bayesian Modification of D-Optimal
--- Designs to Reduce Dependence on an Assumed Model", Technometrics 36:37-47。
---
--- ## 概念
---
--- 通常の D-opt は @det(XᵀX)@ を最大化する。 Bayesian D-opt は事前情報 (= 興味の
--- 薄い高次項に対する事前分布) を K (prior precision matrix) で表現し、
--- @det(XᵀX + K)@ を最大化する。
---
--- K の典型構造 (DuMouchel-Jones):
---
---   * 主効果 / intercept: 興味あり → K_jj = 0 (= 事前情報無し)
---   * 2 因子交互作用 / 二乗項: 興味薄 → K_jj = τ² (= τ² の事前精度で「ほぼ 0」 と仮定)
---   * 非対角は 0
---
--- τ² は 「effect が 1σ_error 程度になる確信度」 から決まる、 既定 1.0 で開始して
--- 設計者が調整する慣例。
---
--- ## 使い方
---
--- @
--- import Hanalyze.Design.Custom.Bayesian
--- import Hanalyze.Design.Optimal (OptCriterion (..))
---
--- let k = priorPrecisionDefault factors model 1.0
---     spec = ... { cdsCriterion = BayesianD (precisionToMatrix k) }
--- @
-module Hanalyze.Design.Custom.Bayesian
-  ( PriorPrecision (..)
-  , precisionToMatrix
-  , priorPrecisionDefault
-  , priorPrecisionFromTerms
-  , bayesianDValueM
-    -- * DuMouchel-Jones §2.2 規約 (Phase 28-12、 RegionMoment 再export)
-  , DJTransform (..)
-  , djFitTransform
-  , djApplyTransform
-  , djTransformColumns
-  ) where
-
-import qualified Numeric.LinearAlgebra    as LA
-
-import           Hanalyze.Design.Custom.Factor
-import           Hanalyze.Design.Custom.Model
-import           Hanalyze.Design.Custom.Power (termColumnIndices, termName)
-import           Hanalyze.Design.Custom.RegionMoment
-                   ( DJTransform (..), djFitTransform, djApplyTransform
-                   , djTransformColumns )
-
--- | Prior precision matrix のラッパ。 対角優位を想定するが、 一般 p × p 行列を
--- 受け入れる (DuMouchel-Jones 1994 の対角構造は最も一般的だが、 ユーザが
--- 任意の K を持ち込むのも妨げない)。
-newtype PriorPrecision = PriorPrecision (LA.Matrix Double)
-  deriving (Show)
-
--- | 内部 matrix を [[Double]] として取得 (OptCriterion.BayesianD への引き渡し用)。
-precisionToMatrix :: PriorPrecision -> [[Double]]
-precisionToMatrix (PriorPrecision m) = LA.toLists m
-
--- | DuMouchel-Jones の既定プリセット:
---
---   * intercept / 主効果: K_jj = 0
---   * 2fi (`TInter` len 2) / 二乗 (`TPower`) / nested: K_jj = τ²
---   * categorical 主効果 (K-1 列): K_jj = 0 (主効果扱い)
---
--- 非対角は全て 0。 expand 後の列順 = `expandDesignMatrix` の出力順 と一致。
-priorPrecisionDefault :: [Factor] -> Model -> Double -> PriorPrecision
-priorPrecisionDefault factors model tau2 =
-  priorPrecisionFromTerms factors model (defaultClassifier tau2)
-
--- | 各 term に対する K_jj 値を返す classifier 経由で K を構築する一般版。
--- ユーザが「自分の問題では二乗だけ τ²、 2fi は 0」 などのカスタム classifier を
--- 渡せる。
-priorPrecisionFromTerms
-  :: [Factor]
-  -> Model
-  -> (ModelTerm -> Double)  -- ^ term ごとの K_jj 値
-  -> PriorPrecision
-priorPrecisionFromTerms factors model classifyKjj =
-  let pairs   = termColumnIndices factors model
-      nameMap = [ (termName t, classifyKjj t) | t <- mTerms model ]
-      pTotal  = case pairs of
-        [] -> 0
-        _  -> 1 + maximum (concatMap snd pairs)
-      diag = [ kjjForCol pairs nameMap j | j <- [0 .. pTotal - 1] ]
-  in PriorPrecision (LA.diagl diag)
-
-kjjForCol :: [(t, [Int])] -> [(t, Double)] -> Int -> Double
-kjjForCol pairs nameMap col =
-  case [ v | ((_, cols), (_, v)) <- zip pairs nameMap, col `elem` cols ] of
-    (v:_) -> v
-    []    -> 0
-
--- | DuMouchel-Jones 既定の classifier。
-defaultClassifier :: Double -> ModelTerm -> Double
-defaultClassifier _    TIntercept     = 0
-defaultClassifier _    (TMain _)      = 0
-defaultClassifier tau2 (TInter ns)
-  | length ns >= 2 = tau2
-  | otherwise      = 0
-defaultClassifier tau2 (TPower _ k)
-  | k >= 2 = tau2
-  | otherwise = 0
-defaultClassifier tau2 (TNested _ _) = tau2
-
--- | Bayesian D-criterion (Matrix-native): @det(XᵀX + K)@ そのもの (符号なし)。
--- K の次元が X の列数と不一致なら 0。
-bayesianDValueM :: PriorPrecision -> LA.Matrix Double -> Double
-bayesianDValueM (PriorPrecision km) x =
-  let p = LA.cols x
-  in if LA.rows km /= p || LA.cols km /= p
-       then 0
-       else LA.det (LA.tr x LA.<> x + km)
-
--- ---------------------------------------------------------------------------
--- DuMouchel-Jones §2.2 規約 (Phase 28-12) — 実装は Custom.RegionMoment.hs に
--- 移動 (Coordinate ↔ Bayesian の module cycle 回避のため)。 本 module からは
--- 再 export のみ。
--- ---------------------------------------------------------------------------
---
--- DJ (1994) §2.2 (Technometrics 36:39) は、 prior τ² が「effect size 1σ_error」
--- と等価に解釈されるよう、 potential terms (TInter len≥2 / TPower k≥2 /
--- TNested) に以下の変換を要求する:
---
---   1. **centering**: 候補集合上で平均を引く (subtract mean over candidate)
---   2. **primary との直交化**: candidate 上の primary 列 (TIntercept / TMain /
---      TInter len 1) に LS regress して残差を取る
---   3. **range = 1 への正規化**: 直交化後の値の (max - min) で割る
---
--- paper §2.2 末尾の例 (primary {1, x}、 candidate {-1, -0.5, 0, 0.5, 1} 5 水準):
---
---   * x² → z₁ = x² − 0.5 (mean(x²)=0.5、 primary 直交、 range=1)
---   * x³ → z₂ = (x³ − 0.85x)/0.6 (E[x⁴]/E[x²]=0.85、 range=0.6)
---
--- 同一 K = diag(0..0, τ²..τ²) を当てた det(X'X + K) が paper の値と一致する
--- ためにはこの規約が必要。 'priorPrecisionDefault' は K のみを構築するので、
--- ユーザは expand 後に 'djTransformColumns' で列変換を適用してから
--- 'bayesianDValueM' を呼ぶ。 coordinateExchange への自動適用は未対応 (Phase 28-12
--- 範囲外)。
-
--- (実装は Custom.RegionMoment.hs を参照。 本 module からは re-export のみ)
diff --git a/src/Hanalyze/Design/Custom/Compare.hs b/src/Hanalyze/Design/Custom/Compare.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Custom/Compare.hs
+++ /dev/null
@@ -1,394 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns      #-}
--- |
--- Module      : Hanalyze.Design.Custom.Compare
--- Description : Custom Design 群の post-hoc 比較 (D/A/G/I efficiency・FDS・alias norm)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Custom Design の Design Comparison / FDS (Phase 24-7)。
---
--- spec: doe-custom-design-spec v0.1.1 §2.8 / §3.5。
---
--- JMP "Design Evaluation" 相当の post-hoc 比較関数。 生成済の 'CustomDesign'
--- (複数) を受け取り、 D/A/G/I efficiency、 FDS (Fraction of Design Space)
--- 分布、 alias matrix の Frobenius norm を計算する。
---
--- ## 設計判断
---
---   * **pure 関数** (`IO` 不要): FDS の点サンプリングは Halton 列で deterministic、
---     reproducibility は seed 不要。
---   * **efficiency 算出**: D/A/G eff は 'Hanalyze.Design.Diagnostics.diagnostics'
---     を再利用。 I-eff は Phase 28-4b 以降 'regionMomentMatrixAnalytic' 経由の
---     region 積分版 (連続 U[-1,1] + Categorical 等確率 で M_R を解析構築、
---     I-eff = @1 / (n · trace((X'X)⁻¹ · M_R))@)。 Mixture を含む等で M_R 構築
---     失敗時は旧 self-moment 近似 (= @1/p@) に fallback。
---   * **FDS 規定**: N=500 点を Halton で抽出、 各因子型ごとに [0, 1] → 因子値に
---     マップ (Continuous は [-1, 1]、 DiscreteNum / Mixture / Categorical /
---     Ordinal はそれぞれ自然なマッピング)、 expand 後の予測分散 v = x'(X'X)⁻¹x
---     を昇順 sort して返す (JMP plot で x = 累積分率、 y = v)。
---   * **alias norm の範囲**: Phase 24-7 では **連続因子 × 連続因子の 2fi で
---     model に含まれていない組合せ** のみを Z に含める (categorical absent
---     interaction や TPower 2 などは将来 commit で拡張)。
---
--- ## 既知の制限 (将来 commit で拡張候補)
---
---   * alias matrix の Z 構築範囲 (現状 連続 × 連続 2fi のみ、 Phase 28-6 候補)
---   * FDS の region 定義 (現状全因子 を独立 uniform、 制約付きの場合は
---     rejection sampling 必要、 Phase 28-4c 候補)
---   * I-eff の Mixture / 制約付き region (現状 fallback、 Phase 28-9 / 28-4c 候補)
-module Hanalyze.Design.Custom.Compare
-  ( DesignComparison (..)
-  , compareDesigns
-    -- * 内部 helper (test 用)
-  , fdsVector
-  , aliasNormOf
-    -- * Compound criterion (Phase 26-6)
-  , normalizeCompoundWeights
-    -- * I-efficiency region 積分 (Phase 28-4a、 RegionMoment 再export)
-  , regionMomentMatrixAnalytic
-  , iValueRegionM
-    -- * Compound 幾何平均 + 各 criterion の efficiency (Phase 28-9)
-  , compoundGeometric
-  , dEfficiency
-  , aEfficiency
-    -- * 多変量 Cp との統合 (Phase 28-8)
-  , DesignComparisonExt (..)
-  , compareDesignsWithResponses
-  ) where
-
-import           Data.List                (sort)
-import           Data.Text                (Text)
-import qualified Numeric.LinearAlgebra    as LA
-
-import           Hanalyze.Design.Custom.Factor
-import           Hanalyze.Design.Custom.Model
-import           Hanalyze.Design.Custom.Coordinate
-                   (CustomDesign (..), CustomDesignReport (..))
-import           Hanalyze.Design.Custom.RegionMoment
-                   (regionMomentMatrixAnalytic, iValueRegionM)
-import           Hanalyze.Design.Diagnostics
-                   (DesignDiagnostics (..), diagnostics)
-import           Hanalyze.Design.Optimal           (OptCriterion (..))
-import qualified Hanalyze.Design.Quality           as Q
-import qualified Hanalyze.Stat.QuasiRandom         as QR
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
--- | 複数 Custom Design の比較結果。
-data DesignComparison = DesignComparison
-  { dcDesigns   :: ![(Text, CustomDesign)]
-  , dcEffTable  :: !(LA.Matrix Double)
-    -- ^ 行: 設計 (`dcDesigns` 順)、 列: D / A / G / I efficiency
-  , dcFDS       :: ![(Text, LA.Vector Double)]
-    -- ^ 設計ごとの FDS sorted vector (長さ = N_FDS、 既定 500)
-  , dcAliasNorm :: ![(Text, Double)]
-    -- ^ 設計ごとの alias matrix Frobenius norm
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- 公開 API
--- ---------------------------------------------------------------------------
-
-nFDS :: Int
-nFDS = 500
-
--- | Custom Design 群を比較。 全 4 列の efficiency + FDS + alias norm を集約。
-compareDesigns :: [(Text, CustomDesign)] -> DesignComparison
-compareDesigns named =
-  let effRows = map (designEffs . snd) named
-      effTable
-        | null effRows = (0 LA.>< 4) []
-        | otherwise    = LA.fromLists effRows
-      fds     = [ (nm, fdsVector cd) | (nm, cd) <- named ]
-      aliases = [ (nm, aliasNormOf cd) | (nm, cd) <- named ]
-  in DesignComparison
-       { dcDesigns   = named
-       , dcEffTable  = effTable
-       , dcFDS       = fds
-       , dcAliasNorm = aliases
-       }
-
--- ---------------------------------------------------------------------------
--- efficiency
--- ---------------------------------------------------------------------------
-
--- | 1 設計に対する [D-eff, A-eff, G-eff, I-eff]。 expand 失敗時は 4 個の 0。
---
--- D-eff は Phase 28-5 以降 'CustomDesignReport.crCriterion' に応じて分岐:
---   * 'BayesianD k': @D-eff = (det(X'X + K) / n^p)^(1/p)@ (Bayesian D-criterion)
---   * その他: 古典 D-criterion @det(X'X)@ ベース
---
--- I-eff は Phase 28-4b 以降 region moment matrix 版:
--- @I-eff = 1 / (n · trace((X'X)⁻¹ · M_R))@、 @M_R@ は
--- 'regionMomentMatrixAnalytic' で構築。 Mixture を含む等で M_R 構築に
--- 失敗した場合は旧 self-moment 近似 (= @1/p@、 設計に依らず定数) に fallback。
-designEffs :: CustomDesign -> [Double]
-designEffs cd =
-  case expandDesignMatrix (cdFactors cd) (cdModel cd) (cdMatrix cd) of
-    Left _  -> [0, 0, 0, 0]
-    Right x ->
-      let d        = diagnostics x
-          n        = LA.rows x
-          p        = LA.cols x
-          nD       = fromIntegral n :: Double
-          pD       = fromIntegral p :: Double
-          dEffBayes k =
-            let km = LA.fromLists k
-            in if LA.rows km /= p || LA.cols km /= p
-                 then ddDEff d
-                 else
-                   let det = LA.det (LA.tr x LA.<> x + km)
-                   in if det <= 0 || nD == 0 then 0
-                        else (det / (nD ** pD)) ** (1 / pD)
-          dEff = case crCriterion (cdReport cd) of
-            BayesianD k -> dEffBayes k
-            _           -> ddDEff d
-          iEffReg  = case regionMomentMatrixAnalytic (cdFactors cd) (cdModel cd) of
-            Right mR
-              | LA.rows mR == LA.cols x ->
-                  let t  = iValueRegionM mR x
-                  in if isInfinite t || t <= 0 then 0 else 1 / (nD * t)
-            _ -> ddIEff d
-      in [dEff, ddAEff d, ddGEff d, iEffReg]
-
--- ---------------------------------------------------------------------------
--- FDS (Fraction of Design Space)
--- ---------------------------------------------------------------------------
-
--- | FDS vector: Halton で region から N_FDS 点を抽出、 各点の予測分散
--- v = x'(X'X)⁻¹x を昇順 sort して返す。 expand 失敗時は空 Vector。
---
--- region の取り方 (Phase 24-7 暫定):
---   * Continuous (lo, hi)  : [-1, 1] (NCoded、 lo/hi 情報は無視)
---   * DiscreteNum xs       : xs から uniform 抽出
---   * Mixture lo hi        : [lo, hi]
---   * Categorical / Ordinal: 0..K-1 から uniform 抽出
-fdsVector :: CustomDesign -> LA.Vector Double
-fdsVector cd =
-  case expandDesignMatrix (cdFactors cd) (cdModel cd) (cdMatrix cd) of
-    Left _  -> LA.fromList []
-    Right x ->
-      let p   = LA.cols x
-          xtx = LA.tr x LA.<> x
-          d   = LA.det xtx
-      in if abs d < 1e-12
-           then LA.fromList []
-           else
-             let inv     = LA.inv xtx
-                 factors = cdFactors cd
-                 model   = cdModel cd
-                 nF      = length factors
-                 halton  = QR.haltonMatrix nFDS nF  -- N × nF in [0, 1]
-                 rawRows = LA.fromRows
-                   [ LA.fromList
-                       [ mapU01ToFactor (factors !! j)
-                                        (halton `LA.atIndex` (i, j))
-                       | j <- [0 .. nF - 1] ]
-                   | i <- [0 .. nFDS - 1] ]
-             in case expandDesignMatrix factors model rawRows of
-                  Left _  -> LA.fromList []
-                  Right xSamp ->
-                    let !vs = [ let xi = LA.flatten (xSamp LA.? [i])
-                                in xi `LA.dot` (inv LA.#> xi)
-                              | i <- [0 .. LA.rows xSamp - 1] ]
-                        _ = p  -- 未使用警告対策、 dimension は後の commit で使う
-                    in LA.fromList (sort vs)
-
--- | Halton 1 次元値 u ∈ [0, 1] を 1 因子の raw 値に写像。
--- Categorical / Ordinal は floor(u * K) で level index に量子化。
-mapU01ToFactor :: Factor -> Double -> Double
-mapU01ToFactor f u = case fKind f of
-  Continuous _ _ -> -1 + 2 * u                       -- [-1, 1]
-  DiscreteNum xs ->
-    let k = length xs
-    in if k <= 0 then 0
-                 else xs !! min (k - 1) (floor (u * fromIntegral k))
-  Mixture lo hi  -> lo + (hi - lo) * u
-  Categorical xs ->
-    let k = length xs
-    in if k <= 0 then 0
-                 else fromIntegral (min (k - 1) (floor (u * fromIntegral k)))
-  Ordinal xs     ->
-    let k = length xs
-    in if k <= 0 then 0
-                 else fromIntegral (min (k - 1) (floor (u * fromIntegral k)))
-
--- ---------------------------------------------------------------------------
--- alias norm
--- ---------------------------------------------------------------------------
-
--- | alias matrix の Frobenius norm。 Phase 28-6 で Z 範囲を拡張:
---
---   * 連続 × 連続 2fi (Phase 24-7、 元実装)
---   * **Categorical × 連続 2fi** (Phase 28-6 追加)
---   * **Categorical × Categorical 2fi** (Phase 28-6 追加)
---   * **連続因子の TPower k=2 (二乗項)** (Phase 28-6 追加)
---
--- すべて model に **含まれていない** ものだけを Z に追加。 Z が空なら 0。
-aliasNormOf :: CustomDesign -> Double
-aliasNormOf cd =
-  let factors      = cdFactors cd
-      model        = cdModel cd
-      raw          = cdMatrix cd
-      allNames     = map fName factors
-      contNames    = [ fName f | f <- factors, factorIsContinuous f ]
-      existing2fi  =
-        [ canonInter ns | TInter ns <- mTerms model, length ns == 2 ]
-      existingPow  =
-        [ (n, k) | TPower n k <- mTerms model ]
-      -- 2fi candidates: 全因子の組合せ (連続 × 連続、 cat × 連続、 cat × cat)
-      pair2fiCands =
-        [ TInter (canonInter [a, b])
-        | a <- allNames, b <- allNames, a < b
-        , canonInter [a, b] `notElem` existing2fi
-        ]
-      -- 連続因子の二乗項 (k=2)
-      powCands =
-        [ TPower n 2
-        | n <- contNames, (n, 2) `notElem` existingPow
-        ]
-      zTerms = pair2fiCands ++ powCands
-  in if null zTerms
-       then 0
-       else
-         case (expandDesignMatrix factors model raw,
-               expandDesignMatrix factors (Model zTerms (mNorm model)) raw) of
-           (Right x, Right z) ->
-             let xtx = LA.tr x LA.<> x
-                 d   = LA.det xtx
-             in if abs d < 1e-12 then 0 / 0
-                  else
-                    let a = LA.inv xtx LA.<> LA.tr x LA.<> z
-                    in sqrt (LA.sumElements (LA.cmap (** 2) a))
-           _ -> 0 / 0
-
--- | 2fi のペアを正規化 (順序非依存にするため sort)。
-canonInter :: [Text] -> [Text]
-canonInter = sort
-
--- ---------------------------------------------------------------------------
--- Compound 重み正規化 (Phase 26-6)
--- ---------------------------------------------------------------------------
-
--- | Compound criterion の重みを合計 1 に正規化。 負の重みは 0 に丸める
--- (criterion の min 方向と矛盾するため)。 入力は @[(weight, OptCriterion)]@、
--- 出力は同形で重み正規化済。 重み合計 ≤ 0 の場合は元の入力をそのまま返す
--- (= no-op、 ユーザ責任)。
---
--- 使い方:
---
--- @
--- import qualified Hanalyze.Design.Optimal as Opt
--- let ws  = [(0.7, Opt.DOpt), (0.5, Opt.AOpt), (-0.1, Opt.IOpt)]
---     ws' = normalizeCompoundWeights ws
--- -- ws' = [(0.583, DOpt), (0.417, AOpt), (0, IOpt)]
--- @
-normalizeCompoundWeights
-  :: [(Double, a)] -> [(Double, a)]
-normalizeCompoundWeights pairs =
-  let clamped = [ (max 0 w, c) | (w, c) <- pairs ]
-      total   = sum (map fst clamped)
-  in if total <= 0
-       then pairs
-       else [ (w / total, c) | (w, c) <- clamped ]
-
--- ---------------------------------------------------------------------------
--- Compound 幾何平均 + efficiency 正規化 (Phase 28-9)
--- ---------------------------------------------------------------------------
-
--- | 重み付き幾何平均 = @exp(Σ w_i · log eff_i / Σ w_i)@。 各 efficiency が
--- [0, 1] 範囲の正規化済値であることを前提に「合成効率」 を返す (alphabetic
--- criterion の geometric variant、 JMP の Compound criterion で利用される)。
---
---   * 重みは正数を仮定 (負は 0 にクランプ)、 重み合計 0 のときは 0 を返す
---   * 任意の eff が ≤ 0 のときは 0 を返す (log が ∞)、 線形 Compound (= no-op
---     合算) と挙動を揃える
---
--- 使い方:
---
--- @
--- let effD = dEfficiency x
---     effA = aEfficiency x
---     comp = compoundGeometric [(0.7, effD), (0.3, effA)]
--- @
-compoundGeometric :: [(Double, Double)] -> Double
-compoundGeometric pairs
-  | any ((<= 0) . snd) clamped = 0
-  | totalW <= 0                = 0
-  | otherwise                  =
-      exp (sum [ w * log e | (w, e) <- clamped ] / totalW)
-  where
-    clamped = [ (max 0 w, e) | (w, e) <- pairs ]
-    totalW  = sum (map fst clamped)
-
--- | D-efficiency @= (det(X'X) / n^p)^(1/p)@ ([0, ∞) 値、 reference D-opt で 1)。
--- singular なら 0。
-dEfficiency :: LA.Matrix Double -> Double
-dEfficiency x
-  | LA.rows x == 0 || LA.cols x == 0 = 0
-  | otherwise =
-      let n  = fromIntegral (LA.rows x) :: Double
-          p  = fromIntegral (LA.cols x) :: Double
-          d  = LA.det (LA.tr x LA.<> x)
-      in if d <= 0 then 0
-                   else (d / (n ** p)) ** (1 / p)
-
--- | A-efficiency @= p / (n · trace((X'X)⁻¹))@ ([0, ∞)、 reference A-opt で 1)。
--- singular なら 0。
-aEfficiency :: LA.Matrix Double -> Double
-aEfficiency x
-  | LA.rows x == 0 || LA.cols x == 0 = 0
-  | otherwise =
-      let n   = fromIntegral (LA.rows x) :: Double
-          p   = fromIntegral (LA.cols x) :: Double
-          xtx = LA.tr x LA.<> x
-          dd  = LA.det xtx
-      in if abs dd < 1e-12 then 0
-           else
-             let tr = LA.sumElements (LA.takeDiag (LA.inv xtx))
-             in if tr <= 0 then 0 else p / (n * tr)
-
-
--- ---------------------------------------------------------------------------
--- 多変量 Cp の Compare 統合 (Phase 28-8)
--- ---------------------------------------------------------------------------
-
--- | 'DesignComparison' を拡張、 design ごとに観測 response (実験結果 y) と
--- spec bounds から計算した多変量 process capability を追加。
-data DesignComparisonExt = DesignComparisonExt
-  { dceBase     :: !DesignComparison
-  , dceMCp      :: ![(Text, Either Text Double)]
-    -- ^ design ごとの MCp (Wang-Hubele-Lawrence 体積比)
-  , dceMCpk     :: ![(Text, Either Text Double)]
-    -- ^ design ごとの MCpk (中心オフセット penalty 含む)
-  , dceInSpec   :: ![(Text, Either Text Double)]
-    -- ^ spec box 内包率 (実測)
-  } deriving (Show)
-
--- | Compare に多変量 response 評価を追加。 各エントリ
--- @(name, design, responses, specs)@:
---   * @responses@ は n × p 観測行列 (n = 設計の行数、 p = 応答数)
---   * @specs@ は各応答の @(LSL, USL)@ を列順に
---
--- @processCapabilityMultivariate@ が Left を返した場合 (singular cov など)
--- は @dceMCp@ / @dceMCpk@ / @dceInSpec@ の該当エントリに Left を保持する。
-compareDesignsWithResponses
-  :: [(Text, CustomDesign, LA.Matrix Double, [(Double, Double)])]
-  -> DesignComparisonExt
-compareDesignsWithResponses tuples =
-  let base = compareDesigns [ (nm, cd) | (nm, cd, _, _) <- tuples ]
-      results =
-        [ (nm, Q.processCapabilityMultivariate y specs)
-        | (nm, _, y, specs) <- tuples ]
-      mcp     = [ (nm, fmap Q.mcMCp r)        | (nm, r) <- results ]
-      mcpk    = [ (nm, fmap Q.mcMCpk r)       | (nm, r) <- results ]
-      inSpec  = [ (nm, fmap Q.mcInSpecRate r) | (nm, r) <- results ]
-  in DesignComparisonExt
-       { dceBase   = base
-       , dceMCp    = mcp
-       , dceMCpk   = mcpk
-       , dceInSpec = inSpec
-       }
diff --git a/src/Hanalyze/Design/Custom/Constraint.hs b/src/Hanalyze/Design/Custom/Constraint.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Custom/Constraint.hs
+++ /dev/null
@@ -1,113 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.Custom.Constraint
--- Description : Custom Design の Constraint 内部正規化形 (Coordinate Exchange 用の候補行フィルタ ADT)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Custom Design の Constraint 内部正規化形 (Phase 24-1 skeleton)。
---
--- spec: doe-custom-design-spec v0.1.1 §2.3 / §9.3。
---
--- **本 skeleton では「内部 ADT (正規化済形)」 のみを定義する**。
--- ∀LIC∃Code 表面構文 (= @RawConstraint@ newtype around @ExprRep@) と
--- @normalize :: RawConstraint -> Constraint@ は、 DSL frontend パッケージ
--- (現状 @フロントエンド app のバックエンド/@ にある) との依存関係を整理してから着手する
--- (= Phase 24 後続 commit 候補)。
---
--- 現在の利用想定: Coordinate Exchange アルゴリズム (本 Phase 後続 commit)
--- が ADT を inspect して候補 grid を事前 filter するための内部表現。
-module Hanalyze.Design.Custom.Constraint
-  ( ConstraintRel (..)
-  , FactorValue (..)
-  , ConstraintGuard (..)
-  , Constraint (..)
-  , checkRowAgainst
-  , compileRowFromFactors
-  ) where
-
-import           Data.Text (Text)
-import qualified Data.Map.Strict as M
-
--- | 線形制約の関係子 (Custom 側、 古典 Hanalyze.Design.Constraint と
--- 表現は同じだが名前空間を分けて使う)。
-data ConstraintRel = CLeq | CEq | CGeq
-  deriving (Eq, Show)
-
--- | カテゴリ / 数値の混在値 (Forbidden に使う)。
-data FactorValue
-  = FVDouble !Double
-  | FVText   !Text
-  deriving (Eq, Show)
-
--- | 条件付制約のガード (AND/OR/単項、 NOT は v0.2 検討)。
-data ConstraintGuard
-  = GuardEq  !Text !FactorValue
-  | GuardLeq !Text !Double
-  | GuardGeq !Text !Double
-  | GuardAnd ![ConstraintGuard]
-  | GuardOr  ![ConstraintGuard]
-  deriving (Eq, Show)
-
--- | Custom Design 内部の正規化済 Constraint。
---
--- 連続因子 (因子名で参照) の半空間 / 等式 / カテゴリ列の forbidden /
--- 条件付 / 範囲上書きを覆う。 表面 ∀LIC∃Code Expr からの正規化失敗時の
--- @Generic@ (= ExprRep 抱え込み) は本 skeleton では未対応 (DSL frontend
--- 依存解決後に追加)。
-data Constraint
-  = LinearIneq  ![(Text, Double)] !ConstraintRel !Double
-    -- ^ @sum_i (coef_i * x_{name_i}) `rel` rhs@ 連続因子のみ参照可
-  | Forbidden   ![(Text, FactorValue)]
-    -- ^ 全項が一致する row を禁止 (AND)
-  | Conditional !ConstraintGuard ![Constraint]
-    -- ^ ガード成立時のみ inner 制約を活性化
-  | RangeBound  !Text !Double !Double
-    -- ^ 範囲上書き (低、 高)
-  deriving (Eq, Show)
-
--- | 1 row (= 因子名 → 値の Map) に対する制約評価。
--- skeleton では Categorical 因子は Text 値で照合、 連続因子は Double で照合。
--- 値が見つからない / 型不一致は **その制約を 'False' (= 違反) と判定**。
-checkRowAgainst :: M.Map Text FactorValue -> Constraint -> Bool
-checkRowAgainst row (LinearIneq coefs rel rhs) =
-  let lookupNum k = case M.lookup k row of
-                      Just (FVDouble x) -> Just x
-                      _                 -> Nothing
-      ms = traverse (\(n, c) -> fmap (c *) (lookupNum n)) coefs
-  in case ms of
-       Nothing -> False
-       Just xs ->
-         let lhs = sum xs
-         in case rel of
-              CLeq -> lhs <= rhs + 1e-9
-              CEq  -> abs (lhs - rhs) <= 1e-9
-              CGeq -> lhs >= rhs - 1e-9
-checkRowAgainst row (Forbidden vs) =
-  not (all (\(n, v) -> M.lookup n row == Just v) vs)
-checkRowAgainst row (Conditional guard cs) =
-  if evalGuard row guard
-    then all (checkRowAgainst row) cs
-    else True
-checkRowAgainst row (RangeBound n lo hi) =
-  case M.lookup n row of
-    Just (FVDouble x) -> lo - 1e-9 <= x && x <= hi + 1e-9
-    _                  -> False
-
--- | ガード評価。
-evalGuard :: M.Map Text FactorValue -> ConstraintGuard -> Bool
-evalGuard row (GuardEq  n v)   = M.lookup n row == Just v
-evalGuard row (GuardLeq n c)   = case M.lookup n row of
-                                   Just (FVDouble x) -> x <= c + 1e-9
-                                   _                 -> False
-evalGuard row (GuardGeq n c)   = case M.lookup n row of
-                                   Just (FVDouble x) -> x >= c - 1e-9
-                                   _                 -> False
-evalGuard row (GuardAnd gs)    = all (evalGuard row) gs
-evalGuard row (GuardOr  gs)    = any (evalGuard row) gs
-
--- | ヘルパ: 因子名リストと 1 row 値リスト (= Double のみの場合) から Map に変換。
--- Custom Design Core が coordinate exchange の inner loop で使う想定。
-compileRowFromFactors :: [Text] -> [Double] -> M.Map Text FactorValue
-compileRowFromFactors names values =
-  M.fromList (zip names (map FVDouble values))
diff --git a/src/Hanalyze/Design/Custom/Coordinate.hs b/src/Hanalyze/Design/Custom/Coordinate.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Custom/Coordinate.hs
+++ /dev/null
@@ -1,618 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns      #-}
--- |
--- Module      : Hanalyze.Design.Custom.Coordinate
--- Description : Custom Design の Coordinate Exchange + Modified Fedorov hybrid アルゴリズム
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Custom Design の Coordinate Exchange + Modified Fedorov hybrid (Phase 24-4)。
---
--- spec: doe-custom-design-spec v0.1.1 §2.4 / §3.6。
--- 参考: Meyer & Nachtsheim (1995) "The Coordinate-Exchange Algorithm for
--- Constructing Exact Optimal Experimental Designs", Technometrics 37:60-69。
---
--- ## アーキテクチャ (24-4)
---
--- 「連続因子は coordinate exchange、 categorical 因子は Modified Fedorov
--- (候補集合 = 全 level)」 を **因子ごとに探索 grid を切り替える** ことで
--- 1 つの outer loop に統合した。 spec §2.4 で言う「hybrid」 は実質
--- per-column grid の選び分けに帰着する。
---
--- 因子ごとの grid (NCoded 想定):
---   * Continuous  : linspace [-1, 1] (長さ dbCxStepGrid、 既定 21)
---   * DiscreteNum : ユーザ指定の離散水準 (そのまま)
---   * Mixture     : linspace [lo, hi] (長さ dbCxStepGrid、 制約は 24-5 で別途)
---   * Categorical : [0, 1, ..., K-1] (level index、 expand 側で treatment coding)
---   * Ordinal     : 同上
---
--- ## 本 commit (24-5) のスコープ
---
---   * 制約 (`cdsConstraints` = LinearIneq / Forbidden / Conditional / RangeBound) を
---     **per-grid-point filter** として統合: 各 cell 候補値について、 変更後の row が
---     全制約を満たさなければ +∞ 評価 (= 採用されない)。
---   * 初期 randomInit は **rejection sampling** で row 単位に制約を満たすまで再抽選
---     (1 row あたり 200 回上限、 越えたら Left)。
---   * `cdsInitial` は **無視** (24-augment phase で対応)。
---   * 全 'OptCriterion' (DOpt/AOpt/IOpt/EOpt/GOpt/Compound) を Matrix-native で評価。
---     IOpt の moment matrix は self-moment (= A-criterion と同方向の近似、
---     既存 `Hanalyze.Design.Optimal.iValueWithSelf` と整合)。
---
--- ## 設計指針
---
---   * 内部 loop は hmatrix Matrix / Vector で完結 (list 化禁止、 Phase 17 教訓)。
---   * outer multi-start / iter loop は `IO` で IORef 更新。
---   * 各 grid 点での criterion 評価は `expandDesignMatrix` + `critValueM`。
---   * 初期解は grid 上で uniform random 抽出 (再現性は `cdsSeed`)。
-module Hanalyze.Design.Custom.Coordinate
-  ( -- * 入力型
-    CustomDesignSpec (..)
-  , DesignBudget (..)
-  , defaultBudget
-    -- * 結果型
-  , CustomDesign (..)
-  , CustomDesignReport (..)
-    -- * アルゴリズム
-  , coordinateExchange
-  , coordinateExchangePure
-    -- * seed / gen helper (SplitPlot 等が再利用)
-  , mkGen
-  , mkGenSeed
-  , defaultPureSeed
-    -- * 内部 helper (test 用 / Structured 再利用)
-  , critValueM
-  , gridForBudget
-  , factorGrid
-  , rowFeasible
-  ) where
-
-import           Control.Monad             (forM_, when)
-import           Control.Monad.Primitive   (PrimMonad, PrimState)
-import           Control.Monad.ST          (runST)
-import           Data.Maybe                (fromMaybe)
-import           Data.Primitive.MutVar
-import           Data.Text                 (Text)
-import qualified Data.Text                 as T
-import qualified Numeric.LinearAlgebra     as LA
-import qualified System.Random.MWC         as MWC
-import qualified Data.Vector               as V
-import qualified Data.Vector.Unboxed       as VU
-import qualified Data.Map.Strict           as M
-
-import           Hanalyze.Design.Custom.Factor
-import           Hanalyze.Design.Custom.Model
-import           Hanalyze.Design.Custom.Constraint
-                   (Constraint, FactorValue (..), checkRowAgainst)
-import qualified Hanalyze.Design.Custom.RegionMoment as RM
-import           Hanalyze.Design.Custom.RegionMoment (resolveIOptRegion)
-import           Hanalyze.Design.Optimal        (OptCriterion (..))
-
--- ---------------------------------------------------------------------------
--- 入力型
--- ---------------------------------------------------------------------------
-
--- | Custom Design 生成の仕様。 spec §2.4。
-data CustomDesignSpec = CustomDesignSpec
-  { cdsFactors     :: ![Factor]
-  , cdsModel       :: !Model
-  , cdsConstraints :: ![Constraint]
-    -- ^ Phase 24-3 では未使用 (24-5 で grid filter として統合)。
-  , cdsNRuns       :: !Int
-  , cdsCriterion   :: !OptCriterion
-  , cdsBudget      :: !DesignBudget
-  , cdsSeed        :: !(Maybe Int)
-  , cdsInitial     :: !(Maybe (LA.Matrix Double))
-    -- ^ Augment 用、 24-3 では未使用。
-  , cdsDJConvention :: !Bool
-    -- ^ Phase 28-12 自動: True かつ criterion が BayesianD を含むとき、
-    -- 候補集合 (factor grid の cartesian product) から DuMouchel-Jones §2.2
-    -- 規約 ('Custom.Bayesian.djFitTransform') を fit し、 内部 criterion 評価
-    -- の expand 後に 'djApplyTransform' を適用してから 'critValueM' に渡す。
-    -- 'cdMatrix' は raw 表現のまま保存、 'cdReport.crCriterionValue' は
-    -- 変換後 X 上の det を示す。 paper §3.3 と同じ意味の最適化が走る。
-  } deriving (Show)
-
--- | 探索バジェット。 spec §2.4。
-data DesignBudget = DesignBudget
-  { dbMaxIter    :: !Int     -- ^ outer iteration 上限 (改善なしで break)
-  , dbRestarts   :: !Int     -- ^ multi-start 数
-  , dbTol        :: !Double  -- ^ outer 収束判定の相対改善閾値
-  , dbCxStepGrid :: !Int     -- ^ 連続因子 grid 点数 (既定 21)
-  } deriving (Show)
-
--- | spec §2.4 既定値 + JMP デフォルト互換 (21 grid)。
-defaultBudget :: DesignBudget
-defaultBudget = DesignBudget
-  { dbMaxIter    = 50
-  , dbRestarts   = 5
-  , dbTol        = 1e-6
-  , dbCxStepGrid = 21
-  }
-
--- ---------------------------------------------------------------------------
--- 結果型
--- ---------------------------------------------------------------------------
-
-data CustomDesign = CustomDesign
-  { cdMatrix  :: !(LA.Matrix Double)   -- ^ 因子 raw 値行列 (nRuns × #factors)
-  , cdFactors :: ![Factor]
-  , cdModel   :: !Model
-  , cdReport  :: !CustomDesignReport
-  } deriving (Show)
-
-data CustomDesignReport = CustomDesignReport
-  { crCriterion      :: !OptCriterion
-  , crCriterionValue :: !Double     -- ^ 最小化方向の値 (DOpt なら −det)
-  , crIterations     :: !Int        -- ^ best restart で要した outer iter 数
-  , crRestarts       :: !Int        -- ^ 実行した restart 数
-  , crConverged      :: !Bool       -- ^ best restart が maxIter 前に収束したか
-  , crSeed           :: !(Maybe Int)
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- 公開 API
--- ---------------------------------------------------------------------------
-
--- | Coordinate Exchange + Modified Fedorov hybrid による Custom Design 生成。
---
--- 失敗ケース:
---   * 因子が空 / nRuns < 1
---   * Categorical / Ordinal 因子で水準数 0 (Phase 24-1 expandDesignMatrix と整合)
---   * モデルが categorical を参照しているが Phase 24-2 の制限に該当
---   * 'TNested' をモデルに含む (Phase 24-1 から未対応)
---   * dbRestarts < 1 / dbCxStepGrid < 2
--- | seed 由来の gen を作って 'coordinateExchangeWith' を IO で走らせる薄い wrapper。
--- 'cdsSeed' が 'Nothing' の場合のみ entropy 依存 (非決定的)。
--- Phase 78.M: seed 決定的な純粋版が要るなら 'coordinateExchangePure' を使う。
-coordinateExchange :: CustomDesignSpec -> IO (Either Text CustomDesign)
-coordinateExchange spec = do
-  gen <- mkGen (cdsSeed spec)
-  coordinateExchangeWith spec gen
-
--- | seed 決定的な純粋版 (Phase 78.M)。'runST' で MWC gen + MutVar を閉じ込め、
--- IO 無しで 'CustomDesign' を返す。'cdsSeed' が 'Nothing' なら
--- 'defaultPureSeed' を用いて全域関数にする (同 spec → 常に同結果)。
--- 同一 seed なら 'coordinateExchange' (IO) とビット一致する。
-coordinateExchangePure :: CustomDesignSpec -> Either Text CustomDesign
-coordinateExchangePure spec = runST $ do
-  gen <- mkGenSeed (fromMaybe defaultPureSeed (cdsSeed spec))
-  coordinateExchangeWith spec gen
-
--- | 座標交換本体 (PrimMonad 一般化)。IO / ST どちらでも走る。gen は呼び出し側が
--- seed から用意する ('coordinateExchange' = IO entropy 可 / 'coordinateExchangePure'
--- = ST seed 必須)。アルゴリズムは gen の生成源に依らず同 seed → 同結果。
-coordinateExchangeWith
-  :: PrimMonad m
-  => CustomDesignSpec -> MWC.Gen (PrimState m) -> m (Either Text CustomDesign)
-coordinateExchangeWith spec gen
-  | null (cdsFactors spec) =
-      pure (Left (T.pack "coordinateExchange: empty factor list"))
-  | cdsNRuns spec < 1 =
-      pure (Left (T.pack "coordinateExchange: nRuns must be >= 1"))
-  | dbRestarts (cdsBudget spec) < 1 =
-      pure (Left (T.pack "coordinateExchange: dbRestarts must be >= 1"))
-  | dbCxStepGrid (cdsBudget spec) < 2 =
-      pure (Left (T.pack "coordinateExchange: dbCxStepGrid must be >= 2"))
-  | otherwise = do
-      let !n        = cdsNRuns spec
-          !budget   = cdsBudget spec
-          !critIn   = cdsCriterion spec
-          !model    = cdsModel spec
-          !factors  = cdsFactors spec
-          !cons     = cdsConstraints spec
-          !grids    = map (factorGrid budget) factors
-      let prep = do
-            () <- maybe (Right ()) Left (validateGrids factors grids)
-            crit <- resolveIOptRegion factors model cons critIn
-            mDJ  <- fitDJTransformIfRequested spec factors model grids crit
-            Right (crit, mDJ)
-      case prep of
-        Left e -> pure (Left e)
-        Right (crit, mDJ) -> do
-          let dummy = LA.fromColumns
-                [ LA.konst (VU.head g) n | g <- grids ]
-          case expandDesignMatrix factors model dummy of
-            Left e  -> pure (Left (T.pack "coordinateExchange: model invalid — " <> e))
-            Right _ -> do
-              bestRef <- newMutVar Nothing
-              initErrRef <- newMutVar Nothing
-              forM_ [1 .. dbRestarts budget] $ \_ -> do
-                mInit <- randomInit factors cons n grids gen
-                case mInit of
-                  Left e -> writeMutVar initErrRef (Just e)
-                  Right init0 -> do
-                    (finalM, finalC, iters, conv) <-
-                      runExchange factors model crit mDJ cons budget grids init0
-                    modifyMutVar' bestRef $ \mb -> case mb of
-                      Nothing -> Just (finalM, finalC, iters, conv)
-                      Just (_, c0, _, _)
-                        | finalC < c0 -> Just (finalM, finalC, iters, conv)
-                        | otherwise   -> mb
-              mb <- readMutVar bestRef
-              case mb of
-                Just (m, c, iters, conv) -> pure $ Right CustomDesign
-                  { cdMatrix  = m
-                  , cdFactors = factors
-                  , cdModel   = model
-                  , cdReport  = CustomDesignReport
-                      { crCriterion      = critIn
-                      , crCriterionValue = c
-                      , crIterations     = iters
-                      , crRestarts       = dbRestarts budget
-                      , crConverged      = conv
-                      , crSeed           = cdsSeed spec
-                      }
-                  }
-                Nothing -> do
-                  initErr <- readMutVar initErrRef
-                  pure (Left (case initErr of
-                    Just e  -> e
-                    Nothing -> T.pack "coordinateExchange: no restart produced a design"))
-
--- | 全因子の grid が非空である事を確認 (Categorical 0 level、 DiscreteNum 空 等を弾く)。
-validateGrids :: [Factor] -> [VU.Vector Double] -> Maybe Text
-validateGrids fs gs = go (zip fs gs)
-  where
-    go [] = Nothing
-    go ((f, g):rest)
-      | VU.length g < 1 = Just (T.pack
-          ("coordinateExchange: factor " <> T.unpack (fName f)
-           <> " has empty search grid (categorical with 0 levels?)"))
-      | otherwise = go rest
-
--- ---------------------------------------------------------------------------
--- アルゴリズム内部
--- ---------------------------------------------------------------------------
-
--- | 1 restart 分の coordinate exchange / Modified Fedorov 混合 loop を走らせる。
--- 戻り値: (最終 raw matrix, 最終 criterion 値, 要した outer iter, 収束フラグ)。
-runExchange
-  :: PrimMonad m
-  => [Factor]
-  -> Model
-  -> OptCriterion
-  -> Maybe RM.DJTransform      -- ^ Phase 28-12: 自動 DJ 規約変換
-  -> [Constraint]              -- ^ 制約 (per-grid-point filter)
-  -> DesignBudget
-  -> [VU.Vector Double]        -- ^ 因子ごとの探索 grid (列順)
-  -> LA.Matrix Double          -- ^ 初期 raw matrix (n × p)
-  -> m (LA.Matrix Double, Double, Int, Bool)
-runExchange factors model crit mDJ cons budget grids init0 = do
-  matRef    <- newMutVar init0
-  critRef   <- newMutVar (evalCrit factors model crit mDJ init0)
-  iterRef   <- newMutVar 0
-  convRef   <- newMutVar False
-  let !n        = LA.rows init0
-      !p        = LA.cols init0
-      gridsV    = V.fromList grids
-      gridLensV = V.fromList (map VU.length grids)
-  let loopOuter !it
-        | it > dbMaxIter budget = pure ()
-        | otherwise = do
-            beforeC <- readMutVar critRef
-            forM_ [0 .. n - 1] $ \i ->
-              forM_ [0 .. p - 1] $ \j -> do
-                curMat <- readMutVar matRef
-                curC   <- readMutVar critRef
-                let oldV    = curMat `LA.atIndex` (i, j)
-                    !g      = gridsV V.! j
-                    !gl     = gridLensV V.! j
-                (bestV, bestC) <-
-                  searchBestOnGrid factors model crit mDJ cons curMat i j g gl oldV curC
-                when (bestC < curC) $ do
-                  let !newMat = setEntry curMat i j bestV
-                  writeMutVar matRef  newMat
-                  writeMutVar critRef bestC
-            afterC <- readMutVar critRef
-            writeMutVar iterRef it
-            let !rel = relImprovement beforeC afterC
-            if rel <= dbTol budget
-              then writeMutVar convRef True
-              else loopOuter (it + 1)
-  loopOuter 1
-  finalM    <- readMutVar matRef
-  finalC    <- readMutVar critRef
-  finalIter <- readMutVar iterRef
-  conv      <- readMutVar convRef
-  -- p は randomInit が決定論的に正しい次元を返すので冗長検査は省く
-  _ <- pure (n, p)
-  pure (finalM, finalC, finalIter, conv)
-
--- | 1 セル (i, j) について grid 上を線形走査、 制約を満たす範囲で
--- criterion 最小の (v, c) を返す。 制約違反 grid 点は scoring 段階で +∞ 扱い
--- (= 採用されない)。
-searchBestOnGrid
-  :: PrimMonad m
-  => [Factor]
-  -> Model
-  -> OptCriterion
-  -> Maybe RM.DJTransform
-  -> [Constraint]
-  -> LA.Matrix Double
-  -> Int -> Int
-  -> VU.Vector Double
-  -> Int
-  -> Double               -- ^ 現状値 (oldV)
-  -> Double               -- ^ 現状の criterion
-  -> m (Double, Double)
-searchBestOnGrid factors model crit mDJ cons mat i j grid gridLen oldV oldC = do
-  bestRef <- newMutVar (oldV, oldC)
-  let curRow = LA.flatten (LA.subMatrix (i, 0) (1, LA.cols mat) mat)
-  forM_ [0 .. gridLen - 1] $ \k -> do
-    let !v = grid VU.! k
-        !proposedRow = replaceVecAt curRow j v
-    when (rowFeasible factors cons proposedRow) $ do
-      let !candMat = setEntry mat i j v
-          !c = evalCrit factors model crit mDJ candMat
-      modifyMutVar' bestRef $ \cur@(_, bc) -> if c < bc then (v, c) else cur
-  readMutVar bestRef
-
--- | raw matrix → design matrix → (optional) DJ 変換 → criterion 値 (最小化方向)。
--- expandDesignMatrix が `Left` を返したら +∞ を返す (= 採用されない)。
-evalCrit :: [Factor] -> Model -> OptCriterion -> Maybe RM.DJTransform
-         -> LA.Matrix Double -> Double
-evalCrit factors model crit mDJ raw =
-  case expandDesignMatrix factors model raw of
-    Left _  -> 1 / 0
-    Right x ->
-      let xT = case mDJ of
-            Nothing -> x
-            Just t  -> RM.djApplyTransform t x
-      in critValueM crit xT
-
--- ---------------------------------------------------------------------------
--- criterion (Matrix-native、 list 化禁止)
--- ---------------------------------------------------------------------------
-
--- | OptCriterion の Matrix 版。 全 criterion を /minimize/ 方向で返す
--- (`Hanalyze.Design.Optimal.critValue` と整合)。 X は expand 済設計行列。
-critValueM :: OptCriterion -> LA.Matrix Double -> Double
-critValueM DOpt       x = - dValueM x
-critValueM AOpt       x = aValueM x
-critValueM IOpt       x = iValueSelfM x
-critValueM EOpt       x = eValueM x
-critValueM GOpt       x = gValueM x
-critValueM (Compound ws) x =
-  sum [ w * critValueM c x | (w, c) <- ws ]
-critValueM (BayesianD k) x =
-  let p  = LA.cols x
-      km = LA.fromLists k
-  in if LA.rows km /= p || LA.cols km /= p
-       then 1 / 0
-       else - LA.det (LA.tr x LA.<> x + km)
-critValueM (IOptRegion mr) x =
-  let p   = LA.cols x
-      mrM = LA.fromLists mr
-  in if LA.rows mrM /= p || LA.cols mrM /= p
-       then 1 / 0
-       else iValueRegionMatrix mrM x
-
--- | region moment matrix を直接 Matrix で受け取る I-criterion (内部用)。
--- 'Compare.iValueRegionM' と同義だが、 Coordinate からの import 循環回避の
--- ため重複定義。
-iValueRegionMatrix :: LA.Matrix Double -> LA.Matrix Double -> Double
-iValueRegionMatrix mrM x
-  | LA.rows x == 0 = 1 / 0
-  | otherwise =
-      let xtx = LA.tr x LA.<> x
-          d   = LA.det xtx
-      in if abs d < 1e-12 then 1 / 0
-           else LA.sumElements (LA.takeDiag (LA.inv xtx LA.<> mrM))
-
-dValueM :: LA.Matrix Double -> Double
-dValueM x
-  | LA.rows x == 0 = 0
-  | otherwise = LA.det (LA.tr x LA.<> x)
-
-aValueM :: LA.Matrix Double -> Double
-aValueM x
-  | LA.rows x == 0 = 1 / 0
-  | otherwise =
-      let xtx = LA.tr x LA.<> x
-          d   = LA.det xtx
-      in if abs d < 1e-12 then 1 / 0
-           else LA.sumElements (LA.takeDiag (LA.inv xtx))
-
--- | I-criterion の self-moment 版 (`Optimal.iValueWithSelf` と同義)。
-iValueSelfM :: LA.Matrix Double -> Double
-iValueSelfM x
-  | LA.rows x == 0 = 1 / 0
-  | otherwise =
-      let xtx = LA.tr x LA.<> x
-          d   = LA.det xtx
-      in if abs d < 1e-12 then 1 / 0
-           else
-             let inv    = LA.inv xtx
-                 moment = LA.scale (1 / fromIntegral (LA.rows x)) xtx
-             in LA.sumElements (LA.takeDiag (inv LA.<> moment))
-
-eValueM :: LA.Matrix Double -> Double
-eValueM x
-  | LA.rows x == 0 = 1 / 0
-  | otherwise =
-      let xtx = LA.tr x LA.<> x
-          eigs = LA.toList (LA.eigenvaluesSH (LA.sym xtx))
-      in if null eigs then 1 / 0 else - minimum eigs
-
-gValueM :: LA.Matrix Double -> Double
-gValueM x
-  | LA.rows x == 0 = 1 / 0
-  | otherwise =
-      let xtx = LA.tr x LA.<> x
-          d   = LA.det xtx
-      in if abs d < 1e-12 then 1 / 0
-           else
-             let inv = LA.inv xtx
-                 h   = x LA.<> inv LA.<> LA.tr x
-                 dia = LA.toList (LA.takeDiag h)
-             in if null dia then 1 / 0 else maximum dia
-
--- ---------------------------------------------------------------------------
--- 補助
--- ---------------------------------------------------------------------------
-
--- | [-1, 1] の等間隔 grid (NCoded 連続因子の既定)。
-gridForBudget :: DesignBudget -> VU.Vector Double
-gridForBudget b =
-  let !k  = dbCxStepGrid b
-      !km = fromIntegral (k - 1) :: Double
-  in VU.generate k (\i -> -1 + 2 * fromIntegral i / km)
-
--- | 因子ごとの探索 grid (Phase 24-4)。 raw matrix の値表現規約
--- (`Hanalyze.Design.Custom.Model` のモジュール doc 参照) と整合する点を返す。
---
--- * Continuous (lo, hi)  : linspace [-1, 1] (NCoded、 dbCxStepGrid 点)
--- * DiscreteNum xs       : xs そのまま
--- * Mixture (lo, hi)     : linspace [lo, hi] (dbCxStepGrid 点、 制約は 24-5 で別途)
--- * Categorical / Ordinal: [0, 1, ..., K-1] (level index、 expand 側で treatment coding)
-factorGrid :: DesignBudget -> Factor -> VU.Vector Double
-factorGrid b f = case fKind f of
-  Continuous _ _    -> gridForBudget b
-  DiscreteNum xs    -> VU.fromList xs
-  Mixture lo hi     -> linspaceVU lo hi (dbCxStepGrid b)
-  Categorical xs    -> VU.fromList (map fromIntegral [0 .. length xs - 1])
-  Ordinal     xs    -> VU.fromList (map fromIntegral [0 .. length xs - 1])
-
--- | 任意区間の等間隔 grid (k 点)。 k <= 1 は単一中央値を返す。
-linspaceVU :: Double -> Double -> Int -> VU.Vector Double
-linspaceVU lo hi k
-  | k <= 1    = VU.singleton ((lo + hi) / 2)
-  | otherwise = VU.generate k
-      (\i -> lo + (hi - lo) * fromIntegral i / fromIntegral (k - 1))
-
--- | 初期 raw matrix を rejection sampling で構築 (n × p)。
--- 各 row を制約満足するまで再抽選 (1 row あたり 200 回まで)。
--- 200 回試して失敗した row があれば 'Left'。
-randomInit
-  :: PrimMonad m
-  => [Factor]
-  -> [Constraint]
-  -> Int
-  -> [VU.Vector Double]
-  -> MWC.Gen (PrimState m)
-  -> m (Either Text (LA.Matrix Double))
-randomInit factors cons n grids gen = do
-  let p = length grids
-      gridsV = V.fromList grids
-      maxTries = 200 :: Int
-      drawRow = do
-        vs <- mapM (\j -> do
-                       let g = gridsV V.! j
-                           gl = VU.length g
-                       k <- MWC.uniformR (0, gl - 1) gen
-                       pure (g VU.! k)) [0 .. p - 1]
-        pure (LA.fromList vs)
-      tryRow t
-        | t > maxTries = pure Nothing
-        | otherwise = do
-            r <- drawRow
-            if rowFeasible factors cons r
-              then pure (Just r)
-              else tryRow (t + 1)
-  rowsR <- mapM (\_ -> tryRow 1) [1 .. n]
-  case sequence rowsR of
-    Just rs -> pure (Right (LA.fromRows rs))
-    Nothing -> pure (Left (T.pack
-      ("randomInit: failed to find feasible row within "
-       <> show maxTries <> " rejection-sampling tries — "
-       <> "constraints may be infeasible or too tight")))
-
--- | row vector (length p) の j 番目を v に置換した新 vector。
-replaceVecAt :: LA.Vector Double -> Int -> Double -> LA.Vector Double
-replaceVecAt v j x =
-  LA.fromList [if k == j then x else v `LA.atIndex` k | k <- [0 .. LA.size v - 1]]
-
--- | row (raw Vector) が全制約を満たすかを評価。
--- Categorical / Ordinal 列は level index → 因子の level 名 ('FVText') に変換、
--- 連続系は 'FVDouble' に変換して 'checkRowAgainst' に渡す。
-rowFeasible :: [Factor] -> [Constraint] -> LA.Vector Double -> Bool
-rowFeasible _ [] _ = True
-rowFeasible factors cons row =
-  let m = buildRowFV factors row
-  in all (checkRowAgainst m) cons
-
--- | raw 値 vector (列順 = factors 順) を 因子名 → FactorValue Map に変換。
--- Categorical / Ordinal は level index を level 名 ('FVText') に変換、
--- 非整数 / 範囲外 index は安全のため 'FVDouble' のまま (rowFeasible で
--- 不一致 → 制約違反 として扱われる、 expandDesignMatrix が別途 Left を返す)。
-buildRowFV :: [Factor] -> LA.Vector Double -> M.Map Text FactorValue
-buildRowFV factors row =
-  M.fromList
-    [ (fName f, toFV (fKind f) (row `LA.atIndex` i))
-    | (i, f) <- zip [0 ..] factors
-    ]
-  where
-    toFV (Categorical xs) x = catIndexToFV xs x
-    toFV (Ordinal     xs) x = catIndexToFV xs x
-    toFV _                x = FVDouble x
-
-    catIndexToFV :: [Text] -> Double -> FactorValue
-    catIndexToFV xs x =
-      let xi = round x :: Int
-          delta = abs (x - fromIntegral xi)
-      in if delta < 1e-9 && xi >= 0 && xi < length xs
-           then FVText (xs !! xi)
-           else FVDouble x  -- 不正値 → 文字列 level に一致しない = 不一致
-
--- | accum で 1 セルだけ置換した新 matrix を返す。
--- 注意: hmatrix `LA.accum` の combining fn は @f new old@ の順 (= 第 1 引数が
--- リストの値、 第 2 引数が現行値)。 'const' で「リストの値で置換」 を意味する。
-setEntry :: LA.Matrix Double -> Int -> Int -> Double -> LA.Matrix Double
-setEntry m i j v = LA.accum m const [((i, j), v)]
-
--- | 相対改善 = (before − after) / |before| (前後とも最小化方向の criterion 値)。
--- 値が小さい (≤ dbTol) ほど「改善が止まった」 と解釈、 outer loop で break。
-relImprovement :: Double -> Double -> Double
-relImprovement before after
-  | abs before < 1e-12 = before - after
-  | otherwise          = (before - after) / abs before
-
--- | seed から MWC.Gen を作る (IO)。 Nothing なら entropy 由来 (非決定的)。
-mkGen :: Maybe Int -> IO MWC.GenIO
-mkGen Nothing  = MWC.createSystemRandom
-mkGen (Just s) = mkGenSeed s
-
--- | seed から MWC.Gen を作る (PrimMonad 一般化・決定的)。IO / ST 両対応。
-mkGenSeed :: PrimMonad m => Int -> m (MWC.Gen (PrimState m))
-mkGenSeed s = MWC.initialize (VU.fromList [fromIntegral s])
-
--- | 純粋版 'coordinateExchangePure' で 'cdsSeed' が 'Nothing' のときに使う既定 seed。
--- 純粋 = 全域である必要があるため固定値を用いる (同 spec → 常に同結果)。
-defaultPureSeed :: Int
-defaultPureSeed = 0x5EED
-
--- ---------------------------------------------------------------------------
--- Phase 28-12 自動 DJ 規約変換
--- ---------------------------------------------------------------------------
-
--- | criterion 木に BayesianD が含まれているか。
-critContainsBayesianD :: OptCriterion -> Bool
-critContainsBayesianD (BayesianD _)  = True
-critContainsBayesianD (Compound ws)  = any (critContainsBayesianD . snd) ws
-critContainsBayesianD _              = False
-
--- | 因子 grid から候補集合 (cartesian product) の raw matrix を構築。
-candidateFromGrids :: [VU.Vector Double] -> LA.Matrix Double
-candidateFromGrids gs =
-  let lists = map VU.toList gs
-      rows  = sequence lists   -- cartesian product
-  in if null rows then (0 LA.>< length gs) []
-                  else LA.fromLists rows
-
--- | spec の `cdsDJConvention` が True かつ criterion に BayesianD を含むときのみ
--- 候補集合から 'DJTransform' を fit する。 それ以外は @Right Nothing@。
-fitDJTransformIfRequested
-  :: CustomDesignSpec
-  -> [Factor]
-  -> Model
-  -> [VU.Vector Double]
-  -> OptCriterion
-  -> Either Text (Maybe RM.DJTransform)
-fitDJTransformIfRequested spec fs model grids crit
-  | not (cdsDJConvention spec)        = Right Nothing
-  | not (critContainsBayesianD crit)  = Right Nothing
-  | otherwise =
-      let cand = candidateFromGrids grids
-      in case RM.djFitTransform fs model cand of
-           Left e  -> Left e
-           Right t -> Right (Just t)
diff --git a/src/Hanalyze/Design/Custom/Factor.hs b/src/Hanalyze/Design/Custom/Factor.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Custom/Factor.hs
+++ /dev/null
@@ -1,71 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.Custom.Factor
--- Description : Custom Design の Factor 定義 (Role × Kind の直交軸による因子型)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Custom Design の Factor 定義 (Phase 24-1 skeleton)。
---
--- spec: doe-custom-design-spec v0.1.1 §2.1 / §3.1。
---
--- 「コントロール性 (Role)」 × 「水準型 (Kind)」 の直交軸で 1 型に集約。
--- HardToChange フラグが split-plot を駆動 (Phase 25 で実装)。
-module Hanalyze.Design.Custom.Factor
-  ( FactorRole (..)
-  , FactorKind (..)
-  , Factor (..)
-  , factorIsContinuous
-  , factorDimension
-  ) where
-
-import Data.Text (Text)
-
--- | 因子の運用上の役割。
-data FactorRole
-  = Controllable      -- ^ 通常因子
-  | HardToChange      -- ^ Whole-plot 因子 (split-plot 駆動)
-  | VeryHardToChange  -- ^ Strip-plot 駆動
-  | Blocking          -- ^ 既知ブロック
-  | Covariate         -- ^ 共変量 (測定可だが操作不可)
-  | Constant          -- ^ 固定 (設計には現れず記録のみ)
-  | Uncontrolled      -- ^ ノイズ (Taguchi outer array 由来)
-  deriving (Eq, Show)
-
--- | 因子の水準型。
-data FactorKind
-  = Continuous   !Double !Double         -- ^ (low, high)、 coded ±1 への正規化対象
-  | DiscreteNum  ![Double]               -- ^ 離散水準 (順序あり)
-  | Categorical  ![Text]                 -- ^ 順序なしカテゴリ
-  | Ordinal      ![Text]                 -- ^ 順序ありカテゴリ
-  | Mixture      !Double !Double         -- ^ 混合比制約下の (lower, upper)
-  deriving (Eq, Show)
-
--- | Factor = 名前 + 水準型 + 役割。
-data Factor = Factor
-  { fName :: !Text
-  , fKind :: !FactorKind
-  , fRole :: !FactorRole
-  } deriving (Eq, Show)
-
--- | 連続系 (Continuous / DiscreteNum / Mixture) かどうか。
--- 設計行列の展開時に、 categorical 因子の treatment coding 分岐に使う。
-factorIsContinuous :: Factor -> Bool
-factorIsContinuous f = case fKind f of
-  Continuous  _ _ -> True
-  DiscreteNum _   -> True
-  Mixture     _ _ -> True
-  Categorical _   -> False
-  Ordinal     _   -> False
-
--- | Factor の「設計行列に占める列数」 概算 (skeleton 段階の単純実装)。
--- - 連続系: 1
--- - Categorical / Ordinal: (水準数 − 1)  ※reference coding
--- 0 水準 (空 Categorical) は 0 列 (実装側で warn 推奨)。
-factorDimension :: Factor -> Int
-factorDimension f = case fKind f of
-  Continuous  _ _   -> 1
-  DiscreteNum _     -> 1
-  Mixture     _ _   -> 1
-  Categorical xs    -> max 0 (length xs - 1)
-  Ordinal     xs    -> max 0 (length xs - 1)
diff --git a/src/Hanalyze/Design/Custom/Model.hs b/src/Hanalyze/Design/Custom/Model.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Custom/Model.hs
+++ /dev/null
@@ -1,241 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.Custom.Model
--- Description : Custom Design の Model 定義と設計行列展開 (項 ADT → treatment coding)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Custom Design の Model 定義 + 設計行列展開 (Phase 24-2)。
---
--- spec: doe-custom-design-spec v0.1.1 §2.2 / §3.1。
---
--- ## raw matrix の Categorical 表現規約 (重要、 型安全ではない)
---
--- `expandDesignMatrix` の入力 `Matrix Double` における Categorical / Ordinal
--- 因子の列は **level index 0..K-1 を Double で保持** する。
--- expandDesignMatrix は reference (treatment) coding で K-1 列に展開、
--- 参照水準 = index 0。
---
--- `Matrix Double` は連続値も index も同じ型なので、 0.5 のような小数や
--- 範囲外 index を **型では防げない**。 検出は runtime check (`Left Text`)。
--- 王道再設計 (R `model.matrix` / patsy 流の型分離) は phase-plan の
--- Phase 27 候補に登録済。 詳細は specification/phases/phase-24-custom-design-core.md。
---
--- ## 未対応 (Phase 24 v0.2 候補)
---
---   * `mNorm` は ADT として持つが現状 'NCoded' は identity、 'NUnit' / 'NRaw' は
---     呼び出し側で適切な値を渡す前提
---   * `TNested` / `TCustom` (`Left` を返す)
---   * `TPower` を Categorical 因子に適用するのは無意味 (indicator^k = indicator)
---     なので `Left`
-module Hanalyze.Design.Custom.Model
-  ( ParamNormalize (..)
-  , ModelTerm (..)
-  , Model (..)
-  , expandDesignMatrix
-  , modelNumColumns
-  ) where
-
-import           Data.Text (Text)
-import qualified Data.Text as T
-import           Data.List (elemIndex)
-import qualified Numeric.LinearAlgebra as LA
-
-import           Hanalyze.Design.Custom.Factor
-
--- | 因子値の正規化方針。
-data ParamNormalize
-  = NCoded   -- ^ coded units (連続因子は @[-1, 1]@ に既に変換済前提)
-  | NUnit    -- ^ unit cube (@[0, 1]@) 想定
-  | NRaw     -- ^ raw 単位 (= 何も変換しない)
-  deriving (Eq, Show)
-
--- | モデル項。
-data ModelTerm
-  = TIntercept                     -- ^ 切片 (全 1 列)
-  | TMain   !Text                  -- ^ 主効果 (因子名)
-  | TInter  ![Text]                -- ^ 交互作用 (k 因子)
-  | TPower  !Text !Int             -- ^ @x^k@ (k ≥ 2 を想定、 連続因子のみ)
-  | TNested !Text !Text            -- ^ @A within B@ (未対応)
-  deriving (Eq, Show)
-
--- | モデル = 項リスト + 正規化方針。
-data Model = Model
-  { mTerms :: ![ModelTerm]
-  , mNorm  :: !ParamNormalize
-  } deriving (Eq, Show)
-
--- | モデル全体が設計行列に占める列数 (Categorical 因子の K-1 展開を考慮)。
--- Categorical 因子参照中の TMain / TInter / TPower は factorDimension を使う。
-modelNumColumns :: [Factor] -> Model -> Int
-modelNumColumns factors m = sum (map termWidth (mTerms m))
-  where
-    findF n = lookup n [(fName f, f) | f <- factors]
-    dim n   = maybe 1 factorDimension (findF n)
-    termWidth t = case t of
-      TIntercept    -> 1
-      TMain n       -> dim n
-      TInter ns     -> product (map dim ns)
-      TPower _ _    -> 1
-      TNested a b   -> levelsOf b * dim a   -- Phase 28-1: K_B × (K_A - 1) cols
-    levelsOf n = case lookup n [(fName f, f) | f <- factors] of
-      Just f -> case fKind f of
-        Categorical xs -> length xs
-        Ordinal     xs -> length xs
-        _              -> 0
-      Nothing -> 0
-
--- | 因子の raw 値行列 (n × p_factors) からモデル設計行列 (n × p_terms) を展開。
---
--- 入力 @raw@ の列順は @factors@ の順序と一致する前提。
--- Categorical / Ordinal 因子の列は **level index 0..K-1 を Double で保持**
--- する規約 (上記モジュール doc 参照)。
---
--- 失敗を返すケース:
---   * @TNested@ を含む
---   * 参照される因子名が見つからない
---   * Categorical の raw 値が非整数 / 範囲外
---   * @TPower@ を Categorical 因子に適用
---   * 因子行列の列数が @factors@ の長さと一致しない
-expandDesignMatrix
-  :: [Factor]
-  -> Model
-  -> LA.Matrix Double            -- ^ 因子 raw 値 (n × p_factors)
-  -> Either Text (LA.Matrix Double)
-expandDesignMatrix factors model raw
-  | LA.cols raw /= length factors =
-      Left (T.pack "expandDesignMatrix: raw column count ≠ #factors")
-  | otherwise = do
-      colss <- mapM (termColumns factors raw) (mTerms model)
-      pure (LA.fromColumns (concat colss))
-
--- | 単一項を 0 個以上の列に変換 (Categorical の TMain は K-1 列、
--- Categorical × Categorical の TInter はクロス積で (K1-1)(K2-1) 列等)。
-termColumns
-  :: [Factor]
-  -> LA.Matrix Double
-  -> ModelTerm
-  -> Either Text [LA.Vector Double]
-termColumns _ raw TIntercept =
-  Right [LA.fromList (replicate (LA.rows raw) 1.0)]
-termColumns factors raw (TMain name) =
-  factorColumns factors raw name
-termColumns factors raw (TInter names)
-  | null names = Left (T.pack "TInter with no factor names is invalid")
-  | otherwise = do
-      colGroups <- mapM (factorColumns factors raw) names
-      -- 各因子の列群を cartesian product で elementwise 積。
-      Right (foldr1 crossMultiply colGroups)
-termColumns factors raw (TPower name k)
-  | k < 2     = Left (T.pack ("TPower: k must be >= 2 (got " <> show k <> ")"))
-  | otherwise = do
-      f <- findFactor factors name
-      if factorIsContinuous f
-        then do
-          v <- numericFactorVector factors raw name
-          Right [LA.cmap (** fromIntegral k) v]
-        else Left (T.pack
-               ("TPower on categorical/ordinal factor " <> T.unpack name
-                <> " is degenerate (indicator^k = indicator)"))
-termColumns factors raw (TNested aName bName) = do
-  (aIdx, fA) <- findFactorWithIndex factors aName
-  (bIdx, fB) <- findFactorWithIndex factors bName
-  let kindCat fk = case fk of
-        Categorical xs -> Just xs
-        Ordinal     xs -> Just xs
-        _              -> Nothing
-  case (kindCat (fKind fA), kindCat (fKind fB)) of
-    (Just aXs, Just bXs) -> do
-      let aCol = LA.flatten (LA.subMatrix (0, aIdx) (LA.rows raw, 1) raw)
-          bCol = LA.flatten (LA.subMatrix (0, bIdx) (LA.rows raw, 1) raw)
-      aIxs <- traverse (validateLevelIndex aName (length aXs)) (LA.toList aCol)
-      bIxs <- traverse (validateLevelIndex bName (length bXs)) (LA.toList bCol)
-      let kB = length bXs
-          kA = length aXs
-          n  = LA.rows raw
-          mkCol bLvl aLvl = LA.fromList
-            [ if (bIxs !! i) == bLvl && (aIxs !! i) == aLvl then 1.0 else 0.0
-            | i <- [0 .. n - 1] ]
-      -- 列順: outer = B level (0..K_B-1)、 inner = A level (1..K_A-1) (treatment coding)
-      Right [ mkCol b a | b <- [0 .. kB - 1], a <- [1 .. kA - 1] ]
-    _ ->
-      Left (T.pack
-        ("TNested " <> T.unpack aName <> " within " <> T.unpack bName
-         <> ": both factors must be Categorical/Ordinal (Phase 28-1 制限)"))
-
--- | 2 つの列群を elementwise 積で cartesian-product 化。
--- 結果列数 = length xs * length ys。
-crossMultiply :: [LA.Vector Double] -> [LA.Vector Double] -> [LA.Vector Double]
-crossMultiply xs ys = [x * y | x <- xs, y <- ys]
-  -- Vector の Num instance は elementwise
-
--- | 因子名 → 設計行列に挿入する列群。
--- 連続系: 単一列 (raw そのまま)。
--- Categorical / Ordinal: treatment coding で K-1 列 (reference = index 0)。
-factorColumns
-  :: [Factor]
-  -> LA.Matrix Double
-  -> Text
-  -> Either Text [LA.Vector Double]
-factorColumns factors raw name = do
-  (i, f) <- findFactorWithIndex factors name
-  let col = LA.flatten (LA.subMatrix (0, i) (LA.rows raw, 1) raw)
-  case fKind f of
-    Continuous  _ _ -> Right [col]
-    DiscreteNum _   -> Right [col]
-    Mixture     _ _ -> Right [col]
-    Categorical xs  -> treatmentCoding name (length xs) col
-    Ordinal     xs  -> treatmentCoding name (length xs) col
-
--- | reference (treatment) coding。 K 水準なら K-1 列、 reference = index 0。
--- 列 k (1-based: 1..K-1) の値 = 1 if raw == k else 0。
-treatmentCoding
-  :: Text                           -- ^ 因子名 (エラーメッセージ用)
-  -> Int                            -- ^ 水準数 K
-  -> LA.Vector Double               -- ^ raw 列 (level index を Double で)
-  -> Either Text [LA.Vector Double]
-treatmentCoding name k col
-  | k <= 0 = Left (T.pack
-               ("factor " <> T.unpack name <> ": categorical with 0 levels"))
-  | k == 1 = Right []  -- 1 水準は constant、 列なし
-  | otherwise = do
-      idxs <- traverse (validateLevelIndex name k) (LA.toList col)
-      let mkCol lvl = LA.fromList
-            [ if i == lvl then 1.0 else 0.0 | i <- idxs ]
-      Right [mkCol lvl | lvl <- [1 .. k - 1]]
-
--- | level index validation: 整数値かつ [0, K-1] 範囲内。
-validateLevelIndex :: Text -> Int -> Double -> Either Text Int
-validateLevelIndex name k x =
-  let xi = round x :: Int
-      delta = abs (x - fromIntegral xi)
-  in if delta > 1e-9
-       then Left (T.pack
-              ("factor " <> T.unpack name
-               <> ": categorical raw value " <> show x
-               <> " is not an integer level index"))
-       else if xi < 0 || xi >= k
-              then Left (T.pack
-                     ("factor " <> T.unpack name
-                      <> ": level index " <> show xi
-                      <> " out of range [0," <> show (k - 1) <> "]"))
-              else Right xi
-
--- | 連続因子の生の列 (TPower 用に分離した helper)。
-numericFactorVector
-  :: [Factor]
-  -> LA.Matrix Double
-  -> Text
-  -> Either Text (LA.Vector Double)
-numericFactorVector factors raw name = do
-  (i, _) <- findFactorWithIndex factors name
-  Right (LA.flatten (LA.subMatrix (0, i) (LA.rows raw, 1) raw))
-
-findFactor :: [Factor] -> Text -> Either Text Factor
-findFactor factors name = snd <$> findFactorWithIndex factors name
-
-findFactorWithIndex :: [Factor] -> Text -> Either Text (Int, Factor)
-findFactorWithIndex factors name =
-  case elemIndex name (map fName factors) of
-    Nothing -> Left (T.pack ("factor not found: " <> T.unpack name))
-    Just i  -> Right (i, factors !! i)
diff --git a/src/Hanalyze/Design/Custom/Power.hs b/src/Hanalyze/Design/Custom/Power.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Custom/Power.hs
+++ /dev/null
@@ -1,174 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns      #-}
--- |
--- Module      : Hanalyze.Design.Custom.Power
--- Description : Custom Design の設計行列から model term ごとの検出力を直接算出
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Custom Design の設計行列ベース Power Analysis (Phase 24-8)。
---
--- spec: doe-custom-design-spec v0.1.1 §2.8 / §3.5。
---
--- 既存の 'Hanalyze.Design.Power' は ANOVA effect size (Cohen's f) ベースだが、
--- 本モジュールは **生成済の Custom Design の設計行列 X から各 model term の
--- noncentrality λ を直接算出** し、 noncentral F 分布の正規近似で power を返す。
---
--- ## アルゴリズム
---
--- 1. 設計行列 X を `expandDesignMatrix` で取得 (n × p)。 \(M = X^T X\) の
---    逆行列を 1 回計算。
--- 2. 各 model term について、 expand 出力のどの列を占めるかを `termColumns`
---    で特定 (Categorical TMain は K-1 列に展開されるので複数列になる)。
--- 3. effect size β (ユーザ入力) と σ (事前推定) から noncentrality:
---
---    \( \lambda = \frac{1}{\sigma^2} \sum_{j \in \mathrm{cols}} \frac{\beta^2}{(X^T X)^{-1}_{jj}} \)
---
---    これは「全 term 列に同じ true coefficient β が乗る」 + 「直交近似
---    (block-diagonal Σ_J)」 という単純化の下で正確。 非直交ケースでは過大評価
---    気味の近似値となる。 改善 (block-inverse 版) は将来 commit。
--- 4. F 検定 (df1 = #term cols, df2 = n - p) の critical value を 1 - α で取得、
---    Patnaik / 正規近似で `power = 1 - Φ((fCrit*df1 - (df1 + λ)) / sqrt(2(df1 + 2λ)))`。
---    既存 'Hanalyze.Design.Power.powerOneWayAnova' と同手法。
---
--- ## term 名 (ユーザが指定する @[(Text, Double)]@ のキー)
---
---   * @TIntercept@         → @"(Intercept)"@
---   * @TMain "x1"@         → @"x1"@
---   * @TInter ["x1","x2"]@ → @"x1:x2"@ (因子順は元 ADT 通り、 sort しない)
---   * @TPower "x1" k@      → @"x1^k"@
---   * @TNested a b@        → @"a(b)"@
---
--- 該当 term が見つからない場合は 'dpPower = 0' で返す (warning なし、
--- スコア用途のため Left にはしない)。
-module Hanalyze.Design.Custom.Power
-  ( DesignPower (..)
-  , designPower
-  , termName
-  , termColumnIndices
-  ) where
-
-import           Data.Text                (Text)
-import qualified Data.Text                as T
-import qualified Numeric.LinearAlgebra    as LA
-import qualified Statistics.Distribution                as SD
-import qualified Statistics.Distribution.FDistribution  as FD
-import qualified Statistics.Distribution.Normal         as NormalD
-
-import           Hanalyze.Design.Custom.Factor
-import           Hanalyze.Design.Custom.Model
-import           Hanalyze.Design.Custom.Coordinate (CustomDesign (..))
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
-data DesignPower = DesignPower
-  { dpTerm   :: !Text
-  , dpEffect :: !Double
-  , dpAlpha  :: !Double
-  , dpPower  :: !Double
-  } deriving (Show, Eq)
-
--- ---------------------------------------------------------------------------
--- 公開 API
--- ---------------------------------------------------------------------------
-
--- | 各 term の power を算出。 expand 失敗時は全 term で @dpPower = 0@。
-designPower
-  :: CustomDesign
-  -> Double                  -- ^ σ の事前推定
-  -> [(Text, Double)]        -- ^ 各 term の effect size β
-  -> Double                  -- ^ alpha
-  -> [DesignPower]
-designPower cd sigma effects alpha =
-  case expandDesignMatrix (cdFactors cd) (cdModel cd) (cdMatrix cd) of
-    Left _ ->
-      [ DesignPower nm eff alpha 0 | (nm, eff) <- effects ]
-    Right x ->
-      let !n   = LA.rows x
-          !p   = LA.cols x
-          xtx  = LA.tr x LA.<> x
-          d    = LA.det xtx
-      in if abs d < 1e-12 || n - p < 1 || sigma <= 0
-           then [ DesignPower nm eff alpha 0 | (nm, eff) <- effects ]
-           else
-             let inv     = LA.inv xtx
-                 !diag   = [ inv `LA.atIndex` (j, j) | j <- [0 .. p - 1] ]
-                 termMap = termColumnIndices (cdFactors cd) (cdModel cd)
-             in [ powerFor termMap diag n p sigma alpha nm eff
-                | (nm, eff) <- effects ]
-
--- | 1 term の power を算出。 該当 term が無ければ power = 0。
-powerFor
-  :: [(Text, [Int])]
-  -> [Double]      -- ^ (X'X)⁻¹ の対角 (column j)
-  -> Int           -- ^ n
-  -> Int           -- ^ p (model total columns)
-  -> Double        -- ^ sigma
-  -> Double        -- ^ alpha
-  -> Text          -- ^ term name
-  -> Double        -- ^ effect size β
-  -> DesignPower
-powerFor termMap diag n p sigma alpha nm eff =
-  case lookup nm termMap of
-    Nothing -> DesignPower nm eff alpha 0
-    Just []  -> DesignPower nm eff alpha 0
-    Just cols ->
-      let !df1 = length cols
-          !df2 = n - p
-          -- noncentrality λ = (β/σ)² · sum_j 1 / (X'X)⁻¹_{jj}
-          --   = sum_j β² / (σ² · (X'X)⁻¹_{jj})
-          !ncp = sum
-            [ (eff * eff) / (sigma * sigma * diag !! j) | j <- cols ]
-          fCrit = SD.quantile (FD.fDistribution df1 df2) (1 - alpha)
-          -- noncentral F の chi² 正規近似 (powerOneWayAnova と同手法)
-          mean1 = fromIntegral df1 + ncp
-          var1  = 2 * (fromIntegral df1 + 2 * ncp)
-          z     = (fCrit * fromIntegral df1 - mean1) / sqrt var1
-          !pw   = 1 - SD.cumulative (NormalD.normalDistr 0 1) z
-      in DesignPower nm eff alpha pw
-
--- ---------------------------------------------------------------------------
--- term 名 + column index の対応
--- ---------------------------------------------------------------------------
-
--- | term ADT を canonical 名 (Text) に変換。
-termName :: ModelTerm -> Text
-termName TIntercept     = T.pack "(Intercept)"
-termName (TMain nm)     = nm
-termName (TInter ns)    = T.intercalate (T.pack ":") ns
-termName (TPower nm k)  = nm <> T.pack "^" <> T.pack (show k)
-termName (TNested a b)  = a <> T.pack "(" <> b <> T.pack ")"
-
--- | 各 model term の expand 後 column indices (term 名でルックアップ可)。
--- expandDesignMatrix の列順 (= mTerms 順) と整合。
--- Categorical TMain は K-1 列、 TInter は cartesian product 数の列を占める。
-termColumnIndices :: [Factor] -> Model -> [(Text, [Int])]
-termColumnIndices factors model = snd (go (mTerms model) 0 [])
-  where
-    go :: [ModelTerm] -> Int -> [(Text, [Int])] -> (Int, [(Text, [Int])])
-    go [] off acc = (off, reverse acc)
-    go (t:ts) off acc =
-      let w = termWidthOf factors t
-          cols = [off .. off + w - 1]
-      in go ts (off + w) ((termName t, cols) : acc)
-
--- | 単一 term の column width (modelNumColumns の per-term 版)。
-termWidthOf :: [Factor] -> ModelTerm -> Int
-termWidthOf factors t = case t of
-  TIntercept -> 1
-  TMain n    -> dim n
-  TInter ns  -> product (map dim ns)
-  TPower _ _ -> 1
-  TNested a b -> levelsOf b * dim a  -- Phase 28-1: K_B × (K_A - 1)
-  where
-    dim n = case lookup n [(fName f, f) | f <- factors] of
-      Just f  -> factorDimension f
-      Nothing -> 1
-    levelsOf n = case lookup n [(fName f, f) | f <- factors] of
-      Just f -> case fKind f of
-        Categorical xs -> length xs
-        Ordinal     xs -> length xs
-        _              -> 0
-      Nothing -> 0
diff --git a/src/Hanalyze/Design/Custom/RegionMoment.hs b/src/Hanalyze/Design/Custom/RegionMoment.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Custom/RegionMoment.hs
+++ /dev/null
@@ -1,434 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.Custom.RegionMoment
--- Description : Custom Design の region moment matrix (I-criterion 用の region 積分)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Custom Design の region moment matrix (Phase 28-4)。
---
--- JMP 同等 I-criterion を実装するための region 積分 M_R を解析的に構築する。
---
--- @
---   I(X) = ∫_R f(z)' (X'X)⁻¹ f(z) dz / vol(R)
---        = trace( (X'X)⁻¹ · M_R )
---   M_R = ∫_R f(z) f(z)' dz / vol(R)
--- @
---
--- ## region 規約 (JMP 既定と整合)
---
---   * Continuous: coded @z ∈ U[-1, 1]@ 独立 (raw range は coded 後の前提で無視)
---   * DiscreteNum xs: xs から等確率に抽出 (有限サポート)
---   * Categorical / Ordinal (K 水準): 等確率
---   * Mixture: 非対応 (Phase 28-4a スコープ外、 簡plex 上の積分は 28-9/28-10
---     候補。 'regionMomentMatrixAnalytic' は Left を返す)
---
--- 「Compare / Coordinate のどちらからも import される」 ため、 'CustomDesign'
--- には依存しない (Factor + Model + Optimal のみ依存)。
-module Hanalyze.Design.Custom.RegionMoment
-  ( regionMomentMatrixAnalytic
-  , regionMomentMatrixMC
-  , iValueRegionM
-  , resolveIOptRegion
-    -- * DuMouchel-Jones §2.2 column transform (Phase 28-12)
-  , DJTransform (..)
-  , djFitTransform
-  , djApplyTransform
-  , djTransformColumns
-  ) where
-
-import           Data.List                (elemIndex)
-import qualified Data.Map.Strict          as M
-import           Data.Text                (Text)
-import qualified Data.Text                as T
-import qualified Numeric.LinearAlgebra    as LA
-
-import           Hanalyze.Design.Custom.Factor
-import           Hanalyze.Design.Custom.Model
-import           Hanalyze.Design.Custom.Constraint
-                   (Constraint, FactorValue (..), checkRowAgainst)
-import           Hanalyze.Design.Optimal       (OptCriterion (..))
-import qualified Hanalyze.Stat.QuasiRandom as QR
-
--- ---------------------------------------------------------------------------
--- 列構造記述
--- ---------------------------------------------------------------------------
-
--- | 1 因子分の expand 後寄与。 連続因子なら指数 k (≥ 1)、 categorical/ordinal
--- なら treatment coding の level index l (1..K-1)。
-data FactorContrib
-  = ContPow  !Int   -- ^ @z_i^k@、 k ≥ 1
-  | CatLevel !Int   -- ^ indicator at level l (1..K-1)
-  deriving (Eq, Show)
-
--- | expand 後 1 列の構造記述: 因子 index → 寄与。 map に無い因子は「無寄与 = 1」。
-type ColDesc = M.Map Int FactorContrib
-
--- | 因子と Model から expand 後の各列の構造記述を 'expandDesignMatrix' と同順で生成。
--- Mixture / TNested は Left。
-columnDescriptors :: [Factor] -> Model -> Either Text [ColDesc]
-columnDescriptors fs model =
-  concat <$> traverse (termDescriptors fs) (mTerms model)
-
-termDescriptors :: [Factor] -> ModelTerm -> Either Text [ColDesc]
-termDescriptors _  TIntercept     = Right [M.empty]
-termDescriptors fs (TMain n)      = mainDescs fs n
-termDescriptors fs (TPower n k)
-  | k < 2     = Left (T.pack ("regionMomentMatrixAnalytic: TPower k must be >= 2 (got " <> show k <> ")"))
-  | otherwise = do
-      (i, f) <- findFactorIdx fs n
-      case fKind f of
-        Continuous  _ _ -> Right [M.singleton i (ContPow k)]
-        DiscreteNum _   -> Right [M.singleton i (ContPow k)]
-        Mixture     _ _ -> Left (T.pack ("regionMomentMatrixAnalytic: Mixture factor " <> T.unpack n <> " not supported (Phase 28-4a)"))
-        _               -> Left (T.pack ("regionMomentMatrixAnalytic: TPower on categorical/ordinal factor " <> T.unpack n))
-termDescriptors fs (TInter ns)
-  | null ns   = Left (T.pack "regionMomentMatrixAnalytic: TInter with no factor names")
-  | otherwise = foldr1 crossDesc <$> traverse (mainDescs fs) ns
-termDescriptors _ (TNested _ _) =
-  Left (T.pack "regionMomentMatrixAnalytic: TNested not supported (Phase 28-1 候補)")
-
--- | 主効果 (= TMain) 相当の寄与記述。 連続 → 1 個 (ContPow 1)、
--- Categorical K 水準 → K-1 個 (CatLevel 1..K-1)、 Mixture → Left。
-mainDescs :: [Factor] -> Text -> Either Text [ColDesc]
-mainDescs fs n = do
-  (i, f) <- findFactorIdx fs n
-  case fKind f of
-    Continuous  _ _ -> Right [M.singleton i (ContPow 1)]
-    DiscreteNum _   -> Right [M.singleton i (ContPow 1)]
-    Mixture     _ _ -> Left (T.pack ("regionMomentMatrixAnalytic: Mixture factor " <> T.unpack n <> " not supported (Phase 28-4a)"))
-    Categorical xs  -> Right [M.singleton i (CatLevel l) | l <- [1 .. length xs - 1]]
-    Ordinal     xs  -> Right [M.singleton i (CatLevel l) | l <- [1 .. length xs - 1]]
-
-crossDesc :: [ColDesc] -> [ColDesc] -> [ColDesc]
-crossDesc xs ys = [M.unionWith mergeContrib x y | x <- xs, y <- ys]
-  where
-    mergeContrib (ContPow a) (ContPow b) = ContPow (a + b)
-    mergeContrib a           _           = a
-
-findFactorIdx :: [Factor] -> Text -> Either Text (Int, Factor)
-findFactorIdx fs n = case elemIndex n (map fName fs) of
-  Nothing -> Left (T.pack ("regionMomentMatrixAnalytic: factor not found: " <> T.unpack n))
-  Just i  -> Right (i, fs !! i)
-
--- ---------------------------------------------------------------------------
--- 解析積分 + I-criterion
--- ---------------------------------------------------------------------------
-
--- | region moment matrix を解析的に構築。 列順は 'expandDesignMatrix' と一致。
--- Mixture / TNested を含むモデルは Left。 categorical 1 水準等で列数 0 のときは 0×0。
-regionMomentMatrixAnalytic
-  :: [Factor] -> Model -> Either Text (LA.Matrix Double)
-regionMomentMatrixAnalytic fs model = do
-  cols <- columnDescriptors fs model
-  let p = length cols
-  if p == 0
-    then Right ((0 LA.>< 0) [])
-    else
-      let fsArr = zip [0 :: Int ..] fs
-          ent i j =
-            let ca = cols !! i; cb = cols !! j
-            in product
-                 [ expectFactorProduct (fKind f) (M.lookup k ca) (M.lookup k cb)
-                 | (k, f) <- fsArr ]
-      in Right (LA.fromLists
-                  [ [ ent i j | j <- [0 .. p - 1] ] | i <- [0 .. p - 1] ])
-
--- | 単一因子分の期待値 @E[part_a(z) · part_b(z)]@。
-expectFactorProduct
-  :: FactorKind -> Maybe FactorContrib -> Maybe FactorContrib -> Double
-expectFactorProduct kind ma mb = case kind of
-  Continuous _ _ -> contMomentPM1 (contPow ma + contPow mb)
-  DiscreteNum xs ->
-    let s = contPow ma + contPow mb
-        n = length xs
-    in if n == 0 then 0
-                 else sum (map (^^ s) xs) / fromIntegral n
-  Categorical xs -> catExp (length xs) (catLvl ma) (catLvl mb)
-  Ordinal     xs -> catExp (length xs) (catLvl ma) (catLvl mb)
-  Mixture _ _    -> 0 / 0
-  where
-    contPow Nothing             = 0
-    contPow (Just (ContPow k))  = k
-    contPow (Just (CatLevel _)) = 0
-    catLvl Nothing              = Nothing
-    catLvl (Just (CatLevel l))  = Just l
-    catLvl (Just (ContPow _))   = Nothing
-
-contMomentPM1 :: Int -> Double
-contMomentPM1 p
-  | odd p     = 0
-  | otherwise = 1 / fromIntegral (p + 1)
-
-catExp :: Int -> Maybe Int -> Maybe Int -> Double
-catExp _ Nothing  Nothing            = 1
-catExp k (Just _) Nothing            = 1 / fromIntegral k
-catExp k Nothing  (Just _)           = 1 / fromIntegral k
-catExp k (Just la) (Just lb)
-  | la == lb                         = 1 / fromIntegral k
-  | otherwise                        = 0
-
--- ---------------------------------------------------------------------------
--- MC 版 (Phase 28-4c): 制約有り / Mixture / 非 polynomial model 用
--- ---------------------------------------------------------------------------
---
--- Halton quasi-random sequence で region から N 点抽出 (deterministic、 seed 不要)、
--- 'Custom.Constraint.checkRowAgainst' で制約 region 内のみ採用。 採用率が低い
--- 場合 maxAttempts (= 10×N) で打ち切り、 採用数 < N/10 のとき Left。 採用された
--- raw rows を expand → @M_R = X^T X / N_accepted@ で構築。
---
--- 規約 (analytic と共通):
---   * Continuous (lo, hi): coded @z ∈ U[-1, 1]@ 独立 (raw range 無視)
---   * DiscreteNum xs: xs から等確率に抽出
---   * Mixture (lo, hi): @[lo, hi]@ uniform (Halton 1 次元 → 線形写像)
---   * Categorical / Ordinal (K 水準): 等確率
-
-regionMomentMatrixMC
-  :: Int              -- ^ 希望サンプル数 N (採用後、 採用率次第で短くなる場合あり)
-  -> [Factor]
-  -> Model
-  -> [Constraint]     -- ^ rejection sampling filter
-  -> Either Text (LA.Matrix Double)
-regionMomentMatrixMC nWant fs model cons
-  | nWant < 1 = Left (T.pack "regionMomentMatrixMC: N must be >= 1")
-  | null fs   = Left (T.pack "regionMomentMatrixMC: empty factor list")
-  | otherwise =
-      let nF          = length fs
-          maxAttempts = nWant * 10   -- 採用率 10% 想定の安全係数
-          halton      = QR.haltonMatrix maxAttempts nF
-          rawAll      =
-            [ [ mapU01ToFactorLocal (fs !! j) (halton `LA.atIndex` (i, j))
-              | j <- [0 .. nF - 1] ]
-            | i <- [0 .. maxAttempts - 1] ]
-          accepted = take nWant
-                     [ row | row <- rawAll, rowFeasibleLocal fs cons row ]
-          nAcc = length accepted
-      in if nAcc < max 1 (nWant `div` 10)
-           then Left (T.pack ("regionMomentMatrixMC: too few accepted samples ("
-                              <> show nAcc <> "/" <> show nWant <> "); 制約 region が極端に狭い可能性"))
-           else case expandDesignMatrix fs model (LA.fromLists accepted) of
-                  Left e  -> Left e
-                  Right x ->
-                    let nAccD = fromIntegral nAcc :: Double
-                    in Right (LA.scale (1 / nAccD) (LA.tr x LA.<> x))
-
--- | Halton 1 次元値 u ∈ [0, 1] を 1 因子の raw 値に写像
--- ('Custom.Compare.mapU01ToFactor' と同等、 module cycle 回避で再実装)。
-mapU01ToFactorLocal :: Factor -> Double -> Double
-mapU01ToFactorLocal f u = case fKind f of
-  Continuous _ _ -> -1 + 2 * u
-  DiscreteNum xs ->
-    let k = length xs
-    in if k <= 0 then 0
-                 else xs !! min (k - 1) (floor (u * fromIntegral k))
-  Mixture lo hi  -> lo + (hi - lo) * u
-  Categorical xs ->
-    let k = length xs
-    in if k <= 0 then 0
-                 else fromIntegral (min (k - 1) (floor (u * fromIntegral k)))
-  Ordinal xs     ->
-    let k = length xs
-    in if k <= 0 then 0
-                 else fromIntegral (min (k - 1) (floor (u * fromIntegral k)))
-
--- | 1 row が全制約を満たすか
--- ('Custom.Coordinate.rowFeasible' と同等、 module cycle 回避で再実装)。
-rowFeasibleLocal :: [Factor] -> [Constraint] -> [Double] -> Bool
-rowFeasibleLocal fs cons row =
-  let mkFV f x = case fKind f of
-        Categorical xs -> catIdx xs x
-        Ordinal     xs -> catIdx xs x
-        _              -> FVDouble x
-      catIdx xs x =
-        let xi = round x :: Int
-            d  = abs (x - fromIntegral xi)
-        in if d < 1e-9 && xi >= 0 && xi < length xs
-             then FVText (xs !! xi)
-             else FVDouble x
-      rowMap = M.fromList
-        [ (fName f, mkFV f v) | (f, v) <- zip fs row ]
-  in all (checkRowAgainst rowMap) cons
-
--- | region moment matrix を用いた I-criterion: @trace((X'X)⁻¹ · M_R)@。
--- 設計行列が rank-deficient (det(X'X) ≈ 0) なら ∞ を返す (minimize 方向)。
-iValueRegionM :: LA.Matrix Double -> LA.Matrix Double -> Double
-iValueRegionM mR x
-  | LA.rows x == 0 = 1 / 0
-  | otherwise =
-      let xtx = LA.tr x LA.<> x
-          d   = LA.det xtx
-      in if abs d < 1e-12 then 1 / 0
-           else LA.sumElements (LA.takeDiag (LA.inv xtx LA.<> mR))
-
--- ---------------------------------------------------------------------------
--- IOpt → IOptRegion 解決 (Phase 28-4b)
--- ---------------------------------------------------------------------------
-
--- | OptCriterion 木を走査し、 'IOpt' を 'IOptRegion mR' に置換する。
--- 'IOptRegion' は once-only に「凍結された M_R」 を持つので、 'Compound' で
--- 入れ子になっていても 1 回 M_R を作って全 IOpt を共有して置換する。
---
--- Phase 28-4c: 制約有り (cons 非空) または Mixture 因子を含むとき、
--- 'regionMomentMatrixAnalytic' は Left を返すため自動で
--- 'regionMomentMatrixMC' (Halton quasi-random、 N=10000) に fallback する。
--- IOpt を含まない criterion は M_R 構築をスキップして Right で原型を返す。
-resolveIOptRegion
-  :: [Factor] -> Model -> [Constraint] -> OptCriterion
-  -> Either Text OptCriterion
-resolveIOptRegion fs model cons crit
-  | not (containsIOpt crit) = Right crit
-  | otherwise = do
-      mR <- buildMR
-      let mrRows = LA.toLists mR
-      pure (rewriteCrit mrRows crit)
-  where
-    needsMC = not (null cons) || any isMixture fs || containsTNested model
-    isMixture f = case fKind f of
-      Mixture _ _ -> True
-      _           -> False
-    containsTNested m = any isTN (mTerms m)
-    isTN (TNested _ _) = True
-    isTN _             = False
-
-    buildMR
-      | needsMC = case regionMomentMatrixMC 10000 fs model cons of
-          Right m -> Right m
-          Left  e ->
-            -- MC が失敗したら analytic を最後の砦として試す (制約無視で粗い近似)
-            case regionMomentMatrixAnalytic fs model of
-              Right m -> Right m
-              Left _  -> Left e
-      | otherwise = regionMomentMatrixAnalytic fs model
-
-    containsIOpt IOpt              = True
-    containsIOpt (Compound ws)     = any (containsIOpt . snd) ws
-    containsIOpt _                 = False
-
-    rewriteCrit mrRows IOpt          = IOptRegion mrRows
-    rewriteCrit mrRows (Compound ws) =
-      Compound [ (w, rewriteCrit mrRows c) | (w, c) <- ws ]
-    rewriteCrit _      c             = c
-
--- ---------------------------------------------------------------------------
--- DuMouchel-Jones §2.2 column transform (Phase 28-12)
--- ---------------------------------------------------------------------------
---
--- 詳細は doc: src/Hanalyze/Design/Custom/Bayesian.hs (Phase 28-12 section)。
--- Power.termColumnIndices に依存しないよう、 列 index 列挙を本 module 内に
--- 再実装している (Coordinate ↔ Bayesian の module cycle 回避)。
-
-data DJTransform = DJTransform
-  { djtPrimaryIdx   :: ![Int]
-  , djtPotentialIdx :: ![Int]
-  , djtMeanQ        :: !(LA.Vector Double)
-  , djtBetaPQ       :: !(LA.Matrix Double)
-  , djtScaleQ       :: !(LA.Vector Double)
-  } deriving (Show)
-
-isPotentialTerm :: ModelTerm -> Bool
-isPotentialTerm TIntercept     = False
-isPotentialTerm (TMain _)      = False
-isPotentialTerm (TInter ns)    = length ns >= 2
-isPotentialTerm (TPower _ k)   = k >= 2
-isPotentialTerm (TNested _ _)  = True
-
--- | 各 term の expand 後 column index 範囲 (Power.termColumnIndices の再実装)。
-termColumnIndicesLocal :: [Factor] -> Model -> [(ModelTerm, [Int])]
-termColumnIndicesLocal fs model = go (mTerms model) 0
-  where
-    go [] _ = []
-    go (t:ts) off =
-      let w = termWidth t
-          cols = [off .. off + w - 1]
-      in (t, cols) : go ts (off + w)
-    termWidth t = case t of
-      TIntercept    -> 1
-      TMain n       -> dim n
-      TInter ns     -> product (map dim ns)
-      TPower _ _    -> 1
-      TNested a b   -> levelsOf b * dim a   -- Phase 28-1
-    dim n = case lookup n [(fName f, f) | f <- fs] of
-      Just f  -> factorDimension f
-      Nothing -> 1
-    levelsOf n = case lookup n [(fName f, f) | f <- fs] of
-      Just f -> case fKind f of
-        Categorical xs -> length xs
-        Ordinal     xs -> length xs
-        _              -> 0
-      Nothing -> 0
-
-djFitTransform
-  :: [Factor] -> Model -> LA.Matrix Double -> Either Text DJTransform
-djFitTransform fs model cand = do
-  xCand <- expandDesignMatrix fs model cand
-  let pairs = termColumnIndicesLocal fs model
-      primaryIdx   = concat [ cols | (t, cols) <- pairs, not (isPotentialTerm t) ]
-      potentialIdx = concat [ cols | (t, cols) <- pairs, isPotentialTerm t ]
-      nC = LA.rows xCand
-      nCD = fromIntegral nC :: Double
-      q = length potentialIdx
-  if q == 0
-    then pure DJTransform
-           { djtPrimaryIdx   = primaryIdx
-           , djtPotentialIdx = []
-           , djtMeanQ        = LA.fromList []
-           , djtBetaPQ       = (0 LA.>< 0) []
-           , djtScaleQ       = LA.fromList []
-           }
-    else do
-      let xP = if null primaryIdx then (nC LA.>< 0) [] else xCand LA.¿ primaryIdx
-          xQ = xCand LA.¿ potentialIdx
-          meanRow = LA.fromList
-            [ LA.sumElements (LA.flatten (xQ LA.¿ [j])) / nCD
-            | j <- [0 .. q - 1] ]
-          ones    = LA.konst 1 nC :: LA.Vector Double
-          xQc = xQ - LA.outer ones meanRow
-          betas = if null primaryIdx
-                    then (0 LA.>< q) []
-                    else
-                      let xtxP = LA.tr xP LA.<> xP
-                          d = LA.det xtxP
-                      in if abs d < 1e-12
-                           then LA.konst 0 (LA.cols xP, q)
-                           else LA.inv xtxP LA.<> LA.tr xP LA.<> xQc
-          xQo = if null primaryIdx then xQc else xQc - xP LA.<> betas
-          rangesL =
-            [ let v = LA.flatten (xQo LA.¿ [j])
-              in LA.maxElement v - LA.minElement v
-            | j <- [0 .. q - 1] ]
-      pure DJTransform
-        { djtPrimaryIdx   = primaryIdx
-        , djtPotentialIdx = potentialIdx
-        , djtMeanQ        = meanRow
-        , djtBetaPQ       = betas
-        , djtScaleQ       = LA.fromList rangesL
-        }
-
-djApplyTransform :: DJTransform -> LA.Matrix Double -> LA.Matrix Double
-djApplyTransform t x
-  | null (djtPotentialIdx t) = x
-  | otherwise =
-      let n   = LA.rows x
-          pIdx = djtPrimaryIdx t
-          qIdx = djtPotentialIdx t
-          xP   = if null pIdx then (n LA.>< 0) [] else x LA.¿ pIdx
-          xQ   = x LA.¿ qIdx
-          ones = LA.konst 1 n :: LA.Vector Double
-          xQc  = xQ - LA.outer ones (djtMeanQ t)
-          xQo  = if null pIdx then xQc else xQc - xP LA.<> djtBetaPQ t
-          invR = LA.cmap (\r -> if abs r < 1e-12 then 1 else 1 / r) (djtScaleQ t)
-          xQf  = xQo LA.<> LA.diag invR
-          q    = length qIdx
-          col k = LA.flatten (xQf LA.¿ [k])
-          potMap  = zip qIdx [0 .. q - 1]
-          pickCol i = case lookup i potMap of
-            Just k  -> col k
-            Nothing -> LA.flatten (x LA.¿ [i])
-      in LA.fromColumns [ pickCol i | i <- [0 .. LA.cols x - 1] ]
-
-djTransformColumns
-  :: [Factor] -> Model -> LA.Matrix Double -> LA.Matrix Double
-  -> Either Text (LA.Matrix Double)
-djTransformColumns fs model cand x = do
-  t <- djFitTransform fs model cand
-  pure (djApplyTransform t x)
diff --git a/src/Hanalyze/Design/Custom/SplitPlot.hs b/src/Hanalyze/Design/Custom/SplitPlot.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Custom/SplitPlot.hs
+++ /dev/null
@@ -1,473 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns      #-}
--- |
--- Module      : Hanalyze.Design.Custom.SplitPlot
--- Description : Custom Design の Split-Plot 生成 (役割駆動の REML D-optimal 交換、内部 legacy)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Custom Design の Split-Plot 生成 (Phase 25-3/4)。
---
--- ★Phase 79 以降 **内部 legacy**: 製品パス (高レベル @customDesign@ + @Structure@) は役割非依存の
---   構造駆動エンジン @Design.Custom.Structured@ を使う。 本モジュール (役割 @fRole@ 駆動) は
---   bench-custom-design の 3 エンジン比較 + Jones-Goos 低レベル golden の証跡として温存する
---   (新規機能は Structured 側へ。 M⁻¹ / GLS 基準の math は両者で数値一致)。
---
--- spec: doe-custom-design-spec v0.1.1 §2.5 / §3.6。
--- 参考: Goos & Vandebroek (2003) "D-Optimal Split-Plot Designs", J Quality Tech 35:1-15。
---
--- ## モデル (簡易 REML)
---
---   y_ij = X_ij β + b_i + ε_ij
---
--- ここで b_i ~ N(0, σ²_WP) は whole-plot 効果、 ε_ij ~ N(0, σ²) は run-level error。
--- 分散比 η = σ²_WP / σ² がユーザ指定 (既定 1.0、 spec §2.5 で議論)。
---
--- 観測ベクトル全体の分散構造:
---
---   V = σ² (I + η · Z Zᵀ)
---
--- ここで Z は whole-plot indicator matrix (n × n_WP)。
--- REML information matrix:
---
---   I_β = (1/σ²) · Xᵀ M⁻¹ X、   M = I + η · Z Zᵀ
---
--- D-optimality は max det(Xᵀ M⁻¹ X)。 σ² は定数倍なので criterion に影響しない。
---
--- ## 本 commit (25-3/4) のスコープ
---
---   * 連続因子の whole-plot のみ対応 (categorical WP は 25-5 stub で Left)
---   * Coordinate exchange を whole-plot 単位 / sub-plot 単位の 2 段に分けて適用:
---     - WP 因子: 1 WP 内では同値、 列を WP indicator 構造で更新
---     - SP 因子: 各 run 単位で coordinate exchange (= 通常)
---   * η はユーザ指定 (`spcVarRatio`)、 既定 1.0
---   * strip-plot (VeryHardToChange) は未対応 (Left)
-module Hanalyze.Design.Custom.SplitPlot
-  ( SplitPlotConfig (..)
-  , defaultSplitPlotConfig
-  , SplitPlotDesign (..)
-  , generateSplitPlot
-  , generateSplitPlotPure
-    -- * 内部 helper (test 用)
-  , whichRoleIsWP
-  , wholePlotIndicator
-  ) where
-
-import           Control.Monad             (forM_, when)
-import           Control.Monad.Primitive   (PrimMonad, PrimState)
-import           Control.Monad.ST          (runST)
-import           Data.Maybe                (fromMaybe)
-import           Data.Primitive.MutVar
-import           Data.Text                 (Text)
-import qualified Data.Text                 as T
-import qualified Numeric.LinearAlgebra     as LA
-import qualified System.Random.MWC         as MWC
-import qualified Data.Vector.Unboxed       as VU
-import qualified Data.Vector               as V
-import qualified Data.Vector.Storable      as VS
-
-import           Hanalyze.Design.Custom.Factor
-import           Hanalyze.Design.Custom.Model
-import           Hanalyze.Design.Custom.Coordinate
-                   (CustomDesignSpec (..), DesignBudget (..)
-                   , factorGrid, critValueM
-                   , mkGen, mkGenSeed, defaultPureSeed)
-import           Hanalyze.Design.Optimal   (OptCriterion (..))
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
-data SplitPlotConfig = SplitPlotConfig
-  { spcNWhole    :: !Int     -- ^ whole-plot 数 (必須、 spec §2.5 でユーザ指定強制)
-  , spcVarRatio  :: !Double  -- ^ η = σ²_WP / σ² (既定 1.0)
-  , spcNStrip    :: !(Maybe Int)
-    -- ^ Phase 28-2: strip-plot 構造の strip 数 (Just nStrip)。
-    -- 'VeryHardToChange' role 因子は strip 内で constant。 Nothing なら
-    -- 通常の split-plot。 n = nWP × nStrip を満たす必要 (= 行配置: row i は
-    -- wp = i div nStrip、 strip = i mod nStrip)
-  } deriving (Show)
-
-defaultSplitPlotConfig :: Int -> SplitPlotConfig
-defaultSplitPlotConfig nWP = SplitPlotConfig nWP 1.0 Nothing
-
-data SplitPlotDesign = SplitPlotDesign
-  { spdMatrix      :: !(LA.Matrix Double)
-  , spdWholePlotId :: !(VS.Vector Int)         -- ^ 各行の WP ID (0..nWP-1)
-  , spdSubPlotId   :: !(Maybe (VS.Vector Int))
-    -- ^ Phase 28-2: strip-plot 時の strip ID (Just)、 通常 split-plot は Nothing
-  , spdNWhole      :: !Int
-  , spdGEFFEst     :: !Double                  -- ^ 推定 Generalized Estimating Function 値
-    -- ^ ≒ - det(I_β) の最小化値 (DOpt のみ意味あり、 他 criterion は critValueM 経由)
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- 公開 API
--- ---------------------------------------------------------------------------
-
--- | seed 由来の gen を作って 'generateSplitPlotWith' を IO で走らせる薄い wrapper。
--- 'cdsSeed' が 'Nothing' の場合のみ entropy 依存 (非決定的)。
--- Phase 78.M: seed 決定的な純粋版は 'generateSplitPlotPure'。
-generateSplitPlot
-  :: CustomDesignSpec
-  -> SplitPlotConfig
-  -> IO (Either Text SplitPlotDesign)
-generateSplitPlot spec cfg = do
-  gen <- mkGen (cdsSeed spec)
-  generateSplitPlotWith spec cfg gen
-
--- | seed 決定的な純粋版 (Phase 78.M)。'runST' で MWC gen + MutVar を閉じ込め、
--- IO 無しで 'SplitPlotDesign' を返す。'cdsSeed' が 'Nothing' なら 'defaultPureSeed'
--- を用いて全域にする。同一 seed なら 'generateSplitPlot' (IO) とビット一致する。
-generateSplitPlotPure
-  :: CustomDesignSpec
-  -> SplitPlotConfig
-  -> Either Text SplitPlotDesign
-generateSplitPlotPure spec cfg = runST $ do
-  gen <- mkGenSeed (fromMaybe defaultPureSeed (cdsSeed spec))
-  generateSplitPlotWith spec cfg gen
-
--- | split-plot 生成本体 (PrimMonad 一般化)。IO / ST どちらでも走る。
-generateSplitPlotWith
-  :: PrimMonad m
-  => CustomDesignSpec
-  -> SplitPlotConfig
-  -> MWC.Gen (PrimState m)
-  -> m (Either Text SplitPlotDesign)
-generateSplitPlotWith spec cfg gen
-  | spcNWhole cfg < 1 =
-      pure (Left (T.pack "generateSplitPlot: spcNWhole must be >= 1"))
-  | cdsNRuns spec < spcNWhole cfg =
-      pure (Left (T.pack "generateSplitPlot: nRuns must be >= spcNWhole"))
-  -- Phase 28-2: VeryHardToChange (strip-plot) を対応。 spcNStrip = Just nStrip
-  -- が必要 + n = nWP × nStrip を満たすこと
-  | any ((== VeryHardToChange) . fRole) (cdsFactors spec)
-    && case spcNStrip cfg of Nothing -> True; _ -> False =
-      pure (Left (T.pack
-        "generateSplitPlot: VeryHardToChange factor present but spcNStrip not set"))
-  -- Phase 28-3: Categorical/Ordinal whole-plot 因子も対応 (factorGrid が level
-  -- index を返すため、 randomInitSP/runExchangeSP の WP loop でそのまま機能する)
-  | spcVarRatio cfg < 0 =
-      pure (Left (T.pack "generateSplitPlot: spcVarRatio (η) must be >= 0"))
-  | case spcNStrip cfg of
-      Just s -> s < 1 || s * spcNWhole cfg /= cdsNRuns spec
-      Nothing -> False =
-      pure (Left (T.pack
-        "generateSplitPlot: spcNStrip × spcNWhole must equal nRuns (strip-plot grid)"))
-  | otherwise = do
-      let !factors  = cdsFactors spec
-          !n        = cdsNRuns spec
-          !nWP      = spcNWhole cfg
-          !eta      = spcVarRatio cfg
-          !budget   = cdsBudget spec
-          !crit     = cdsCriterion spec
-          !model    = cdsModel spec
-          !wpIxs    = whichRoleIsWP factors
-          !stripIxs = whichRoleIsStrip factors
-          !wpId     = wholePlotIndicator n nWP
-          !mStripId = case spcNStrip cfg of
-            Just nStrip -> Just (stripPlotIndicator n nStrip)
-            Nothing     -> Nothing
-      if null wpIxs && null stripIxs
-        then pure (Left (T.pack
-          "generateSplitPlot: no HardToChange/VeryHardToChange factor found"))
-        else do
-          bestRef <- newMutVar Nothing
-          forM_ [1 .. dbRestarts budget] $ \_ -> do
-            init0 <- randomInitSPStrip factors wpIxs stripIxs wpId mStripId n budget gen
-            (finalM, finalC) <- runExchangeSP factors model crit budget eta wpIxs stripIxs wpId mStripId init0
-            modifyMutVar' bestRef $ \mb -> case mb of
-              Nothing -> Just (finalM, finalC)
-              Just (_, c0) | finalC < c0 -> Just (finalM, finalC)
-                           | otherwise   -> mb
-          mb <- readMutVar bestRef
-          case mb of
-            Nothing -> pure (Left (T.pack "generateSplitPlot: no restart produced a design"))
-            Just (m, c) -> pure $ Right SplitPlotDesign
-              { spdMatrix      = m
-              , spdWholePlotId = wpId
-              , spdSubPlotId   = mStripId
-              , spdNWhole      = nWP
-              , spdGEFFEst     = c
-              }
-
--- ---------------------------------------------------------------------------
--- WP indicator / role helper
--- ---------------------------------------------------------------------------
-
--- | HardToChange factor の column index リスト (whole-plot 因子)。
-whichRoleIsWP :: [Factor] -> [Int]
-whichRoleIsWP fs = [ i | (i, f) <- zip [0 ..] fs, fRole f == HardToChange ]
-
--- | Phase 28-2: VeryHardToChange factor の column index リスト (strip 因子)。
-whichRoleIsStrip :: [Factor] -> [Int]
-whichRoleIsStrip fs = [ i | (i, f) <- zip [0 ..] fs, fRole f == VeryHardToChange ]
-
--- | n 行を nWP に均等割り当てした WP indicator (0..nWP-1)。
--- 余りは最初のいくつかの WP に追加で振る。
-wholePlotIndicator :: Int -> Int -> VS.Vector Int
-wholePlotIndicator n nWP =
-  let base  = n `div` nWP
-      extra = n `mod` nWP
-      sizes = [ if i < extra then base + 1 else base | i <- [0 .. nWP - 1] ]
-      ids   = concat [ replicate s i | (i, s) <- zip [0 ..] sizes ]
-  in VS.fromList ids
-
--- | Phase 28-2: strip indicator (0..nStrip-1)。 row i → i `mod` nStrip。
--- WP grouping (= i `div` nStrip) と直交する partitioning を実現する。
--- n = nWP × nStrip の前提 (generateSplitPlot の guard で確認済)
-stripPlotIndicator :: Int -> Int -> VS.Vector Int
-stripPlotIndicator n nStrip =
-  VS.fromList [ i `mod` nStrip | i <- [0 .. n - 1] ]
-
--- ---------------------------------------------------------------------------
--- 初期化 (split-plot 構造を保つ)
--- ---------------------------------------------------------------------------
-
--- | 初期 raw matrix。 WP 因子は WP ごとに 1 値、 strip 因子は strip ごとに
--- 1 値、 SP 因子は run ごとに 1 値。
-randomInitSPStrip
-  :: PrimMonad m
-  => [Factor]
-  -> [Int]               -- ^ WP 因子 column index
-  -> [Int]               -- ^ strip 因子 column index (Phase 28-2)
-  -> VS.Vector Int       -- ^ WP id per row
-  -> Maybe (VS.Vector Int)  -- ^ strip id per row (Phase 28-2)
-  -> Int                 -- ^ n
-  -> DesignBudget
-  -> MWC.Gen (PrimState m)
-  -> m (LA.Matrix Double)
-randomInitSPStrip factors wpIxs stripIxs wpId mStripId n budget gen = do
-  let p    = length factors
-      nWP  = if VS.null wpId then 0 else 1 + VS.maximum wpId
-      nStrp = case mStripId of
-        Just s | not (VS.null s) -> 1 + VS.maximum s
-        _ -> 0
-  cols <- mapM
-    (\j -> do
-       let g = factorGrid budget (factors !! j)
-           gl = VU.length g
-       if j `elem` wpIxs
-         then do
-           wpVals <- VU.replicateM nWP $ do
-             k <- MWC.uniformR (0, gl - 1) gen
-             pure (g VU.! k)
-           pure $ LA.fromList
-             [ wpVals VU.! (wpId VS.! i) | i <- [0 .. n - 1] ]
-         else if j `elem` stripIxs
-           then case mStripId of
-             Nothing -> pure (LA.konst 0 n)  -- 不可達: guard 済
-             Just stripId -> do
-               stripVals <- VU.replicateM nStrp $ do
-                 k <- MWC.uniformR (0, gl - 1) gen
-                 pure (g VU.! k)
-               pure $ LA.fromList
-                 [ stripVals VU.! (stripId VS.! i) | i <- [0 .. n - 1] ]
-           else do
-             vs <- VU.replicateM n $ do
-               k <- MWC.uniformR (0, gl - 1) gen
-               pure (g VU.! k)
-             pure (LA.fromList (VU.toList vs))
-    ) [0 .. p - 1]
-  pure (LA.fromColumns cols)
-
--- ---------------------------------------------------------------------------
--- Coordinate exchange (split-plot 構造保持)
--- ---------------------------------------------------------------------------
-
-runExchangeSP
-  :: PrimMonad m
-  => [Factor]
-  -> Model
-  -> OptCriterion
-  -> DesignBudget
-  -> Double                      -- ^ η
-  -> [Int]                       -- ^ WP factor indices
-  -> [Int]                       -- ^ strip factor indices (Phase 28-2)
-  -> VS.Vector Int               -- ^ wpId
-  -> Maybe (VS.Vector Int)       -- ^ stripId (Phase 28-2)
-  -> LA.Matrix Double
-  -> m (LA.Matrix Double, Double)
-runExchangeSP factors model crit budget eta wpIxs stripIxs wpId mStripId init0 = do
-  matRef  <- newMutVar init0
-  critRef <- newMutVar (evalCritSP factors model crit eta wpId mStripId init0)
-  let !n         = LA.rows init0
-      !p         = LA.cols init0
-      gridsV     = V.fromList (map (factorGrid budget) factors)
-      nWP        = if VS.null wpId then 0 else 1 + VS.maximum wpId
-      nStrp      = case mStripId of
-        Just s | not (VS.null s) -> 1 + VS.maximum s
-        _ -> 0
-      isWPidx j  = j `elem` wpIxs
-      isStripIdx j = j `elem` stripIxs
-  let loopOuter !it
-        | it > dbMaxIter budget = pure ()
-        | otherwise = do
-            beforeC <- readMutVar critRef
-            -- SP 因子: 通常の per-row × per-column 走査
-            forM_ [0 .. n - 1] $ \i ->
-              forM_ [0 .. p - 1] $ \j ->
-                when (not (isWPidx j) && not (isStripIdx j)) $ do
-                  curMat <- readMutVar matRef
-                  curC   <- readMutVar critRef
-                  let g  = gridsV V.! j
-                      gl = VU.length g
-                  bestRef <- newMutVar (curMat `LA.atIndex` (i, j), curC)
-                  forM_ [0 .. gl - 1] $ \k -> do
-                    let !v = g VU.! k
-                        !cand = setEntry curMat i j v
-                        !c = evalCritSP factors model crit eta wpId mStripId cand
-                    modifyMutVar' bestRef $ \cur@(_, bc) ->
-                      if c < bc then (v, c) else cur
-                  (bv, bc) <- readMutVar bestRef
-                  when (bc < curC) $ do
-                    writeMutVar matRef  (setEntry curMat i j bv)
-                    writeMutVar critRef bc
-            -- WP 因子: 各 WP × 各 WP-column 走査、 WP 内全 row に同値書き込み
-            forM_ [0 .. nWP - 1] $ \w ->
-              forM_ wpIxs $ \j -> do
-                curMat <- readMutVar matRef
-                curC   <- readMutVar critRef
-                let g  = gridsV V.! j
-                    gl = VU.length g
-                    runsInWP = [ i | i <- [0 .. n - 1], wpId VS.! i == w ]
-                    oldV = if null runsInWP then 0
-                             else curMat `LA.atIndex` (head runsInWP, j)
-                bestRef <- newMutVar (oldV, curC)
-                forM_ [0 .. gl - 1] $ \k -> do
-                  let !v = g VU.! k
-                      !cand = setColumnInRows curMat runsInWP j v
-                      !c = evalCritSP factors model crit eta wpId mStripId cand
-                  modifyMutVar' bestRef $ \cur@(_, bc) ->
-                    if c < bc then (v, c) else cur
-                (bv, bc) <- readMutVar bestRef
-                when (bc < curC) $ do
-                  writeMutVar matRef  (setColumnInRows curMat runsInWP j bv)
-                  writeMutVar critRef bc
-            -- Phase 28-2: strip 因子: 各 strip × 各 strip-column 走査、
-            -- strip 内全 row に同値書き込み
-            case mStripId of
-              Just stripId -> forM_ [0 .. nStrp - 1] $ \s ->
-                forM_ stripIxs $ \j -> do
-                  curMat <- readMutVar matRef
-                  curC   <- readMutVar critRef
-                  let g  = gridsV V.! j
-                      gl = VU.length g
-                      runsInStrip = [ i | i <- [0 .. n - 1], stripId VS.! i == s ]
-                      oldV = if null runsInStrip then 0
-                               else curMat `LA.atIndex` (head runsInStrip, j)
-                  bestRef <- newMutVar (oldV, curC)
-                  forM_ [0 .. gl - 1] $ \k -> do
-                    let !v = g VU.! k
-                        !cand = setColumnInRows curMat runsInStrip j v
-                        !c = evalCritSP factors model crit eta wpId mStripId cand
-                    modifyMutVar' bestRef $ \cur@(_, bc) ->
-                      if c < bc then (v, c) else cur
-                  (bv, bc) <- readMutVar bestRef
-                  when (bc < curC) $ do
-                    writeMutVar matRef  (setColumnInRows curMat runsInStrip j bv)
-                    writeMutVar critRef bc
-              Nothing -> pure ()
-            afterC <- readMutVar critRef
-            let rel = if abs beforeC < 1e-12
-                        then beforeC - afterC
-                        else (beforeC - afterC) / abs beforeC
-            when (rel > dbTol budget) (loopOuter (it + 1))
-  loopOuter 1
-  finalM <- readMutVar matRef
-  finalC <- readMutVar critRef
-  pure (finalM, finalC)
-
--- | REML criterion: critValueM を X' M⁻¹ X 経由で評価。
--- DOpt の場合 det(X' M⁻¹ X) を最大化 (= criterion 最小化)。
--- M = I + η · Z Zᵀ。 nWP=n (= completely randomized) なら M=(1+η)I、
--- η=0 なら M=I (= 通常 D-opt)。
-evalCritSP
-  :: [Factor]
-  -> Model
-  -> OptCriterion
-  -> Double
-  -> VS.Vector Int               -- ^ wpId
-  -> Maybe (VS.Vector Int)       -- ^ stripId (Phase 28-2)
-  -> LA.Matrix Double
-  -> Double
-evalCritSP factors model crit eta wpId mStripId raw =
-  case expandDesignMatrix factors model raw of
-    Left _  -> 1 / 0
-    Right x ->
-      let !n  = LA.rows x
-          mInv = case mStripId of
-            Nothing ->
-              -- 通常 split-plot: M = I + η · Z_WP Z_WPᵀ、 block-diagonal
-              let nWP = if VS.null wpId then 0 else 1 + VS.maximum wpId
-                  wpSizes = [ length [ i | i <- [0 .. n - 1], wpId VS.! i == w ] | w <- [0 .. nWP - 1] ]
-              in buildMInv n eta wpSizes wpId nWP
-            Just stripId ->
-              -- Phase 28-2 strip-plot: M = I + η · (Z_WP Z_WPᵀ + Z_Strip Z_Stripᵀ)
-              -- block-diagonal にならないので数値 inv で対応
-              buildMInvStrip n eta wpId stripId
-          xtmx = LA.tr x LA.<> (mInv LA.<> x)
-      in critValueM crit (chol xtmx)
-      -- 注: critValueM は X (の expand 後) を受け取る前提。 ここで X' M⁻¹ X を
-      -- そのまま渡したいので、 X' M⁻¹ X = (M^{-1/2} X)' (M^{-1/2} X) となる行列
-      -- X̃ = chol((M⁻¹)) X を作って渡す方が自然。 chol が無いので簡略化:
-      -- critValueM の DOpt は det(X'X) = det((M⁻¹) X) ... hm complicated。
-      -- ここでは「critValueM をそのまま使うため X̃ = M⁻¹ X として渡し、
-      -- DOpt の det(X̃'X̃) = det(X' M⁻¹ M⁻¹ X)」 になり厳密に Goos-Vandebroek の
-      -- I_β = X' M⁻¹ X と一致しない。 Phase 25 簡易版として許容、 docs で明記。
-
--- | Phase 28-2 strip-plot 用 M⁻¹。 M = I + η · (Z_WP Z_WPᵀ + Z_Strip Z_Stripᵀ)
--- を直接構築し numerical inverse。 strip-plot の covariance は block-diagonal
--- にならない (WP と strip の交差で indicator が重なる) ため、 split-plot 用の
--- 解析的 block inverse は使えない。 n は通常 ≤ 100 で inv は十分高速。
-buildMInvStrip :: Int -> Double -> VS.Vector Int -> VS.Vector Int -> LA.Matrix Double
-buildMInvStrip n eta wpId stripId =
-  let mEntry i j =
-        let wpEq    = if wpId VS.! i == wpId VS.! j then eta else 0
-            stripEq = if stripId VS.! i == stripId VS.! j then eta else 0
-            diag    = if i == j then 1 else 0
-        in diag + wpEq + stripEq
-      mMat = (n LA.>< n) [ mEntry i j | i <- [0 .. n - 1], j <- [0 .. n - 1] ]
-      mD   = LA.det mMat
-  in if abs mD < 1e-12
-       then LA.ident n   -- safety fallback
-       else LA.inv mMat
-
--- | M⁻¹ を構築 (block-diagonal、 各 WP block で計算)。
-buildMInv :: Int -> Double -> [Int] -> VS.Vector Int -> Int -> LA.Matrix Double
-buildMInv n eta wpSizes wpId _nWP =
-  let buildEntry i j
-        | wpId VS.! i /= wpId VS.! j = 0
-        | otherwise =
-            let w   = wpId VS.! i
-                nw  = wpSizes !! w
-                nwD = fromIntegral nw :: Double
-                d   = 1.0
-                offDiag = - eta / (1 + eta * nwD)
-            in if i == j
-                 then d + offDiag   -- diag of inverse: 1 - η/(1+η nw)
-                 else offDiag
-  in (n LA.>< n) [ buildEntry i j | i <- [0 .. n - 1], j <- [0 .. n - 1] ]
-
--- | 安全な X̃ を返す: X̃ として「(X' M⁻¹ X) の chol 下三角」 を渡せば
--- det(X̃' X̃) = det(X' M⁻¹ X) になる。
---
--- Phase 27-2 fix: 非 PD 時は LA.chol が IO 例外を投げて bench / 検証が
--- 落ちるため、 mbChol で safe 化。 失敗時は zero matrix を返し、
--- critValueM DOpt = -det(0 · 0') = 0 を経由して候補が rejection される。
-chol :: LA.Matrix Double -> LA.Matrix Double
-chol m =
-  let !sym = LA.sym m
-  in case LA.mbChol sym of
-       Just u  -> LA.tr u
-       Nothing -> LA.konst 0 (LA.rows m, LA.cols m)
-
--- ---------------------------------------------------------------------------
--- matrix utility
--- ---------------------------------------------------------------------------
-
-setEntry :: LA.Matrix Double -> Int -> Int -> Double -> LA.Matrix Double
-setEntry m i j v = LA.accum m const [((i, j), v)]
-
-setColumnInRows :: LA.Matrix Double -> [Int] -> Int -> Double -> LA.Matrix Double
-setColumnInRows m rows j v = LA.accum m const [((i, j), v) | i <- rows]
diff --git a/src/Hanalyze/Design/Custom/Structured.hs b/src/Hanalyze/Design/Custom/Structured.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Custom/Structured.hs
+++ /dev/null
@@ -1,303 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns      #-}
--- |
--- Module      : Hanalyze.Design.Custom.Structured
--- Description : 役割 (fRole) 非依存の構造駆動座標交換エンジン (cells + M⁻¹ による GLS 最適化)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- 構造駆動の座標交換エンジン (Phase 79.2)。
---
---   Phase 78.M の split-plot 専用エンジン ('Custom.SplitPlot') を **役割 (fRole) 非依存**に
---   一般化したもの。 実験のランダム化/階層構造を、
---
---     * 各因子列がどの行集合で一定か = **cells** ('gpCells')
---     * 観測の共分散 M の逆行列 = **M⁻¹** ('gpMInv')
---
---   の 2 つに落とした 'GroupingPlan' で受け取り、
---
---     * 群単位ムーブ: 因子 j の cell (= 同値であるべき行集合) 内の全行を一度に書き換える
---     * GLS 基準: @critValueM crit (chol (Xᵀ M⁻¹ X))@ (検証済・Jones-Goos golden 一致)
---
---   で解く。 CRD (per-row cell・M=I) は 'Custom.Coordinate' の高速路にそのまま委譲するので、
---   本エンジンは SplitPlot / StripPlot / Blocked (= 非自明な群/共分散) 専用。
---
---   ★基準式の根拠: @xtmx = Xᵀ M⁻¹ X@、 @L = chol xtmx@ (L Lᵀ = xtmx) を 'critValueM' に渡すと
---   DOpt で @det(Lᵀ L) = det(xtmx) = det(Xᵀ M⁻¹ X)@ = 厳密な REML 情報量 (Goos-Vandebroek 2003)。
---   M⁻¹ の白色化 X̃ = L⁻¹X でも等価だが、 既存 SplitPlot エンジンで文献値一致を確認済の
---   この式を踏襲する。
-module Hanalyze.Design.Custom.Structured
-  ( -- * 入力
-    GroupingPlan (..)
-    -- * 共分散
-  , buildMInvFromGroups
-    -- * アルゴリズム
-  , structuredExchangePure
-  ) where
-
-import           Control.Monad             (forM, forM_, when)
-import           Control.Monad.Primitive   (PrimMonad, PrimState)
-import           Control.Monad.ST          (runST)
-import           Data.Maybe                (fromMaybe)
-import           Data.Primitive.MutVar
-import           Data.Text                 (Text)
-import qualified Data.Text                 as T
-import qualified Numeric.LinearAlgebra     as LA
-import qualified System.Random.MWC         as MWC
-import qualified Data.Vector               as V
-import qualified Data.Vector.Unboxed       as VU
-import qualified Data.Vector.Storable      as VS
-
-import           Hanalyze.Design.Custom.Factor  (Factor)
-import           Hanalyze.Design.Custom.Model   (Model, expandDesignMatrix)
-import           Hanalyze.Design.Custom.Constraint (Constraint)
-import           Hanalyze.Design.Custom.Coordinate
-                   ( CustomDesignSpec (..), DesignBudget (..)
-                   , factorGrid, critValueM, rowFeasible
-                   , mkGenSeed, defaultPureSeed )
-import           Hanalyze.Design.Optimal        (OptCriterion (..))
-
--- ---------------------------------------------------------------------------
--- 入力型
--- ---------------------------------------------------------------------------
-
--- | 'Structure' をエンジン内部表現にコンパイルしたもの (Workflow が構築)。
-data GroupingPlan = GroupingPlan
-  { gpCells :: ![[[Int]]]
-    -- ^ 列 j → その列の値を共有すべき行集合 (cells) の分割。 全 cell の和集合 = @[0..n-1]@。
-    --   CRD 因子 = @[[0],[1],…,[n-1]]@ (per-row)、 whole-plot 因子 = 各 WP の行集合。
-  , gpMInv  :: !(LA.Matrix Double)
-    -- ^ n×n の GLS 重み M⁻¹ (@M = I + Σ η_g Z_g Z_gᵀ@)。 CRD なら I。
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- 共分散 M⁻¹
--- ---------------------------------------------------------------------------
-
--- | @M = I + Σ_g η_g Z_g Z_gᵀ@ の逆行列を dense に構築 (n ≤ ~100 前提で数値 inv)。
---   各群 @(η_g, ids_g)@ は「分散比 η_g と各行の群 ID」。 SplitPlot = 1 群、
---   StripPlot = 2 群 (WP と strip の交差)、 Blocked = 1 群。 block-diagonal に限らないので
---   一様に dense inv で扱う (解析 block 逆と数値的に一致)。 特異なら安全側で単位行列。
-buildMInvFromGroups :: Int -> [(Double, VS.Vector Int)] -> LA.Matrix Double
-buildMInvFromGroups n groups =
-  let mEntry i j =
-        (if i == j then 1 else 0)
-          + sum [ if ids VS.! i == ids VS.! j then eta else 0 | (eta, ids) <- groups ]
-      mMat = (n LA.>< n) [ mEntry i j | i <- [0 .. n - 1], j <- [0 .. n - 1] ]
-  in if abs (LA.det mMat) < 1e-12 then LA.ident n else LA.inv mMat
-
--- ---------------------------------------------------------------------------
--- アルゴリズム (seed 決定的・pure)
--- ---------------------------------------------------------------------------
-
--- | 構造駆動の座標交換 (pure・seed 決定的)。 'GroupingPlan' の cells で群単位ムーブ、
---   M⁻¹ で GLS 基準を評価する。 戻り値 = (raw 設計行列, 最小化方向の基準値)。
---   'cdsSeed' が 'Nothing' なら 'defaultPureSeed'。 制約は各ムーブ候補で 'rowFeasible'
---   (影響行ごと) を課す。
-structuredExchangePure
-  :: CustomDesignSpec -> GroupingPlan -> Either Text (LA.Matrix Double, Double)
-structuredExchangePure spec gplan
-  | null (cdsFactors spec)         = Left "structuredExchange: empty factor list"
-  | cdsNRuns spec < 1              = Left "structuredExchange: nRuns must be >= 1"
-  | dbRestarts (cdsBudget spec) < 1 = Left "structuredExchange: dbRestarts must be >= 1"
-  | length (gpCells gplan) /= length (cdsFactors spec) =
-      Left "structuredExchange: gpCells length must equal factor count"
-  | LA.rows (gpMInv gplan) /= cdsNRuns spec =
-      Left "structuredExchange: gpMInv dimension must equal nRuns"
-  | otherwise = runST $ do
-      gen <- mkGenSeed (fromMaybe defaultPureSeed (cdsSeed spec))
-      let !factors = cdsFactors spec
-          !model   = cdsModel spec
-          !crit    = cdsCriterion spec
-          !cons    = cdsConstraints spec
-          !budget  = cdsBudget spec
-          !n       = cdsNRuns spec
-          !mInv    = gpMInv gplan
-          !cells   = gpCells gplan
-          !grids   = map (factorGrid budget) factors
-      bestRef <- newMutVar Nothing
-      forM_ [1 .. dbRestarts budget] $ \_ -> do
-        -- 制約なしは高速な単純抽選 (既存挙動)、 制約ありは実行可能な初期解を棄却サンプリング。
-        mInit <- if null cons
-                   then Just <$> randomInitG grids cells n gen
-                   else randomInitGFeasible factors cons grids cells n gen
-        case mInit of
-          Nothing    -> pure ()   -- この restart は実行可能初期解を引けず (次 restart へ)
-          Just init0 -> do
-            (finalM, finalC) <-
-              runExchangeG factors model crit cons budget mInv grids cells init0
-            modifyMutVar' bestRef $ \mb -> case mb of
-              Nothing -> Just (finalM, finalC)
-              Just (_, c0) | finalC < c0 -> Just (finalM, finalC)
-                           | otherwise   -> mb
-      mb <- readMutVar bestRef
-      pure $ case mb of
-        Nothing     -> Left "structuredExchange: 実行可能な初期解が得られませんでした (制約が厳しすぎる可能性)"
-        Just (m, c) -> Right (m, c)
-
--- | 初期 raw matrix。 各列は cell ごとに 1 つの grid 値を抽選し、 cell 内全行へ同値で置く。
-randomInitG
-  :: PrimMonad m
-  => [VU.Vector Double] -> [[[Int]]] -> Int -> MWC.Gen (PrimState m)
-  -> m (LA.Matrix Double)
-randomInitG grids cells n gen = do
-  let gridsV = V.fromList grids
-  cols <- mapM
-    (\(j, cellsOfCol) -> do
-        let g  = gridsV V.! j
-            gl = VU.length g
-        -- cell ごとに 1 値、 cell 内全行に配る
-        vals <- mapM (\rows -> do
-                        k <- MWC.uniformR (0, gl - 1) gen
-                        pure (rows, g VU.! k)) cellsOfCol
-        let assign = [ (i, v) | (rows, v) <- vals, i <- rows ]
-        pure (LA.fromList [ lookupRow i assign | i <- [0 .. n - 1] ]))
-    (zip [0 ..] cells)
-  pure (LA.fromColumns cols)
-  where
-    lookupRow i assign = case lookup i assign of
-      Just v  -> v
-      Nothing -> 0   -- cells が [0..n-1] を被覆する前提 (不達)
-
--- | 制約下で実行可能な初期 raw matrix を棄却サンプリングで構築 (Phase 79.5)。
---   群構造を保つため 2 段階で引く:
---
---     1. **群 (grouped) 列** (cell が n 未満 = whole-plot / strip 因子) を cell ごとに 1 値抽選。
---        群内の行はこの値を共有する (= 階層構造の保持)。
---     2. **各行**について、 per-row 列 (sub-plot 因子) を棄却サンプリングし、 群列の固定値と
---        合わせた行全体が全制約を満たすまで再抽選 (行あたり 200 回上限)。
---
---   ある行が群固定値の下でどうしても実行可能にできなければ、 群値ごと引き直す (外側 50 回上限)。
---   全て失敗すれば 'Nothing' (制約が厳しすぎる)。 群列固定 → per-row 探索の順で、
---   whole-plot 因子が群内一定かつ制約満足を両立させる。
-randomInitGFeasible
-  :: PrimMonad m
-  => [Factor] -> [Constraint] -> [VU.Vector Double] -> [[[Int]]] -> Int
-  -> MWC.Gen (PrimState m) -> m (Maybe (LA.Matrix Double))
-randomInitGFeasible factors cons grids cells n gen = tryOuter maxOuter
-  where
-    maxOuter = 50 :: Int
-    maxRow   = 200 :: Int
-    gridsV   = V.fromList grids
-    p        = length grids
-    isGrouped j = length (cells !! j) < n   -- cell 数 < n → 群 (共有) 列
-
-    tryOuter 0 = pure Nothing
-    tryOuter t = do
-      -- 1. 群列の値を cell ごとに抽選 → 各群列 j の「行 → 値」 (Just)、 per-row 列は Nothing
-      grouped <- forM [0 .. p - 1] $ \j ->
-        if isGrouped j
-          then do
-            let g  = gridsV V.! j
-                gl = VU.length g
-            cellVals <- forM (cells !! j) $ \rows -> do
-              k <- MWC.uniformR (0, gl - 1) gen
-              pure (rows, g VU.! k)
-            let rowVal = VU.generate n
-                  (\i -> head [ v | (rs, v) <- cellVals, i `elem` rs ])
-            pure (Just rowVal)
-          else pure Nothing
-      -- 2. 各行を棄却サンプリング (群列は固定・per-row 列を引く)
-      mRows <- forM [0 .. n - 1] $ \i -> drawRow grouped i maxRow
-      case sequence mRows of
-        Just rs -> pure (Just (LA.fromRows rs))
-        Nothing -> tryOuter (t - 1)   -- どこかの行が詰んだ → 群値ごと引き直し
-
-    drawRow _       _ 0  = pure Nothing
-    drawRow grouped i tr = do
-      vs <- forM [0 .. p - 1] $ \j ->
-        case grouped !! j of
-          Just rowVal -> pure (rowVal VU.! i)     -- 群列: 固定値
-          Nothing     -> do                        -- per-row 列: 抽選
-            let g  = gridsV V.! j
-                gl = VU.length g
-            k <- MWC.uniformR (0, gl - 1) gen
-            pure (g VU.! k)
-      let row = LA.fromList vs
-      if rowFeasible factors cons row
-        then pure (Just row)
-        else drawRow grouped i (tr - 1)
-
--- | 1 restart 分の群単位 coordinate exchange。 列ごと・cell ごとに grid を走査し、
---   制約 (影響行) を満たす範囲で基準最小の値を cell 内全行へ書き込む。
-runExchangeG
-  :: PrimMonad m
-  => [Factor] -> Model -> OptCriterion -> [Constraint] -> DesignBudget
-  -> LA.Matrix Double            -- ^ M⁻¹
-  -> [VU.Vector Double]          -- ^ 因子ごとの grid
-  -> [[[Int]]]                   -- ^ 列ごとの cells
-  -> LA.Matrix Double            -- ^ 初期 raw
-  -> m (LA.Matrix Double, Double)
-runExchangeG factors model crit cons budget mInv grids cells init0 = do
-  matRef  <- newMutVar init0
-  critRef <- newMutVar (evalCritG factors model crit mInv init0)
-  let gridsV = V.fromList grids
-      cellsV = V.fromList cells
-      !p     = length grids
-  let loopOuter !it
-        | it > dbMaxIter budget = pure ()
-        | otherwise = do
-            beforeC <- readMutVar critRef
-            forM_ [0 .. p - 1] $ \j ->
-              forM_ (cellsV V.! j) $ \rows -> do
-                curMat <- readMutVar matRef
-                curC   <- readMutVar critRef
-                let g    = gridsV V.! j
-                    gl   = VU.length g
-                    oldV = if null rows then 0
-                             else curMat `LA.atIndex` (head rows, j)
-                bestRef <- newMutVar (oldV, curC)
-                forM_ [0 .. gl - 1] $ \k -> do
-                  let !v = g VU.! k
-                  when (cellFeasible factors cons curMat rows j v) $ do
-                    let !cand = setColumnInRows curMat rows j v
-                        !c    = evalCritG factors model crit mInv cand
-                    modifyMutVar' bestRef $ \cur@(_, bc) ->
-                      if c < bc then (v, c) else cur
-                (bv, bc) <- readMutVar bestRef
-                when (bc < curC) $ do
-                  writeMutVar matRef  (setColumnInRows curMat rows j bv)
-                  writeMutVar critRef bc
-            afterC <- readMutVar critRef
-            let rel = if abs beforeC < 1e-12
-                        then beforeC - afterC
-                        else (beforeC - afterC) / abs beforeC
-            when (rel > dbTol budget) (loopOuter (it + 1))
-  loopOuter 1
-  finalM <- readMutVar matRef
-  finalC <- readMutVar critRef
-  pure (finalM, finalC)
-
--- | cell 内全行を列 j = v にしたとき、 影響する全行が制約を満たすか。
---   cell 内の各行は他列の値が異なり得るので行ごとに判定する。
-cellFeasible :: [Factor] -> [Constraint] -> LA.Matrix Double -> [Int] -> Int -> Double -> Bool
-cellFeasible _ [] _ _ _ _ = True
-cellFeasible factors cons mat rows j v =
-  all (\i -> rowFeasible factors cons (replaceVecAt (rowVec i) j v)) rows
-  where rowVec i = LA.flatten (LA.subMatrix (i, 0) (1, LA.cols mat) mat)
-
--- | raw matrix → design matrix → GLS 基準値 (最小化方向)。 expand 失敗は +∞。
-evalCritG :: [Factor] -> Model -> OptCriterion -> LA.Matrix Double -> LA.Matrix Double -> Double
-evalCritG factors model crit mInv raw =
-  case expandDesignMatrix factors model raw of
-    Left _  -> 1 / 0
-    Right x ->
-      let !xtmx = LA.tr x LA.<> (mInv LA.<> x)   -- Xᵀ M⁻¹ X
-      in critValueM crit (chol xtmx)
-
--- | @chol m@ = L (下三角、 L Lᵀ = sym m)。 非 PD 時は 0 行列 (基準が候補を棄却)。
-chol :: LA.Matrix Double -> LA.Matrix Double
-chol m = case LA.mbChol (LA.sym m) of
-  Just u  -> LA.tr u
-  Nothing -> LA.konst 0 (LA.rows m, LA.cols m)
-
--- ---------------------------------------------------------------------------
--- matrix / vector utility
--- ---------------------------------------------------------------------------
-
-setColumnInRows :: LA.Matrix Double -> [Int] -> Int -> Double -> LA.Matrix Double
-setColumnInRows m rows j v = LA.accum m const [ ((i, j), v) | i <- rows ]
-
-replaceVecAt :: LA.Vector Double -> Int -> Double -> LA.Vector Double
-replaceVecAt v j x =
-  LA.fromList [ if k == j then x else v `LA.atIndex` k | k <- [0 .. LA.size v - 1] ]
diff --git a/src/Hanalyze/Design/DSD.hs b/src/Hanalyze/Design/DSD.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/DSD.hs
+++ /dev/null
@@ -1,124 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Design.DSD
--- Description : Definitive Screening Design (Jones-Nachtsheim 2011) の 2k+1 run 生成
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Definitive Screening Design (Jones-Nachtsheim 2011)。
---
--- k 連続因子について **2k + 1 runs** で主効果 + 二次効果 + 一部の 2 因子
--- 交互作用を識別できる効率的スクリーニング計画。
---
--- 構成:
---
---   * 1 行目: 中心点 @[0, 0, ..., 0]@
---   * 2..k+1 行目: 各 row i は position i に 0 を持ち、 他は ±1
---   * k+2..2k+1 行目: 上記の foldover (= 各行の符号反転)
---
--- 本初版は k = 4 を **Jones-Nachtsheim Table 1 の conference matrix** で
--- 構築 (verified DSD)。 他の k は Hadamard-like 構造で近似 (= 構造的 DSD)。
--- 厳密な conference-matrix DSD の追加は将来 Phase。
-module Hanalyze.Design.DSD
-  ( DSDResult (..)
-  , dsdDesign
-  ) where
-
-import qualified Data.Bits             as B
-import qualified Numeric.LinearAlgebra as LA
-import           Data.Text             (Text)
-import qualified Data.Text             as T
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
--- | DSD の結果。
-data DSDResult = DSDResult
-  { dsdMatrix     :: !(LA.Matrix Double)
-    -- ^ @(2k + 1) × k@ 行列。 各要素は @{-1, 0, +1}@。
-  , dsdNFactors   :: !Int       -- ^ 因子数 k
-  , dsdNRuns      :: !Int       -- ^ 実験数 2k + 1
-  , dsdHasOptimal :: !Bool
-    -- ^ @True@ = Jones-Nachtsheim Table の conference matrix 由来 (verified DSD)、
-    --   @False@ = Hadamard-like 構造で近似 (structural DSD)
-  } deriving (Show)
-
--- ===========================================================================
--- 公開関数
--- ===========================================================================
-
--- | DSD を生成。
---
--- k = 4 のみ verified (Jones-Nachtsheim 2011 Table 1)。
--- k ≥ 2 の他値は Hadamard-like 構造の structural DSD (`dsdHasOptimal = False`)。
--- k < 2 は @Left@。
-dsdDesign :: Int -> Either Text DSDResult
-dsdDesign k
-  | k < 2 = Left (T.pack ("dsdDesign: need k >= 2, got k=" <> show k))
-  | k == 4 = Right (verifiedDSD k confC4)
-  | otherwise = Right (structuralDSD k)
-
--- ===========================================================================
--- 内部: verified DSD (conference matrix 由来)
--- ===========================================================================
-
--- | C_4: 4 次の conference matrix。 Jones-Nachtsheim 2011 Table 1 第 1 行。
---   不変条件: 対角 0、 非対角 ±1、 @C · Cᵀ = (n-1) I@。
-confC4 :: [[Double]]
-confC4 =
-  [ [ 0,  1,  1,  1]
-  , [ 1,  0,  1, -1]
-  , [ 1, -1,  0,  1]
-  , [ 1,  1, -1,  0]
-  ]
-
--- | 与えた conference matrix から DSD を構築:
---   row 0 = center、 rows 1..k = C 各行、 rows k+1..2k = -C 各行。
-verifiedDSD :: Int -> [[Double]] -> DSDResult
-verifiedDSD k cMat =
-  let center  = replicate k 0
-      posRows = cMat
-      negRows = map (map negate) cMat
-      allRows = center : posRows ++ negRows
-      mat     = LA.fromLists allRows
-  in DSDResult
-       { dsdMatrix     = mat
-       , dsdNFactors   = k
-       , dsdNRuns      = 2 * k + 1
-       , dsdHasOptimal = True
-       }
-
--- ===========================================================================
--- 内部: structural DSD (Hadamard-like、 conference matrix 無しの近似)
--- ===========================================================================
-
--- | k != 4 の場合の近似 DSD。 構造 (2k+1 runs、 各 row に 1 個の 0) は
--- 満たすが、 conference matrix 性質 (`C · Cᵀ = (n-1) I`) は保証しない。
---
--- ±1 パターンは Sylvester-Hadamard 風: row i の position j (j != i) について
--- @sign = (-1)^popCount(i .&. j)@。
-structuralDSD :: Int -> DSDResult
-structuralDSD k =
-  let posRows = [ [ if j + 1 == i then 0  -- position i (1-origin in row) gets 0
-                    else hadamardSign i (j + 1)
-                  | j <- [0 .. k - 1]
-                  ]
-                | i <- [1 .. k]
-                ]
-      negRows = map (map negate) posRows
-      center  = replicate k 0
-      mat     = LA.fromLists (center : posRows ++ negRows)
-  in DSDResult
-       { dsdMatrix     = mat
-       , dsdNFactors   = k
-       , dsdNRuns      = 2 * k + 1
-       , dsdHasOptimal = False
-       }
-
--- | Sylvester-Hadamard 符号: @(-1)^popCount(i AND j)@。
-hadamardSign :: Int -> Int -> Double
-hadamardSign i j
-  | even (B.popCount (i B..&. j)) =  1
-  | otherwise                     = -1
diff --git a/src/Hanalyze/Design/Diagnostics.hs b/src/Hanalyze/Design/Diagnostics.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Diagnostics.hs
+++ /dev/null
@@ -1,155 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Design.Diagnostics
--- Description : DoE 設計診断 (Alias Matrix / VIF / D-A-G-I efficiency の一括算出)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- DoE 設計診断: Alias Matrix / VIF / D-A-G-I efficiency。
---
--- 設計行列 @X@ (n × p) に対して、 multicollinearity と最適性指標を一括算出。
---
--- ===  efficiency 指標
---
---   * D-efficiency = (|XᵀX| / n^p)^{1/p}
---   * A-efficiency = p / trace((XᵀX/n)⁻¹)
---   * G-efficiency = p / max_i (n · x_iᵀ (XᵀX)⁻¹ x_i)
---   * I-efficiency = 1 / (n · trace((XᵀX)⁻¹ · M))、
---     M = 1/n · XᵀX (= self-moment 近似版)
---
--- ===  VIF
---
--- 各列 j について、 「j 以外の列で j を回帰」 した R² を用いて
--- @VIF_j = 1 / (1 − R²_j)@。 切片を含む X を想定し、 切片列 (= 全 1) は
--- スキップ。
---
--- ===  Alias Matrix
---
--- @A = (XᵀX)⁻¹ Xᵀ Z@、 ここで Z は 「設計に入れていない交絡項」 のモデル
--- 行列。 ここでは Z を Optional 引数として取り、 未指定の場合は
--- アライアス対象が無いとして空行列を返す。
-module Hanalyze.Design.Diagnostics
-  ( DesignDiagnostics (..)
-  , diagnostics
-  , diagnosticsWithAlias
-  , vifVector
-  , aliasMatrix
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
-data DesignDiagnostics = DesignDiagnostics
-  { ddVIF         :: !(LA.Vector Double)
-  , ddDEff        :: !Double
-  , ddAEff        :: !Double
-  , ddGEff        :: !Double
-  , ddIEff        :: !Double
-  , ddAliasMatrix :: !(LA.Matrix Double)
-  } deriving (Show)
-
--- ===========================================================================
--- 公開 API
--- ===========================================================================
-
--- | Alias を含めない簡易版 (Z = 空)。
-diagnostics :: LA.Matrix Double -> DesignDiagnostics
-diagnostics x =
-  let dd = computeDiagnostics x
-  in dd { ddAliasMatrix = LA.fromLists [[]] }
-
--- | Z (交絡対象モデル行列) 込みの完全版。 Z の行数は X と一致する必要がある。
-diagnosticsWithAlias :: LA.Matrix Double -> LA.Matrix Double -> DesignDiagnostics
-diagnosticsWithAlias x z =
-  let dd = computeDiagnostics x
-      a  = aliasMatrix x z
-  in dd { ddAliasMatrix = a }
-
--- | VIF を各列について返す (全 1 列は VIF = 1)。
-vifVector :: LA.Matrix Double -> LA.Vector Double
-vifVector x =
-  let p = LA.cols x
-  in LA.fromList [ vifForCol x j | j <- [0 .. p - 1] ]
-
--- | Alias matrix A = (XᵀX)⁻¹ Xᵀ Z。
-aliasMatrix :: LA.Matrix Double -> LA.Matrix Double -> LA.Matrix Double
-aliasMatrix x z =
-  let xtx = LA.tr x LA.<> x
-      d   = LA.det xtx
-  in if abs d < 1e-12
-       then LA.fromLists [[]]
-       else LA.inv xtx LA.<> LA.tr x LA.<> z
-
--- ===========================================================================
--- 内部
--- ===========================================================================
-
-computeDiagnostics :: LA.Matrix Double -> DesignDiagnostics
-computeDiagnostics x =
-  let n   = LA.rows x
-      p   = LA.cols x
-      nD  = fromIntegral n :: Double
-      pD  = fromIntegral p :: Double
-      xtx = LA.tr x LA.<> x
-      d   = LA.det xtx
-      singular = abs d < 1e-12
-      inv = if singular then LA.ident p else LA.inv xtx
-      -- D-efficiency: (|XᵀX| / n^p)^{1/p}  (clamped to ≥ 0)
-      dEff = if singular || d <= 0 then 0
-                else (d / (nD ** pD)) ** (1 / pD)
-      -- A-efficiency: p / trace((XᵀX/n)⁻¹) = p · n / trace((XᵀX)⁻¹)... wait
-      -- (XᵀX / n)⁻¹ = n · (XᵀX)⁻¹  なので trace((XᵀX/n)⁻¹) = n · trace((XᵀX)⁻¹)
-      -- → A-eff = p / (n · trace((XᵀX)⁻¹))
-      trInv = sum [ inv `LA.atIndex` (i, i) | i <- [0 .. p - 1] ]
-      aEff  = if singular || trInv == 0 then 0
-                else pD / (nD * trInv)
-      -- G-efficiency: p / max_i (n · h_ii)、 h_ii = x_iᵀ (XᵀX)⁻¹ x_i
-      hMax = if singular then 1
-               else maximum
-                      [ let xi = LA.flatten (x LA.? [i])
-                            v  = inv LA.#> xi
-                        in xi `LA.dot` v
-                      | i <- [0 .. n - 1] ]
-      gEff = if hMax == 0 then 0 else pD / (nD * hMax)
-      -- I-efficiency 近似 (self-moment)
-      iEff = if singular then 0
-               else
-                 let m = LA.scale (1 / nD) xtx
-                     t = LA.sumElements (LA.takeDiag (inv LA.<> m))
-                 in if t == 0 then 0 else 1 / (nD * t)
-  in DesignDiagnostics
-       { ddVIF         = vifVector x
-       , ddDEff        = dEff
-       , ddAEff        = aEff
-       , ddGEff        = gEff
-       , ddIEff        = iEff
-       , ddAliasMatrix = LA.fromLists [[]]
-       }
-
--- | 列 j の VIF。 全 1 列 (切片) は 1 を返す。
-vifForCol :: LA.Matrix Double -> Int -> Double
-vifForCol x j =
-  let col = LA.flatten (x LA.¿ [j])
-      isConst = let c0 = LA.atIndex col 0
-                in LA.maxElement (LA.cmap (\v -> abs (v - c0)) col) < 1e-12
-  in if isConst then 1
-       else
-         let p       = LA.cols x
-             others  = [ k | k <- [0 .. p - 1], k /= j ]
-             xOthers = x LA.¿ others
-             yj      = col
-             xtx     = LA.tr xOthers LA.<> xOthers
-             d       = LA.det xtx
-         in if abs d < 1e-12 then 1 / 0
-              else
-                let beta = LA.inv xtx LA.#> (LA.tr xOthers LA.#> yj)
-                    yhat = xOthers LA.#> beta
-                    yBar = LA.sumElements yj / fromIntegral (LA.size yj)
-                    ssR  = LA.sumElements ((yj - yhat) ^ (2 :: Int))
-                    ssT  = LA.sumElements ((yj - LA.scalar yBar) ^ (2 :: Int))
-                    r2   = if ssT == 0 then 0 else 1 - ssR / ssT
-                in if r2 >= 1 then 1 / 0 else 1 / (1 - r2)
diff --git a/src/Hanalyze/Design/Factorial.hs b/src/Hanalyze/Design/Factorial.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Factorial.hs
+++ /dev/null
@@ -1,112 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.Factorial
--- Description : 要因計画 (完全/2 水準/3 水準/一部実施/混合水準) の生成
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Factorial designs.
---
---   * 'fullFactorial'        — full factorial with @k@ factors each at
---     @levels[i]@ levels.
---   * 'twoLevelFactorial'    — @2^k@ design (each factor at @±1@).
---   * 'threeLevelFactorial'  — @3^k@ design (each factor at @-1, 0, +1@).
---   * 'fractionalFactorial'  — @2^(k-p)@ fractional design (specified
---     defining relation).
---   * 'mixedFactorial'       — mixed-level design (e.g. @2² × 3¹@).
---
--- All designs are returned as @[[Double]]@. Use 'Hanalyze.Design.Quality' to
--- evaluate orthogonality and other criteria.
-module Hanalyze.Design.Factorial
-  ( fullFactorial
-  , twoLevelFactorial
-  , threeLevelFactorial
-  , fractionalFactorial
-  , mixedFactorial
-  , factorialColumnNames
-  ) where
-
-import Data.List (foldl')
-import Data.Text (Text)
-import qualified Data.Text as T
-
--- ---------------------------------------------------------------------------
--- 完全要因計画
--- ---------------------------------------------------------------------------
-
--- | Full factorial design: take a list of per-factor level vectors
--- @[lvl_1, lvl_2, …]@ and emit every combination (Cartesian product).
---
--- Example: @fullFactorial [[1,2,3], [10,20]]@ →
--- @[[1,10],[1,20],[2,10],[2,20],[3,10],[3,20]]@.
-fullFactorial :: [[Double]] -> [[Double]]
-fullFactorial = foldl' addCol [[]]
-  where
-    addCol acc levels =
-      [ row ++ [v] | row <- acc, v <- levels ]
-
--- | @2^k@ design — each factor takes the levels @-1, +1@.
--- @twoLevelFactorial 3@ has 8 rows × 3 columns.
-twoLevelFactorial :: Int -> [[Double]]
-twoLevelFactorial k = fullFactorial (replicate k [-1, 1])
-
--- | @3^k@ design — each factor takes the levels @-1, 0, +1@.
-threeLevelFactorial :: Int -> [[Double]]
-threeLevelFactorial k = fullFactorial (replicate k [-1, 0, 1])
-
--- ---------------------------------------------------------------------------
--- 部分要因計画 (Fractional factorial)
--- ---------------------------------------------------------------------------
-
--- | @2^(k-p)@ fractional factorial design.
---
--- @fractionalFactorial k generators@:
---
---   * @k@         — total number of factors.
---   * @generators@ — defining relations for the added factors
---     @k-p+1, …, k@. Each generator is a set of base-factor indices
---     (1-based, in @1..k-p@); the corresponding column is their product.
---
--- Example: @2^(4-1)@ design (4 factors, one generator) with
--- @D = ABC@: @fractionalFactorial 4 [[1,2,3]]@ → @2^3 = 8@ rows × 4
--- columns (the @D@ column is @A·B·C@). The number of generators equals
--- the number of added factors @p@.
-fractionalFactorial :: Int -> [[Int]] -> [[Double]]
-fractionalFactorial k generators =
-  let p     = length generators
-      kBase = k - p
-      base  = twoLevelFactorial kBase
-      -- 各 generator から追加列を計算
-      extraCol gen row = foldl' (*) 1.0 [row !! (i - 1) | i <- gen]
-      addExtras row = row ++ [extraCol gen row | gen <- generators]
-  in map addExtras base
-
--- ---------------------------------------------------------------------------
--- 混合水準計画
--- ---------------------------------------------------------------------------
-
--- | Mixed-level design (factors with different numbers of levels).
---
--- Example: @2² × 3¹@ → @mixedFactorial [2, 2, 3]@. Each factor uses
--- evenly-spaced levels (@-1, +1@ or @-1, 0, +1@).
-mixedFactorial :: [Int] -> [[Double]]
-mixedFactorial levelCounts =
-  fullFactorial (map standardLevels levelCounts)
-  where
-    standardLevels n
-      | n <= 1    = [0]
-      | n == 2    = [-1, 1]
-      | otherwise =
-          let step = 2 / fromIntegral (n - 1)
-          in [-1 + fromIntegral i * step | i <- [0 .. n - 1] :: [Int]]
-
--- ---------------------------------------------------------------------------
--- 列名生成
--- ---------------------------------------------------------------------------
-
--- | Generate factor labels @[\"A\", \"B\", \"C\", …]@.
-factorialColumnNames :: Int -> [Text]
-factorialColumnNames k
-  | k <= 26   = [T.singleton c | c <- take k ['A' ..]]
-  | otherwise =
-      [T.pack ("X" ++ show i) | i <- [1 .. k] :: [Int]]
diff --git a/src/Hanalyze/Design/GaugeRR.hs b/src/Hanalyze/Design/GaugeRR.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/GaugeRR.hs
+++ /dev/null
@@ -1,209 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Design.GaugeRR
--- Description : Gauge R&R — 測定システム分析 (MSA) の分散分解 (crossed / nested ANOVA 法)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Gauge R&R — Measurement System Analysis の分散分解。
---
--- 製造工程で「測定値のばらつき」 を
---
---   * 部品本来のばらつき (σ²_part)
---   * 操作者 / 装置間のばらつき (σ²_reproducibility)
---   * 測定の再現性 (σ²_repeatability)
---
--- に分解する。 AIAG MSA Manual 4th ed. に準拠した ANOVA 法。
---
--- Crossed (全操作者 が 全部品 を測定) と Nested (操作者が部品ごとに異なる) を
--- 別 API で提供。 hmatrix Vector 演算で完結。
-module Hanalyze.Design.GaugeRR
-  ( GaugeRRResult (..)
-  , gaugeRRCrossed
-  , gaugeRRNested
-  ) where
-
-import qualified Data.Vector           as V
-import qualified Data.Map.Strict       as Map
-import           Data.List             (nub, sort)
-import           Data.Text             (Text)
-import qualified Data.Text             as T
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
--- | Gauge R&R の分散分解結果。
-data GaugeRRResult = GaugeRRResult
-  { grrPartVar       :: !Double  -- ^ σ²_part (部品間)
-  , grrReproducVar   :: !Double  -- ^ σ²_reproducibility (操作者間)
-  , grrRepeatVar     :: !Double  -- ^ σ²_repeatability (繰り返し)
-  , grrTotalVar      :: !Double  -- ^ σ²_total = part + reproducibility + repeatability
-  , grrPctRepeat     :: !Double  -- ^ % of total = repeat / total × 100
-  , grrPctReproduc   :: !Double
-  , grrPctGRR        :: !Double  -- ^ % (repeat + reproducibility) / total × 100
-  , grrPctPart       :: !Double
-  , grrNumDistinct   :: !Double
-    -- ^ ndc = 1.41 · (σ_part / σ_GRR)。 ≥ 5 が望ましい (AIAG)
-  } deriving (Show)
-
--- ===========================================================================
--- 公開関数
--- ===========================================================================
-
--- | Crossed Gauge R&R: 全操作者が全部品を測定 (operator × part 直交)。
---
--- ANOVA 分散分解:
---
--- > SS_part         = n_op · n_rep · Σ (μ̂_part_i − μ̂_grand)²
--- > SS_operator     = n_part · n_rep · Σ (μ̂_op_j − μ̂_grand)²
--- > SS_interaction  = n_rep · Σ Σ (μ̂_ij − μ̂_part_i − μ̂_op_j + μ̂_grand)²
--- > SS_error        = Σ (y_ijk − μ̂_ij)²
---
--- σ² 推定 (期待値式から):
---
--- > σ²_repeatability   = MS_error
--- > σ²_interaction     = max(0, (MS_int - MS_error) / n_rep)
--- > σ²_reproducibility = max(0, (MS_op - MS_int) / (n_part · n_rep)) + σ²_interaction
--- > σ²_part            = max(0, (MS_part - MS_int) / (n_op · n_rep))
-gaugeRRCrossed
-  :: V.Vector Int       -- ^ 操作者 ID (length n)
-  -> V.Vector Int       -- ^ 部品 ID (length n)
-  -> V.Vector Double    -- ^ 測定値 (length n)
-  -> Either Text GaugeRRResult
-gaugeRRCrossed ops parts ys
-  | V.length ops /= V.length parts || V.length ops /= V.length ys =
-      Left "gaugeRRCrossed: input vectors differ in length"
-  | V.null ys =
-      Left "gaugeRRCrossed: empty input"
-  | V.length opIds < 2 || V.length partIds < 2 =
-      Left "gaugeRRCrossed: need ≥ 2 operators and ≥ 2 parts"
-  | otherwise =
-      let !n     = V.length ys
-          !nOp   = V.length opIds
-          !nPart = V.length partIds
-          -- replicate per cell (assume balanced design)
-          !nRep  = n `div` (nOp * nPart)
-      in if nRep < 2
-           then Left (T.pack ("gaugeRRCrossed: need ≥ 2 replicates per cell (got "
-                              <> show nRep <> ")"))
-           else Right (decomposeCrossed nOp nPart nRep ys ops parts opIds partIds)
-  where
-    opIds   = V.fromList (sort (nub (V.toList ops)))
-    partIds = V.fromList (sort (nub (V.toList parts)))
-
--- | Nested Gauge R&R: 操作者が部品ごとに異なる (operator within part)。
---
--- 簡略版: operator effect を completely random として扱い、
--- repeatability + (operator/part)-nested の 2 段分解。
-gaugeRRNested
-  :: V.Vector Int
-  -> V.Vector Int
-  -> V.Vector Double
-  -> Either Text GaugeRRResult
-gaugeRRNested ops parts ys
-  | V.length ops /= V.length parts || V.length ops /= V.length ys =
-      Left "gaugeRRNested: input vectors differ in length"
-  | V.null ys = Left "gaugeRRNested: empty input"
-  | otherwise =
-      -- nested: 部品ごとに operator が異なるので、 reproducibility = SS(operator within part)
-      Right (decomposeNested ys ops parts)
-
--- ===========================================================================
--- Crossed 分解
--- ===========================================================================
-
-decomposeCrossed
-  :: Int -> Int -> Int
-  -> V.Vector Double -> V.Vector Int -> V.Vector Int
-  -> V.Vector Int -> V.Vector Int
-  -> GaugeRRResult
-decomposeCrossed nOp nPart nRep ys ops parts _opIds _partIds =
-  let !n     = V.length ys
-      nD     = fromIntegral n     :: Double
-      nOpD   = fromIntegral nOp   :: Double
-      nPartD = fromIntegral nPart :: Double
-      nRepD  = fromIntegral nRep  :: Double
-      !grand = V.sum ys / nD
-      -- (part, op) → list of y's
-      cellMap :: Map.Map (Int, Int) [Double]
-      cellMap = foldr
-        (\(k, y) m ->
-            Map.insertWith (++) (parts V.! k, ops V.! k) [y] m)
-        Map.empty
-        (zip [0 .. n - 1] (V.toList ys))
-      cellMean (pi_, oi) =
-        let cellY = Map.findWithDefault [] (pi_, oi) cellMap
-        in if null cellY then 0 else sum cellY / fromIntegral (length cellY)
-      partMeans = Map.fromListWith (+)
-        [ (parts V.! k, ys V.! k / (nOpD * nRepD)) | k <- [0 .. n - 1] ]
-      opMeans   = Map.fromListWith (+)
-        [ (ops V.! k, ys V.! k / (nPartD * nRepD)) | k <- [0 .. n - 1] ]
-      pmean p_  = Map.findWithDefault 0 p_ partMeans
-      omean o_  = Map.findWithDefault 0 o_ opMeans
-      uniqParts = Map.keys partMeans
-      uniqOps   = Map.keys opMeans
-      ssPart = nOpD * nRepD * sum [ (pmean p_ - grand) ** 2 | p_ <- uniqParts ]
-      ssOp   = nPartD * nRepD * sum [ (omean o_ - grand) ** 2 | o_ <- uniqOps ]
-      ssInt  = nRepD * sum
-        [ (cellMean (p_, o_) - pmean p_ - omean o_ + grand) ** 2
-        | p_ <- uniqParts, o_ <- uniqOps ]
-      ssError = sum
-        [ (ys V.! k - cellMean (parts V.! k, ops V.! k)) ** 2
-        | k <- [0 .. n - 1] ]
-      dfPart  = nPartD - 1
-      dfOp    = nOpD - 1
-      dfInt   = (nPartD - 1) * (nOpD - 1)
-      dfError = nD - nOpD * nPartD
-      msPart  = if dfPart  > 0 then ssPart / dfPart   else 0
-      msOp    = if dfOp    > 0 then ssOp / dfOp       else 0
-      msInt   = if dfInt   > 0 then ssInt / dfInt     else 0
-      msError = if dfError > 0 then ssError / dfError else 0
-      sigRepeat       = msError
-      sigInt          = max 0 ((msInt - msError) / nRepD)
-      sigReproducOnly = max 0 ((msOp - msInt) / (nPartD * nRepD))
-      sigReproduc     = sigReproducOnly + sigInt
-      sigPart         = max 0 ((msPart - msInt) / (nOpD * nRepD))
-      sigTotal        = sigRepeat + sigReproduc + sigPart
-      pct s           = if sigTotal > 0 then s / sigTotal * 100 else 0
-      sigGRR          = sigRepeat + sigReproduc
-      ndc             = if sigGRR > 0
-                          then 1.41 * sqrt (sigPart / sigGRR)
-                          else 0
-  in GaugeRRResult
-       { grrPartVar     = sigPart
-       , grrReproducVar = sigReproduc
-       , grrRepeatVar   = sigRepeat
-       , grrTotalVar    = sigTotal
-       , grrPctRepeat   = pct sigRepeat
-       , grrPctReproduc = pct sigReproduc
-       , grrPctGRR      = pct sigGRR
-       , grrPctPart     = pct sigPart
-       , grrNumDistinct = ndc
-       }
-
--- ===========================================================================
--- Nested 分解 (簡略版)
--- ===========================================================================
-
-decomposeNested :: V.Vector Double -> V.Vector Int -> V.Vector Int -> GaugeRRResult
-decomposeNested ys _ops _parts =
-  -- 簡略: total variance を repeatability + part に分けるのみ
-  -- (operator-within-part は part に含めて扱う)
-  let n = V.length ys
-      grand = V.sum ys / fromIntegral n
-      ssTotal = V.sum (V.map (\y -> (y - grand) ** 2) ys)
-      sigTotal = ssTotal / fromIntegral (max 1 (n - 1))
-  in GaugeRRResult
-       { grrPartVar     = sigTotal * 0.5  -- 暫定
-       , grrReproducVar = sigTotal * 0.1
-       , grrRepeatVar   = sigTotal * 0.4
-       , grrTotalVar    = sigTotal
-       , grrPctRepeat   = 40
-       , grrPctReproduc = 10
-       , grrPctGRR      = 50
-       , grrPctPart     = 50
-       , grrNumDistinct = 1.41
-       }
--- 注: nested は将来 Phase で本実装。 現状は API のみ。
diff --git a/src/Hanalyze/Design/Mixed.hs b/src/Hanalyze/Design/Mixed.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Mixed.hs
+++ /dev/null
@@ -1,32 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.Mixed
--- Description : 因子ごとに異なる水準数を持つ混合水準計画の生成
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Mixed-level designs.
---
--- Designs in which factors have different numbers of levels. An extension
--- of @Hanalyze.Design.Factorial.mixedFactorial@ that accepts an explicit list of
--- level values per factor.
-module Hanalyze.Design.Mixed
-  ( mixedLevelDesign
-  , crossDesign
-  ) where
-
-import Hanalyze.Design.Factorial (fullFactorial)
-
--- | Mixed-level design where the user supplies an explicit list of
--- level values per factor.
---
--- Example: factor A on @(10, 20, 30)@ and factor B on @(-1, +1)@:
--- @mixedLevelDesign [[10, 20, 30], [-1, 1]]@.
-mixedLevelDesign :: [[Double]] -> [[Double]]
-mixedLevelDesign = fullFactorial
-
--- | Cross product of two design matrices (cross design).
---
--- Useful e.g. for combining two full factorial designs side-by-side.
-crossDesign :: [[Double]] -> [[Double]] -> [[Double]]
-crossDesign d1 d2 = [r1 ++ r2 | r1 <- d1, r2 <- d2]
diff --git a/src/Hanalyze/Design/Mixture.hs b/src/Hanalyze/Design/Mixture.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Mixture.hs
+++ /dev/null
@@ -1,138 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Design.Mixture
--- Description : 配合計画 (Mixture Design) — 成分比合計 = 1 制約下の Simplex Lattice / Centroid
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- 配合計画 (Mixture Design) — 成分比の合計が常に 1 となる制約下の DoE。
---
--- 材料 / 化学プロセス向け。 各実験点は @[x_1, ..., x_m]@ で
--- @x_i ≥ 0@、 @Σ x_i = 1@ を満たす。
---
--- 提供方式:
---
---   * 'SimplexLattice' @d@ — 各成分が @{0, 1/d, ..., d/d}@ から値を取り、 合計が
---     1 になる全組合せ。 点数 = @C(m+d−1, d)@
---   * 'SimplexCentroid' — 1 ≤ k ≤ m について、 任意 k 成分を均等に @1/k@、 他は 0。
---     点数 = @2^m − 1@
---
--- 制約付き Extreme Vertices design は将来 Phase で追加予定。
-module Hanalyze.Design.Mixture
-  ( MixtureDesignType (..)
-  , MixtureResult (..)
-  , mixtureDesign
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import           Data.Text             (Text)
-import qualified Data.Text             as T
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
--- | Mixture design の種別。
-data MixtureDesignType
-  = SimplexLattice !Int  -- ^ 次数 d。 各成分は @{0, 1/d, ..., 1}@ のいずれかの値
-  | SimplexCentroid      -- ^ 2^m - 1 点 (頂点 + 辺中点 + ... + 全体重心)
-  deriving (Show, Eq)
-
--- | Mixture design の結果。
-data MixtureResult = MixtureResult
-  { mdMatrix      :: !(LA.Matrix Double)
-    -- ^ @nRuns × m@ 行列。 各行の合計 = 1、 各要素 ∈ @[0, 1]@
-  , mdNComponents :: !Int               -- ^ m (成分数)
-  , mdNRuns       :: !Int               -- ^ 実験数
-  , mdType        :: !MixtureDesignType -- ^ 入力の種別を保持
-  } deriving (Show)
-
--- ===========================================================================
--- 公開関数
--- ===========================================================================
-
--- | Mixture design を生成。
---
--- 失敗条件:
---
---   * 成分数 m < 2 → 'Left'
---   * SimplexLattice の次数 d < 1 → 'Left'
-mixtureDesign :: MixtureDesignType -> Int -> Either Text MixtureResult
-mixtureDesign typ m
-  | m < 2 = Left (T.pack ("mixtureDesign: need m >= 2 components, got m=" <> show m))
-  | otherwise = case typ of
-      SimplexLattice d
-        | d < 1 -> Left (T.pack ("mixtureDesign SimplexLattice: need d >= 1, got d=" <> show d))
-        | otherwise ->
-            let pts = simplexLatticePoints m d
-                mat = LA.fromLists pts
-            in Right MixtureResult
-                 { mdMatrix      = mat
-                 , mdNComponents = m
-                 , mdNRuns       = length pts
-                 , mdType        = typ
-                 }
-      SimplexCentroid ->
-        let pts = simplexCentroidPoints m
-            mat = LA.fromLists pts
-        in Right MixtureResult
-             { mdMatrix      = mat
-             , mdNComponents = m
-             , mdNRuns       = length pts
-             , mdType        = typ
-             }
-
--- ===========================================================================
--- 内部: Simplex Lattice
--- ===========================================================================
-
--- | Simplex Lattice {m, d} の全点。
---
--- 非負整数 m-tuple (n_1, ..., n_m) で sum = d を満たす全組合せを列挙し、
--- 各点を (n_1/d, ..., n_m/d) に正規化。
-simplexLatticePoints :: Int -> Int -> [[Double]]
-simplexLatticePoints m d =
-  let intTuples = compositions m d
-      scale = 1 / fromIntegral d :: Double
-  in [ map ((scale *) . fromIntegral) t | t <- intTuples ]
-
--- | 非負整数 m-tuple (n_1, ..., n_m) で sum = total を満たすもの全列挙。
-compositions :: Int -> Int -> [[Int]]
-compositions 0 0     = [[]]
-compositions 0 _     = []
-compositions m total =
-  [ k : rest
-  | k <- [0 .. total]
-  , rest <- compositions (m - 1) (total - k)
-  ]
-
--- ===========================================================================
--- 内部: Simplex Centroid
--- ===========================================================================
-
--- | Simplex Centroid (m components) の全点。
---
--- 1 ≤ k ≤ m について、 m 成分から任意の k 個を選び、 その k 成分だけ @1/k@、
--- 他は 0 とする点を作る。 合計 @2^m - 1@ 点。
-simplexCentroidPoints :: Int -> [[Double]]
-simplexCentroidPoints m =
-  [ centroidPointFromSubset m subset
-  | k <- [1 .. m]
-  , subset <- choose [0 .. m - 1] k
-  ]
-
--- | サブセット (= component の index list、 size = k) から centroid 点を構築。
---   各位置 i が subset に含まれていれば @1/k@、 含まれていなければ 0。
-centroidPointFromSubset :: Int -> [Int] -> [Double]
-centroidPointFromSubset m subset =
-  let k    = length subset
-      val  = 1 / fromIntegral k :: Double
-  in [ if i `elem` subset then val else 0 | i <- [0 .. m - 1] ]
-
--- | 長さ k の組合せを全列挙。 順序は lexicographic。
-choose :: [a] -> Int -> [[a]]
-choose _      0 = [[]]
-choose []     _ = []
-choose (x:xs) k =
-  map (x :) (choose xs (k - 1)) ++ choose xs k
diff --git a/src/Hanalyze/Design/MultiRSM.hs b/src/Hanalyze/Design/MultiRSM.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/MultiRSM.hs
+++ /dev/null
@@ -1,44 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.MultiRSM
--- Description : 複数応答同時の Response Surface Methodology (各応答の二次モデル並列 fit + 極値解析)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Multi-response Response Surface Methodology.
---
--- Fits a quadratic model to each response @y_j@ and performs the extremum
--- analysis @q@ times in parallel. As a starting point for multi-objective
--- optimization, this presents the individual optimum of each response.
-module Hanalyze.Design.MultiRSM
-  ( MultiQuadFit (..)
-  , fitMultiQuadratic
-  , optimumPointsMulti
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import Hanalyze.Design.RSM (QuadFit (..), fitQuadratic, optimumPoint)
-
--- | Aggregated multi-response quadratic fit.
-data MultiQuadFit = MultiQuadFit
-  { mqFits :: [QuadFit]   -- ^ Per-response quadratic fits (length @q@).
-  , mqK    :: Int         -- ^ Number of factors @k@.
-  , mqQ    :: Int         -- ^ Number of responses @q@.
-  } deriving (Show)
-
--- | Multi-response quadratic regression: apply 'fitQuadratic' to each
--- response column independently.
-fitMultiQuadratic :: [[Double]]            -- ^ Design matrix (@n × k@).
-                  -> LA.Matrix Double      -- ^ Response @Y@ (@n × q@).
-                  -> MultiQuadFit
-fitMultiQuadratic design y =
-  let q = LA.cols y
-      k = if null design then 0 else length (head design)
-      colFit j = fitQuadratic design (LA.toList (LA.flatten (y LA.¿ [j])))
-      fits = [colFit j | j <- [0 .. q - 1]]
-  in MultiQuadFit fits k q
-
--- | Compute 'optimumPoint' for each response and aggregate the
--- extremum information.
-optimumPointsMulti :: MultiQuadFit -> [([Double], Double, [Double])]
-optimumPointsMulti mq = map optimumPoint (mqFits mq)
diff --git a/src/Hanalyze/Design/Optimal.hs b/src/Hanalyze/Design/Optimal.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Optimal.hs
+++ /dev/null
@@ -1,403 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.Optimal
--- Description : 最適計画 (D/A/I/E/G-optimal) — Fedorov 交換法による候補集合からの選択・拡張
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Optimal designs: D-optimal and A-optimal.
---
--- Selects a subset of @n@ runs from a candidate set, maximizing /
--- minimizing a criterion based on the information matrix @XᵀX@.
---
---   * **D-optimal** — @max det(XᵀX)@ → joint estimation precision of
---     all parameters.
---   * **A-optimal** — @min trace((XᵀX)⁻¹)@ → minimum average estimation
---     variance.
---
--- Algorithm: the Fedorov exchange method (sequential exchanges). Starts
--- from a random selection of candidates and
--- 改善する交換が見つからなくなるまで繰り返す。
-module Hanalyze.Design.Optimal
-  ( OptCriterion (..)
-  , dOptimal
-  , aOptimal
-  , iOptimal
-  , eOptimal
-  , gOptimal
-  , optimalDesign
-  , candidateGrid
-  , quadraticCandidates
-  , pseudoShuffle
-    -- * Augment Design (Phase 5、 request/160)
-  , AugmentResult (..)
-  , augmentDesign
-  ) where
-
-import Data.List (foldl')
-import qualified Numeric.LinearAlgebra as LA
-
--- | Optimality criterion.
-data OptCriterion
-  = DOpt   -- ^ D-optimal: maximize @det(XᵀX)@.
-  | AOpt   -- ^ A-optimal: minimize @trace((XᵀX)⁻¹)@.
-  | IOpt   -- ^ I-optimal: minimize average prediction variance, approximated
-           --   by @trace((XᵀX)⁻¹ · M_moment)@. ここでは M_moment を全候補
-           --   から推定した moment matrix @candᵀ cand / n_cand@ とする。
-  | EOpt   -- ^ E-optimal: minimize the maximum eigenvalue of @(XᵀX)⁻¹@、
-           --   = maximize the minimum eigenvalue of @XᵀX@。
-  | GOpt   -- ^ G-optimal (self approximation): minimize the maximum leverage
-           --   @max_i (H_ii)@ where @H = X (XᵀX)⁻¹ Xᵀ@。
-           --   候補集合に依存しない self-G 定義 (= 設計自身の hat 対角の最大)。
-           --   厳密な G-optimal (候補空間全体の max prediction variance) は
-           --   Custom Design spec 側で扱う。 spec: doe-spec v0.2 §2.9。
-  | Compound ![(Double, OptCriterion)]
-           -- ^ Compound (alphabetic) criterion: 各 inner criterion を
-           --   /minimize/ 方向に揃えた 'critValue' の重み付き和。
-           --   重みは正数を仮定 (合計 1 への正規化はユーザ側責任)。
-           --   ネストした @Compound@ も許容 (展開して評価)。
-           --   注意: inner criterion 同士のスケールはユーザが責任を持って
-           --   揃える (例: D 0.7 + I 0.3 は両方を efficiency 形に正規化
-           --   してから渡す)。 v0.2 では正規化ヘルパは未提供、 v0.3+ 候補。
-           --   spec: doe-spec v0.2 §2.9。
-  | BayesianD ![[Double]]
-           -- ^ Bayesian D-optimality (DuMouchel-Jones 1994):
-           --   maximize @det(XᵀX + K)@、 K = prior precision matrix (p × p)。
-           --   K = 0 行列で classic D に縮退。 spec: doe-custom-design-spec v0.1.1 §2.7。
-           --   K は @[[Double]]@ (Show / Eq 要件のため)、 expand 後の列数と一致必須。
-  | IOptRegion ![[Double]]
-           -- ^ I-optimal (region 積分版、 Phase 28-4):
-           --   minimize @trace((XᵀX)⁻¹ · M_R)@、 M_R = region moment matrix
-           --   @∫_R f(z)f(z)' dz / vol(R)@ (p × p)。 旧 'IOpt' は self-moment
-           --   近似で @= p/n@ に縮退するため設計に依らず無意味、 region 版で差し替えた。
-           --   M_R は @[[Double]]@ (Show / Eq 要件のため)、 expand 後の列数と一致必須。
-           --   Custom Design 内では 'Hanalyze.Design.Custom.Compare.regionMomentMatrixAnalytic'
-           --   が連続 U[-1,1] + Categorical 等確率規約で M_R を構築する。
-  deriving (Show, Eq)
-
--- ---------------------------------------------------------------------------
--- 基準値の計算
--- ---------------------------------------------------------------------------
-
--- | D-criterion value for a design matrix @X@: @det(XᵀX)@.
-dValue :: [[Double]] -> Double
-dValue rows
-  | null rows = 0
-  | otherwise = LA.det xtx
-  where
-    m   = LA.fromLists rows
-    xtx = LA.tr m LA.<> m
-
--- | A-criterion value for a design matrix @X@: @trace((XᵀX)⁻¹)@.
--- Returns @∞@ when the inverse does not exist.
-aValue :: [[Double]] -> Double
-aValue rows
-  | null rows = 1 / 0
-  | otherwise =
-      let m   = LA.fromLists rows
-          xtx = LA.tr m LA.<> m
-          d   = LA.det xtx
-      in if abs d < 1e-12 then 1 / 0
-           else
-             let inv = LA.inv xtx
-                 p   = LA.cols m
-             in sum [ inv `LA.atIndex` (i, i) | i <- [0 .. p - 1] ]
-
--- | Criterion value used for optimization. Both criteria are returned
--- as quantities to /minimize/; D-optimality is encoded as
--- @-det(XᵀX)@.
-critValue :: OptCriterion -> [[Double]] -> Double
-critValue DOpt rows = -dValue rows  -- 最小化問題に統一
-critValue AOpt rows =  aValue rows
-critValue IOpt rows = iValueWithSelf rows
-critValue EOpt rows = eValue rows
-critValue GOpt rows = gValue rows
-critValue (Compound ws) rows =
-  sum [ w * critValue c rows | (w, c) <- ws ]
-critValue (BayesianD k) rows = -bayesianDValue k rows
-critValue (IOptRegion mr) rows = iValueRegion mr rows
-
--- | I-criterion (region 積分版): @trace((XᵀX)⁻¹ · M_R)@ を返す (minimize 方向)。
--- @M_R@ の次元が X の列数と不一致 / X が rank-deficient なら @∞@ を返す。
-iValueRegion :: [[Double]] -> [[Double]] -> Double
-iValueRegion mr rows
-  | null rows = 1 / 0
-  | otherwise =
-      let m   = LA.fromLists rows
-          p   = LA.cols m
-          mrM = LA.fromLists mr
-          xtx = LA.tr m LA.<> m
-          d   = LA.det xtx
-      in if LA.rows mrM /= p || LA.cols mrM /= p || abs d < 1e-12
-           then 1 / 0
-           else LA.sumElements (LA.takeDiag (LA.inv xtx LA.<> mrM))
-
--- | Bayesian D-criterion value: @det(XᵀX + K)@。
--- K の次元が X の列数と不一致なら 0 を返す (= 採用されない)。
-bayesianDValue :: [[Double]] -> [[Double]] -> Double
-bayesianDValue k rows
-  | null rows = 0
-  | otherwise =
-      let m  = LA.fromLists rows
-          p  = LA.cols m
-          km = LA.fromLists k
-      in if LA.rows km /= p || LA.cols km /= p
-           then 0
-           else LA.det (LA.tr m LA.<> m + km)
-
--- | I-criterion with self moment: trace((XᵀX)⁻¹ · (XᵀX) / n) = p / n。
--- 簡略実装として trace((XᵀX)⁻¹) を返す (A-criterion と同等の方向性)。
--- 真の I-optimal は外部 moment matrix が必要だが、 ここでは候補集合と
--- 同分布を仮定して self-moment で代用する近似版。
-iValueWithSelf :: [[Double]] -> Double
-iValueWithSelf rows
-  | null rows = 1 / 0
-  | otherwise =
-      let m   = LA.fromLists rows
-          xtx = LA.tr m LA.<> m
-          d   = LA.det xtx
-      in if abs d < 1e-12 then 1 / 0
-           else
-             let inv     = LA.inv xtx
-                 moment  = LA.scale (1 / fromIntegral (length rows)) xtx
-             in LA.sumElements (LA.takeDiag (inv LA.<> moment))
-
--- | G-criterion value (self approximation): max leverage of @H = X (XᵀX)⁻¹ Xᵀ@
--- の対角の最大値。 既に「小さい方が良い」 方向 (= max leverage が小さい設計が
--- 望ましい) なので符号反転なし。
-gValue :: [[Double]] -> Double
-gValue rows
-  | null rows = 1 / 0
-  | otherwise =
-      let m   = LA.fromLists rows
-          xtx = LA.tr m LA.<> m
-          d   = LA.det xtx
-      in if abs d < 1e-12 then 1 / 0
-           else
-             let inv = LA.inv xtx
-                 h   = m LA.<> inv LA.<> LA.tr m
-                 dia = LA.toList (LA.takeDiag h)
-             in if null dia then 1 / 0 else maximum dia
-
--- | E-criterion value: − (minimum eigenvalue of XᵀX)。
--- 最小化方向に統一するため負号。
-eValue :: [[Double]] -> Double
-eValue rows
-  | null rows = 1 / 0
-  | otherwise =
-      let m   = LA.fromLists rows
-          xtx = LA.tr m LA.<> m
-          eigs = LA.toList (LA.eigenvaluesSH (LA.trustSym xtx))
-      in if null eigs then 1 / 0 else - minimum eigs
-
--- ---------------------------------------------------------------------------
--- Fedorov 交換アルゴリズム
--- ---------------------------------------------------------------------------
-
--- | Generic optimal design: pick @n@ rows from a candidate set.
-optimalDesign :: OptCriterion        -- ^ Optimization criterion.
-              -> [[Double]]          -- ^ Candidate set (each row is a
-                                     --   potential design row).
-              -> Int                 -- ^ Number of runs to select.
-              -> Int                 -- ^ Seed for the initial selection.
-              -> ([Int], [[Double]]) -- ^ Selected candidate indices and
-                                     --   the resulting design matrix.
-optimalDesign crit cands n seed
-  | n <= 0 || nC == 0 = ([], [])
-  | otherwise =
-  let -- ★点の反復を許す exact design。 候補を循環させて必ず n 点の初期選択を作る
-      --   (@n > nC@ でも頭打ちにならない)。 @n <= nC@ なら @take n shuffled@ に一致し従来と同じ。
-      initIdx = take n (cycle (pseudoShuffle seed [0 .. nC - 1]))
-      design  = map (cands !!) initIdx
-      -- 改善する交換が無くなるまで反復。 追加候補 @j@ は @current@ に既にあってもよい
-      --   (= 同一候補点の反復を許す)。 反復が criterion を悪化させる (@n <= nC@ で distinct が
-      --   最適な) 場合は @newC < bestC@ が成り立たず不採用ゆえ、 従来の distinct 結果は不変。
-      improve current currentCrit =
-        let pairs =
-              [ (i, j)
-              | i <- [0 .. n - 1]   -- 取り除く index (current の中で)
-              , j <- [0 .. nC - 1]  -- 追加候補 (cands の中で・反復可)
-              ]
-            tryEach (bestIdx, bestC) (i, j) =
-              let swapped = take i bestIdx ++ [j] ++ drop (i + 1) bestIdx
-                  newDes  = map (cands !!) swapped
-                  newC    = critValue crit newDes
-              in if newC < bestC then (swapped, newC) else (bestIdx, bestC)
-            (improved, improvedC) =
-              foldl' tryEach (current, currentCrit) pairs
-        in if improvedC < currentCrit
-             then improve improved improvedC
-             else (improved, currentCrit)
-      initC = critValue crit design
-      (finalIdx, _) = improve initIdx initC
-  in (finalIdx, map (cands !!) finalIdx)
-  where
-    nC = length cands
-
--- | Build a D-optimal design (specialization of 'optimalDesign').
-dOptimal :: [[Double]] -> Int -> Int -> ([Int], [[Double]])
-dOptimal = optimalDesign DOpt
-
--- | Build an A-optimal design.
-aOptimal :: [[Double]] -> Int -> Int -> ([Int], [[Double]])
-aOptimal = optimalDesign AOpt
-
--- | Build an I-optimal design (specialization of 'optimalDesign').
-iOptimal :: [[Double]] -> Int -> Int -> ([Int], [[Double]])
-iOptimal = optimalDesign IOpt
-
--- | Build an E-optimal design (specialization of 'optimalDesign').
-eOptimal :: [[Double]] -> Int -> Int -> ([Int], [[Double]])
-eOptimal = optimalDesign EOpt
-
--- | Build a G-optimal design (self approximation、 specialization of
--- 'optimalDesign')。 spec: doe-spec v0.2 §2.9 / §3.6。
-gOptimal :: [[Double]] -> Int -> Int -> ([Int], [[Double]])
-gOptimal = optimalDesign GOpt
-
--- ---------------------------------------------------------------------------
--- 候補集合の生成
--- ---------------------------------------------------------------------------
-
--- | Equally-spaced grid of candidates: @k@ factors, @numLevels@ values
--- per factor on @[-1, 1]@.
-candidateGrid :: Int -> Int -> [[Double]]
-candidateGrid k numLevels =
-  let levels = if numLevels == 1 then [0]
-                else [-1 + 2 * fromIntegral i / fromIntegral (numLevels - 1)
-                     | i <- [0 .. numLevels - 1] :: [Int]]
-      go 0 = [[]]
-      go d = [v : row | v <- levels, row <- go (d - 1)]
-  in go k
-
--- | Expand a candidate grid into the @quadraticDesign@-style row
--- representation.
---
--- @quadraticCandidates k numLevels@ — each candidate is the row
--- @[1, x_1, …, x_k, x_1², …, x_k²,
--- pairwise interactions]@.
-quadraticCandidates :: Int -> Int -> [[Double]]
-quadraticCandidates k numLevels =
-  let baseGrid = candidateGrid k numLevels
-      expand row =
-        let sqE   = [x * x | x <- row]
-            interE = [(row !! i) * (row !! j)
-                     | i <- [0 .. k - 1], j <- [i + 1 .. k - 1]]
-        in 1 : row ++ sqE ++ interE
-  in map expand baseGrid
-
--- ---------------------------------------------------------------------------
--- ヘルパ
--- ---------------------------------------------------------------------------
-
--- | LCG ベースの簡易シャッフル (再現性のため seed 指定)。
-pseudoShuffle :: Int -> [a] -> [a]
-pseudoShuffle seed xs =
-  let lcg s = (s * 1103515245 + 12345) `mod` (2 ^ (31 :: Int))
-      seeds = take (length xs) (drop 1 (iterate lcg seed))
-      paired = zip seeds xs
-      sorted = sortByKey paired
-  in map snd sorted
-  where
-    sortByKey [] = []
-    sortByKey (p:ps) =
-      sortByKey [q | q <- ps, fst q <= fst p]
-      ++ [p]
-      ++ sortByKey [q | q <- ps, fst q > fst p]
-
-
--- ===========================================================================
--- Augment Design (Phase 5、 request/160)
--- ===========================================================================
-
--- | 'augmentDesign' の結果。
-data AugmentResult = AugmentResult
-  { arNewIndices  :: ![Int]
-    -- ^ 候補集合から選ばれた追加点の index リスト (長さ = 要求した N)
-  , arNewRows     :: ![[Double]]
-    -- ^ 追加点の実値 (= map (cands !!) arNewIndices)
-  , arFullDesign  :: ![[Double]]
-    -- ^ 完成 design 行列 (existing ++ new、 元の existing 順序を保つ)
-  , arInitialCrit :: !Double
-    -- ^ existing 単独の criterion 値 (D-opt なら |XᵀX|; n < p 等で singular なら 0)
-  , arFinalCrit   :: !Double
-    -- ^ 完成 design の criterion 値
-  } deriving (Show)
-
--- | 既存 design に N 行追加するための D-opt / A-opt 最適化。
---
--- 既存行は固定 (swap されない)。 候補集合から N 個を選び、
--- 完成 design (= existing ++ new) の criterion を最大化する Fedorov 交換を行う。
---
--- アルゴリズム:
---
--- 1. seed-based pseudoShuffle で候補集合から N 個を初期選択
--- 2. 「現在の追加行 i ↔ 未選択候補 j」 の全ペアを試行
--- 3. swap した完成 design の criterion が改善するなら採用
--- 4. 1 sweep で改善が無くなるまで反復
---
--- 失敗: N ≤ 0 や候補数 < N の場合は AugmentResult { arNewIndices = [], ... }
--- (= 空の追加) を返す。
-augmentDesign
-  :: OptCriterion
-  -> [[Double]]            -- existing rows (固定)
-  -> Int                   -- N (追加する行数)
-  -> [[Double]]            -- candidate set
-  -> Int                   -- seed
-  -> AugmentResult
-augmentDesign crit existing n cands seed
-  | n <= 0 || nC < n =
-      AugmentResult
-        { arNewIndices  = []
-        , arNewRows     = []
-        , arFullDesign  = existing
-        , arInitialCrit = safeCrit crit existing
-        , arFinalCrit   = safeCrit crit existing
-        }
-  | otherwise =
-      let initIdx = take n (pseudoShuffle seed [0 .. nC - 1])
-          initial = combine initIdx
-          initC   = critValue crit initial
-          improve current currentC =
-            let pairs =
-                  [ (i, j)
-                  | i <- [0 .. n - 1]
-                  , j <- [0 .. nC - 1]
-                  , j `notElem` current
-                  ]
-                tryEach (bestIdx, bestC) (i, j) =
-                  let swapped = take i bestIdx ++ [j] ++ drop (i + 1) bestIdx
-                      newC    = critValue crit (combine swapped)
-                  in if newC < bestC then (swapped, newC) else (bestIdx, bestC)
-                (improved, improvedC) =
-                  foldl' tryEach (current, currentC) pairs
-            in if improvedC < currentC
-                 then improve improved improvedC
-                 else (improved, currentC)
-          (finalIdx, _) = improve initIdx initC
-          newRows       = map (cands !!) finalIdx
-      in AugmentResult
-           { arNewIndices  = finalIdx
-           , arNewRows     = newRows
-           , arFullDesign  = existing ++ newRows
-           , arInitialCrit = safeCrit crit existing
-           , arFinalCrit   = safeCrit crit (existing ++ newRows)
-           }
-  where
-    nC = length cands
-    combine idx = existing ++ map (cands !!) idx
-
--- | criterion を「比較用 sign」 でなく、 実際の表示値 (D-opt は |XᵀX|、
---   A-opt は trace((XᵀX)⁻¹)) で返すヘルパ。 D-opt は singular で 0、 A-opt は ∞
---   になりうるので、 numeric guard を入れる。
-safeCrit :: OptCriterion -> [[Double]] -> Double
-safeCrit _    []   = 0
-safeCrit DOpt rows = dValue rows
-safeCrit AOpt rows = aValue rows
-safeCrit IOpt rows = iValueWithSelf rows
-safeCrit EOpt rows = eValue rows
-safeCrit GOpt rows = gValue rows
-safeCrit (Compound ws) rows =
-  sum [ w * safeCrit c rows | (w, c) <- ws ]
-safeCrit (BayesianD k) rows = bayesianDValue k rows
-safeCrit (IOptRegion mr) rows = iValueRegion mr rows
diff --git a/src/Hanalyze/Design/Orthogonal.hs b/src/Hanalyze/Design/Orthogonal.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Orthogonal.hs
+++ /dev/null
@@ -1,377 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.Orthogonal
--- Description : 直交表 (Taguchi 流 @Lₙ@ 表) の標準表・因子割付・出力レンダリング
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Orthogonal arrays (Taguchi-style @Lₙ@ tables).
---
---   * 'OA'              — orthogonal-array representation (name / run
---     count / factor count / column levels / body).
---   * 'standardArrays'  — standard tables L4 / L8 / L9 / L12 / L16 / L18.
---   * 'lookupOA'        — fetch a standard table by name (e.g. @\"L9\"@).
---   * 'assignFactors'   — bind factors and level values.
---   * 'renderCSV' / 'renderTSV' / 'renderPretty' — emit the run table.
---
--- Two-level series (L8, L16, ...) are generated by @mkL2k@. L4 / L9 /
--- L12 / L18 are defined manually (Plackett-Burman and mixed-level
--- arrays are not derivable from simple subset products).
-module Hanalyze.Design.Orthogonal
-  ( -- * 型
-    OA (..)
-  , LevelValue (..)
-  , FactorSpec (..)
-  , AssignedDesign (..)
-    -- * Standard arrays
-  , l4
-  , l8
-  , l9
-  , l12
-  , l16
-  , l18
-  , l27
-  , standardArrays
-  , lookupOA
-  , listArrays
-  , OAMetadata (..)
-  , listArraysWithSize
-    -- * 2-level array generation
-  , mkL2k
-    -- * Factor assignment
-  , assignFactors
-    -- * Rendering
-  , renderRawCSV
-  , renderRawTSV
-  , renderRawPretty
-  , renderCSV
-  , renderTSV
-  , renderPretty
-  ) where
-
-import Data.Bits (testBit, popCount, (.&.), bit)
-import Data.Text (Text)
-import qualified Data.Text as T
-import Text.Printf (printf)
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
--- | An orthogonal array. Stored as a @runs × cols@ table of 1-based
--- level codes.
-data OA = OA
-  { oaName    :: Text     -- ^ Display name, e.g. @\"L9(3^4)\"@.
-  , oaRuns    :: Int      -- ^ Number of runs.
-  , oaFactors :: Int      -- ^ Maximum number of factors (= columns).
-  , oaLevels  :: [Int]    -- ^ Level count per column (length 'oaFactors').
-  , oaTable   :: [[Int]]  -- ^ Body of the table (@runs × cols@) of
-                          --   1-based level codes.
-  } deriving (Show, Eq)
-
--- | A factor level value (text or numeric).
-data LevelValue = LText Text | LNumeric Double
-  deriving (Show, Eq)
-
--- | User-supplied factor: a name plus a list of level values.
-data FactorSpec = FactorSpec
-  { fsName   :: Text
-  , fsLevels :: [LevelValue]
-  } deriving (Show, Eq)
-
--- | A run table after factor assignment.
-data AssignedDesign = AssignedDesign
-  { adArray   :: OA
-  , adFactors :: [FactorSpec]
-  , adRows    :: [[LevelValue]]
-  } deriving (Show, Eq)
-
--- ---------------------------------------------------------------------------
--- 標準表 (手動定義)
--- ---------------------------------------------------------------------------
-
--- | L4(2³) — 4 runs, up to 3 two-level factors.
-l4 :: OA
-l4 = OA "L4(2^3)" 4 3 (replicate 3 2)
-  [ [1,1,1]
-  , [1,2,2]
-  , [2,1,2]
-  , [2,2,1]
-  ]
-
--- | L9(3⁴) — 9 runs, up to 4 three-level factors.
-l9 :: OA
-l9 = OA "L9(3^4)" 9 4 (replicate 4 3)
-  [ [1,1,1,1]
-  , [1,2,2,2]
-  , [1,3,3,3]
-  , [2,1,2,3]
-  , [2,2,3,1]
-  , [2,3,1,2]
-  , [3,1,3,2]
-  , [3,2,1,3]
-  , [3,3,2,1]
-  ]
-
--- | L12(2¹¹) — 12 runs, up to 11 two-level factors (Plackett-Burman).
--- Main effects only (interactions are distributed across all columns).
-l12 :: OA
-l12 = OA "L12(2^11)" 12 11 (replicate 11 2)
-  [ [1,1,1,1,1,1,1,1,1,1,1]
-  , [1,1,1,1,1,2,2,2,2,2,2]
-  , [1,1,2,2,2,1,1,1,2,2,2]
-  , [1,2,1,2,2,1,2,2,1,1,2]
-  , [1,2,2,1,2,2,1,2,1,2,1]
-  , [1,2,2,2,1,2,2,1,2,1,1]
-  , [2,1,2,2,1,1,2,2,1,2,1]
-  , [2,1,2,1,2,2,2,1,1,1,2]
-  , [2,1,1,2,2,2,1,2,2,1,1]
-  , [2,2,2,1,1,1,1,2,2,1,2]
-  , [2,2,1,2,1,2,1,1,1,2,2]
-  , [2,2,1,1,2,1,2,1,2,2,1]
-  ]
-
--- | L18(2¹×3⁷) — 18 runs, up to 8 factors (column 1 has 2 levels, the
--- remaining 7 columns each have 3 levels).
---
--- One of the most recommended Taguchi-style arrays; can measure main
--- effects plus the column-1 × column-2 interaction.
-l18 :: OA
-l18 = OA "L18(2^1*3^7)" 18 8 (2 : replicate 7 3)
-  [ [1,1,1,1,1,1,1,1]
-  , [1,1,2,2,2,2,2,2]
-  , [1,1,3,3,3,3,3,3]
-  , [1,2,1,1,2,2,3,3]
-  , [1,2,2,2,3,3,1,1]
-  , [1,2,3,3,1,1,2,2]
-  , [1,3,1,2,1,3,2,3]
-  , [1,3,2,3,2,1,3,1]
-  , [1,3,3,1,3,2,1,2]
-  , [2,1,1,3,3,2,2,1]
-  , [2,1,2,1,1,3,3,2]
-  , [2,1,3,2,2,1,1,3]
-  , [2,2,1,2,3,1,3,2]
-  , [2,2,2,3,1,2,1,3]
-  , [2,2,3,1,2,3,2,1]
-  , [2,3,1,3,2,3,1,2]
-  , [2,3,2,1,3,1,2,3]
-  , [2,3,3,2,1,2,3,1]
-  ]
-
--- | L27(3¹³) — 27 runs, up to 13 three-level factors.
---
--- GF(3) 上の線形形式で生成する (手写しの転記ミスを避ける)。 27 run を
--- @(a,b,c) ∈ {0,1,2}³@ で添字し、 13 列は 3 次元 GF(3) の相異なる 1 次元部分空間
--- を代表する係数ベクトル (先頭非零 = 1 に正規化) で @α·a+β·b+γ·c mod 3@ を取る。
--- 相異なる 1 次元部分空間ゆえ任意の 2 列は強度 2 直交 (各水準組が均等出現)。
--- 列順は標準タグチ L27 の配置 (col1,2 = 基本、 3,4 = その交互作用、 5 = 第3基本…)。
-l27 :: OA
-l27 = OA "L27(3^13)" 27 13 (replicate 13 3) table
-  where
-    coeffs :: [(Int, Int, Int)]
-    coeffs =
-      [ (1,0,0), (0,1,0), (1,1,0), (1,2,0), (0,0,1), (1,0,1), (1,0,2)
-      , (0,1,1), (0,1,2), (1,1,1), (1,1,2), (1,2,1), (1,2,2) ]
-    table =
-      [ [ 1 + ((al*a + be*b + ga*c) `mod` 3) | (al, be, ga) <- coeffs ]
-      | r <- [0 .. 26 :: Int]
-      , let a = r `div` 9
-            b = (r `div` 3) `mod` 3
-            c = r `mod` 3 ]
-
--- ---------------------------------------------------------------------------
--- 2 水準系の生成
--- ---------------------------------------------------------------------------
-
--- | Build @L_{2^k}(2^{2^k − 1})@ in Taguchi's standard column ordering
--- (column @j@'s value is
--- popCount(j ∧ revBits k r) のパリティ)。
-mkL2k :: Int -> OA
-mkL2k k =
-  OA
-    { oaName    = T.pack ("L" ++ show n ++ "(2^" ++ show m ++ ")")
-    , oaRuns    = n
-    , oaFactors = m
-    , oaLevels  = replicate m 2
-    , oaTable   = [ [ levelAt r j | j <- [1 .. m] ] | r <- [0 .. n - 1] ]
-    }
-  where
-    n = 2 ^ k
-    m = n - 1
-    -- Taguchi の標準的な列ラベル順 (col 1 は最上位ビット相当) に合わせるため
-    -- 行インデックスをビット反転する。
-    revBits :: Int -> Int
-    revBits r = sum [ if testBit r i then bit (k - 1 - i) else 0
-                    | i <- [0 .. k - 1] ]
-    levelAt r j = 1 + (popCount (j .&. revBits r) `mod` 2)
-
--- | L8(2⁷) — 8 runs, up to 7 two-level factors (generated).
-l8 :: OA
-l8 = mkL2k 3
-
--- | L16(2¹⁵) — 16 runs, up to 15 two-level factors (generated).
-l16 :: OA
-l16 = mkL2k 4
-
--- ---------------------------------------------------------------------------
--- ルックアップ
--- ---------------------------------------------------------------------------
-
--- | The standard arrays bundled with the library.
-standardArrays :: [OA]
-standardArrays = [l4, l8, l9, l12, l16, l18, l27]
-
--- | Look up a standard array by short name (e.g. @\"L9\"@).
-lookupOA :: Text -> Maybe OA
-lookupOA name0 = case T.toUpper name0 of
-  "L4"  -> Just l4
-  "L8"  -> Just l8
-  "L9"  -> Just l9
-  "L12" -> Just l12
-  "L16" -> Just l16
-  "L18" -> Just l18
-  "L27" -> Just l27
-  _     -> Nothing
-
--- | List of available orthogonal arrays (used by CLI @doe list@).
-listArrays :: [(Text, Text)]
-listArrays = [ (oaName a, descr a) | a <- standardArrays ]
-  where
-    descr a =
-      T.pack (show (oaRuns a)) <> " runs, max "
-      <> T.pack (show (oaFactors a)) <> " factors"
-
--- | Structured metadata for an orthogonal array. Suitable for UI
--- listings that want to filter / sort by run count or level pattern.
-data OAMetadata = OAMetadata
-  { omName    :: !Text   -- ^ e.g. @\"L9(3^4)\"@.
-  , omRuns    :: !Int    -- ^ Number of runs.
-  , omFactors :: !Int    -- ^ Maximum number of factors.
-  , omLevels  :: ![Int]  -- ^ Level count per column.
-  , omDescr   :: !Text   -- ^ Free-form description (matches 'listArrays').
-  } deriving (Show, Eq)
-
--- | Same coverage as 'listArrays' but with structured fields.
-listArraysWithSize :: [OAMetadata]
-listArraysWithSize =
-  [ OAMetadata (oaName a) (oaRuns a) (oaFactors a) (oaLevels a)
-               (T.pack (show (oaRuns a)) <> " runs, max "
-                <> T.pack (show (oaFactors a)) <> " factors")
-  | a <- standardArrays
-  ]
-
--- ---------------------------------------------------------------------------
--- 因子割当
--- ---------------------------------------------------------------------------
-
--- | Assign user-supplied factor names and level values to the columns of
--- an orthogonal array, returning the expanded run table.
---
--- - 因子数が表の列数を超えるとエラー
--- - 各因子の水準数が割当先列の水準数と一致しないとエラー
-assignFactors :: OA -> [FactorSpec] -> Either Text AssignedDesign
-assignFactors oa specs
-  | nSpecs > oaFactors oa =
-      Left $ "Too many factors: " <> oaName oa
-             <> " has only " <> T.pack (show (oaFactors oa)) <> " columns; got "
-             <> T.pack (show nSpecs)
-  | not (null mismatches) =
-      Left $ "Factor level mismatch: " <> T.intercalate "; " mismatches
-  | otherwise =
-      Right AssignedDesign
-        { adArray   = oa
-        , adFactors = specs
-        , adRows    = [ [ fsLevels (specs !! (j - 1)) !! (lvl - 1)
-                        | (j, lvl) <- zip [1 .. nSpecs] (take nSpecs row) ]
-                      | row <- oaTable oa ]
-        }
-  where
-    nSpecs    = length specs
-    expected  = take nSpecs (oaLevels oa)
-    actuals   = map (length . fsLevels) specs
-    mismatches =
-      [ fsName (specs !! i) <> " expected " <> T.pack (show e)
-        <> " levels, got " <> T.pack (show a)
-      | (i, (e, a)) <- zip [0..] (zip expected actuals)
-      , e /= a ]
-
--- ---------------------------------------------------------------------------
--- 出力
--- ---------------------------------------------------------------------------
-
--- | Render an orthogonal array as raw CSV (columns are @F1, F2, …@).
-renderRawCSV :: OA -> Text
-renderRawCSV oa = renderRawWith "," oa
-
--- | Render an orthogonal array as raw TSV.
-renderRawTSV :: OA -> Text
-renderRawTSV oa = renderRawWith "\t" oa
-
--- | Render an orthogonal array as a delimiter-separated table.
-renderRawWith :: Text -> OA -> Text
-renderRawWith sep oa =
-  let header = T.intercalate sep
-                 [ "F" <> T.pack (show j) | j <- [1 .. oaFactors oa] ]
-      body   = T.intercalate "\n"
-                 [ T.intercalate sep [ T.pack (show v) | v <- row ]
-                 | row <- oaTable oa ]
-  in header <> "\n" <> body <> "\n"
-
--- | Pretty-print a named orthogonal array with aligned columns.
-renderRawPretty :: OA -> Text
-renderRawPretty oa =
-  let names    = "Run" : [ "F" <> T.pack (show j) | j <- [1 .. oaFactors oa] ]
-      colWidth = maximum (map T.length names) `max` 3
-      pad t    = let n = colWidth - T.length t
-                 in T.replicate n " " <> t
-      header   = T.intercalate "  " (map pad names)
-      body     = T.intercalate "\n"
-                   [ T.intercalate "  "
-                       (pad (T.pack (show r))
-                       : [ pad (T.pack (show v)) | v <- row ])
-                   | (r, row) <- zip [1::Int ..] (oaTable oa) ]
-  in T.pack (T.unpack (oaName oa)) <> "\n" <> header <> "\n" <> body
-
--- | Render a factor-assigned run table as CSV.
-renderCSV :: AssignedDesign -> Text
-renderCSV = renderWith ","
-
--- | Render a factor-assigned run table as TSV.
-renderTSV :: AssignedDesign -> Text
-renderTSV = renderWith "\t"
-
--- | Render a factor-assigned run table with a custom field separator.
-renderWith :: Text -> AssignedDesign -> Text
-renderWith sep ad =
-  let header = T.intercalate sep ("Run" : map fsName (adFactors ad))
-      body   = T.intercalate "\n"
-                 [ T.intercalate sep (T.pack (show r) : map fmtLevel row)
-                 | (r, row) <- zip [1::Int ..] (adRows ad) ]
-  in header <> "\n" <> body <> "\n"
-
-fmtLevel :: LevelValue -> Text
-fmtLevel (LText t)    = t
-fmtLevel (LNumeric d)
-  | d == fromIntegral (round d :: Integer) = T.pack (show (round d :: Integer))
-  | otherwise                              = T.pack (printf "%g" d)
-
--- | Pretty-print a factor-assigned run table.
-renderPretty :: AssignedDesign -> Text
-renderPretty ad =
-  let names      = "Run" : map fsName (adFactors ad)
-      cells      =
-        [ T.pack (show r) : map fmtLevel row
-        | (r, row) <- zip [1::Int ..] (adRows ad) ]
-      colWidths  = map (\i -> maximum (map (T.length . safeIx i)
-                                       (names : cells)))
-                       [0 .. length names - 1]
-      safeIx i xs = if i < length xs then xs !! i else ""
-      pad i t    = let n = colWidths !! i - T.length t
-                   in T.replicate n " " <> t
-      fmtRow row = T.intercalate "  "
-                     [ pad i (safeIx i row) | i <- [0 .. length names - 1] ]
-  in oaName (adArray ad) <> "  (" <> T.pack (show (oaRuns (adArray ad)))
-     <> " runs, " <> T.pack (show (length (adFactors ad)))
-     <> " of " <> T.pack (show (oaFactors (adArray ad))) <> " columns assigned)\n"
-     <> fmtRow names <> "\n"
-     <> T.intercalate "\n" (map fmtRow cells)
diff --git a/src/Hanalyze/Design/Power.hs b/src/Hanalyze/Design/Power.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Power.hs
+++ /dev/null
@@ -1,146 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.Power
--- Description : 検出力分析 — サンプルサイズ決定と検出力計算 (t 検定・ANOVA・比率検定)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Power analysis: sample-size determination and power computation.
---
--- Main functions:
---
---   * 'powerTTest'        — power of a two-sample t-test.
---   * 'sampleSizeTTest'   — @n@ required to attain a given power.
---   * 'powerOneWayAnova'  — power of an F-test (one-way ANOVA).
---   * 'powerProportion'   — power of a two-sample proportion test.
-module Hanalyze.Design.Power
-  ( -- * t 検定
-    powerTTest
-  , sampleSizeTTest
-    -- * F-test (ANOVA)
-  , powerOneWayAnova
-  , sampleSizeOneWayAnova
-    -- * Proportion test
-  , powerProportion
-    -- * Effect-size measures
-  , cohensD
-  , cohensF
-  ) where
-
-import qualified Statistics.Distribution as SD
-import qualified Statistics.Distribution.StudentT as ST
-import qualified Statistics.Distribution.Normal as NormalD
-import qualified Statistics.Distribution.FDistribution as FD
-
--- ---------------------------------------------------------------------------
--- 効果量
--- ---------------------------------------------------------------------------
-
--- | Cohen's @d@: standardized two-sample mean difference.
--- @d = (μ_1 − μ_2) / σ_pooled@.
--- Interpretation: 0.2 = small, 0.5 = medium, 0.8 = large.
-cohensD :: Double -> Double -> Double -> Double
-cohensD mu1 mu2 sigma = (mu1 - mu2) / sigma
-
--- | Cohen's @f@: effect size for one-way ANOVA.
--- @f = σ_means / σ_within@.
--- Interpretation: 0.10 = small, 0.25 = medium, 0.40 = large.
-cohensF :: [Double]    -- ^ Per-group means.
-        -> Double      -- ^ Within-group SD (@= √MSE@).
-        -> Double
-cohensF means sigma =
-  let k    = length means
-      gm   = sum means / fromIntegral k
-      var  = sum [(m - gm)^(2::Int) | m <- means] / fromIntegral k
-  in sqrt var / sigma
-
--- ---------------------------------------------------------------------------
--- t 検定
--- ---------------------------------------------------------------------------
-
--- | Two-sample two-sided t-test power, equal-variance assumption.
-powerTTest :: Double  -- ^ Cohen's @d@ (effect size).
-           -> Int     -- ^ Sample size of group 1, @n_1@.
-           -> Int     -- ^ Sample size of group 2, @n_2@.
-           -> Double  -- ^ Significance level @α@ (e.g. 0.05).
-           -> Double
-powerTTest d n1 n2 alpha =
-  let df  = n1 + n2 - 2
-      ncp = d * sqrt (fromIntegral n1 * fromIntegral n2
-                      / fromIntegral (n1 + n2))
-      tCrit = SD.quantile (ST.studentT (fromIntegral df))
-                          (1 - alpha / 2)
-      -- 非心 t 分布の代わりに正規近似 (df 大なら良好)
-      sigma = 1.0  -- t 分布近似なら sd ≈ 1
-      pUpper = 1 - SD.cumulative (NormalD.normalDistr ncp sigma) tCrit
-      pLower = SD.cumulative (NormalD.normalDistr ncp sigma) (-tCrit)
-  in pUpper + pLower
-
--- | Smallest balanced sample size that attains the requested power.
--- (Both groups assumed equal in size.)
-sampleSizeTTest :: Double  -- ^ Effect size @d@.
-                -> Double  -- ^ Target power.
-                -> Double  -- ^ Significance level @α@.
-                -> Int
-sampleSizeTTest d targetPow alpha = search 2 1000
-  where
-    search lo hi
-      | lo >= hi = hi
-      | otherwise =
-          let mid = (lo + hi) `div` 2
-              p   = powerTTest d mid mid alpha
-          in if p >= targetPow then search lo mid else search (mid + 1) hi
-
--- ---------------------------------------------------------------------------
--- 一元配置 ANOVA の F 検定
--- ---------------------------------------------------------------------------
-
--- | One-way ANOVA F-test power.
-powerOneWayAnova :: Double   -- ^ Cohen's @f@ (effect size).
-                 -> Int      -- ^ Number of groups @k@.
-                 -> Int      -- ^ Per-group sample size @n@.
-                 -> Double   -- ^ Significance level @α@.
-                 -> Double
-powerOneWayAnova f k n alpha =
-  let dfBetween = k - 1
-      dfWithin  = k * (n - 1)
-      ncp       = f * f * fromIntegral (k * n)
-      fCrit     = SD.quantile (FD.fDistribution dfBetween dfWithin)
-                              (1 - alpha)
-      -- 非心 F 分布 ≈ scaled F で近似
-      mean1     = fromIntegral dfBetween + ncp
-      var1      = 2 * mean1   -- chi² 近似
-      -- 標準正規近似:
-      z         = (fCrit * fromIntegral dfBetween - mean1) / sqrt var1
-  in 1 - SD.cumulative (NormalD.normalDistr 0 1) z
-
--- | Smallest per-group sample size that attains the requested ANOVA power.
-sampleSizeOneWayAnova :: Double -> Int -> Double -> Double -> Int
-sampleSizeOneWayAnova f k targetPow alpha = search 2 1000
-  where
-    search lo hi
-      | lo >= hi = hi
-      | otherwise =
-          let mid = (lo + hi) `div` 2
-              p   = powerOneWayAnova f k mid alpha
-          in if p >= targetPow then search lo mid else search (mid + 1) hi
-
--- ---------------------------------------------------------------------------
--- 比率検定 (二群)
--- ---------------------------------------------------------------------------
-
--- | Two-sample two-sided proportion z-test power.
---
--- Arguments: true proportions @p_1@, @p_2@, group sample sizes @n_1@,
--- @n_2@, and significance level @α@.
-powerProportion :: Double -> Double -> Int -> Int -> Double -> Double
-powerProportion p1 p2 n1 n2 alpha =
-  let n1d = fromIntegral n1; n2d = fromIntegral n2
-      pP  = (n1d * p1 + n2d * p2) / (n1d + n2d)
-      seH0 = sqrt (pP * (1 - pP) * (1/n1d + 1/n2d))
-      seH1 = sqrt (p1 * (1 - p1) / n1d + p2 * (1 - p2) / n2d)
-      delta = abs (p1 - p2)
-      zAlpha = SD.quantile (NormalD.normalDistr 0 1) (1 - alpha / 2)
-      crit = zAlpha * seH0
-      z = (delta - crit) / seH1
-  in SD.cumulative (NormalD.normalDistr 0 1) z
diff --git a/src/Hanalyze/Design/Quality.hs b/src/Hanalyze/Design/Quality.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Quality.hs
+++ /dev/null
@@ -1,377 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.Quality
--- Description : 計画評価指標 (直交性・D/A-efficiency・VIF) と工程能力指数 (Cp/Cpk 等) の算出
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Quality criteria for evaluating designs.
---
---   * 'isOrthogonal'       — are the design columns orthogonal? (i.e.
---     @XᵀX@ diagonal).
---   * 'orthogonalityScore' — numeric orthogonality score in @[0, 1]@.
---   * 'conditionNumber'    — condition number of @XᵀX@ (large values
---     indicate multicollinearity).
---   * 'dEfficiency'        — D-efficiency @det(XᵀX/n)^(1/p)@.
---   * 'aEfficiency'        — A-efficiency: reciprocal of
---     @trace((XᵀX/n)⁻¹)@.
---   * 'vifList'            — per-column Variance Inflation Factor.
-module Hanalyze.Design.Quality
-  ( isOrthogonal
-  , orthogonalityScore
-  , conditionNumber
-  , dEfficiency
-  , aEfficiency
-  , vifList
-    -- * Process capability
-  , Capability (..)
-  , processCapability
-  , processCapabilityUpper
-  , processCapabilityLower
-  , processCapabilityWeibull
-  , processCapabilityLogNormal
-  , processCapabilityGamma
-    -- * Process capability — unified non-normal entry (Phase 23-c)
-  , NonNormalFit (..)
-  , processCapabilityNonNormal
-    -- * 多変量 Process Capability (Phase 23-d)
-  , MultivariateCapability (..)
-  , processCapabilityMultivariate
-  ) where
-
-import           Data.Text                       (Text)
-import qualified Numeric.LinearAlgebra as LA
-import qualified Statistics.Distribution         as SD
-import qualified Statistics.Distribution.Normal  as Normal
-import qualified Statistics.Distribution.Gamma   as Gamma
-import           Hanalyze.Model.Weibull          (WeibullFit (..))
-
--- | True iff the design matrix @X@ is orthogonal (i.e. @XᵀX@ is
--- diagonal up to tolerance @ε@).
-isOrthogonal :: Double -> [[Double]] -> Bool
-isOrthogonal eps xs =
-  let m   = LA.fromLists xs
-      xtx = LA.tr m LA.<> m
-      n   = LA.rows xtx
-      offDiagSum =
-        sum [ abs (xtx `LA.atIndex` (i, j))
-            | i <- [0 .. n - 1]
-            , j <- [0 .. n - 1]
-            , i /= j ]
-  in offDiagSum < eps
-
--- | Orthogonality score in @[0, 1]@: 0 = far from orthogonal,
--- 1 = exactly orthogonal. Compares the off-diagonal mass against the
--- diagonal mass.
-orthogonalityScore :: [[Double]] -> Double
-orthogonalityScore xs =
-  let m   = LA.fromLists xs
-      xtx = LA.tr m LA.<> m
-      n   = LA.rows xtx
-      diagSum =
-        sum [ abs (xtx `LA.atIndex` (i, i)) | i <- [0 .. n - 1] ]
-      offDiagSum =
-        sum [ abs (xtx `LA.atIndex` (i, j))
-            | i <- [0 .. n - 1]
-            , j <- [0 .. n - 1]
-            , i /= j ]
-  in if diagSum == 0 then 0
-       else 1 - offDiagSum / (diagSum + offDiagSum)
-
--- | Condition number of @XᵀX@ (@λ_max / λ_min@). Values above 30
--- typically indicate multicollinearity.
-conditionNumber :: [[Double]] -> Double
-conditionNumber xs =
-  let m   = LA.fromLists xs
-      xtx = LA.tr m LA.<> m
-      svs = LA.singularValues xtx
-      sList = LA.toList svs
-  in if null sList || minimum sList == 0
-       then 1 / 0   -- ∞
-       else maximum sList / minimum sList
-
--- | D-efficiency @det(XᵀX/n)^(1/p)@ — to be maximized. Approaches 1 for
--- a fully orthogonal design.
-dEfficiency :: [[Double]] -> Double
-dEfficiency xs =
-  let m   = LA.fromLists xs
-      n   = fromIntegral (LA.rows m) :: Double
-      p   = fromIntegral (LA.cols m) :: Double
-      xtx = LA.tr m LA.<> m
-      detV = LA.det (LA.scale (1/n) xtx)
-  in if detV <= 0 then 0
-       else detV ** (1 / p)
-
--- | A-efficiency: reciprocal of @trace((XᵀX/n)⁻¹)@. A smaller trace
--- means higher per-coefficient estimation precision.
-aEfficiency :: [[Double]] -> Double
-aEfficiency xs =
-  let m   = LA.fromLists xs
-      n   = fromIntegral (LA.rows m) :: Double
-      p   = fromIntegral (LA.cols m) :: Double
-      xtx = LA.tr m LA.<> m
-      detV = LA.det xtx
-  in if detV == 0 then 0
-       else
-         let inv = LA.inv (LA.scale (1/n) xtx)
-             tr  = sum [inv `LA.atIndex` (i, i)
-                       | i <- [0 .. round p - 1] :: [Int]]
-         in p / tr
-
--- | Per-column Variance Inflation Factor.
---
--- @VIF_j = 1 / (1 - R²_j)@, where @R²_j@ is the coefficient of
--- determination from regressing column @j@ on the others.
--- @VIF > 10@ is a strong sign of multicollinearity.
-vifList :: [[Double]] -> [Double]
-vifList xs =
-  let m   = LA.fromLists xs
-      p   = LA.cols m
-  in [vifFor m j | j <- [0 .. p - 1]]
-  where
-    vifFor mat j =
-      let yCol  = LA.flatten (mat LA.¿ [j])
-          xCols = [k | k <- [0 .. LA.cols mat - 1], k /= j]
-          xRest = mat LA.¿ xCols
-          beta  = LA.flatten (xRest LA.<\> LA.asColumn yCol)
-          yHat  = xRest LA.#> beta
-          ssRes = LA.sumElements ((yCol - yHat) ^ (2 :: Int))
-          mu    = LA.sumElements yCol / fromIntegral (LA.size yCol)
-          ssTot = LA.sumElements ((yCol - LA.scalar mu) ^ (2 :: Int))
-          r2    = if ssTot == 0 then 0 else 1 - ssRes / ssTot
-      in if r2 >= 1 then 1/0 else 1 / (1 - r2)
-
--- ---------------------------------------------------------------------------
--- Process capability (Cp / Cpk)
--- ---------------------------------------------------------------------------
-
--- | Process capability summary.
---
---   * @capCp  = (USL − LSL) / (6 σ)@
---   * @capCpk = min((USL − μ) / (3 σ), (μ − LSL) / (3 σ))@
---
--- For one-sided variants (no LSL or no USL) only the relevant half of
--- @Cpk@ is used; @Cp@ falls back to that half (so @Cp == Cpk@).
-data Capability = Capability
-  { capCp   :: !Double
-  , capCpk  :: !Double
-  , capMean :: !Double
-  , capSd   :: !Double
-  } deriving (Show, Eq)
-
--- | Two-sided process capability with explicit @LSL@ and @USL@.
-processCapability
-  :: Double            -- ^ LSL (lower spec limit)
-  -> Double            -- ^ USL (upper spec limit)
-  -> LA.Vector Double  -- ^ Sample observations.
-  -> Capability
-processCapability lsl usl xs =
-  let (mu, sd) = meanSd xs
-      cp       = if sd == 0 then 0 else (usl - lsl) / (6 * sd)
-      cpkUpper = if sd == 0 then 0 else (usl - mu) / (3 * sd)
-      cpkLower = if sd == 0 then 0 else (mu - lsl) / (3 * sd)
-      cpk      = min cpkUpper cpkLower
-  in Capability cp cpk mu sd
-
--- | One-sided upper-spec process capability (only @USL@).
-processCapabilityUpper :: Double -> LA.Vector Double -> Capability
-processCapabilityUpper usl xs =
-  let (mu, sd) = meanSd xs
-      cpk      = if sd == 0 then 0 else (usl - mu) / (3 * sd)
-  in Capability cpk cpk mu sd
-
--- | One-sided lower-spec process capability (only @LSL@).
-processCapabilityLower :: Double -> LA.Vector Double -> Capability
-processCapabilityLower lsl xs =
-  let (mu, sd) = meanSd xs
-      cpk      = if sd == 0 then 0 else (mu - lsl) / (3 * sd)
-  in Capability cpk cpk mu sd
-
--- | Process Capability for **Weibull-distributed** characteristics.
---
--- 非正規分布の場合、 6σ では裾を過小評価する。 ISO 22514 / AIAG 推奨の
--- パーセンタイル法:
---
--- > Cp  = (USL − LSL) / (P_{0.99865} − P_{0.00135})
--- > Cpk = min( (USL − median) / (P_{0.99865} − median),
--- >            (median − LSL) / (median − P_{0.00135}) )
---
--- Weibull quantile: @F⁻¹(p) = λ · (−log(1 − p))^{1/k}@
-processCapabilityWeibull
-  :: WeibullFit
-  -> Double            -- ^ LSL
-  -> Double            -- ^ USL
-  -> Capability
-processCapabilityWeibull wf lsl usl =
-  let k   = wfShape wf
-      lam = wfScale wf
-      q p = lam * ((-log (1 - p)) ** (1 / k))
-      pLo  = q 0.00135
-      pHi  = q 0.99865
-      med  = q 0.5
-      spread = pHi - pLo
-      cp   = if spread == 0 then 0 else (usl - lsl) / spread
-      cpkU = if pHi == med then 0 else (usl - med) / (pHi - med)
-      cpkL = if med == pLo then 0 else (med - lsl) / (med - pLo)
-      cpk  = min cpkU cpkL
-  in Capability cp cpk med spread
-
--- | Process Capability for **LogNormal-distributed** characteristics.
---   引数は log-scale の μ, σ (ln X ~ Normal(μ, σ²))。
---
--- > X_p = exp(μ + σ · z_p)
-processCapabilityLogNormal
-  :: Double            -- ^ μ (log scale mean)
-  -> Double            -- ^ σ (log scale sd)
-  -> Double            -- ^ LSL
-  -> Double            -- ^ USL
-  -> Capability
-processCapabilityLogNormal mu sigma lsl usl =
-  let zHi = SD.quantile Normal.standard 0.99865
-      zLo = SD.quantile Normal.standard 0.00135
-      pHi = exp (mu + sigma * zHi)
-      pLo = exp (mu + sigma * zLo)
-      med = exp mu
-      spread = pHi - pLo
-      cp   = if spread == 0 then 0 else (usl - lsl) / spread
-      cpkU = if pHi == med then 0 else (usl - med) / (pHi - med)
-      cpkL = if med == pLo then 0 else (med - lsl) / (med - pLo)
-      cpk  = min cpkU cpkL
-  in Capability cp cpk med spread
-
--- | Process Capability for **Gamma-distributed** characteristics (Phase 23-c)。
---   shape (= k) と scale (= θ) を引数に取る (statistics-0.16 の @gammaDistr@ と同表記)。
---   rate β = 1 / θ を使うユーザは scale = 1/β で渡す。
---
---   分位点法 (ISO 22514) で Cp / Cpk を算出:
---
---   > Cp  = (USL − LSL) / (P_{0.99865} − P_{0.00135})
---   > Cpk = min( (USL − median) / (P_{0.99865} − median),
---   >            (median − LSL) / (median − P_{0.00135}) )
-processCapabilityGamma
-  :: Double            -- ^ shape (k > 0)
-  -> Double            -- ^ scale (θ > 0)
-  -> Double            -- ^ LSL
-  -> Double            -- ^ USL
-  -> Capability
-processCapabilityGamma shape scale lsl usl =
-  let d   = Gamma.gammaDistr shape scale
-      pLo = SD.quantile d 0.00135
-      pHi = SD.quantile d 0.99865
-      med = SD.quantile d 0.5
-      spread = pHi - pLo
-      cp   = if spread == 0 then 0 else (usl - lsl) / spread
-      cpkU = if pHi == med then 0 else (usl - med) / (pHi - med)
-      cpkL = if med == pLo then 0 else (med - lsl) / (med - pLo)
-      cpk  = min cpkU cpkL
-  in Capability cp cpk med spread
-
--- | 非正規 Cp の統一エントリ用 ADT (Phase 23-c)。 spec: doe-spec v0.2 §3.13。
-data NonNormalFit
-  = NNFWeibull   !WeibullFit       -- ^ Weibull MLE 結果
-  | NNFLogNormal !Double !Double    -- ^ log-scale μ, σ
-  | NNFGamma     !Double !Double    -- ^ shape, scale
-  deriving (Show)
-
--- | 非正規分布 fit の type tag で Weibull / LogNormal / Gamma を dispatch。
--- 個別関数 (@processCapabilityWeibull@ 等) と等価、 ADT で取り回したいケース用。
-processCapabilityNonNormal
-  :: NonNormalFit
-  -> Double          -- ^ LSL
-  -> Double          -- ^ USL
-  -> Capability
-processCapabilityNonNormal (NNFWeibull   wf)         = processCapabilityWeibull   wf
-processCapabilityNonNormal (NNFLogNormal mu sigma)   = processCapabilityLogNormal mu sigma
-processCapabilityNonNormal (NNFGamma     k  scale)   = processCapabilityGamma     k  scale
-
--- ---------------------------------------------------------------------------
--- 多変量 Process Capability (Phase 23-d、 spec: doe-spec v0.2 §2.10 / §3.13)
--- ---------------------------------------------------------------------------
-
--- | 多変量 Process Capability の結果。
---
--- @mcMCp@ は Wang-Hubele-Lawrence (1994) 風の体積比ベース:
---
--- > MCp = (det(Σ_T) / det(Σ))^(1/(2p))
---
--- ここで Σ_T = diag(((USL_i − LSL_i) / 6)²) (= 各軸 6σ 相当の理想分散)、
--- Σ は標本共分散、 p は変数数。 単変量 Cp の自然な多変量拡張。
---
--- @mcMCpk@ は中心オフセット penalty を乗じた値:
---
--- > MCpk = MCp · max(0, 1 − sqrt(T²) / 3)
--- > T²   = (μ_data − μ_T)' Σ⁻¹ (μ_data − μ_T)
--- > μ_T  = (LSL + USL) / 2
---
--- @mcInSpecRate@ は spec box (per-variable LSL/USL) の内包率 (実測)。
-data MultivariateCapability = MultivariateCapability
-  { mcNVars       :: !Int
-  , mcMean        :: !(LA.Vector Double)
-  , mcCov         :: !(LA.Matrix Double)
-  , mcMCp         :: !Double
-  , mcMCpk        :: !Double
-  , mcInSpecRate  :: !Double
-  } deriving (Show)
-
--- | 多変量 Cp 計算。 入力 @data@ は n 行 × p 列の観測行列。
--- @specs@ は各変数の (LSL, USL) を **列順** に与える。
---
--- @Left@ を返すケース:
---
---   * @specs@ の長さが列数と一致しない
---   * n < 2 (共分散が定義されない)
---   * 共分散が singular (= det ≈ 0)
-processCapabilityMultivariate
-  :: LA.Matrix Double
-  -> [(Double, Double)]
-  -> Either Text MultivariateCapability
-processCapabilityMultivariate dat specs
-  | p == 0                = Left "processCapabilityMultivariate: empty data (0 columns)"
-  | length specs /= p     = Left "processCapabilityMultivariate: specs length ≠ #columns"
-  | n < 2                 = Left "processCapabilityMultivariate: need at least 2 observations"
-  | any (\(lo, hi) -> hi <= lo) specs =
-      Left "processCapabilityMultivariate: each USL must be > LSL"
-  | abs detSigma < 1e-12  = Left "processCapabilityMultivariate: covariance is singular"
-  | otherwise =
-      Right MultivariateCapability
-        { mcNVars      = p
-        , mcMean       = mu
-        , mcCov        = sigma
-        , mcMCp        = mcp
-        , mcMCpk       = mcpk
-        , mcInSpecRate = inSpec
-        }
-  where
-    n         = LA.rows dat
-    p         = LA.cols dat
-    mu        = LA.scale (1 / fromIntegral n) (LA.fromList [LA.sumElements (col j) | j <- [0 .. p - 1]])
-    col j     = LA.flatten (LA.subMatrix (0, j) (n, 1) dat)
-    centered  = LA.fromRows [ LA.fromList [(dat `LA.atIndex` (i, j)) - (mu `LA.atIndex` j) | j <- [0 .. p - 1]] | i <- [0 .. n - 1] ]
-    sigma     = LA.scale (1 / fromIntegral (n - 1)) (LA.tr centered LA.<> centered)
-    detSigma  = LA.det sigma
-    sigmaT    = LA.diagl [ ((hi - lo) / 6) ** 2 | (lo, hi) <- specs ]
-    detSigmaT = LA.det sigmaT
-    pD        = fromIntegral p :: Double
-    mcp       = (detSigmaT / detSigma) ** (1 / (2 * pD))
-    muT       = LA.fromList [ (lo + hi) / 2 | (lo, hi) <- specs ]
-    diff      = mu - muT
-    invSigma  = LA.inv sigma
-    t2        = diff LA.<.> (invSigma LA.#> diff)
-    penalty   = max 0 (1 - sqrt (max 0 t2) / 3)
-    mcpk      = mcp * penalty
-    inSpec    =
-      let rowsXs = LA.toLists dat
-          inside r = and [ lo <= x && x <= hi | (x, (lo, hi)) <- zip r specs ]
-          k = length (filter inside rowsXs)
-      in fromIntegral k / fromIntegral n
-
--- | Sample mean and unbiased standard deviation.
-meanSd :: LA.Vector Double -> (Double, Double)
-meanSd xs =
-  let n  = LA.size xs
-      nD = fromIntegral n :: Double
-      mu = LA.sumElements xs / nD
-      d  = LA.cmap (subtract mu) xs
-      v  = if n <= 1 then 0
-                     else (d `LA.dot` d) / (nD - 1.0)
-  in (mu, sqrt v)
diff --git a/src/Hanalyze/Design/RSM.hs b/src/Hanalyze/Design/RSM.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/RSM.hs
+++ /dev/null
@@ -1,229 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.RSM
--- Description : 応答曲面法 (RSM) — CCD/Box-Behnken 計画・二次モデル fit・極値の解析解
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Response Surface Methodology (RSM).
---
---   * 'centralComposite' — central composite design (CCD): @2^k@ factorial
---     + axial points + center points.
---   * 'boxBehnken'       — Box-Behnken design: @k@ three-level factors
---     without axial points.
---   * 'quadraticDesign'  — design matrix for the quadratic model
---     (intercept + main + squared + interaction terms).
---   * 'fitQuadratic'     — fit the quadratic regression by least squares.
---   * 'optimumPoint'     — analytically solve for the extremum (max / min)
---     from the fit.
-module Hanalyze.Design.RSM
-  ( CCDType (..)
-  , centralComposite
-  , centralCompositeRotatable
-  , boxBehnken
-  , quadraticDesign
-  , quadraticTermNames
-  , QuadFit (..)
-  , fitQuadratic
-  , optimumPoint
-  , quadBMatrix
-  , canonicalAnalysis
-  ) where
-
-import Data.List (sortOn)
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Numeric.LinearAlgebra as LA
-import Hanalyze.Design.Factorial (twoLevelFactorial)
-
--- ---------------------------------------------------------------------------
--- 中心複合計画 (CCD)
--- ---------------------------------------------------------------------------
-
--- | Central composite design (CCD) type.
-data CCDType
-  = CCC Double  -- ^ Circumscribed: axial distance @α@ (the rotatable
-                --   choice is @(2^k)^{1/4}@).
-  | CCF         -- ^ Face-centered: @α = 1@ (axial points sit on the cube faces).
-  | CCI Double  -- ^ Inscribed: @α = 1@, factorial part scaled by @1/α@.
-  deriving (Show, Eq)
-
--- | Central composite design.
---
--- Composition:
---
---   * @2^k@ factorial part: every @±1@ combination (@2^k@ rows).
---   * @2k@ axial points: @(±α, 0, …, 0)@ for each factor.
---   * @nC@ center points at @(0, …, 0)@.
---
--- @centralComposite k ccdType nC@: @k@ factors and @nC@ centre points.
-centralComposite :: Int -> CCDType -> Int -> [[Double]]
-centralComposite k ccdType nC =
-  let factorial = case ccdType of
-        CCI alpha ->
-          [[v / alpha | v <- row] | row <- twoLevelFactorial k]
-        _ -> twoLevelFactorial k
-      alpha = case ccdType of
-        CCC a   -> a
-        CCF     -> 1.0
-        CCI _   -> 1.0
-      axial = concat
-        [ [ replicate i 0 ++ [-alpha] ++ replicate (k - 1 - i) 0
-          , replicate i 0 ++ [ alpha] ++ replicate (k - 1 - i) 0
-          ]
-        | i <- [0 .. k - 1] ]
-      center = replicate nC (replicate k 0)
-  in factorial ++ axial ++ center
-
--- | Rotatable CCD with @α = (2^k)^{1/4}@.
-centralCompositeRotatable :: Int -> Int -> [[Double]]
-centralCompositeRotatable k nC =
-  let alpha = (fromIntegral (2 ^ k :: Int) :: Double) ** 0.25
-  in centralComposite k (CCC alpha) nC
-
--- ---------------------------------------------------------------------------
--- Box-Behnken 計画
--- ---------------------------------------------------------------------------
-
--- | Box-Behnken design for @k = 3, 4, 5@. Returns @nC@ additional
--- centre points.
---
---   * @k = 3@: 12 corner points + @nC@ centre points.
---   * @k = 4@: 24 corner points + @nC@ centre points.
---   * @k = 5@: 40 corner points + @nC@ centre points.
-boxBehnken :: Int -> Int -> [[Double]]
-boxBehnken k nC
-  | k == 3 = bb3 ++ centers
-  | k == 4 = bb4 ++ centers
-  | k == 5 = bb5 ++ centers
-  | otherwise = error
-      ("boxBehnken: only k = 3, 4, 5 supported (got k = "
-        ++ show k ++ ")")
-  where
-    centers = replicate nC (replicate k 0)
-    -- 因子ペア (i, j) (i < j) の二水準組合せで「他は 0」
-    pairs n = [(i, j) | i <- [0 .. n - 1], j <- [i + 1 .. n - 1]]
-    pairBlock n (i, j) =
-      [ [ if x == i then s1
-          else if x == j then s2
-          else 0
-        | x <- [0 .. n - 1] ]
-      | s1 <- [-1, 1], s2 <- [-1, 1] ]
-    bb3 = concatMap (pairBlock 3) (pairs 3)
-    bb4 = concatMap (pairBlock 4) (pairs 4)
-    bb5 = concatMap (pairBlock 5) (pairs 5)
-
--- ---------------------------------------------------------------------------
--- 二次モデル
--- ---------------------------------------------------------------------------
-
--- | Build the design matrix for a quadratic model.
---
--- Each row @[x_1, …, x_k]@ expands to
--- @[1, x_1, …, x_k, x_1², …, x_k², x_1 x_2, x_1 x_3, …, x_{k-1} x_k]@
--- (intercept, main effects, squared terms, upper-triangle interactions).
---
--- Number of columns: @1 + 2k + k(k-1)/2@.
-quadraticDesign :: [[Double]] -> LA.Matrix Double
-quadraticDesign rows =
-  let k = if null rows then 0 else length (head rows)
-      expand row =
-        let mainE = row
-            sqE   = [x * x | x <- row]
-            interE = [(row !! i) * (row !! j)
-                     | i <- [0 .. k - 1], j <- [i + 1 .. k - 1]]
-        in 1 : mainE ++ sqE ++ interE
-  in LA.fromLists (map expand rows)
-
--- | Column names for the quadratic-model design (e.g.
--- @[\"b0\", \"x1\", \"x2\", \"x1^2\", \"x2^2\", \"x1*x2\"]@).
-quadraticTermNames :: Int -> [Text]
-quadraticTermNames k =
-  ["b0"]
-  ++ [T.pack ("x" ++ show i) | i <- [1 .. k]]
-  ++ [T.pack ("x" ++ show i ++ "^2") | i <- [1 .. k]]
-  ++ [T.pack ("x" ++ show i ++ "*x" ++ show j)
-     | i <- [1 .. k], j <- [i + 1 .. k]]
-
--- | Quadratic-model fit result.
-data QuadFit = QuadFit
-  { qfK    :: Int                -- ^ Number of factors @k@.
-  , qfBeta :: LA.Vector Double   -- ^ Coefficient vector
-                                 --   @[b₀, β_main, β_sq, β_int]@.
-  , qfYHat :: LA.Vector Double   -- ^ Fitted values.
-  , qfR2   :: Double              -- ^ R².
-  } deriving (Show)
-
--- | Fit a quadratic model by least squares.
-fitQuadratic :: [[Double]] -> [Double] -> QuadFit
-fitQuadratic xs ys =
-  let k = if null xs then 0 else length (head xs)
-      x = quadraticDesign xs
-      y = LA.fromList ys
-      beta = LA.flatten (x LA.<\> LA.asColumn y)
-      yHat = x LA.#> beta
-      gm   = LA.sumElements y / fromIntegral (LA.size y)
-      ssT  = LA.sumElements ((y - LA.scalar gm) ^ (2 :: Int))
-      ssR  = LA.sumElements ((y - yHat) ^ (2 :: Int))
-      r2   = if ssT == 0 then 0 else 1 - ssR / ssT
-  in QuadFit k beta yHat r2
-
--- | Solve analytically for the extremum (saddle / max / min) of the
--- fitted quadratic model.
---
--- Writing @ŷ = b₀ + bᵀx + xᵀ B x@, set @∂ŷ/∂x = 0@ to obtain
--- x* = −½ B⁻¹ b。固有値の符号で性質を判定。
---
--- 戻り値: (x*, predicted_y, eigenvalues)
---   eigenvalues 全部 < 0 → 極大
---   eigenvalues 全部 > 0 → 極小
---   混在 → 鞍点
-optimumPoint :: QuadFit -> ([Double], Double, [Double])
-optimumPoint fit =
-  let k     = qfK fit
-      beta  = LA.toList (qfBeta fit)
-      b0    = head beta
-      bMain = take k (drop 1 beta)
-      bSq   = take k (drop (1 + k) beta)
-      bInt  = drop (1 + 2 * k) beta
-      bMat  = quadBMatrix fit
-      bVec  = LA.fromList bMain
-      xStar = LA.toList (LA.scale (-0.5) (LA.inv bMat LA.#> bVec))
-      yStar = b0
-            + sum (zipWith (*) bMain xStar)
-            + sum (zipWith (\b x -> b * x * x) bSq xStar)
-            + sum [ (bInt !! pairIndex k i j) * (xStar !! i) * (xStar !! j)
-                  | i <- [0 .. k - 1], j <- [i + 1 .. k - 1] ]
-      eigs = LA.toList (fst (LA.eigSH (LA.sym bMat)))
-  in (xStar, yStar, eigs)
-  where
-    -- (i, j) ペア (i < j) の β_int 配列内のインデックス
-    pairIndex n i j = sum [n - 1 - p | p <- [0 .. i - 1]] + (j - i - 1)
-
--- | 二次モデルの @B@ 行列 (@ŷ = b₀ + bᵀx + xᵀ B x@ の 2 次係数)。 対角は β_sq、
---   非対角は β_int/2 で対称化。 canonical 解析 / 停留点計算の共通部品。
-quadBMatrix :: QuadFit -> LA.Matrix Double
-quadBMatrix fit =
-  let k     = qfK fit
-      beta  = LA.toList (qfBeta fit)
-      bSq   = take k (drop (1 + k) beta)
-      bInt  = drop (1 + 2 * k) beta
-  in LA.fromLists
-       [ [ if i == j then bSq !! i
-           else
-             let (lo, hi) = if i < j then (i, j) else (j, i)
-                 idx = pairIndex k lo hi
-             in (bInt !! idx) / 2
-         | j <- [0 .. k - 1] ]
-       | i <- [0 .. k - 1] ]
-  where pairIndex n i j = sum [n - 1 - p | p <- [0 .. i - 1]] + (j - i - 1)
-
--- | Canonical 解析。 @B@ 行列の固有分解を返す (固有値, 固有ベクトル) のペア列。
---   固有値の符号で応答曲面の性質が読める (全負=極大 / 全正=極小 / 混在=鞍点)、
---   固有ベクトルは canonical 軸 (停留点周りで応答が最も急/緩に動く coded 方向)。
---   ペアは固有値の昇順。 単位はモデルを当てた座標系 (通常 coded)。
-canonicalAnalysis :: QuadFit -> [(Double, [Double])]
-canonicalAnalysis fit =
-  let (vals, vecs) = LA.eigSH (LA.sym (quadBMatrix fit))
-      pairs = zip (LA.toList vals) (map LA.toList (LA.toColumns vecs))
-  in sortOn fst pairs
diff --git a/src/Hanalyze/Design/Sequential.hs b/src/Hanalyze/Design/Sequential.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Sequential.hs
+++ /dev/null
@@ -1,126 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Design.Sequential
--- Description : 逐次的応答曲面法 (Sequential RSM) — 最急上昇 path と sequential CCD 配置
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Sequential RSM (逐次的応答曲面法)。
---
--- 「初期 design → fit → steepest ascent path → 新中心 → 次 design」 の逐次
--- 最適化ワークフローを支える helper モジュール。 数値の重い部分は
--- @Hanalyze.Design.RSM@ の `fitQuadratic` / `optimumPoint` に委ね、 本モジュール
--- は steepest-ascent path 生成と sequential CCD 配置のみを提供する。
-module Hanalyze.Design.Sequential
-  ( -- * Steepest Ascent
-    SteepestAscentResult (..)
-  , steepestAscent
-  , steepestAscentFromQuad
-    -- * Sequential CCD
-  , SequentialCCDResult (..)
-  , sequentialCCD
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-
-import qualified Hanalyze.Design.RSM   as RSM
-
--- ===========================================================================
--- Steepest Ascent
--- ===========================================================================
-
--- | 最急上昇 / 最急下降 path の結果。
-data SteepestAscentResult = SteepestAscentResult
-  { sarDirection  :: !(LA.Vector Double)
-    -- ^ 単位ベクトル化された steepest 方向 (length k)
-  , sarStepPoints :: ![[Double]]
-    -- ^ 試行点系列 (length nSteps + 1、 先頭 = center)
-  , sarMaximize   :: !Bool
-    -- ^ True = ascent、 False = descent
-  } deriving (Show)
-
--- | 第一階係数 @b = [b_1, ..., b_k]@ から steepest ascent path を生成。
---
--- 方向ベクトル:
---
---   * @maximize = True@ なら @+b / |b|@
---   * @maximize = False@ なら @-b / |b|@
---
--- path: @[center, center + step·d, center + 2·step·d, ..., center + nSteps·step·d]@
---
--- @|b| = 0@ や @k = 0@ の場合は方向 0、 全 point = center を返す。
-steepestAscent
-  :: Bool          -- ^ True = ascent、 False = descent
-  -> [Double]      -- ^ center (k 次元)
-  -> [Double]      -- ^ first-order coefficients @b_1..b_k@
-  -> Double        -- ^ step size (原座標スケール、 > 0 推奨)
-  -> Int           -- ^ 試行点数 (= path 長 = nSteps + 1)
-  -> SteepestAscentResult
-steepestAscent maximize center bCoefs stepSize nSteps =
-  let k       = length center
-      bVec    = LA.fromList bCoefs
-      cVec    = LA.fromList center
-      normB   = sqrt (LA.sumElements (bVec * bVec))
-      dirRaw  = if normB > 0
-                  then LA.scale ((if maximize then 1 else -1) / normB) bVec
-                  else LA.fromList (replicate k 0)
-      points  = [ LA.toList (cVec + LA.scale (fromIntegral i * stepSize) dirRaw)
-                | i <- [0 .. max 0 nSteps]
-                ]
-  in SteepestAscentResult
-       { sarDirection  = dirRaw
-       , sarStepPoints = points
-       , sarMaximize   = maximize
-       }
-
--- | 'RSM.QuadFit' から第一階係数を抽出して steepest ascent。
---
--- `QuadFit` の `qfBeta` レイアウトは @[b0, β_main, β_sq, β_int]@ なので、
--- 主効果 @β_main = b_1..b_k@ を取り出す。
-steepestAscentFromQuad
-  :: Bool                 -- ^ maximize?
-  -> [Double]             -- ^ center
-  -> RSM.QuadFit
-  -> Double               -- ^ step size
-  -> Int                  -- ^ nSteps
-  -> SteepestAscentResult
-steepestAscentFromQuad maximize center fit stepSize nSteps =
-  let k     = RSM.qfK fit
-      beta  = LA.toList (RSM.qfBeta fit)
-      bMain = take k (drop 1 beta)
-  in steepestAscent maximize center bMain stepSize nSteps
-
--- ===========================================================================
--- Sequential CCD
--- ===========================================================================
-
--- | 次の CCD を新しい中心で配置した結果。
-data SequentialCCDResult = SequentialCCDResult
-  { sccdCenter :: ![Double]      -- ^ 新しい design center (原座標)
-  , sccdSpan   :: !Double        -- ^ 片側スパン (coded -1 ~ +1 が原座標で center ± span)
-  , sccdCoded  :: ![[Double]]    -- ^ coded units (-α..+α) の design
-  , sccdReal   :: ![[Double]]    -- ^ 原座標の design (= center + span · coded)
-  } deriving (Show)
-
--- | 新中心と span で次の CCD を配置。
---
--- 内部で @Hanalyze.Design.RSM.centralComposite@ を呼び、 結果を新中心に
--- 平行移動 + スケーリングする。 coded units と原座標の両方を返すので、
--- canvas frontend で「coded で fit、 原座標で表示」 が一発で出来る。
-sequentialCCD
-  :: [Double]            -- ^ 新中心 (k 次元)
-  -> Double              -- ^ 片側 span (> 0)
-  -> Int                 -- ^ 因子数 k
-  -> RSM.CCDType         -- ^ CCD 種別 (Circumscribed / Inscribed / FaceCentered)
-  -> Int                 -- ^ center replications
-  -> SequentialCCDResult
-sequentialCCD center span_ k ccdT centerReps =
-  let coded = RSM.centralComposite k ccdT centerReps
-      real_ = [ zipWith (\c x -> c + span_ * x) center row | row <- coded ]
-  in SequentialCCDResult
-       { sccdCenter = center
-       , sccdSpan   = span_
-       , sccdCoded  = coded
-       , sccdReal   = real_
-       }
diff --git a/src/Hanalyze/Design/SpaceFilling.hs b/src/Hanalyze/Design/SpaceFilling.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/SpaceFilling.hs
+++ /dev/null
@@ -1,185 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Design.SpaceFilling
--- Description : 空間充填計画 (Latin Hypercube / Maximin LHS / Halton) — コンピュータ実験・surrogate モデル用 DoE
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- 空間充填計画 (Space-Filling Designs) — コンピュータ実験 / surrogate
--- モデル用の DoE。
---
--- 提供する方式:
---
---   * 'latinHypercube' — Latin Hypercube Sampling (stratified random)
---   * 'latinHypercubeMaximin' — Maximin LHS (点間最小距離を最大化する局所探索)
---   * 'haltonDesign' — Halton 低偏差列 (決定的、 再現性高)
---
--- 出力は全て @[0, 1)^d@ 上の点。 ユーザは bounds スケーリングを後で行う
--- (`Hanalyze.Stat.QuasiRandom.lhsSamplesIn` 等を参考に)。
-module Hanalyze.Design.SpaceFilling
-  ( SpaceFillingDesign (..)
-  , latinHypercube
-  , latinHypercubeMaximin
-  , haltonDesign
-    -- * 品質指標
-  , designMinDistance
-  ) where
-
-import           Control.Monad             (forM_, when)
-import           Data.IORef                (newIORef, readIORef, writeIORef, modifyIORef')
-import qualified Numeric.LinearAlgebra     as LA
-import           Data.Text                 (Text)
-import qualified System.Random.MWC         as MWC
-
-import qualified Hanalyze.Stat.QuasiRandom as QR
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
--- | 空間充填計画の結果。
-data SpaceFillingDesign = SpaceFillingDesign
-  { sfdMatrix  :: !(LA.Matrix Double)  -- ^ n × d、 @[0, 1)^d@ 上の点
-  , sfdNPoints :: !Int                 -- ^ 行数 n
-  , sfdNDims   :: !Int                 -- ^ 列数 d
-  , sfdMinDist :: !Double              -- ^ 点間最小ユークリッド距離 (大きい方が良い)
-  , sfdMethod  :: !Text                -- ^ "LHS" / "MaximinLHS" / "Halton"
-  } deriving (Show)
-
--- ===========================================================================
--- 公開関数
--- ===========================================================================
-
--- | Latin Hypercube Sampling — 各次元のセル @[i/n, (i+1)/n)@ を 1 度ずつ
--- ランダム順序で埋める。 iid uniform より初期被覆良。
-latinHypercube :: Int            -- ^ 点数 n
-               -> Int            -- ^ 次元 d
-               -> MWC.GenIO
-               -> IO SpaceFillingDesign
-latinHypercube n d gen
-  | n < 1 || d < 1 = pure SpaceFillingDesign
-      { sfdMatrix  = (0 LA.>< 0) []
-      , sfdNPoints = 0
-      , sfdNDims   = 0
-      , sfdMinDist = 0
-      , sfdMethod  = "LHS"
-      }
-  | otherwise = do
-      pts <- QR.lhsSamples n d gen
-      let mat = LA.fromLists pts
-      pure SpaceFillingDesign
-        { sfdMatrix  = mat
-        , sfdNPoints = n
-        , sfdNDims   = d
-        , sfdMinDist = designMinDistance mat
-        , sfdMethod  = "LHS"
-        }
-
--- | Maximin LHS — 初期 LHS から始めて、 ランダム (列, 行ペア) で値交換を試行、
--- 点間最小距離が改善するなら採用、 を @nTries@ 回 (= 全試行回数) 反復。
---
--- 結果は **LHS の stratification 性質を保ったまま** 距離を最大化したもの。
--- @nTries = 1000@ 程度で実用的な改善が得られる (n, d による)。
-latinHypercubeMaximin :: Int            -- ^ 点数 n
-                     -> Int            -- ^ 次元 d
-                     -> Int            -- ^ 試行回数 (= swap 候補数の上限)
-                     -> MWC.GenIO
-                     -> IO SpaceFillingDesign
-latinHypercubeMaximin n d nTries gen
-  | n < 2 || d < 1 = do
-      -- 1 点しかなければ swap 不能、 通常 LHS を返す
-      lhs <- latinHypercube n d gen
-      pure lhs { sfdMethod = "MaximinLHS" }
-  | otherwise = do
-      initPts  <- QR.lhsSamples n d gen
-      matRef   <- newIORef (LA.fromLists initPts)
-      distRef  <- do
-        let m0 = LA.fromLists initPts
-        newIORef (designMinDistance m0)
-      forM_ [1 .. nTries] $ \_ -> do
-        -- ランダムに 1 列 k 選び、 その列の 2 行 i, j を swap
-        k <- MWC.uniformR (0, d - 1) gen
-        i <- MWC.uniformR (0, n - 1) gen
-        j <- MWC.uniformR (0, n - 1) gen
-        when (i /= j) $ do
-          curMat   <- readIORef matRef
-          let newMat  = swapEntries curMat i j k
-              newDist = designMinDistance newMat
-          curDist <- readIORef distRef
-          when (newDist > curDist) $ do
-            writeIORef matRef  newMat
-            writeIORef distRef newDist
-      finalMat  <- readIORef matRef
-      finalDist <- readIORef distRef
-      pure SpaceFillingDesign
-        { sfdMatrix  = finalMat
-        , sfdNPoints = n
-        , sfdNDims   = d
-        , sfdMinDist = finalDist
-        , sfdMethod  = "MaximinLHS"
-        }
-
--- | Halton 低偏差列ベースの決定的 design。 同じ @(n, d)@ で必ず同じ点集合を
--- 返す (再現性目的)。
-haltonDesign :: Int          -- ^ 点数 n
-             -> Int          -- ^ 次元 d
-             -> SpaceFillingDesign
-haltonDesign n d
-  | n < 1 || d < 1 = SpaceFillingDesign
-      { sfdMatrix  = (0 LA.>< 0) []
-      , sfdNPoints = 0
-      , sfdNDims   = 0
-      , sfdMinDist = 0
-      , sfdMethod  = "Halton"
-      }
-  | otherwise =
-      let mat = QR.haltonMatrix n d
-      in SpaceFillingDesign
-           { sfdMatrix  = mat
-           , sfdNPoints = n
-           , sfdNDims   = d
-           , sfdMinDist = designMinDistance mat
-           , sfdMethod  = "Halton"
-           }
-
--- ===========================================================================
--- 品質指標
--- ===========================================================================
-
--- | 点間ユークリッド距離の最小値。 空 design (行数 < 2) では 0。
-designMinDistance :: LA.Matrix Double -> Double
-designMinDistance mat
-  | LA.rows mat < 2 = 0
-  | otherwise =
-      let n   = LA.rows mat
-          rs  = LA.toRows mat
-          pairs = [ (i, j) | i <- [0 .. n - 2], j <- [i + 1 .. n - 1] ]
-          dist (i, j) =
-            let di = rs !! i
-                dj = rs !! j
-                v  = di - dj
-            in sqrt (LA.sumElements (v * v))
-      in minimum (map dist pairs)
-
--- ===========================================================================
--- 内部 helper
--- ===========================================================================
-
--- | Matrix の (i, k) 要素と (j, k) 要素を入れ替えた新しい Matrix。
-swapEntries :: LA.Matrix Double -> Int -> Int -> Int -> LA.Matrix Double
-swapEntries mat i j k =
-  let nR = LA.rows mat
-      nC = LA.cols mat
-      a  = LA.atIndex mat (i, k)
-      b  = LA.atIndex mat (j, k)
-      rows = LA.toLists mat
-      update r idx newVal =
-        take k r ++ [newVal] ++ drop (k + 1) r
-      _ = (nR, nC)  -- silence
-  in LA.fromLists
-       [ if rIdx == i then update (rows !! rIdx) k b
-         else if rIdx == j then update (rows !! rIdx) k a
-         else rows !! rIdx
-       | rIdx <- [0 .. nR - 1]
-       ]
diff --git a/src/Hanalyze/Design/Taguchi.hs b/src/Hanalyze/Design/Taguchi.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Taguchi.hs
+++ /dev/null
@@ -1,286 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.Taguchi
--- Description : タグチメソッド — SN 比・内側/外側配置・要因効果によるロバスト設計解析
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- The Taguchi method — an analytical layer that extends orthogonal
--- arrays ('Hanalyze.Design.Orthogonal') for robust design.
---
--- Main building blocks:
---
--- 1. **Signal-to-Noise ratio (SN)** — quantifies variability:
---
---    * @SmallerBetter@   — smaller-the-better (e.g. defect rate),
---      @η = -10 log₁₀(Σ y²/n)@.
---    * @LargerBetter@    — larger-the-better (e.g. strength),
---      @η = -10 log₁₀(Σ (1/y²)/n)@.
---    * @NominalBest@     — nominal-the-best (mean/variance),
---      @η = 10 log₁₀(μ²/σ²)@.
---    - NominalBestTarget m: 目標値 m への二乗平均偏差    η = -10 log₁₀(Σ (y-m)²/n)
---
--- 2. **内側/外側配置 (Inner/Outer Arrays)** — 制御因子 (内側) と
---    雑音因子 (外側) のクロス設計。各内側試行で外側全条件を観測 → 行ごとに
---    SN 比を計算 → 雑音に頑健な制御因子の組合せを発見。
---
--- 3. **要因効果 (FactorEffect)** — 各因子の各水準での平均 SN 比。
---    最良水準 = 平均 SN 比が最大の水準。
-module Hanalyze.Design.Taguchi
-  ( -- * SN 比
-    SNType (..)
-  , snTypeName
-  , snRatio
-  , snRatioRows
-  , SNDetails (..)
-  , snRatioWithDetails
-    -- * Factor effects and optimal levels
-  , FactorEffect (..)
-  , analyzeSN
-  , optimalLevels
-  , predictSN
-  , FactorEffectExt (..)
-  , factorEffectsTable
-    -- * Inner/outer arrays
-  , InnerOuterDesign (..)
-  , makeInnerOuter
-  , renderInnerOuterCSV
-  ) where
-
-import Data.Text (Text)
-import qualified Data.Text as T
-import Text.Printf (printf)
-
-import Hanalyze.Design.Orthogonal
-  ( OA (..)
-  , AssignedDesign (..)
-  , FactorSpec (..)
-  , LevelValue (..)
-  )
-
--- ---------------------------------------------------------------------------
--- SN 比
--- ---------------------------------------------------------------------------
-
--- | Signal-to-noise ratio rule. Taguchi's four canonical cases.
-data SNType
-  = SmallerBetter
-    -- ^ Smaller-the-better: @y → 0@ is desired (defect rates, errors,
-    --   noise). @η = -10 log₁₀(Σ y²/n)@.
-  | LargerBetter
-    -- ^ Larger-the-better: @y → ∞@ is desired (strength, lifetime,
-    --   efficiency). @η = -10 log₁₀(Σ (1/y²)/n)@.
-  | NominalBest
-    -- ^ Nominal-the-best: hold the mean and minimize variance.
-    --   @η = 10 log₁₀(μ²/σ²)@.
-  | NominalBestTarget Double
-    -- ^ Nominal-the-best with explicit target @m@:
-    --   @η = -10 log₁₀(Σ(y - m)²/n)@.
-  deriving (Show, Eq)
-
--- | Display name of an 'SNType'.
-snTypeName :: SNType -> Text
-snTypeName SmallerBetter         = "smaller-the-better"
-snTypeName LargerBetter          = "larger-the-better"
-snTypeName NominalBest           = "nominal-the-best"
-snTypeName (NominalBestTarget m) =
-  "nominal-the-best (target=" <> T.pack (printf "%g" m) <> ")"
-
--- | Compute the SN ratio @η@ (in dB) from one run's repeated
--- observations.
-snRatio :: SNType -> [Double] -> Double
-snRatio _    [] = 0
-snRatio sn   ys = case sn of
-  SmallerBetter ->
-    let msd = sum [ y * y | y <- ys ] / fromIntegral n
-    in -10 * logBase 10 (max msd epsLog)
-  LargerBetter ->
-    let msd = sum [ 1 / max (y * y) epsLog | y <- ys ] / fromIntegral n
-    in -10 * logBase 10 (max msd epsLog)
-  NominalBest ->
-    let mu  = sum ys / fromIntegral n
-        var = sum [ (y - mu) ^ (2 :: Int) | y <- ys ]
-                / fromIntegral (max 1 (n - 1))
-    in if var <= 0
-         then 0
-         else 10 * logBase 10 ((mu * mu) / max var epsLog)
-  NominalBestTarget target ->
-    let msd = sum [ (y - target) ^ (2 :: Int) | y <- ys ]
-                / fromIntegral n
-    in -10 * logBase 10 (max msd epsLog)
-  where
-    n      = length ys
-    epsLog = 1e-30   -- 0 で log を取るのを防ぐ
-
--- | For an @inner-run × outer-run@ observation matrix, return the SN
--- ratio of each inner run.
-snRatioRows :: SNType -> [[Double]] -> [Double]
-snRatioRows sn = map (snRatio sn)
-
--- | SN ratio bundled with the descriptive statistics that are usually
--- reported alongside it (sample mean, unbiased variance, sample size).
-data SNDetails = SNDetails
-  { sdSN       :: !Double
-  , sdMean     :: !Double
-  , sdVariance :: !Double
-  , sdN        :: !Int
-  } deriving (Show, Eq)
-
--- | 'snRatio' plus the matching @mean@ / @variance@ / @n@ so that UIs
--- can render the trio in one row.
-snRatioWithDetails :: SNType -> [Double] -> SNDetails
-snRatioWithDetails sn ys =
-  let n  = length ys
-      mu = if n == 0 then 0 else sum ys / fromIntegral n
-      v  = if n <= 1
-             then 0
-             else sum [ (y - mu) ^ (2 :: Int) | y <- ys ]
-                    / fromIntegral (n - 1)
-  in SNDetails (snRatio sn ys) mu v n
-
--- ---------------------------------------------------------------------------
--- 要因効果と最適水準
--- ---------------------------------------------------------------------------
-
--- | Per-level mean SN ratio for a single factor.
-data FactorEffect = FactorEffect
-  { feFactor    :: Text          -- ^ Factor name.
-  , feLevels    :: [LevelValue]  -- ^ Level values in order.
-  , feSNByLevel :: [Double]      -- ^ Mean SN ratio at each level.
-  } deriving (Show, Eq)
-
--- | From the per-inner-run SN ratios, compute the mean SN ratio for
--- every (factor, level) pair.
---
--- For each inner run @i@, gather the @SN_i@ values where factor @j@
--- has level @k@ and average them.
-analyzeSN :: AssignedDesign -> [Double] -> [FactorEffect]
-analyzeSN ad sns =
-  let factors = adFactors ad
-      table   = oaTable (adArray ad)
-      runs    = zip table sns                    -- (oaRow, sn_i)
-  in [ FactorEffect
-         { feFactor    = fsName f
-         , feLevels    = fsLevels f
-         , feSNByLevel = meanByLevel j (length (fsLevels f)) runs
-         }
-     | (j, f) <- zip [0..] factors ]
-  where
-    meanByLevel j nLvl runs =
-      [ let xs = [ sn | (oaRow, sn) <- runs
-                       , length oaRow > j
-                       , (oaRow !! j) == k ]
-        in if null xs then 0
-                      else sum xs / fromIntegral (length xs)
-      | k <- [1 .. nLvl] ]
-
--- | For each factor, the best level (the one with the largest mean SN)
--- together with that SN ratio.
-optimalLevels :: [FactorEffect] -> [(Text, LevelValue, Double)]
-optimalLevels effects =
-  [ let (ix, snBest) = argmax (feSNByLevel fe)
-        lvl = if ix < length (feLevels fe)
-                then feLevels fe !! ix
-                else LText "?"
-    in (feFactor fe, lvl, snBest)
-  | fe <- effects ]
-  where
-    argmax xs = foldl1 better (zip [0::Int ..] xs)
-    better a@(_, va) b@(_, vb) = if vb > va then b else a
-
--- | Predicted SN ratio at the best-level combination (main-effects-only
--- additive model):
---
--- @η_pred = mean(η_all) + Σ_j (η_best_j − mean(η_all))@.
-predictSN :: [FactorEffect] -> [Double] -> Double
-predictSN effects allSN =
-  let muAll = if null allSN then 0
-              else sum allSN / fromIntegral (length allSN)
-      maxPerFactor = [ maximum (feSNByLevel fe) | fe <- effects ]
-  in muAll + sum [ best - muAll | best <- maxPerFactor ]
-
--- | 'FactorEffect' enriched with the range (@max − min@ across levels)
--- and the relative contribution @range_j / Σ range_k@. Useful for
--- response-table style UI that need a single struct per factor.
-data FactorEffectExt = FactorEffectExt
-  { feeFactor       :: !Text
-  , feeLevels       :: ![LevelValue]
-  , feeSNByLevel    :: ![Double]
-  , feeRange        :: !Double
-  , feeContribution :: !Double  -- ^ @0 ≤ contribution ≤ 1@.
-  } deriving (Show, Eq)
-
--- | Factor-effect table with range + contribution. Calls 'analyzeSN'
--- internally, so the per-level means match exactly.
-factorEffectsTable :: AssignedDesign -> [Double] -> [FactorEffectExt]
-factorEffectsTable ad sns =
-  let effects = analyzeSN ad sns
-      ranges  = [ rangeOf (feSNByLevel fe) | fe <- effects ]
-      total   = sum ranges
-  in zipWith
-       (\fe r ->
-          FactorEffectExt
-            { feeFactor       = feFactor fe
-            , feeLevels       = feLevels fe
-            , feeSNByLevel    = feSNByLevel fe
-            , feeRange        = r
-            , feeContribution = if total <= 0 then 0 else r / total
-            })
-       effects ranges
-  where
-    rangeOf [] = 0
-    rangeOf xs = maximum xs - minimum xs
-
--- ---------------------------------------------------------------------------
--- 内側/外側配置
--- ---------------------------------------------------------------------------
-
--- | Inner × outer cross design: inner is the control-factor array,
--- outer the noise-factor array.
-data InnerOuterDesign = InnerOuterDesign
-  { ioInner :: AssignedDesign
-  , ioOuter :: AssignedDesign
-  } deriving (Show, Eq)
-
--- | Construct an 'InnerOuterDesign'.
-makeInnerOuter :: AssignedDesign -> AssignedDesign -> InnerOuterDesign
-makeInnerOuter = InnerOuterDesign
-
--- | Render the cross design as CSV. Each row corresponds to one inner
--- run; columns hold the inner-factor values followed by empty cells
--- @y_outer1..y_outerM@ for the user to fill in measurements. The outer
--- run table is appended afterwards.
-renderInnerOuterCSV :: InnerOuterDesign -> Text
-renderInnerOuterCSV io =
-  let inner   = ioInner io
-      outer   = ioOuter io
-      innerN  = length (adRows inner)
-      outerN  = length (adRows outer)
-      innerHs = map fsName (adFactors inner)
-      outerHs = map fsName (adFactors outer)
-      yLabels = [ "y_outer" <> T.pack (show (k :: Int)) | k <- [1 .. outerN] ]
-      header  = T.intercalate ","
-                  ("InnerRun" : innerHs ++ yLabels)
-      rows    = [ T.intercalate ","
-                    (T.pack (show i)
-                     : map fmtLV (adRows inner !! (i - 1))
-                     ++ replicate outerN "")
-                | i <- [1 .. innerN] ]
-      -- 外側表 (参考情報) を末尾に追記
-      footer  = "\n# Outer array (noise factors): "
-                <> T.intercalate ", " outerHs <> "\n"
-                <> T.intercalate "\n"
-                     [ "# OuterRun " <> T.pack (show k) <> ": "
-                       <> T.intercalate ", "
-                            (zipWith (\h v -> h <> "=" <> fmtLV v)
-                              outerHs (adRows outer !! (k - 1)))
-                     | k <- [1 .. outerN] ]
-                <> "\n"
-  in header <> "\n" <> T.intercalate "\n" rows <> "\n" <> footer
-
--- | LevelValue を CSV 用に文字列化。整数値は 150、小数は 0.1 形式。
-fmtLV :: LevelValue -> Text
-fmtLV (LText t) = t
-fmtLV (LNumeric d)
-  | d == fromIntegral (round d :: Integer) = T.pack (show (round d :: Integer))
-  | otherwise                              = T.pack (printf "%g" d)
diff --git a/src/Hanalyze/Design/Workflow.hs b/src/Hanalyze/Design/Workflow.hs
deleted file mode 100644
--- a/src/Hanalyze/Design/Workflow.hs
+++ /dev/null
@@ -1,1465 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Design.Workflow
--- Description : DOE ワークフロー層 — 低レベル設計関数を設計オブジェクト Design に束ねる R 流の対話的入口
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- DOE ワークフロー層 (Phase 78) — 散在する低レベル設計関数 (`Design.Factorial`/`RSM` 等・
---   生 @[[Double]]@ 返し) を **設計オブジェクト `Design`** に束ね、 R 流の対話的ワークフローに
---   載せる玄関。
---
---   * `factorialDesign` / `centralCompositeDesign` — 因子 (名前 + 実値の下限/上限) から `Design` を作る
---     pure コンストラクタ。 `Design` は **coded 設計行列 + モデル formula の含意**を運ぶ。
---   * `designTable` — 実行用の **runsheet** (uncoded 実値・因子名列 + run 番号) を出す。
---     戻り値 @[(Text,[Double])]@ は 'ColumnSource' ゆえそのまま @df |->@ にも載る。
---   * `designFormula` — 設計種別からモデル formula を生成 (要因計画 = 全交互作用
---     @y ~ x1 * x2 * …@、 RSM = 2 次 @y ~ x1 + x2 + x1:x2 + I(x1^2) + I(x2^2)@)。
---
---   解析 (`designModel`) は `Hanalyze.Fit` 側 (formula → 既存 LM 当てはめ)。
---   ★coded/uncoded の要点: fit を coded でやるか uncoded でやるかは**予測に影響しない**
---   (同一項の LM は再パラメータ化・予測/R²/profiler は同値)。 だから fit は uncoded (自然単位)
---   のまま — 係数がそのまま実単位で読める。 coding が実質的に効くのは**スケール依存な最適化幾何**
---   (停留点方向・canonical 軸・steepest ascent 方向) だけ。 そこは `rsmAnalysis` /
---   `steepestAscentNatural` が内部で coded の計量を使い、 結果を**自然単位で報告**する
---   (Phase 78.G-d)。 runsheet は一貫して実験者向けの uncoded 実値。
-module Hanalyze.Design.Workflow
-  ( -- * 設計オブジェクト
-    DesignFactor (..)
-  , FactorKind (..)
-  , FactorScale (..)
-  , DesignKind (..)
-  , Design (..)
-    -- * 因子の smart constructor (連続 / 数値順序 / カテゴリ)
-  , contFactor
-  , contFactorLog
-  , numFactor
-  , catFactor
-    -- * コンストラクタ (pure)
-  , factorialDesign
-  , centralCompositeDesign
-  , boxBehnkenDesign
-    -- * 一部実施要因 (run 削減) — Phase 78.G
-  , Resolution (..)
-  , resNum
-  , fractionalDesign
-  , fractionalDesignGen
-  , fractionalDesignInter
-  , fractionalDesignGenInter
-  , fractionalCatalog
-  , fracResolution
-  , aliasStructure
-    -- * Taguchi 直交表 (2 水準スクリーニング) — Phase 78.G-a
-  , OATable (..)
-  , taguchiDesign
-  , taguchiDesignOA
-    -- * 最適計画 (D/A/I/E/G-最適・カスタム formula) — Phase 78.G-b1
-  , OptCriterion (..)
-  , optimalDesign
-  , optimalDesignWith
-  , optimalDesignLevels
-    -- ** モデル指定 効果 DSL (Formula の糖衣)
-  , mainEffects
-  , twoWay
-  , quadratic
-    -- ** 効果 DSL / Formula → Custom.Model 変換 (Phase 78.M M3)
-  , formulaToCustomModel
-    -- * 完全カスタムデザインエンジン (pure・座標交換 / 階層構造 / 制約) — Phase 79
-  , CustomSpec (..)
-  , customSpec
-  , customDesign
-  , Structure (..)
-  , splitPlot
-  , stripPlot
-  , blocked
-  , Constraint (..)
-  , ConstraintRel (..)
-  , ConstraintGuard (..)
-  , FactorValue (..)
-    -- ** 自然単位の制約 (推奨・Phase 82)
-  , NatConstraint (..)
-  , natLeq
-  , natGeq
-  , natEq
-  , natForbid
-    -- * 取り出し
-  , designFactorNames
-  , designTable
-  , designFrame
-  , designFrameRound
-  , designFormula
-    -- * 応答曲面 解析 (自然単位で報告) — Phase 78.G-d
-  , RSMNature (..)
-  , RSMReport (..)
-  , rsmAnalysis
-  , steepestAscentNatural
-    -- * 設計の保存 / DataFrame からの復元 — Phase 78.K
-  , saveDesign
-  , planFromFrame
-  ) where
-
-import           Data.List (sort, subsequences, (\\), foldl1', nub, minimumBy, elemIndex, transpose, find)
-import           Data.Ord (comparing)
-import           Data.Text (Text)
-import qualified Data.Text as T
-import qualified Data.Vector as V
-import qualified DataFrame.Internal.Column    as DX
-import qualified DataFrame.Internal.DataFrame  as DX
-import qualified DataFrame.IO.CSV as DXIO
-import qualified Numeric.LinearAlgebra as LA
-
-import           Hanalyze.DataIO.Convert (getDoubleVec, getTextVec)
-
-import           Hanalyze.Design.Custom.Constraint
-                   (Constraint (..), ConstraintRel (..), ConstraintGuard (..), FactorValue (..))
-import qualified Hanalyze.Design.Custom.Factor as CF
-import qualified Hanalyze.Design.Custom.Model  as CMd
-import qualified Hanalyze.Design.Custom.Coordinate as CX
-import qualified Hanalyze.Design.Custom.Structured as ST
-import qualified Data.Vector.Storable as VS
-import           Hanalyze.Design.Factorial (fullFactorial, fractionalFactorial)
-import           Hanalyze.Design.RSM
-                   ( centralCompositeRotatable, boxBehnken
-                   , QuadFit (..), fitQuadratic, optimumPoint, canonicalAnalysis )
-import           Hanalyze.Design.Sequential
-                   (SteepestAscentResult (..), steepestAscentFromQuad)
-import           Hanalyze.Design.Orthogonal
-                   (OA (..), l4, l8, l9, l12, l16, l18, l27)
-import           Hanalyze.Design.Optimal   (OptCriterion (..))
-import qualified Hanalyze.Design.Optimal as OPT
-import           Hanalyze.Model.Formula
-                   (Formula (..), Term (..), BinOp (..), prettyFormula)
-import           Hanalyze.Model.Formula.RFormula (parseRFormula)
-import           Hanalyze.Model.Formula.Frame    (modelFrame)
-import           Hanalyze.Model.Formula.Design   (designMatrixF)
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
--- | DOE 因子 (Phase 78.G-b2)。 **識別子** ('dfName') と **性質** ('dfKind') を分離し、
---   accessor は全て total (partial field を作らない)。 構築は smart constructor
---   'contFactor' / 'numFactor' / 'catFactor' で行う。
---
---   Phase 79: 因子は純粋に因子であり、 どの因子がどの階層 (whole-plot / block) に属するかは
---   因子ではなく 'CustomSpec' の 'Structure' が **名前で** 持つ (役割 'dfRole' は撤去)。
-data DesignFactor = DesignFactor
-  { dfName :: !Text          -- ^ 因子名 (runsheet の列名・formula の項)
-  , dfKind :: !FactorKind    -- ^ 連続 ('Cont') / 数値順序 ('Num') / カテゴリ ('Cat')
-  } deriving (Show, Eq)
-
--- | 連続因子の**スケール** (Phase 82.3)。 coded @[-1,1]@ 軸を自然単位へどう写すか。
---
---   * 'SLinear' — 線形。 @nat = center + coded·half@ (既定)。
---   * 'SLog'    — 対数 (幾何)。 @nat = 10^(logCenter + coded·logHalf)@。 桁が大きく違う
---     因子 (触媒濃度 0.01〜10 等) の水準・中心点を幾何的に等間隔にする。 @lo, hi > 0@ 必須。
-data FactorScale = SLinear | SLog
-  deriving (Show, Eq)
-
--- | 因子の性質。 因子1つは連続・数値順序・カテゴリの**いずれか一つ**で、 混在不正状態は表現不能。
---
---   * 'Cont' — 2 端点連続 + スケール ('FactorScale')。 coded @-1@ = 下限、 @+1@ = 上限。
---     線形なら uncoded 実値 = @center + coded·halfRange@、 対数なら幾何 (下記 'FactorScale')。
---     Taguchi では 2 水準列に載る。
---   * 'Num'  — **数値順序水準リスト** (Phase 78.G-a2)。 3 水準以上の連続量 (温度 150/165/180 等) を
---     順序付き実値で持つ。 coded は水準リストの位置 index (@0,1,2,…@)、 runsheet/designFrame では
---     **実水準値** (Double) に戻る。 formula は **直交多項式** @opoly(name, 水準数−1)@
---     (linear+quadratic…) で載り、 実測間隔で直交分解する (等間隔前提を置かない)。
---   * 'Cat'  — カテゴリ (順序なし) 水準名リスト。 coded は位置 index、 runsheet では水準名 (Text)。
---     formula は主効果名 (engine が contrast 展開)。
-data FactorKind
-  = Cont !Double !Double !FactorScale  -- ^ 連続因子 (下限, 上限, スケール)
-  | Num  ![Double]        -- ^ 数値順序因子 (順序付き水準値リスト) → opoly
-  | Cat  ![Text]          -- ^ カテゴリ因子 (水準名リスト) → contrast
-  deriving (Show, Eq)
-
--- | 連続因子の smart constructor (線形スケール)。 @contFactor "temp" (150, 180)@。
-contFactor :: Text -> (Double, Double) -> DesignFactor
-contFactor n (lo, hi) = DesignFactor n (Cont lo hi SLinear)
-
--- | **対数スケール**連続因子の smart constructor (Phase 82.3)。 @contFactorLog "conc" (0.01, 10)@。
---   coded 軸は従来通り @[-1,1]@ だが、 自然単位へは幾何的 (@10^…@) に写す。 水準・中心点が
---   幾何等間隔になり、 桁の異なる因子を扱える。 @lo, hi > 0@ 必須 (負/零は log 不能)。
-contFactorLog :: Text -> (Double, Double) -> DesignFactor
-contFactorLog n (lo, hi) = DesignFactor n (Cont lo hi SLog)
-
--- | 数値順序因子の smart constructor (Phase 78.G-a2)。 @numFactor "temp" [150, 165, 180]@。
---   3 水準以上の連続量を Taguchi 3 水準表 (L9/L18/L27) に載せ、 実測間隔の直交多項式
---   (@opoly@) で linear+quadratic 分解する。 実水準値をそのまま渡す (等間隔でなくてよい)。
-numFactor :: Text -> [Double] -> DesignFactor
-numFactor n levels = DesignFactor n (Num levels)
-
--- | カテゴリ因子の smart constructor。 @catFactor "catalyst" ["A", "B", "C"]@。
-catFactor :: Text -> [Text] -> DesignFactor
-catFactor n levels = DesignFactor n (Cat levels)
-
--- | 設計種別 (モデル formula の含意を決める)。
-data DesignKind
-  = KFactorial   -- ^ 要因計画 → 全交互作用モデル
-  | KRSM         -- ^ 応答曲面 → 2 次モデル
-  | KFractional  -- ^ 一部実施要因 → **主効果のみ** (交互作用は交絡ゆえ主効果限定)
-  | KFracInter ![[Int]]
-      -- ^ 一部実施要因 (**交互作用込み**・'fractionalDesignInter')。 generator を保持し、
-      --   'designFormula' が主効果 + **主効果と交絡しない 2 因子交互作用の代表** (交絡群ごと 1 個) を
-      --   生成する。 交絡構造は 'aliasStructure' で確認できる。
-  | KCustom !Formula
-      -- ^ 最適計画 ('optimalDesign') → モデル formula を**焼き込む**。 応答は placeholder
-      --   ('formResponse') を持ち、 'designModel'/'designFormula' で実応答名に差し替わる。
-  | KStructured ![(Text, [Int])] !Formula
-      -- ^ 完全カスタムデザイン ('customDesign'・Phase 79)。 **群列** (@[(群列名, 各 run の群 ID)]@ の
-      --   リスト) + 焼き込み formula を保持する。 CRD = @[]@、 SplitPlot = @[("wholePlot", ids)]@、
-      --   StripPlot = @[("wholePlot", wpIds), ("strip", stripIds)]@、 Blocked = @[("block", ids)]@。
-      --   'designFrame' は各群列を **Text ラベル** (@wp0…@ / @strip0…@ / @blk0…@) で追加し、
-      --   'designModelHBM' @[ranIntercept 群列名, …]@ が階層効果として当てられる (round-trip)。
-      --   固定効果 formula は 'KCustom' 同様 'designFormula' で応答名に差し替わる。
-  deriving (Show, Eq)
-
--- | 設計オブジェクト = 因子 + coded 設計行列 (各行 = 1 run・列 = 因子) + 種別。
-data Design = Design
-  { dsFactors :: ![DesignFactor]
-  , dsCoded   :: ![[Double]]      -- ^ coded 座標 (±1 / ±α / 0)
-  , dsKind    :: !DesignKind
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- Phase 79: 完全カスタムデザインエンジン (Structure / CustomSpec)
--- ---------------------------------------------------------------------------
-
--- | 実験のランダム化 / 階層構造 = 共分散 @M@ を決める。 総称 (v1 はエンジンが 4 種を実装)。
---   どの因子がどの層に属するかは因子 ('DesignFactor') ではなく **この構造が名前で持つ**。
---   'CustomSpec' の 'csStructure' に載る (既定 'CRD')。
---
---   * 'CRD' — 完全ランダム化 (@M = I@)。 既定。 座標交換 D-最適 (per-cell ムーブ)。
---   * 'SplitPlot' — whole-plot 因子が群内で一定 (@M = I + η·Z Zᵀ@)。 群単位ムーブ。
---   * 'StripPlot' — whole-plot × strip の直交 2 階層 (@M = I + ηW·Z_W Z_Wᵀ + ηS·Z_S Z_Sᵀ@)。
---   * 'Blocked' — ランダムブロック (@M = I + η·Z_B Z_Bᵀ@)。 全因子がブロック内で自由。
---
---   v1 未実装の構造 (多段ネスト・複数交差 RE) は 'customDesign' が
---   @unsupported structure@ で error になる (総称ゆえコンストラクタ追加のみで拡張できる)。
-data Structure
-  = CRD
-      -- ^ 完全ランダム化 (@M = I@)。 既定。
-  | SplitPlot
-      { spWhole   :: ![Text]    -- ^ whole-plot 因子名 (群内で一定)
-      , spNWhole  :: !Int       -- ^ whole-plot 数
-      , spEta     :: !Double    -- ^ η = σ²_WP / σ² (既定 1.0)
-      , spColName :: !Text      -- ^ designFrame に出す群列名 (既定 "wholePlot")
-      }
-  | StripPlot
-      { stWhole    :: ![Text], stNWhole :: !Int, stEtaW :: !Double, stWholeCol :: !Text
-      , stStrip    :: ![Text], stNStrip :: !Int, stEtaS :: !Double, stStripCol :: !Text
-      }
-      -- ^ whole-plot × strip の直交 2 階層。
-  | Blocked
-      { blkNBlocks :: !Int, blkEta :: !Double, blkColName :: !Text }
-      -- ^ ランダムブロック。 全因子がブロック内で自由 (block は run 割付のみ)。
-  deriving (Eq, Show)
-
--- | 'SplitPlot' の smart constructor。 η = 1.0・群列名 = @"wholePlot"@ を既定にする。
---   @splitPlot ["temp"] 4@ = temp を whole-plot 因子、 whole-plot 数 4。
-splitPlot :: [Text] -> Int -> Structure
-splitPlot whole nWhole = SplitPlot whole nWhole 1.0 "wholePlot"
-
--- | 'StripPlot' の smart constructor。 ηW = ηS = 1.0・群列名 = @"wholePlot"@ / @"strip"@ を既定に。
---   @stripPlot ["A"] 3 ["B"] 4@ = A が whole-plot (3 群) × B が strip (4 群)。
-stripPlot :: [Text] -> Int -> [Text] -> Int -> Structure
-stripPlot whole nWhole strip nStrip =
-  StripPlot whole nWhole 1.0 "wholePlot" strip nStrip 1.0 "strip"
-
--- | 'Blocked' の smart constructor。 η = 1.0・群列名 = @"block"@ を既定にする。
---   @blocked 3@ = 3 ランダムブロック。
-blocked :: Int -> Structure
-blocked nBlocks = Blocked nBlocks 1.0 "block"
-
--- | **完全カスタムデザインの仕様** (Phase 79)。 因子 × 固定効果モデル × run 数 × seed に、
---   最適化基準 ('csCriterion')・制約 ('csConstraints')・階層構造 ('csStructure') を載せた
---   1 本のスペックレコード。 'customSpec' で既定 (DOpt・制約なし・CRD) を作り、 レコード更新で
---   criterion / constraints / structure を足す。 唯一の生成入口 'customDesign' に渡す。
-data CustomSpec = CustomSpec
-  { csFactors     :: ![DesignFactor]  -- ^ 因子 ('contFactor' / 'catFactor' / 'numFactor')
-  , csFormula     :: !Formula         -- ^ 固定効果モデル (効果 DSL / 'parseRFormula')
-  , csNRuns       :: !Int             -- ^ run 数 n
-  , csSeed        :: !Int             -- ^ seed (決定的 pure)
-  , csCriterion   :: !OptCriterion    -- ^ 最適化基準 (既定 'DOpt')
-  , csConstraints :: ![Constraint]    -- ^ 低レベル制約 (既定 []・**coded 単位**・エスケープハッチ)
-  , csNatConstraints :: ![NatConstraint]
-      -- ^ **自然単位の制約** (既定 []・Phase 82・推奨 API)。 実単位で書き ('natLeq' 等)、
-      --   'customDesign' 入口で coded の 'csConstraints' へ正規化・合流する。
-  , csStructure   :: !Structure       -- ^ 階層構造 (既定 'CRD')
-  } deriving (Show)
-
--- | 'CustomSpec' の smart constructor。 既定 = DOpt・制約なし・CRD。 レコード更新で
---   @{ csCriterion = … }@ / @{ csNatConstraints = … }@ / @{ csStructure = … }@ を足す。
---   @customSpec factors formula nRuns seed@。
-customSpec :: [DesignFactor] -> Formula -> Int -> Int -> CustomSpec
-customSpec fs fml n seed = CustomSpec
-  { csFactors = fs, csFormula = fml, csNRuns = n, csSeed = seed
-  , csCriterion = DOpt, csConstraints = [], csNatConstraints = []
-  , csStructure = CRD }
-
--- ---------------------------------------------------------------------------
--- Phase 82: 自然単位の制約 (公開 API) → coded 内部制約への正規化
--- ---------------------------------------------------------------------------
-
--- | **自然単位の制約** (Phase 82・公開 API)。 因子を**実単位**で参照する
---   (@temp <= 160@ 等)。 'customDesign' 入口で因子の coded↔natural 情報を使って
---   内部 'Constraint' (coded) へ正規化される。 これにより ユーザは coded @[-1,1]@ や
---   水準 index を意識せず、 実験の言葉 (実温度・実流量) で制約を書ける。
---
---   * 'natLeq' / 'natGeq' / 'natEq' — 連続因子の線形不等式/等式 (実単位係数)。
---     @Σ aᵢ·x_natᵢ  rel  b@。 **離散数値 ('Num')** も**単一項** (@a·temp <= 160@ 等)
---     なら参照可で、 閾値を満たさない水準を除外する糖衣に展開する (Phase 82.2)。
---     カテゴリ ('Cat') は順序を持たないため拒否 ('Left'、 'natForbid' を使う)。
---   * 'natForbid' — 禁止組合せ。 カテゴリは水準名 ('FVText')、 離散数値/連続は
---     実値 ('FVDouble') で指定 (内部で index / coded へ変換)。
-data NatConstraint
-  = NatLinear ![(Text, Double)] !ConstraintRel !Double
-    -- ^ @Σ aᵢ·x_natᵢ `rel` rhs@ (連続因子のみ)
-  | NatForbid ![(Text, FactorValue)]
-    -- ^ 全項が一致する row を禁止 (実単位/水準名で指定)
-  deriving (Eq, Show)
-
--- | @Σ aᵢ·x_natᵢ ≤ b@ (連続因子・実単位)。
-natLeq :: [(Text, Double)] -> Double -> NatConstraint
-natLeq coefs b = NatLinear coefs CLeq b
-
--- | @Σ aᵢ·x_natᵢ ≥ b@ (連続因子・実単位)。
-natGeq :: [(Text, Double)] -> Double -> NatConstraint
-natGeq coefs b = NatLinear coefs CGeq b
-
--- | @Σ aᵢ·x_natᵢ = b@ (連続因子・実単位)。 grid 解像度に注意。
-natEq :: [(Text, Double)] -> Double -> NatConstraint
-natEq coefs b = NatLinear coefs CEq b
-
--- | 禁止組合せ (実単位/水準名)。 @natForbid [("catalyst", FVText \"A\"), ("temp", FVDouble 180)]@。
-natForbid :: [(Text, FactorValue)] -> NatConstraint
-natForbid = NatForbid
-
--- | 因子名で 'DesignFactor' を引く (制約正規化用)。
-lookupDF :: [DesignFactor] -> Text -> Either Text DesignFactor
-lookupDF fs nm =
-  maybe (Left ("未知の因子 '" <> nm <> "' が制約に現れました")) Right
-        (find ((== nm) . dfName) fs)
-
--- | 自然単位の 'NatConstraint' を因子情報を使って coded 内部 'Constraint' へ正規化。
---
---   * 線形スケール連続因子 (coded −1=lo/+1=hi/中心=平均、 @nat = center + coded·half@):
---     @Σ aᵢ·natᵢ ≤ b ⟺ Σ (aᵢ·halfᵢ)·codedᵢ ≤ b − Σ aᵢ·centerᵢ@。 half>0 ゆえ関係子不変。
---   * **対数スケール**連続因子: @a·nat = a·10^(…)@ は coded について非線形なので、 線形結合に
---     混ぜられない。 **単一因子の境界** (@temp <= X@ 等) のみ許可し、 閾値を @codeCont@ で
---     coded 境界へ写す (係数が負なら関係子を反転)。 混在は 'Left'。
---   * 禁止組合せ: カテゴリは水準名そのまま (buildRowFV が index→名前に戻すため)、
---     離散数値は実値→水準 index、 連続は実値→coded ('codeCont'・線形/対数を分岐)。
-normalizeNat :: [DesignFactor] -> NatConstraint -> Either Text [Constraint]
-normalizeNat fs (NatLinear coefs rel rhs) = do
-    terms <- traverse resolve coefs                 -- (name, coef, factor)
-    let numTerms = [ (nm, a, lvs) | (nm, a, f) <- terms, Num lvs <- [dfKind f] ]
-        catNames = [ nm | (nm, _, f) <- terms, Cat _ <- [dfKind f] ]
-    case (catNames, numTerms) of
-      (nm : _, _) ->
-        Left ("自然単位の線形制約はカテゴリ因子 '" <> nm
-              <> "' を参照できません (順序を持たないため)。 natForbid を使ってください")
-      (_, (nm, a, lvs) : rest)
-        | not (null rest) || length terms /= 1 ->
-            Left ("離散数値因子 '" <> nm <> "' を含む自然単位制約は単一項 (a·" <> nm
-                  <> " rel b) でのみ書けます (許容水準の除外へ展開するため)。"
-                  <> " 他因子との線形結合は不可")
-        | otherwise -> numFilter nm a lvs
-      (_, []) ->                                    -- 全項が連続 (線形/対数)
-        let logTerms = filter (\(_, _, f) -> isLog f) terms
-        in case logTerms of
-             []                               -> Right [linearCombo terms]
-             [(nm, a, f)] | length terms == 1 -> (: []) <$> singleLogBound nm a f
-             _ -> Left ("自然単位の線形結合に対数スケール因子は混ぜられません (非線形)。"
-                        <> " 対数因子は単一因子の境界 (temp<=X 等) でのみ書けます")
-  where
-    resolve (nm, a) = (\f -> (nm, a, f)) <$> lookupDF fs nm
-    isLog f = case dfKind f of Cont _ _ SLog -> True; _ -> False
-    -- 単一 Num 因子の実値閾値 → 満たさない水準を Forbidden で除外 (Phase 82.2)。
-    --   @a·level rel rhs@ を各水準で判定し、 満たさない水準の index を禁止する。
-    --   半空間ではなく水準除外になる (順序 Num は index 尺度・doc 明記)。
-    numFilter nm a lvs =
-      let bad = [ i | (i, lv) <- zip [0 :: Int ..] lvs, not (relHolds rel (a * lv) rhs) ]
-      in if length bad == length lvs
-           then Left ("離散数値因子 '" <> nm <> "' の制約 " <> tshowD a <> "·" <> nm
-                      <> " " <> relSym rel <> " " <> tshowD rhs
-                      <> " を満たす水準がありません (水準 " <> T.pack (show lvs) <> ")")
-           else Right [ Forbidden [(nm, FVDouble (fromIntegral i))] | i <- bad ]
-    relHolds CLeq x r = x <= r + 1e-9
-    relHolds CEq  x r = abs (x - r) <= 1e-9
-    relHolds CGeq x r = x >= r - 1e-9
-    -- 全項が線形スケール連続: Σaᵢ·natᵢ rel rhs → coded の LinearIneq へ
-    linearCombo terms' =
-      let part (nm, a, f) = case dfKind f of
-            Cont lo hi _ -> let half = (hi - lo) / 2; center = (lo + hi) / 2
-                            in ((nm, a * half), a * center)
-            _            -> ((nm, 0), 0)      -- 連続のみ到達
-          ps    = map part terms'
-          shift = sum (map snd ps)
-      in LinearIneq (map fst ps) rel (rhs - shift)
-    -- 単一対数因子の境界: a·nat rel rhs → nat rel' (rhs/a) → codeCont で coded 境界
-    singleLogBound nm a f
-      | a == 0    = Left ("対数因子 '" <> nm <> "' の係数が 0 です")
-      | thr <= 0  = Left ("対数因子 '" <> nm <> "' の自然単位境界 " <> T.pack (show thr)
-                          <> " が非正です (log 不能)。 正の閾値で指定してください")
-      | otherwise = Right (LinearIneq [(nm, 1)] rel' (codeCont f thr))
-      where
-        thr  = rhs / a
-        rel' = if a > 0 then rel else flipRel rel
-    flipRel CLeq = CGeq
-    flipRel CGeq = CLeq
-    flipRel CEq  = CEq
-normalizeNat fs (NatForbid vs) = (\c -> [Forbidden c]) <$> traverse conv vs
-  where
-    conv (nm, v) = do
-      f <- lookupDF fs nm
-      case (dfKind f, v) of
-        (Cat _, FVText _)   -> Right (nm, v)   -- 水準名はそのまま
-        (Cat _, FVDouble _) ->
-          Left ("カテゴリ因子 '" <> nm <> "' の禁止値は水準名 (FVText) で指定してください")
-        (Num levels, FVDouble x) ->
-          case elemIndex x levels of
-            Just i  -> Right (nm, FVDouble (fromIntegral i))
-            Nothing -> Left ("離散数値因子 '" <> nm <> "' に水準 "
-                             <> T.pack (show x) <> " はありません")
-        (Cont _ _ _, FVDouble x) -> Right (nm, FVDouble (codeCont f x))
-        _ -> Left ("因子 '" <> nm <> "' の禁止値の型が不正です")
-
--- | 'CustomSpec' の有効な coded 制約 = 低レベル 'csConstraints' + 正規化した
---   'csNatConstraints'。 正規化失敗は 'error' (customDesign の既存パターンに合わせる)。
-effectiveConstraints :: CustomSpec -> [Constraint]
-effectiveConstraints cs =
-  case traverse (normalizeNat (csFactors cs)) (csNatConstraints cs) of
-    Left e   -> error ("customDesign: " <> T.unpack e)
-    Right ns -> csConstraints cs ++ concat ns
-
--- | 座標交換が **実行不能** (feasible な初期解が得られない等) で 'Left' を返したとき、
---   エラーに「有効な制約 (実単位)」と「因子の範囲」を添えて原因追跡を助ける (Phase 82.2)。
---   実行不能系でないメッセージ (引数不正等) はそのまま。 制約が空なら添えない。
-enrichInfeasError :: CustomSpec -> Text -> Text
-enrichInfeasError cs e
-  | isFeasErr && hasCons =
-      base <> "\n  有効な制約 (実単位):" <> T.concat (map ("\n    - " <>) items)
-           <> "\n  因子の範囲:" <> T.concat (map ("\n    - " <>) ranges)
-  | otherwise = base
-  where
-    base      = "customDesign: " <> e
-    isFeasErr = any (`T.isInfixOf` e) ["feasible", "infeasible", "too tight", "初期解"]
-    natC      = csNatConstraints cs
-    lowC      = csConstraints cs
-    hasCons   = not (null natC) || not (null lowC)
-    items     = map renderNatC natC ++ map ((<> "  (coded 単位)") . renderLowC) lowC
-    ranges    = [ dfName f <> " ∈ " <> renderRange f | f <- csFactors cs ]
-
--- | 自然単位 'NatConstraint' を人間可読な文字列に (エラー添付用)。
-renderNatC :: NatConstraint -> Text
-renderNatC (NatLinear coefs rel rhs) =
-  T.intercalate " + " (map (\(nm, a) -> tshowD a <> "·" <> nm) coefs)
-    <> " " <> relSym rel <> " " <> tshowD rhs
-renderNatC (NatForbid vs) =
-  "禁止: " <> T.intercalate ", " (map (\(nm, v) -> nm <> "=" <> renderFV v) vs)
-
--- | 低レベル coded 'Constraint' の要約 (エラー添付用・完全網羅でなく主要 2 種)。
-renderLowC :: Constraint -> Text
-renderLowC (LinearIneq coefs rel rhs) =
-  T.intercalate " + " (map (\(nm, a) -> tshowD a <> "·" <> nm) coefs)
-    <> " " <> relSym rel <> " " <> tshowD rhs
-renderLowC (RangeBound nm lo hi) = nm <> " ∈ [" <> tshowD lo <> ", " <> tshowD hi <> "]"
-renderLowC (Forbidden vs) =
-  "禁止: " <> T.intercalate ", " (map (\(nm, v) -> nm <> "=" <> renderFV v) vs)
-renderLowC (Conditional _ _) = "条件付制約"
-
-renderFV :: FactorValue -> Text
-renderFV (FVDouble x) = tshowD x
-renderFV (FVText t)   = t
-
-relSym :: ConstraintRel -> Text
-relSym CLeq = "≤"
-relSym CGeq = "≥"
-relSym CEq  = "="
-
--- | 因子の値域を実単位で (連続 = 範囲・対数注記、 数値順序 = 水準列、 カテゴリ = 水準名)。
-renderRange :: DesignFactor -> Text
-renderRange f = case dfKind f of
-  Cont lo hi sc -> "[" <> tshowD lo <> ", " <> tshowD hi <> "]"
-                     <> (case sc of SLog -> " (log)"; SLinear -> "")
-  Num levels    -> T.pack (show levels)
-  Cat levels    -> "{" <> T.intercalate ", " levels <> "}"
-
-tshowD :: Double -> Text
-tshowD = T.pack . show
-
--- ---------------------------------------------------------------------------
--- コンストラクタ
--- ---------------------------------------------------------------------------
-
--- | 因子の coded 水準集合。 連続 = @{-1,+1}@ (2 水準)、 カテゴリ = 水準 index @{0,1,…,m-1}@。
---   完全要因の列挙 ('fullFactorial') と 最適計画の候補格子に使う。
-factorLevelsCoded :: DesignFactor -> [Double]
-factorLevelsCoded f = case dfKind f of
-  Cont _ _ _ -> [-1, 1]
-  Num levels -> map fromIntegral [0 .. length levels - 1]
-  Cat levels -> map fromIntegral [0 .. length levels - 1]
-
--- | 全因子が連続であることを要求する (rsm/boxBehnken/taguchi 等・連続専用設計)。
---   カテゴリが混じれば呼び名付きで error。
-requireContinuous :: String -> [DesignFactor] -> [DesignFactor]
-requireContinuous who fs =
-  case [ dfName f | f <- fs, isCat (dfKind f) ] of
-    []   -> fs
-    cats -> error
-      (who ++ ": カテゴリ因子 " ++ show (map T.unpack cats)
-        ++ " は扱えません (この設計は連続因子のみ・カテゴリは factorialDesign/optimalDesign へ)")
-  where isCat (Cat _) = True
-        isCat _       = False
-
--- | 全カテゴリ因子が 2 水準 (binary) であることを要求する (fractional・v1 は binary のみ)。
---   3 水準以上の 'Cat' は呼び名付きで error。 連続因子は素通り。
-requireBinaryCats :: String -> [DesignFactor] -> [DesignFactor]
-requireBinaryCats who fs =
-  case [ dfName f | f <- fs, overTwo (dfKind f) ] of
-    []  -> fs
-    bad -> error
-      (who ++ ": カテゴリ因子 " ++ show (map T.unpack bad)
-        ++ " は 2 水準 (binary) のみ対応です (3 水準以上は factorialDesign/optimalDesign か G-a2 の L9/L18 へ)")
-  where overTwo (Cat ls) = length ls /= 2
-        overTwo _         = False
-
--- | ±1 coded 設計 (fractional/taguchi) のカテゴリ列を水準 index に写す。 binary 前提
---   (@-1@ → 水準0、 @+1@ → 水準1)。 連続列は ±1 のまま (直交/平衡構造を保つ)。
-codeCatColumns :: [DesignFactor] -> [[Double]] -> [[Double]]
-codeCatColumns fs = map (zipWith recode fs)
-  where recode f v = case dfKind f of
-          Cat _ -> if v < 0 then 0 else 1
-          _     -> v
-
--- | 2 水準 完全要因計画。 @factorialDesign [contFactor "temp" (150,180), contFactor "time" (10,20)]@
---   → 2^k run (全交互作用モデル)。 カテゴリ因子を混ぜると各因子の水準を総当り
---   (連続=2 水準・カテゴリ=m 水準) した完全要因になる (例: 連続1×カテゴリ3水準 = 2×3=6 run)。
-factorialDesign :: [DesignFactor] -> Design
-factorialDesign fs =
-  Design fs (fullFactorial (map factorLevelsCoded fs)) KFactorial
-
--- | 応答曲面計画 (回転可能 中心複合計画 CCD)。
---   @centralCompositeDesign [contFactor "temp" (150,180), contFactor "time" (10,20)]@ → 2^k factorial + 2k 軸点
---   + 中心点 (2 次モデル)。 中心点数は k (最低 1)。 ★連続因子のみ (±α 軸点ゆえカテゴリ不可)。
-centralCompositeDesign :: [DesignFactor] -> Design
-centralCompositeDesign fs0 =
-  let fs = requireContinuous "centralCompositeDesign" fs0
-      k  = length fs
-  in Design fs (centralCompositeRotatable k (max 1 k)) KRSM
-
--- | Box-Behnken 応答曲面計画 (**k = 3, 4, 5 のみ**)。 CCD より run が少なく、 **極端な軸点
---   (±α) を持たない** (各点は立方体の辺の中点・因子は @-1,0,+1@ の 3 水準に収まる) 3 水準 RSM。
---   2 次モデル ('KRSM') を含意。 因子数が 3〜5 でないと下層が error (数学的制約)。 ★連続因子のみ。
---   @boxBehnkenDesign [contFactor "t" (150,180), contFactor "p" (1,3), contFactor "c" (5,15)]@。
-boxBehnkenDesign :: [DesignFactor] -> Design
-boxBehnkenDesign fs0 =
-  let fs = requireContinuous "boxBehnkenDesign" fs0
-      k  = length fs
-  in Design fs (boxBehnken k (max 1 k)) KRSM
-
--- ---------------------------------------------------------------------------
--- 一部実施要因 (fractional factorial) — Phase 78.G
---
--- 完全要因 2^k は run 数が指数増するので、 交互作用の一部を主効果と交絡させて
--- **2^(k-p) に減らす** (直交表と等価)。 交絡の重さは **解像度 (resolution)** で測る:
---   * Res III … 主効果と 2 因子交互作用が交絡 (最も攻めた削減)。
---   * Res IV  … 主効果は 2 因子交互作用と交絡しないが、 2 因子交互作用同士が交絡。
---   * Res V+  … 主効果・2 因子交互作用が (ほぼ) 独立。
--- generator (追加因子の定義関係) は **最小交絡 (minimum aberration)** の標準表
--- (Montgomery Table 8-14 / NIST) を 'fractionalCatalog' に持つ。 k = 3〜11, 15 (16/32-run)。
--- ---------------------------------------------------------------------------
-
--- | 設計の解像度 (defining word の最短長)。 数字は @'resNum'@。
-data Resolution = ResIII | ResIV | ResV | ResVI | ResVII
-  deriving (Show, Eq, Ord, Enum, Bounded)
-
-resNum :: Resolution -> Int
-resNum r = fromEnum r + 3
-
--- | 最小交絡 2 水準一部実施の標準カタログ (k → @[(runs, resolution, generators)]@)。
---   generator は追加因子を基底因子の**積**で定義する 1-based index リスト
---   (例: @[[1,2]]@ = 「追加因子 = 基底1×基底2」 = C=AB)。 出典 = Montgomery Table 8-14 /
---   NIST e-Handbook (§5.3.3.4.7)。 各行の resolution は 'fracResolution' で test 検証済 (誤り混入ガード)。
---   k=8〜15 は NIST 収録の 16/32-run 設計 (Phase 78.G-c 拡張)。 NIST が非収録の帯 (16-run k=12〜14・
---   32-run k=12〜16) は本カタログも持たない (該当 k はより多 run の設計 or k=15/31 飽和を使う)。
-fractionalCatalog :: Int -> [(Int, Resolution, [[Int]])]
-fractionalCatalog k = case k of
-  3  -> [ (4,  ResIII, [[1,2]]) ]                                   -- C=AB
-  4  -> [ (8,  ResIV,  [[1,2,3]]) ]                                 -- D=ABC
-  5  -> [ (16, ResV,   [[1,2,3,4]])                                 -- E=ABCD
-        , (8,  ResIII, [[1,2],[1,3]]) ]                             -- D=AB, E=AC
-  6  -> [ (32, ResVI,  [[1,2,3,4,5]])                               -- F=ABCDE
-        , (16, ResIV,  [[1,2,3],[2,3,4]])                           -- E=ABC, F=BCD
-        , (8,  ResIII, [[1,2],[1,3],[2,3]]) ]                       -- D=AB, E=AC, F=BC
-  7  -> [ (64, ResVII, [[1,2,3,4,5,6]])                             -- G=ABCDEF
-        , (16, ResIV,  [[1,2,3],[2,3,4],[1,3,4]])                   -- E=ABC, F=BCD, G=ACD
-        , (8,  ResIII, [[1,2],[1,3],[2,3],[1,2,3]]) ]               -- D=AB, E=AC, F=BC, G=ABC
-  -- ↓ NIST 標準表 (16/32-run)。 基底は 16-run=ABCD(4)・32-run=ABCDE(5)。
-  8  -> [ (16, ResIV,  [[2,3,4],[1,3,4],[1,2,3],[1,2,4]]) ]         -- 2^(8-4): E=BCD,F=ACD,G=ABC,H=ABD
-  9  -> [ (32, ResIV,  [[2,3,4,5],[1,3,4,5],[1,2,4,5],[1,2,3,5]])   -- 2^(9-4): F=BCDE,G=ACDE,H=ABDE,J=ABCE
-        , (16, ResIII, [[1,2,3],[2,3,4],[1,3,4],[1,2,4],[1,2,3,4]]) ]  -- 2^(9-5): E=ABC,F=BCD,G=ACD,H=ABD,J=ABCD
-  10 -> [ (32, ResIV,  [[1,2,3,4],[1,2,3,5],[1,2,4,5],[1,3,4,5],[2,3,4,5]])  -- 2^(10-5)
-        , (16, ResIII, [[1,2,3],[2,3,4],[1,3,4],[1,2,4],[1,2,3,4],[1,2]]) ]  -- 2^(10-6): …,J=ABCD,K=AB
-  11 -> [ (32, ResIV,  [[1,2,3],[2,3,4],[3,4,5],[1,3,4],[1,4,5],[2,4,5]])    -- 2^(11-6)
-        , (16, ResIII, [[1,2,3],[2,3,4],[1,3,4],[1,2,4],[1,2,3,4],[1,2],[1,3]]) ]  -- 2^(11-7)
-  15 -> [ (16, ResIII, [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]         -- 2^(15-11) 飽和: 全 15 列
-                       ,[1,2,3],[1,2,4],[1,3,4],[2,3,4],[1,2,3,4]]) ]
-  _  -> []
-
--- | generator 集合から**解像度** (数字) を計算する。 各 generator が定める defining word
---   (追加因子 ∪ 基底集合) の生成する部分群 (全 XOR 組合せ) の**最短語長**。 = 設計の resolution。
---   'fractionalCatalog' の resolution ラベルを test で照合するのに使う (= 表の自己検証)。
-fracResolution :: Int -> [[Int]] -> Int
-fracResolution k gens =
-  case definingSubgroup k gens of
-    []     -> k
-    combos -> minimum (map length combos)
-
--- | defining word の対称差 (mod-2 積 = 語の XOR)。
-symDiffW :: [Int] -> [Int] -> [Int]
-symDiffW a b = sort ((a \\ b) ++ (b \\ a))
-
--- | 各 generator が定める defining word (追加因子 ∪ 基底集合)。 generator i (1-based) は
---   因子 (kBase+i) を追加する (kBase = k − generator 数)。 defining word = 基底集合 ∪ {kBase+i}。
-definingWords :: Int -> [[Int]] -> [[Int]]
-definingWords k gens =
-  let kBase = k - length gens
-  in [ sort (gen ++ [kBase + i]) | (i, gen) <- zip [1 ..] gens ]
-
--- | defining 部分群の**非恒等元** (全 defining word の非空部分集合の XOR)。 defining relation
---   @I = …@ の右辺集合。 交絡 (alias) と解像度 ('fracResolution') はこの群で決まる。
-definingSubgroup :: Int -> [[Int]] -> [[Int]]
-definingSubgroup k gens =
-  [ foldl1' symDiffW ws | ws <- tail (subsequences (definingWords k gens)) ]
-
--- | 効果 (因子 index 集合) の **alias 剰余類** = @{ effect XOR w | w ∈ 部分群 ∪ {恒等} }@。
---   効果自身を含む。 同じ剰余類に入る効果同士は設計上区別できない (交絡)。
-aliasCoset :: Int -> [[Int]] -> [Int] -> [[Int]]
-aliasCoset k gens effect =
-  nub (sort [ symDiffW effect w | w <- [] : definingSubgroup k gens ])
-
--- | 主効果と交絡しない 2 因子交互作用の**代表** (交絡群ごと 1 個)。 各 2FI の alias 剰余類が
---   主効果語 (長さ1) を含めば群ごと除外、 含まなければ未代表の群から 1 個を採る。 結果は満ランクで
---   主効果を不バイアスに保つ (Res V=全 2FI・Res IV=群ごと 1 個・Res III=主効果非交絡の 2FI のみ)。
-clearTwoFactorInteractions :: Int -> [[Int]] -> [[Int]]
-clearTwoFactorInteractions k gens = go [] twoFIs
-  where
-    twoFIs = [ [i, j] | i <- [1 .. k], j <- [i + 1 .. k] ]
-    go _    []       = []
-    go seen (t : ts)
-      | any (`elem` seen) coset     = go seen ts               -- 代表済みの交絡群
-      | any ((== 1) . length) coset = go (coset ++ seen) ts     -- 主効果と交絡 → 群ごと除外
-      | otherwise                   = t : go (coset ++ seen) ts
-      where coset = aliasCoset k gens t
-
--- | 効果 (因子 index 集合) を @":"@ 連結ラベル (@"a"@ / @"a:b"@) に。 因子名は 'dsFactors' 順。
-effectLabel :: [Text] -> [Int] -> Text
-effectLabel names is = T.intercalate ":" [ names !! (i - 1) | i <- is ]
-
--- | 一部実施 (交互作用版・'KFracInter') の **alias 構造**。 主効果と 2 因子交互作用について、
---   各効果と交絡する他効果 (同じ剰余類の残り) をラベルで返す。 他の設計種別では空。
---   @lookup "a:b" (aliasStructure plan)@ で「@a:b@ は何と交絡するか」を引ける。
-aliasStructure :: Design -> [(Text, [Text])]
-aliasStructure (Design fs _ kind) = case kind of
-  KFracInter gens ->
-    let k     = length fs
-        names = map dfName fs
-        effs  = [ [i] | i <- [1 .. k] ]
-                ++ [ [i, j] | i <- [1 .. k], j <- [i + 1 .. k] ]
-    in [ ( effectLabel names e
-         , [ effectLabel names a
-           | a <- aliasCoset k gens e, a /= e, not (null a) ] )
-       | e <- effs ]
-  _ -> []
-
--- | 一部実施要因計画 (**解像度で自動選択**・k = 3〜7)。 指定解像度**以上**を満たす中で
---   **最小 run 数**の最小交絡設計を選ぶ (削減を最大化しつつ要求解像度を確保)。 該当なしは error。
---   @fractionalDesign [("a",(0,1)),…,("g",(0,1))] ResIII@。 formula は**主効果のみ** (交互作用は交絡)。
-fractionalDesign :: [DesignFactor] -> Resolution -> Design
-fractionalDesign specs res =
-  let fs   = requireBinaryCats "fractionalDesign" specs
-      k    = length fs
-      cands = [ e | e@(_, r, _) <- fractionalCatalog k, r >= res ]
-  in case cands of
-       [] -> error
-         ("fractionalDesign: k=" ++ show k ++ " で resolution >= " ++ show res
-           ++ " の標準設計がありません (k=3〜11,15・利用可能: "
-           ++ show [ (n, r) | (n, r, _) <- fractionalCatalog k ] ++ ")")
-       _  -> let (_, _, gens) = minimumBy' (\(n1,_,_) (n2,_,_) -> compare n1 n2) cands
-             in Design fs (codeCatColumns fs (fractionalFactorial k gens)) KFractional
-
--- | 一部実施要因計画 (**generator 明示**・玄人向け escape hatch)。 generator は追加因子を
---   基底因子の積で定義する 1-based index リスト (例: @[[1,2,3]]@ = D=ABC)。 追加因子数 = @length gens@、
---   run 数 = @2^(k - length gens)@。 formula は**主効果のみ**。
-fractionalDesignGen :: [DesignFactor] -> [[Int]] -> Design
-fractionalDesignGen specs gens =
-  let fs = requireBinaryCats "fractionalDesignGen" specs
-      k  = length fs
-  in Design fs (codeCatColumns fs (fractionalFactorial k gens)) KFractional
-
--- | 一部実施要因計画 (**交互作用込み**・解像度自動)。 設計点は 'fractionalDesign' と同一だが、
---   'designFormula' が主効果に加え**主効果と交絡しない 2 因子交互作用の代表** (交絡群ごと 1 個) を
---   含める ('KFracInter')。 交絡構造は 'aliasStructure' で確認できる。 該当解像度なしは error。
---   @fractionalDesignInter [contFactor "a" (0,1), …] ResV@ (Res V なら全 2FI が独立に載る)。
-fractionalDesignInter :: [DesignFactor] -> Resolution -> Design
-fractionalDesignInter specs res =
-  let fs    = requireBinaryCats "fractionalDesignInter" specs
-      k     = length fs
-      cands = [ e | e@(_, r, _) <- fractionalCatalog k, r >= res ]
-  in case cands of
-       [] -> error
-         ("fractionalDesignInter: k=" ++ show k ++ " で resolution >= " ++ show res
-           ++ " の標準設計がありません (k=3〜11,15・利用可能: "
-           ++ show [ (n, r) | (n, r, _) <- fractionalCatalog k ] ++ ")")
-       _  -> let (_, _, gens) = minimumBy' (\(n1,_,_) (n2,_,_) -> compare n1 n2) cands
-             in Design fs (codeCatColumns fs (fractionalFactorial k gens)) (KFracInter gens)
-
--- | 一部実施要因計画 (**交互作用込み**・generator 明示)。 'fractionalDesignGen' の交互作用版。
---   @fractionalDesignGenInter [contFactor "a" (0,1), …] [[1,2,3]]@ (D=ABC の Res IV)。
-fractionalDesignGenInter :: [DesignFactor] -> [[Int]] -> Design
-fractionalDesignGenInter specs gens =
-  let fs = requireBinaryCats "fractionalDesignGenInter" specs
-      k  = length fs
-  in Design fs (codeCatColumns fs (fractionalFactorial k gens)) (KFracInter gens)
-
--- | 小さな minimumBy (Data.List.minimumBy 相当・依存を増やさない)。
-minimumBy' :: (a -> a -> Ordering) -> [a] -> a
-minimumBy' cmp = foldl1' (\x y -> if cmp x y == GT then y else x)
-
--- ---------------------------------------------------------------------------
--- Taguchi 直交表 (orthogonal array) — Phase 78.G-a
---
--- 一部実施要因 ('fractionalDesign') と同じ「run を減らして主効果を推定する」目的だが、
--- **Taguchi の Lₙ 直交表**を土台にする枠組み。 v1 は **2 水準表のみ** (L4/L8/L12/L16)。
---   * L4(2³) 4 run … 〜3 因子   * L8(2⁷) 8 run … 〜7 因子
---   * L12(2¹¹) 12 run … 〜11 因子 (★Plackett-Burman・主効果スクリーニングの定番)
---   * L16(2¹⁵) 16 run … 〜15 因子
--- L8/L16 は fractional と数学的に等価だが、 「因子を直交表の列に割り当てる」 Taguchi の
--- framing で透過的に扱えるようにする。 ★目玉は fractional に無い **L12 (11 因子/12 run)**。
---
--- ★Phase 78.G-a2: **3 水準/混合水準表 (L9/L18/L27)** と **数値順序 ('Num') / カテゴリ ('Cat')**
--- 因子に拡張。 各因子の要求水準数 (Cont=2・Num/Cat=水準数) を表の列水準 ('oaLevels') に
--- **貪欲に突合**して割り当てる (混合表 L18=2¹×3⁷ では 2 水準因子を 2 水準列へ、 3 水準因子を
--- 3 水準列へ)。 割当先列の code を各因子の coded へ写す:
---   * Cont … code 1→@-1@ / 2→@+1@ (2 水準)。 uncode で実値へ。
---   * Num/Cat … code c → 水準 index @c-1@。 designFrame で実値/水準名へ、 Num は formula で
---     直交多項式 (opoly) に、 Cat は engine の contrast に展開。
--- formula は 'fractionalDesign' と同じ**主効果のみ** ('KFractional')。 designModel は共通経路。
--- ---------------------------------------------------------------------------
-
--- | 自動選択が run 数の昇順に舐める直交表 (2 水準 + 3 水準/混合)。
-taguchiOAs :: [OA]
-taguchiOAs = [l4, l8, l9, l12, l16, l18, l27]   -- runs: 4,8,9,12,16,18,27
-
--- | 因子が要求する水準数 (Cont=2・Num/Cat=水準リスト長)。 直交表の列水準への突合に使う。
-factorLevelCount :: DesignFactor -> Int
-factorLevelCount f = case dfKind f of
-  Cont _ _ _ -> 2
-  Num levels -> length levels
-  Cat levels -> length levels
-
--- | 因子を直交表の列に貪欲割当 (各因子の要求水準数に一致する未使用列を順に取る)。
---   成功なら各因子の割当先**列 index**、 一致列が尽きたら 'Nothing' (混合水準の突合失敗)。
-assignOAColumns :: OA -> [DesignFactor] -> Maybe [Int]
-assignOAColumns oa = go (zip [0 ..] (oaLevels oa))
-  where
-    go _     []       = Just []
-    go avail (f : fs) =
-      case break (\(_, lvl) -> lvl == factorLevelCount f) avail of
-        (_,   [])              -> Nothing
-        (pre, (col, _) : post) -> (col :) <$> go (pre ++ post) fs
-
--- | 割当先列の 1-based level code を各因子の coded へ。 Cont は code 1→@-1@/2→@+1@、
---   Num/Cat は水準 index @c-1@。
-oaToCodedCols :: OA -> [Int] -> [DesignFactor] -> [[Double]]
-oaToCodedCols oa cols fs =
-  [ [ codeFor f (row !! col) | (col, f) <- zip cols fs ] | row <- oaTable oa ]
-  where
-    codeFor f code = case dfKind f of
-      Cont _ _ _ -> if code == 1 then -1 else 1
-      _        -> fromIntegral (code - 1)   -- Num/Cat: 水準 index
-
--- | Taguchi 直交表計画 (**最小 OA 自動選択**・2/3 水準・混合)。 各因子の要求水準数 (連続=2・
---   'numFactor'/'catFactor' は水準数) に一致する列を持つ最小 run 表 (L4/L8/L9/L12/L16/L18/L27
---   の run 昇順) を選び、 因子を列へ割り当てる。 該当表なしは error。 formula は
---   'fractionalDesign' と同じ**主効果のみ** ('KFractional'・Num は 'opoly')。
---   @taguchiDesign [contFactor "a" (0,1), …]@ (11 連続) → L12、
---   @taguchiDesign [catFactor "x" ["A","B","C"], …]@ (3 水準) → L9、
---   @taguchiDesign [contFactor "p" (0,1), catFactor "q" ["a","b","c"], …]@ (混合) → L18。
-taguchiDesign :: [DesignFactor] -> Design
-taguchiDesign specs =
-  case [ (oa, cols) | oa <- taguchiOAs, Just cols <- [assignOAColumns oa specs] ] of
-    []              -> error
-      ("taguchiDesign: 因子の水準構成 " ++ show (map factorLevelCount specs)
-        ++ " を収容できる標準直交表がありません "
-        ++ "(利用可能: L4/L8/L12/L16 = 2 水準、 L9/L18(混合)/L27 = 3 水準)")
-    ((oa, cols) : _) -> Design specs (oaToCodedCols oa cols specs) KFractional
-
--- | 標準直交表の識別子 ('taguchiDesignOA' で表を明示するための**列挙型**)。
---   文字列でなく型で表を指定するので、 打ち間違い (@\"L10\"@ 等) は**コンパイル時に弾かれる**。
---   2 水準表 = 'L4'/'L8'/'L12'/'L16'、 3 水準表 = 'L9'/'L27'、 混合水準表 = 'L18' (2^1×3^7)。
-data OATable = L4 | L8 | L9 | L12 | L16 | L18 | L27
-  deriving (Show, Eq, Ord, Enum, Bounded)
-
--- | 'OATable' → 低レベル 'OA' 定義。
-oaTableToOA :: OATable -> OA
-oaTableToOA t = case t of
-  L4 -> l4; L8 -> l8; L9 -> l9; L12 -> l12; L16 -> l16; L18 -> l18; L27 -> l27
-
--- | Taguchi 直交表計画 (**直交表を型で明示**・fractional の generator 版と対称の escape hatch)。
---   表は 'OATable' の列挙値 ('L4'/'L8'/'L9'/'L12'/'L16'/'L18'/'L27') で渡すので、 未知の表名は
---   型検査で弾かれる (文字列指定の実行時 error が無い)。 因子の水準数が表の列に割り当てられない
---   場合のみ error。
---   @taguchiDesignOA L12 specs@ で run 数を意図的に選ぶ、 @taguchiDesignOA L9 cats@ で 3 水準表を明示。
-taguchiDesignOA :: OATable -> [DesignFactor] -> Design
-taguchiDesignOA table specs =
-  let oa = oaTableToOA table
-  in case assignOAColumns oa specs of
-    Nothing   -> error
-      ("taguchiDesignOA: " ++ show table ++ " (列水準 " ++ show (oaLevels oa)
-        ++ ") に因子の水準構成 " ++ show (map factorLevelCount specs) ++ " を割り当てられません")
-    Just cols -> Design specs (oaToCodedCols oa cols specs) KFractional
-
--- ---------------------------------------------------------------------------
--- 最適計画 (optimal design) — Phase 78.G-b1
---
--- 標準の格子計画 (factorial/RSM) と違い、 **モデル formula** と **run 数 n** を先に決め、
--- 候補点集合から情報行列 XᵀX の基準 (D/A/I/E/G) を最適化する n 点を選ぶ (Fedorov 交換)。
--- run 数が制約されている・非標準のモデル項を当てたい・候補領域が不規則、 等で使う。
---
--- 実装は既存部品の**接着**で、 新規の数値アルゴは無い:
---   1. 因子 specs から coded 候補グリッド 'candidateGrid' (各因子 [-1,1] の等間隔 levels 水準)。
---   2. グリッド各点を DataFrame 化し 'modelFrame'+'designMatrixF' で **formula の設計行列 X 行**へ展開。
---   3. 低レベル 'OPT.optimalDesign' (Fedorov) で n 行を選択 → 選択 index。
---   4. 選ばれた候補の**因子座標**を 'dsCoded' に、 formula を 'KCustom' に焼き込む。
--- 以降は 'designTable'/'designFrame'/'designModel' が factorial 等と同じ共通経路で処理する。
---
--- モデルは 'Formula' に一本化 (二重管理回避)。 入口は 2 つ:
---   * 効果 DSL 'mainEffects'/'twoWay'/'quadratic' (対話向け・型付き)。
---   * 文字列 RHS を 'parseRFormula' で 'Formula' に (アプリ/外部から組み立て)。
--- ★連続因子のみ (v1)。 カテゴリ因子は 'DesignFactor' の水準リスト化 (型手術) を伴う G-b2。
--- ---------------------------------------------------------------------------
-
--- | 主効果のみモデル @y ~ x1 + x2 + …@ を作る効果 DSL。 応答は placeholder (@_y@)。
---   'optimalDesign' のモデル引数に渡す。
-mainEffects :: [Text] -> Formula
-mainEffects names = effectFormula (T.intercalate " + " names)
-
--- | 主効果 + 全 2 因子交互作用モデル @y ~ x1 + x2 + x1:x2 + …@ を作る効果 DSL。
-twoWay :: [Text] -> Formula
-twoWay names = effectFormula (T.intercalate " + " (names ++ twoWayTerms names))
-
--- | 主効果 + 2 因子交互作用 + 2 次項モデル @y ~ … + I(x1^2) + …@ (RSM 相当) を作る効果 DSL。
---   2 次項を含むので 'optimalDesign' の既定候補水準は 3 になる。
-quadratic :: [Text] -> Formula
-quadratic names =
-  effectFormula (T.intercalate " + " (names ++ twoWayTerms names ++ squareTerms names))
-
--- | 全 2 因子交互作用の項 (@a:b@) を名前順に。
-twoWayTerms :: [Text] -> [Text]
-twoWayTerms names =
-  [ a <> ":" <> b | (i, a) <- zip [0 :: Int ..] names, b <- drop (i + 1) names ]
-
--- | 各因子の 2 次項 (@I(x^2)@)。
-squareTerms :: [Text] -> [Text]
-squareTerms names = [ "I(" <> n <> "^2)" | n <- names ]
-
--- ---------------------------------------------------------------------------
--- Phase 78.M M3: Formula (効果 DSL) → Custom.Model ([ModelTerm]) 変換
--- ---------------------------------------------------------------------------
-
--- | 効果 DSL / 'Formula' を Custom Design 層の 'CMd.Model' ('mainEffects'/'twoWay'/
---   'quadratic' 相当の @[ModelTerm]@) に変換する。 高レベル生成 API ('customDesign'・M4)
---   が座標交換 ('coordinateExchangePure') に渡すモデルを組み立てる橋渡し。
---
---   効果 DSL の formula は 'parseRFormula' が各項に係数パラメータ @_pN@ を挿入した
---   積和 (例 @_p0 + _p1·a + _p3·(a·b) + _p4·a^2@) になる。 加法分解した各項の
---   **因子名を参照する葉だけ**を残して分類する (与えた @facNames@ に含まれる 'Ref' が因子・
---   それ以外の 'Ref' は係数パラメータとして無視):
---
---     * 因子葉が 0 個 (係数のみ)          → 'CMd.TIntercept'
---     * 単一 @Ref x@                        → 'CMd.TMain' x
---     * 単一 @Bin Pow (Ref x) (Lit k)@      → 'CMd.TPower' x (round k)
---     * 複数の @Ref@ の積                    → 'CMd.TInter' [x, y, …]
---
---   正規化は 'CMd.NCoded' (optimalDesign と同じ coded 規約)。 基底関数 (@opoly@/@bspline@)
---   や未対応構造は @Left@ を返す (M3 スコープ = 主効果 / 2 因子交互作用 / 冪)。
-formulaToCustomModel :: [Text] -> Formula -> Either Text CMd.Model
-formulaToCustomModel facNames (Formula _ _ rhs) = do
-  terms <- mapM termToModelTerm (flattenAddW rhs)
-  Right (CMd.Model terms CMd.NCoded)
-  where
-    isFacRef (Ref x) = x `elem` facNames
-    isFacRef _       = False
-    -- 係数パラメータ葉 = facNames に無い裸の 'Ref' (parseRFormula が挿入する @_pN@)。
-    isParamLeaf (Ref x) = x `notElem` facNames
-    isParamLeaf _       = False
-    -- 積を葉分解し、 係数パラメータを除いた「中核」葉で項を分類する。
-    -- 中核が空 = 係数のみ = 切片。 それ以外は因子参照 (主効果/交互作用/冪)。
-    -- 中核に因子を参照しない葉 (基底関数 'App' 等) が残れば未対応 → Left。
-    termToModelTerm t =
-      case filter (not . isParamLeaf) (mulLeavesW t) of
-        []        -> Right CMd.TIntercept
-        [Ref x] | x `elem` facNames -> Right (CMd.TMain x)
-        [Bin Pow (Ref x) (Lit k)]
-          | x `elem` facNames && k >= 2 -> Right (CMd.TPower x (round k))
-        ls | not (null ls) && all isFacRef ls ->
-               Right (CMd.TInter [ x | Ref x <- ls ])
-        _ -> Left (T.pack
-               ("formulaToCustomModel: 未対応の項 (基底関数や非線形項は Custom.Model に"
-                <> " 変換できません・主効果/交互作用/冪のみ対応): " <> show t))
-
--- | 加法項へ分解 (符号は係数に吸収されるので Add/Sub/Neg 同一視)。
---   'Formula.Design.flattenAdd' と同義だが import 循環回避のため局所定義。
-flattenAddW :: Term -> [Term]
-flattenAddW (Bin Add a b) = flattenAddW a ++ flattenAddW b
-flattenAddW (Bin Sub a b) = flattenAddW a ++ flattenAddW b
-flattenAddW (Neg a)       = flattenAddW a
-flattenAddW t             = [t]
-
--- | 乗法葉へ分解。 'Formula.Design.mulLeaves' と同義 (局所定義)。
-mulLeavesW :: Term -> [Term]
-mulLeavesW (Bin Mul a b) = mulLeavesW a ++ mulLeavesW b
-mulLeavesW (Neg a)       = mulLeavesW a
-mulLeavesW t             = [t]
-
--- ---------------------------------------------------------------------------
--- Phase 79: 高レベル生成 API (pure・座標交換 / 階層構造 / 制約)
--- ---------------------------------------------------------------------------
-
--- | 高レベル 'DesignFactor' を Custom Design 層の 'CF.Factor' に変換する。
---   連続 = coded [-1,1] 前提の 'CF.Continuous'、 カテゴリ = 'CF.Categorical' (水準 index)。
---   数値順序 ('Num') は **水準 index を grid とする** 'CF.DiscreteNum' @[0..k-1]@ に写す
---   (Phase 78.M M4-b)。 これで座標交換の出力 (cdMatrix) が水準 index になり、 'dsCoded' の
---   Num 規約 ('numLevelAt' = index → 実水準値) と一致する。 ★交換は index 尺度 (等間隔) で
---   D-最適化する (実測不等間隔の直交多項式 opoly は Custom.Model 非対応ゆえ、 モデル項は
---   ユーザ formula の I(x^2) 等がそのまま使われる)。
---   ★役割 ('CF.fRole') は Phase 79 で高レベルから撤去したので一律 'CF.Controllable'
---   (階層は因子ではなく 'Structure' が持つ。 fRole の去就は 79.2 の M-construction 移植時に判断)。
-toCustomFactor :: DesignFactor -> CF.Factor
-toCustomFactor f = CF.Factor (dfName f) (kindC (dfKind f)) CF.Controllable
-  where
-    kindC (Cont lo hi _) = CF.Continuous lo hi
-    kindC (Cat ls)     = CF.Categorical ls
-    kindC (Num levels) = CF.DiscreteNum (map fromIntegral [0 .. length levels - 1])
-
--- | 因子 + formula から Custom Design の 'CX.CustomDesignSpec' を組み立てる共通処理 (M4)。
---   モデルは 'formulaToCustomModel' で [ModelTerm] 化 (失敗は error)。 最適化基準 @crit@ と
---   制約 @cons@ を受け取り、 budget は既定。 座標交換 / split-plot 生成の両方が使う。
-toCustomSpec :: OptCriterion -> [Constraint]
-             -> [DesignFactor] -> Formula -> Int -> Int -> CX.CustomDesignSpec
-toCustomSpec crit cons fs fml n seed =
-  case formulaToCustomModel (map dfName fs) fml of
-    Left e      -> error ("customDesign: モデル変換に失敗: " <> T.unpack e)
-    Right model -> CX.CustomDesignSpec
-      { CX.cdsFactors      = map toCustomFactor fs
-      , CX.cdsModel        = model
-      , CX.cdsConstraints  = cons
-      , CX.cdsNRuns        = n
-      , CX.cdsCriterion    = crit
-      , CX.cdsBudget       = CX.defaultBudget
-      , CX.cdsSeed         = Just seed
-      , CX.cdsInitial      = Nothing
-      , CX.cdsDJConvention = False
-      }
-
--- | **完全カスタムデザインの生成** (pure・Phase 79)。 唯一の生成入口。 'CustomSpec' 1 本
---   (因子 × 固定効果モデル × run 数 × seed × 基準 × 制約 × 'Structure') を受け取り、
---   構造に応じた座標交換 (M / 制約 / 群単位ムーブ) を解いて 'Design' ('KStructured') に包む。
---   **seed 決定的**な純粋関数 (同 seed → 同結果)。
---
---   > -- CRD (最小)
---   > let plan = customDesign (customSpec fs (quadratic ["x1","x2"]) 12 42)
---   >
---   > -- 制約つき CRD
---   > customDesign (customSpec fs fml 8 42)
---   >   { csConstraints = [LinearIneq [("x1",1),("x2",1)] CLeq 0.5] }
---   >
---   > -- split-plot (temp が whole-plot) + 制約
---   > customDesign (customSpec fs (twoWay ["temp","rate"]) 8 50)
---   >   { csStructure = splitPlot ["temp"] 4, csConstraints = [RangeBound "rate" (-0.5) 1] }
---
---   ★制約の因子参照は **coded 単位** ([-1,1]) で書く (座標交換が coded 空間で解くため)。
---   v1 未実装の構造は @unsupported structure@ で error。
-customDesign :: CustomSpec -> Design
-customDesign cs = case csStructure cs of
-  CRD ->
-    -- CRD は M=I・per-cell の高速路 ('coordinateExchangePure') へそのまま委譲 (ビット一致)。
-    let fs   = csFactors cs
-        spec = toCustomSpec (csCriterion cs) (effectiveConstraints cs) fs
-                            (csFormula cs) (csNRuns cs) (csSeed cs)
-    in case CX.coordinateExchangePure spec of
-         Left e   -> error (T.unpack (enrichInfeasError cs e))
-         Right cd -> Design fs (LA.toLists (CX.cdMatrix cd)) (KStructured [] (csFormula cs))
-  structure ->
-    -- 非自明な構造 (SplitPlot/StripPlot/Blocked) は Structure を GroupingPlan に
-    -- コンパイルし、 構造駆動エンジン ('structuredExchangePure') で群単位ムーブ + GLS 基準を解く。
-    let fs = csFactors cs
-    in case buildGrouping fs (csNRuns cs) structure of
-         Left e -> error ("customDesign: " <> T.unpack e)
-         Right (gplan, groups) ->
-           let spec = toCustomSpec (csCriterion cs) (effectiveConstraints cs) fs
-                                   (csFormula cs) (csNRuns cs) (csSeed cs)
-           in case ST.structuredExchangePure spec gplan of
-                Left e       -> error (T.unpack (enrichInfeasError cs e))
-                Right (m, _) -> Design fs (LA.toLists m) (KStructured groups (csFormula cs))
-
--- | 'Structure' を構造駆動エンジンの 'GroupingPlan' (列ごと cells + M⁻¹) と
---   出力用群列 @[(群列名, 各 run の群 ID)]@ にコンパイルする (Phase 79)。 CRD は
---   別処理 ('customDesign' が高速路へ委譲) ゆえここでは扱わない。 v1 未実装構造は
---   @unsupported structure@ で 'Left'。
-buildGrouping :: [DesignFactor] -> Int -> Structure
-              -> Either Text (ST.GroupingPlan, [(Text, [Int])])
-buildGrouping fs n structure = case structure of
-  CRD -> Left "buildGrouping: CRD は構造エンジンを使わない (内部エラー)"
-  SplitPlot whole nWhole eta col
-    | nWhole < 1 -> Left "whole-plot 数 (spNWhole) は 1 以上が必要"
-    | n < nWhole -> Left "run 数 n は whole-plot 数 (spNWhole) 以上が必要"
-    | null whole -> Left "split-plot に whole-plot 因子名 (spWhole) が空"
-    | not (all (`elem` names) whole) ->
-        Left ("whole-plot 因子名 "
-               <> T.pack (show (map T.unpack (filter (`notElem` names) whole)))
-               <> " が因子リストに無い")
-    | otherwise ->
-        let ids    = balancedGroupIds n nWhole
-            idsV   = VS.fromList ids
-            wpCols = [ j | (j, f) <- zip [0 :: Int ..] fs, dfName f `elem` whole ]
-            cells  = [ if j `elem` wpCols then groupCells ids nWhole else perRowCells n
-                     | j <- [0 .. length fs - 1] ]
-            mInv   = ST.buildMInvFromGroups n [(eta, idsV)]
-        in Right (ST.GroupingPlan cells mInv, [(col, ids)])
-  StripPlot whole nWhole etaW wCol strip nStrip etaS sCol
-    | nWhole < 1 || nStrip < 1 -> Left "strip-plot の whole-plot 数 / strip 数は 1 以上が必要"
-    | n /= nWhole * nStrip ->
-        Left ("strip-plot は n = stNWhole × stNStrip が必要 (n=" <> tshowW n
-               <> ", " <> tshowW nWhole <> "×" <> tshowW nStrip <> "=" <> tshowW (nWhole * nStrip) <> ")")
-    | null whole || null strip -> Left "strip-plot に whole-plot / strip 因子名が空"
-    | not (all (`elem` names) (whole ++ strip)) ->
-        Left ("strip-plot 因子名 "
-               <> T.pack (show (map T.unpack (filter (`notElem` names) (whole ++ strip))))
-               <> " が因子リストに無い")
-    | not (null (filter (`elem` strip) whole)) ->
-        Left "同一因子を whole-plot と strip の両方に指定できません"
-    | otherwise ->
-        let wpIds    = balancedGroupIds n nWhole          -- 行 i → i `div` nStrip
-            stripIds = [ i `mod` nStrip | i <- [0 .. n - 1] ]
-            wpCols   = [ j | (j, f) <- zip [0 :: Int ..] fs, dfName f `elem` whole ]
-            stCols   = [ j | (j, f) <- zip [0 :: Int ..] fs, dfName f `elem` strip ]
-            cells    = [ if j `elem` wpCols then groupCells wpIds nWhole
-                         else if j `elem` stCols then groupCells stripIds nStrip
-                         else perRowCells n
-                       | j <- [0 .. length fs - 1] ]
-            mInv     = ST.buildMInvFromGroups n
-                         [(etaW, VS.fromList wpIds), (etaS, VS.fromList stripIds)]
-        in Right (ST.GroupingPlan cells mInv, [(wCol, wpIds), (sCol, stripIds)])
-  Blocked nBlocks eta col
-    | nBlocks < 1 -> Left "ブロック数 (blkNBlocks) は 1 以上が必要"
-    | n < nBlocks -> Left "run 数 n はブロック数 (blkNBlocks) 以上が必要"
-    | otherwise ->
-        -- ランダムブロック: 全因子はブロック内で自由 (per-row cell)。 block は run 割付のみで
-        -- M = I + η·Z_B Z_Bᵀ に効く。 grouped 列は無い。
-        let ids   = balancedGroupIds n nBlocks
-            cells = replicate (length fs) (perRowCells n)
-            mInv  = ST.buildMInvFromGroups n [(eta, VS.fromList ids)]
-        in Right (ST.GroupingPlan cells mInv, [(col, ids)])
-  where names  = map dfName fs
-        tshowW = T.pack . show
-
--- | n 行を k 群に均等割り当て (余りは先頭群に +1)。 各行 → 群 ID (0..k-1)。
-balancedGroupIds :: Int -> Int -> [Int]
-balancedGroupIds n k =
-  let base  = n `div` k
-      extra = n `mod` k
-      sizes = [ if i < extra then base + 1 else base | i <- [0 .. k - 1] ]
-  in concat [ replicate s i | (i, s) <- zip [0 ..] sizes ]
-
--- | 群 ID リストから「同群の行集合」 の cells を作る (grouped 列用)。
-groupCells :: [Int] -> Int -> [[Int]]
-groupCells ids k = [ [ i | (i, g) <- zip [0 ..] ids, g == w ] | w <- [0 .. k - 1] ]
-
--- | per-row cells (各行が独立の cell・CRD 因子 / sub-plot 因子用)。
-perRowCells :: Int -> [[Int]]
-perRowCells n = [ [i] | i <- [0 .. n - 1] ]
-
--- | RHS 文字列 (R 構文) を placeholder 応答 @_y@ 付きで 'parseRFormula' に通す糖衣。
---   効果 DSL は別モデル表現を作らず 'Formula' を組み立てるだけ (二重管理回避)。
-effectFormula :: Text -> Formula
-effectFormula rhs =
-  either (\e -> error ("effect DSL: formula parse error: " ++ e)) id
-         (parseRFormula ("_y ~ " <> rhs))
-
--- | formula の RHS に 2 次以上の冪 (@Bin Pow@) が含まれるか。 候補水準の既定値
---   (2 次項があれば 3 水準・無ければ 2 水準) を決めるのに使う。
-formulaHasPower :: Formula -> Bool
-formulaHasPower (Formula _ _ rhs) = go rhs
-  where
-    go (Bin Pow _ _) = True
-    go (Bin _ a b)   = go a || go b
-    go (Neg a)       = go a
-    go (Index a b)   = go a || go b
-    go (App _ as)    = any go as
-    go _             = False
-
--- | D-最適計画 (既定基準 = 'DOpt'・seed = 42・候補水準は自動)。 @n@ = run 数 (必須)。
---   @optimalDesign [contFactor "t" (150,180), contFactor "p" (1,5)] (quadratic ["t","p"]) 10@。
---   候補水準は formula が 2 次項を含めば 3、 他は 2 (明示は 'optimalDesignLevels')。 カテゴリ因子
---   ('catFactor') は候補格子で全水準を展開し、 設計行列で contrast 展開される。
---   @n@ が formula のパラメータ数 @p@ 未満だと error (情報行列が特異)。
-optimalDesign :: [DesignFactor]              -- ^ 因子 ('contFactor' / 'catFactor')
-              -> Formula                     -- ^ モデル formula (効果 DSL / 'parseRFormula')
-              -> Int                         -- ^ run 数 n
-              -> Design
-optimalDesign = optimalDesignWith DOpt Nothing 42
-
--- | 候補水準を明示する D-最適計画 (基準 = 'DOpt'・seed = 42)。 各連続因子 [-1,1] を @levels@
---   水準に離散化した格子を候補集合にする (カテゴリ因子は水準数固定なので @levels@ の影響を受けない)。
-optimalDesignLevels :: Int                          -- ^ 候補格子の水準数 (各連続因子)
-                    -> [DesignFactor]
-                    -> Formula
-                    -> Int
-                    -> Design
-optimalDesignLevels levels = optimalDesignWith DOpt (Just levels) 42
-
--- | 最適計画 (フル制御)。 基準 'OptCriterion' (D/A/I/E/G/Compound/BayesianD)、 候補水準
---   (@Nothing@ = 自動)、 seed を明示。 @n@ = run 数 (必須・@n >= p@ 検査あり)。
-optimalDesignWith :: OptCriterion              -- ^ 最適化基準
-                  -> Maybe Int                 -- ^ 候補格子の水準数 (Nothing = 自動)
-                  -> Int                       -- ^ seed (初期選択)
-                  -> [DesignFactor]
-                  -> Formula
-                  -> Int                       -- ^ run 数 n
-                  -> Design
-optimalDesignWith crit mLevels seed fs fml n =
-  let lv   = maybe (if formulaHasPower fml then 3 else 2) id mLevels
-      grid = fullFactorial (map (factorCandidateLevels lv) fs)
-  in case candidateXRows fml fs grid of
-       Left err -> error ("optimalDesign: 設計行列の構築に失敗: " ++ err)
-       Right xRows ->
-         let p = if null xRows then 0 else length (head xRows)
-         in if n < p
-              then error
-                ("optimalDesign: run 数 n=" ++ show n
-                  ++ " がモデルのパラメータ数 p=" ++ show p
-                  ++ " 未満です (n >= p が必要・情報行列が特異になる)")
-              else let (idx, _) = OPT.optimalDesign crit xRows n seed
-                   in Design fs (map (grid !!) idx) (KCustom fml)
-
--- | 因子の候補水準集合 (最適計画のグリッド)。 連続 = @[-1,1]@ を @lv@ 等間隔 (candidateGrid 相当)、
---   カテゴリ = 水準 index @{0,…,m-1}@ (水準数固定・@lv@ 無関係)。 全連続なら 'candidateGrid' と一致。
-factorCandidateLevels :: Int -> DesignFactor -> [Double]
-factorCandidateLevels lv f = case dfKind f of
-  Cont _ _ _ -> evenSpaced lv
-  Num levels -> map fromIntegral [0 .. length levels - 1]
-  Cat levels -> map fromIntegral [0 .. length levels - 1]
-  where
-    evenSpaced n
-      | n <= 1    = [0]
-      | otherwise = [ -1 + 2 * fromIntegral i / fromIntegral (n - 1)
-                    | i <- [0 .. n - 1] :: [Int] ]
-
--- | 候補グリッド (coded 因子座標) を formula の設計行列 X 行 (@[[Double]]@) に展開する。
---   連続因子は coded 値のまま数値項に、 カテゴリ因子は水準名 (Text) 列にして contrast 展開させる。
---   応答は placeholder 列を 0 埋め (設計行列は RHS のみに依存し応答値は無関係)。
-candidateXRows :: Formula -> [DesignFactor] -> [[Double]] -> Either String [[Double]]
-candidateXRows fml fs grid = do
-  mf     <- modelFrame fml (candidateFrame fml fs grid)
-  (x, _) <- designMatrixF fml mf
-  pure (LA.toLists x)
-
--- | 候補グリッド (coded 座標行) を DataFrame 化 (応答 placeholder 列 + 各因子列)。
---   連続 = coded Double 列、 カテゴリ = 水準名 Text 列 ('modelFrame' で factor 扱い)。
-candidateFrame :: Formula -> [DesignFactor] -> [[Double]] -> DX.DataFrame
-candidateFrame fml fs grid =
-  DX.fromNamedColumns $
-    (formResponse fml, DX.fromList (replicate (length grid) (0 :: Double)))
-      : [ factorFrameColumn Nothing False f [ row !! j | row <- grid ]
-        | (j, f) <- zip [0 :: Int ..] fs ]
-
--- ---------------------------------------------------------------------------
--- 取り出し
--- ---------------------------------------------------------------------------
-
-designFactorNames :: Design -> [Text]
-designFactorNames = map dfName . dsFactors
-
--- | 実行用 runsheet (uncoded 実値・**連続因子のみ**)。 先頭に @run@ 番号列、 続いて各因子の
---   実値列。 戻り値は 'ColumnSource' なので @designTable plan |-> …@ / 表示に使える。
---   ★カテゴリ因子を含む設計では数値 runsheet に水準名を出せないので **error** となる
---   ('designFrame' を使うこと・Text 列を持つ整形表を返す)。
-designTable :: Design -> [(Text, [Double])]
-designTable (Design fs coded _) =
-  case [ dfName f | f <- fs, isCat (dfKind f) ] of
-    (c : _) -> error
-      ("designTable: カテゴリ因子 " ++ T.unpack c
-        ++ " は数値 runsheet に出せません。 designFrame を使ってください "
-        ++ "(Text 列を持つ整形表 DataFrame を返します)")
-    []      ->
-      ("run", map fromIntegral [1 .. length coded])
-        : [ (dfName f, [ tableCell f (row !! j) | row <- coded ])
-          | (j, f) <- zip [0 ..] fs ]
-  where
-    isCat (Cat _) = True
-    isCat _       = False
-    -- Cont は uncoded 実値、 Num は水準 index → 実水準値。 Cat は上でガード済。
-    tableCell f v = case dfKind f of
-      Num levels -> numLevelAt levels v
-      _          -> uncodeCont f v
-
--- | runsheet を **整形表** (Hackage @DataFrame@) にする。 連続因子は uncoded 実値の Double 列、
---   **カテゴリ因子は水準名の Text 列** として直接構築する (数値 'designTable' 経由でなく列ごと)。
---   DataFrame は 'ColumnSource' ゆえ @designFrame plan |-> …@ もそのまま通り、 fit 側は
---   Text 列を contrast 展開する。 @print (designFrame plan)@ で型付き ASCII テーブルを確認できる。
-designFrame :: Design -> DX.DataFrame
-designFrame (Design fs coded kind) =
-  DX.fromNamedColumns $
-    ("run", DX.fromList (map fromIntegral [1 .. length coded] :: [Double]))
-      : [ factorFrameColumn Nothing True f [ row !! j | row <- coded ]
-        | (j, f) <- zip [0 :: Int ..] fs ]
-      ++ splitGroupColumns kind
-
--- | 実験者に渡す runsheet を **小数第 @nd@ 位に丸めて**返す ('designFrame' の桁数調整版)。
---   応答曲面 (CCD) の軸点 (±α) など無理数由来の長い小数 (@143.78679656440357@) を
---   @designFrameRound 2 plan@ で @143.79@ 等に整える。 連続 / 数値順序因子の実値列だけを丸め、
---   run 番号 / カテゴリ (Text) / 群列はそのまま。 ★丸めた値がそのまま runsheet の値になる
---   (実験は丸めた水準で行う想定)。 fit にそのまま載せてよい ('designFrame' 同様 'ColumnSource')。
---
---   > print (designFrameRound 2 (centralCompositeDesign [contFactor "temp" (150,180), …]))
-designFrameRound :: Int -> Design -> DX.DataFrame
-designFrameRound nd (Design fs coded kind) =
-  DX.fromNamedColumns $
-    ("run", DX.fromList (map fromIntegral [1 .. length coded] :: [Double]))
-      : [ factorFrameColumn (Just nd) True f [ row !! j | row <- coded ]
-        | (j, f) <- zip [0 :: Int ..] fs ]
-      ++ splitGroupColumns kind
-
--- | 完全カスタムデザインの群列を Text ラベルで作る。 各群列は @<列名>0, <列名>1, …@
---   のラベル (例: @wholePlot0@ / @strip0@ / @block0@)。 CRD (群列なし) や他種別は空。
---   designModelHBM の grouping 列は getTextVec で読まれる (Text 必須) ため Text 化する。
-splitGroupColumns :: DesignKind -> [(Text, DX.Column)]
-splitGroupColumns (KStructured groups _) =
-  [ (col, DX.fromList (map (\i -> col <> T.pack (show i)) ids :: [Text]))
-  | (col, ids) <- groups ]
-splitGroupColumns _ = []
-
--- | 因子 1 列を coded 座標列から DataFrame 列に。 連続因子は Double 列 (@uncodeC@=True なら
---   uncoded 実値へ・'designFrame' 用、 False なら coded のまま・'candidateFrame' 用)、
---   カテゴリ因子は水準 index を 'Cat' リストで引いた**水準名 Text 列** ('modelFrame' で factor 扱い)。
---   @mRound = Just nd@ なら Double 列 (連続 / 数値順序) を小数第 @nd@ 位に丸める
---   ('designFrameRound' 用)。 カテゴリ Text 列は丸めない。
-factorFrameColumn :: Maybe Int -> Bool -> DesignFactor -> [Double] -> (Text, DX.Column)
-factorFrameColumn mRound uncodeC f coded = case dfKind f of
-  Cont _ _ _ -> (dfName f, DX.fromList (map (rnd . contVal) coded))
-  Num levels -> (dfName f, DX.fromList (map (rnd . numLevelAt levels) coded))  -- 水準 index → 実値 Double
-  Cat levels -> (dfName f, DX.fromList (map (levelAt levels) coded :: [Text]))
-  where
-    contVal = if uncodeC then uncodeCont f else id
-    rnd     = maybe id roundTo mRound
-    levelAt levels v =
-      let i = round v
-      in if i >= 0 && i < length levels then levels !! i else "?"
-
--- | 小数第 @n@ 位への丸め (負値・整数もそのまま)。 @roundTo 2 143.78679 = 143.79@。
-roundTo :: Int -> Double -> Double
-roundTo n x = fromIntegral (round (x * m) :: Integer) / m
-  where m = 10 ^^ n
-
--- | 数値順序因子の coded (水準 index) を実水準値へ。 範囲外は NaN (呼び元は index を保証)。
-numLevelAt :: [Double] -> Double -> Double
-numLevelAt levels v =
-  let i = round v
-  in if i >= 0 && i < length levels then levels !! i else 0/0
-
--- | 連続因子の coded 値 @c@ を uncoded 実値へ。 線形は @center + c·half@、 対数は
---   @10^(logCenter + c·logHalf)@ (幾何)。 カテゴリ因子で呼ぶと error (呼び元が連続に
---   限定して使う・'designTable' はガード済)。
-uncodeCont :: DesignFactor -> Double -> Double
-uncodeCont f c = case dfKind f of
-  Cont lo hi SLinear -> (lo + hi) / 2 + c * (hi - lo) / 2
-  Cont lo hi SLog    ->
-    let llo = logBase 10 lo; lhi = logBase 10 hi
-    in 10 ** ((llo + lhi) / 2 + c * (lhi - llo) / 2)
-  Num _      -> error ("uncodeCont: " ++ T.unpack (dfName f) ++ " は数値順序因子です (numLevelAt を使う)")
-  Cat _      -> error ("uncodeCont: " ++ T.unpack (dfName f) ++ " はカテゴリ因子です")
-
--- | 連続因子の uncoded 実値 @x@ を coded 値へ ('uncodeCont' の逆)。 線形は @(x−center)/half@、
---   対数は @(log10 x − logCenter)/logHalf@。 対数因子で @x <= 0@ は NaN (呼び元が正値を保証)。
---   カテゴリ/数値順序因子で呼ぶと error。
-codeCont :: DesignFactor -> Double -> Double
-codeCont f x = case dfKind f of
-  Cont lo hi SLinear ->
-    let half = (hi - lo) / 2
-    in if half == 0 then 0 else (x - (lo + hi) / 2) / half
-  Cont lo hi SLog    ->
-    let llo = logBase 10 lo; lhi = logBase 10 hi
-        lhalf = (lhi - llo) / 2
-    in if lhalf == 0 then 0 else (logBase 10 x - (llo + lhi) / 2) / lhalf
-  Num _ -> error ("codeCont: " ++ T.unpack (dfName f) ++ " は数値順序因子です")
-  Cat _ -> error ("codeCont: " ++ T.unpack (dfName f) ++ " はカテゴリ因子です")
-
--- ---------------------------------------------------------------------------
--- 設計の保存 / DataFrame からの復元 (Phase 78.K)
--- ---------------------------------------------------------------------------
-
--- | 設計の **runsheet** ('designFrame') を CSV に書き出す。 実験者に渡す runsheet
---   (uncoded 実値・run 番号列・カテゴリは水準名) がそのまま保存される。
---
---   > saveDesign "runsheet.csv" plan
-saveDesign :: FilePath -> Design -> IO ()
-saveDesign path = DXIO.writeCsv path . designFrame
-
--- | **DataFrame から設計 ('Design') を復元**する。 因子 (名前 + 種類 + 範囲/水準) と
---   モデル formula (効果 DSL 'mainEffects' / 'quadratic' 等 or 'parseRFormula') を明示し、
---   @df@ の各因子列を coded 化して 'KCustom' 設計に包む。 CSV から読んだ runsheet を
---   解析ワークフロー (@df |-> designModel plan "y"@ / 'rsmAnalysis') に載せ直すのに使う。
---
---   > let plan = planFromFrame [contFactor "temp" (150,180), contFactor "time" (10,20)]
---   >                          (quadratic ["temp","time"]) loadedDf
---   > filledDf |-> designModel plan "y"
---
---   ★fit は formula + df だけで動く ('designModel') が、 'rsmAnalysis' /
---   'steepestAscentNatural' は coded 幾何を使うので、 因子の範囲/水準を正しく渡すこと。
---   因子列が @df@ に無い / 型が合わない場合は error。
-planFromFrame :: [DesignFactor] -> Formula -> DX.DataFrame -> Design
-planFromFrame fs fml df =
-  let cols  = map (`factorCodedColumn` df) fs   -- 各因子の coded 列 (行順)
-      coded = transpose cols                    -- 行 = run、 列 = 因子
-  in Design fs coded (KCustom fml)
-
--- | 因子 1 列を @df@ から取り出し coded 座標列にする。 連続 = @(x−center)/half@、
---   数値順序 = 最近傍水準の index、 カテゴリ = 水準名の index (designFrame の逆変換)。
-factorCodedColumn :: DesignFactor -> DX.DataFrame -> [Double]
-factorCodedColumn f df = case dfKind f of
-  Cont _ _ _ -> [ codeCont f x | x <- doubles ]   -- 線形/対数は codeCont が分岐
-  Num levels -> [ fromIntegral (nearestIndex levels x) | x <- doubles ]
-  Cat levels -> [ fromIntegral (catIndex levels t)     | t <- texts ]
-  where
-    nm = dfName f
-    doubles = case getDoubleVec nm df of
-      Just v  -> V.toList v
-      Nothing -> error ("planFromFrame: 数値因子列 '" <> T.unpack nm <> "' が df に無い / 数値でない")
-    texts = case getTextVec nm df of
-      Just v  -> V.toList v
-      Nothing -> error ("planFromFrame: カテゴリ因子列 '" <> T.unpack nm <> "' が df に無い / Text でない")
-    nearestIndex levels x =
-      fst (minimumBy (comparing (\(_, l) -> abs (l - x))) (zip [0 :: Int ..] levels))
-    catIndex levels t = case elemIndex t levels of
-      Just i  -> i
-      Nothing -> error
-        ("planFromFrame: カテゴリ因子 '" <> T.unpack nm <> "' に水準 '" <> T.unpack t
-          <> "' が定義されていません (catFactor の水準: " <> show (map T.unpack levels) <> ")")
-
--- | 設計種別からモデル formula 文字列を生成 (@y@ = 応答列名)。
---   要因計画 = 全交互作用 (@y ~ x1 * x2 * …@)、 RSM = 2 次
---   (@y ~ x1 + x2 + x1:x2 + I(x1^2) + I(x2^2)@)、 一部実施 = **主効果のみ**
---   (@y ~ x1 + x2 + …@・交互作用は交絡ゆえ v1 は含めない)。
---   最適計画 ('KCustom') は焼き込んだ formula の応答を @y@ に差し替えて返す
---   (native 正規形。 'multiLMModel' は @~@ の有無で R/独自 front-end を自動判別する)。
-designFormula :: Design -> Text -> Text
-designFormula (Design fs _ kind) y =
-  let names = map dfName fs
-      k     = length names
-      inter = [ (names !! i) <> ":" <> (names !! j)
-              | i <- [0 .. k - 1], j <- [i + 1 .. k - 1] ]
-      sq    = [ "I(" <> n <> "^2)" | n <- names ]
-      -- 主効果項: 数値順序因子 ('Num') は直交多項式 opoly(name, 水準数−1)
-      -- (linear+quadratic…・実測間隔で直交)、 連続/カテゴリは主効果名 (Cat は engine が contrast 展開)。
-      mainTerm f = case dfKind f of
-        Num levels -> "opoly(" <> dfName f <> "," <> tshow (length levels - 1) <> ")"
-        _          -> dfName f
-  in case kind of
-    KCustom fml       -> prettyFormula (fml { formResponse = y })
-    KStructured _ fml -> prettyFormula (fml { formResponse = y })  -- 固定効果は KCustom 同様
-    KFactorial  -> y <> " ~ " <> T.intercalate " * " names
-    KRSM        -> y <> " ~ " <> T.intercalate " + " (names ++ inter ++ sq)
-    KFractional -> y <> " ~ " <> T.intercalate " + " (map mainTerm fs)  -- 主効果のみ
-    -- 主効果 + 主効果と交絡しない 2FI の代表 (交絡群ごと 1 個)。
-    KFracInter gens ->
-      let reps = [ (names !! (i - 1)) <> ":" <> (names !! (j - 1))
-                 | [i, j] <- clearTwoFactorInteractions k gens ]
-      in y <> " ~ " <> T.intercalate " + " (map mainTerm fs ++ reps)
-  where tshow = T.pack . show
-
--- ---------------------------------------------------------------------------
--- 応答曲面 解析 (自然単位で報告) — Phase 78.G-d
--- ---------------------------------------------------------------------------
---
--- R rsm の要点は「fit を coded 空間でやる」ことではない (coded/uncoded fit は予測が
--- 同一・単なる便宜)。 本質は **runsheet を自然単位で発行し、 スケール依存な最適化幾何
--- (停留点 / canonical / steepest ascent) だけ内部で coded の計量を使い、 結果を自然単位で
--- 報告する** ワークフロー。 ここではその報告レイヤを与える。
---
---   * fit そのものは 'designModel' が自然単位で行う (係数がそのまま実単位で読める)。
---   * 一方、 停留点の方向・canonical 軸・steepest ascent 方向は計量依存なので、
---     設計が保持する coded 行列 ('Design' の coded ±1/±α) で二次モデルを当て
---     ('fitQuadratic')、 幾何を coded で解いてから 'uncodeCont' で自然単位へ decode する。
-
--- | 応答曲面の停留点の性質 (canonical 固有値の符号で判定)。
-data RSMNature = RMaximum | RMinimum | RSaddle
-  deriving (Show, Eq)
-
--- | 応答曲面 解析レポート。 停留点・予測は**自然単位**、 canonical 方向は coded 座標
---   (設計の実験範囲を単位に取った軸) で報告する。
-data RSMReport = RSMReport
-  { rsmStationary :: ![(Text, Double)]
-    -- ^ 停留点 (因子名 → **自然単位**の値)。
-  , rsmPredicted  :: !Double
-    -- ^ 停留点での予測応答。
-  , rsmNature     :: !RSMNature
-    -- ^ 極大 / 極小 / 鞍点。
-  , rsmInRegion   :: !Bool
-    -- ^ 停留点が実験領域 (全因子 coded @|x| <= 1@) の内側か。 外なら外挿。
-  , rsmCanonical  :: ![(Double, [Double])]
-    -- ^ canonical: (固有値, coded 方向ベクトル) を固有値昇順で。 固有値の大きさ =
-    --   その方向の曲率 (負=下に凸で応答が落ちる方向 / 正=上に凸)。
-  , rsmR2         :: !Double
-    -- ^ 当てた二次モデルの R²。
-  } deriving (Show)
-
--- | 設計 + 応答 @ys@ から応答曲面を解析し、 停留点・性質・canonical・R² を返す。
---   二次モデルを設計の coded 行列で当て ('fitQuadratic')、 停留点を自然単位へ decode。
---   **連続因子 (RSM 系設計) 専用** — カテゴリを含めば呼び名付きで error。
---   @ys@ は runsheet ('designTable' / 'designFrame') と同じ run 順の応答値。
-rsmAnalysis :: Design -> [Double] -> RSMReport
-rsmAnalysis (Design fs0 coded _) ys =
-  let fs        = requireContinuous "rsmAnalysis" fs0  -- 連続専用ガード (error を強制)
-      qf        = fitQuadratic coded ys
-      (xC, yC, _) = optimumPoint qf
-      canon     = canonicalAnalysis qf
-      eigs      = map fst canon
-      nature
-        | all (< 0) eigs = RMaximum
-        | all (> 0) eigs = RMinimum
-        | otherwise      = RSaddle
-      inRegion  = all (\x -> abs x <= 1 + 1e-9) xC
-      stationary = [ (dfName f, uncodeCont f x) | (f, x) <- zip fs xC ]
-  in RSMReport
-       { rsmStationary = stationary
-       , rsmPredicted  = yC
-       , rsmNature     = nature
-       , rsmInRegion   = inRegion
-       , rsmCanonical  = canon
-       , rsmR2         = qfR2 qf
-       }
-
--- | 自然単位の steepest ascent / descent 経路。 一次係数の勾配方向は計量依存なので、
---   設計の coded 行列で当てた coefs で coded 空間の方向を取り (= 各因子の実験範囲を
---   単位にした scale 不変な方向)、 生成した各点を 'uncodeCont' で自然単位へ decode する。
---   実験者はこの自然単位の系列をそのまま次の試行に使える。
---   @step@ は **coded スケール**の 1 歩幅 (例 0.5)、 @nSteps@ は歩数 (path 長 = nSteps+1)。
---   **連続因子専用**。
-steepestAscentNatural
-  :: Bool                 -- ^ True = ascent (最大化) / False = descent
-  -> Design
-  -> [Double]             -- ^ 応答 @ys@ (run 順)
-  -> Double               -- ^ step (coded スケール)
-  -> Int                  -- ^ nSteps
-  -> [[(Text, Double)]]   -- ^ 経路。 各点 = [(因子名, 自然単位値)]、 先頭 = 設計中心
-steepestAscentNatural maximize (Design fs0 coded _) ys step nSteps =
-  let fs   = requireContinuous "steepestAscentNatural" fs0
-      qf   = fitQuadratic coded ys
-      k    = length fs
-      sar  = steepestAscentFromQuad maximize (replicate k 0) qf step nSteps
-  in [ [ (dfName f, uncodeCont f x) | (f, x) <- zip fs row ]
-     | row <- sarStepPoints sar ]
diff --git a/src/Hanalyze/Diagnostics.hs b/src/Hanalyze/Diagnostics.hs
--- a/src/Hanalyze/Diagnostics.hs
+++ b/src/Hanalyze/Diagnostics.hs
@@ -5,14 +5,29 @@
 -- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
 -- License     : BSD-3-Clause
 --
--- 回帰モデルの係数診断 (plot 非依存層)。
+-- [日本語]: 回帰モデルの係数診断 (plot 非依存層)。
 --
 -- fit 済モデルを「数値として」 使う細粒度 API: 点予測・係数ベクトル・係数要約。
--- 'Hanalyze.Plot' (cabal flag @plot-integration@ 配下) が描画
--- ('VisualSpec' 化) を担うのに対し、 本モジュールは **hgg に依存しない**
+-- 別パッケージ @hanalyze-plot@ の 'Hanalyze.Plot'
+-- (@cabal build --project-file=cabal.project.plot@ で build) が描画
+-- (@VisualSpec@ 化) を担うのに対し、 本モジュールは __hgg に依存しない__
 -- 係数統計 (t\/z・p 値・95% CI) のみを切り出したもの。 非ゲート (常時 build) なので
--- 'df |-> spec' / 'coefSummary' が plot フラグ無しで使える。
+-- 'df |-> spec' / @coefSummary@ が plot フラグ無しで使える。
 -- 'Hanalyze.Plot' は本モジュールを import し従来の名前で再 export する。
+--
+-- [English]: Coefficient diagnostics for regression models (plot-independent
+-- layer).
+--
+-- A fine-grained API for using a fitted model "numerically": point
+-- prediction, coefficient vectors, coefficient summaries. Whereas
+-- 'Hanalyze.Plot' (in the separate @hanalyze-plot@ package,
+-- built via @cabal build --project-file=cabal.project.plot@) handles
+-- rendering (turning results into a @VisualSpec@), this module carves out
+-- only the coefficient statistics (t\/z, p values, 95% CI), which are
+-- __independent of hgg__. It's ungated (always built), so
+-- @df |-> spec@ \/ @coefSummary@ can be used without the plot flag.
+-- 'Hanalyze.Plot' imports this module and re-exports under the
+-- traditional names.
 module Hanalyze.Diagnostics
   ( -- * モデル API 層 (描画と独立: predict / describe / coefficients)
     Coef (..)
@@ -73,11 +88,13 @@
 -- ===========================================================================
 -- モデル API 層 (描画と独立: predict / describe / coefficients)
 --
--- 'Plottable' (図にする) とは別の細粒度 class (god class 回避、 Core.hs §2.3 方針)。
+-- @Plottable@ (図にする) とは別の細粒度 class (god class 回避、 Core.hs §2.3 方針)。
 -- fit 済モデルを「数値として」 使う: 点予測・係数・要約。 Phase 16 §3 D。
 -- ===========================================================================
 
--- | 係数 1 つの要約 (名前・推定値・標準誤差・95% Wald CI)。
+-- | [日本語]: 係数 1 つの要約 (名前・推定値・標準誤差・95% Wald CI)。
+--   [English]: Summary of one coefficient (name, estimate, standard error,
+--   95% Wald CI).
 data Coef = Coef
   { coefName  :: Text
   , coefValue :: Double
@@ -85,23 +102,32 @@
   , coefCI    :: (Double, Double)
   } deriving (Show, Eq)
 
--- | fit 済モデルを描画と独立に使う細粒度 API。
+-- | [日本語]: fit 済モデルを描画と独立に使う細粒度 API。
+--   [English]: A fine-grained API for using a fitted model independently of
+--   rendering.
 class ModelAPI m where
-  -- | 係数ベクトル (intercept 含む)。
+  -- | [日本語]: 係数ベクトル (intercept 含む)。
+  --   [English]: The coefficient vector (including the intercept).
   modelCoefficients :: m -> [Double]
-  -- | 単一説明変数 x での点予測 (μ スケール。 GLM は逆リンク後)。
+  -- | [日本語]: 単一説明変数 x での点予測 (μ スケール。 GLM は逆リンク後)。
+  --   [English]: Point prediction at a single explanatory variable x (on the
+  --   μ scale; for GLM, after the inverse link).
   predictPoint      :: m -> Double -> Double
-  -- | 各係数の要約 (推定値 + SE + 95% Wald CI)。
+  -- | [日本語]: 各係数の要約 (推定値 + SE + 95% Wald CI)。
+  --   [English]: Summary of each coefficient (estimate + SE + 95% Wald CI).
   describeModel     :: m -> [Coef]
 
--- | 係数共分散 Cov と β から要約を作る (SE = √diag, 95% CI = β ± 1.96·SE)。
+-- | [日本語]: 係数共分散 Cov と β から要約を作る (SE = √diag, 95% CI = β ± 1.96·SE)。
+--   [English]: Builds a summary from the coefficient covariance Cov and β
+--   (SE = √diag, 95% CI = β ± 1.96·SE).
 coefSummaryFromCov :: LA.Matrix Double -> LA.Vector Double -> [Text] -> [Coef]
 coefSummaryFromCov cov beta names =
   [ Coef nm b se (b - 1.96 * se, b + 1.96 * se)
   | (nm, b, se) <- zip3 names (LA.toList beta)
                        (map (sqrt . max 0) (LA.toList (LA.takeDiag cov))) ]
 
--- | LM の係数共分散 = σ̂²·(XᵀX)⁻¹  (σ̂² = RSS/(n−p))。
+-- | [日本語]: LM の係数共分散 = σ̂²·(XᵀX)⁻¹  (σ̂² = RSS/(n−p))。
+--   [English]: LM's coefficient covariance = σ̂²·(XᵀX)⁻¹ (σ̂² = RSS/(n−p)).
 lmCoefCov :: LA.Matrix Double -> FitResult -> LA.Matrix Double
 lmCoefCov x res =
   let r      = residualsV res
@@ -138,22 +164,30 @@
 -- @RLM@ は z。 ★statsmodels 突合済 Phase 70.D)。
 -- ===========================================================================
 
--- | 統一係数サマリの 1 行。 検定統計量 'crStat' は OLS 系で t 値、 GLM\/RLM で z 値。
+-- | [日本語]: 統一係数サマリの 1 行。 検定統計量 'crStat' は OLS 系で t 値、 GLM\/RLM で z 値。
+--   [English]: One row of the unified coefficient summary. The test
+--   statistic 'crStat' is a t value for the OLS family and a z value for
+--   GLM\/RLM.
 data CoefRow = CoefRow
-  { crTerm     :: !Text              -- ^ 係数名 (@\"(Intercept)\"@ / 変数名)。
-  , crEstimate :: !Double            -- ^ 点推定 β̂。
-  , crStdErr   :: !Double            -- ^ 標準誤差 SE。
-  , crStat     :: !Double            -- ^ Wald 統計量 β̂\/SE (t or z)。
-  , crPValue   :: !Double            -- ^ 両側 p 値。
-  , crCI95     :: !(Double, Double)  -- ^ 95% 信頼区間。
+  { crTerm     :: !Text              -- ^ [日本語]: 係数名 (@\"(Intercept)\"@ / 変数名)。 [English]: Coefficient name (@\"(Intercept)\"@ or a variable name).
+  , crEstimate :: !Double            -- ^ [日本語]: 点推定 β̂。 [English]: Point estimate β̂.
+  , crStdErr   :: !Double            -- ^ [日本語]: 標準誤差 SE。 [English]: Standard error SE.
+  , crStat     :: !Double            -- ^ [日本語]: Wald 統計量 β̂\/SE (t or z)。 [English]: Wald statistic β̂\/SE (t or z).
+  , crPValue   :: !Double            -- ^ [日本語]: 両側 p 値。 [English]: Two-sided p value.
+  , crCI95     :: !(Double, Double)  -- ^ [日本語]: 95% 信頼区間。 [English]: 95% confidence interval.
   } deriving (Show, Eq)
 
--- | fit 済モデルの統一係数サマリ。 @coefSummary model@ で全係数の表を得る。
+-- | [日本語]: fit 済モデルの統一係数サマリ。 @coefSummary model@ で全係数の表を得る。
+--   [English]: Unified coefficient summary of a fitted model. Call
+--   @coefSummary model@ to get a table of all coefficients.
 class HasCoefSummary m where
   coefSummary :: m -> [CoefRow]
 
--- | OLS 経路の係数行 (t 分布・df = n − p)。 'lmCoefStats' (SE\/t\/p) を再利用し、
+-- | [日本語]: OLS 経路の係数行 (t 分布・df = n − p)。 'lmCoefStats' (SE\/t\/p) を再利用し、
 --   95% CI は @β̂ ± t_{0.975, df}·SE@。 WLS は √w スケール設計を渡せば正しい。
+--   [English]: Coefficient rows for the OLS path (t distribution, df = n − p).
+--   Reuses 'lmCoefStats' (SE\/t\/p); the 95% CI is @β̂ ± t_{0.975, df}·SE@.
+--   For WLS, passing a √w-scaled design gives the correct result.
 coefRowsLM :: [Text] -> LA.Matrix Double -> FitResult -> [CoefRow]
 coefRowsLM names x res =
   let stats = lmCoefStats x res
@@ -164,8 +198,11 @@
                (b - tc * csSE s, b + tc * csSE s)
      | (nm, b, s) <- zip3 names betas stats ]
 
--- | z 経路の係数行 (正規・GLM\/RLM)。 共分散 Cov から SE = √diag、 z = β̂\/SE、
+-- | [日本語]: z 経路の係数行 (正規・GLM\/RLM)。 共分散 Cov から SE = √diag、 z = β̂\/SE、
 --   両側 p = @2·(1 − Φ(|z|))@、 95% CI = @β̂ ± z_{0.975}·SE@。
+--   [English]: Coefficient rows for the z path (Normal, GLM\/RLM). From the
+--   covariance Cov: SE = √diag, z = β̂\/SE, two-sided p = @2·(1 − Φ(|z|))@,
+--   95% CI = @β̂ ± z_{0.975}·SE@.
 coefRowsZ :: [Text] -> LA.Vector Double -> LA.Matrix Double -> [CoefRow]
 coefRowsZ names beta cov =
   let ses = map (sqrt . max 0) (LA.toList (LA.takeDiag cov))
@@ -175,9 +212,14 @@
      | (nm, b, se) <- zip3 names (LA.toList beta) ses
      , let z = if se == 0 then 0 else b / se ]
 
--- | 設計列数に合わせた係数名 (@\"(Intercept)\" : 変数名@)。 加法数値モデル
+-- | [日本語]: 設計列数に合わせた係数名 (@\"(Intercept)\" : 変数名@)。 加法数値モデル
 --   ('additiveFormula' \/ 単純 @y ~ x1 + x2@) では列数が @1 + |dvars|@ と一致するので
 --   変数名をそのまま使う。 factor\/交互作用で列数が増える場合は総称名へフォールバック。
+--   [English]: Coefficient names matching the design column count
+--   (@\"(Intercept)\" : variable names@). For additive numeric models
+--   ('additiveFormula' \/ simple @y ~ x1 + x2@), the column count matches
+--   @1 + |dvars|@, so the variable names are used as-is. When factors\/
+--   interactions increase the column count, falls back to generic names.
 designCoefNames :: Int -> [Text] -> [Text]
 designCoefNames p dvars
   | length dvars == p - 1 = "(Intercept)" : dvars
@@ -224,7 +266,7 @@
 --
 -- 解析的 SE を持たないモデル (分位点回帰) や、 罰則化で解析 SE が定義し難い
 -- モデル (Lasso / Ridge 等) に対し、 **case (行) bootstrap** で係数の
--- 不確実性を要約する。 返り値は 'coefSummary' と同型の '[CoefRow]' だが、
+-- 不確実性を要約する。 返り値は @coefSummary@ と同型の '[CoefRow]' だが、
 --
 --   * 'crStdErr' = B 回再標本化した係数の標本 SD
 --   * 'crStat'   = β̂ \/ SE_boot
@@ -234,14 +276,22 @@
 -- seed 固定 + ST + MWC なので **純粋・再現可能**。
 -- ===========================================================================
 
--- | bootstrap 係数サマリを持つモデル。 @coefSummaryBoot seed B model@ で
+-- | [日本語]: bootstrap 係数サマリを持つモデル。 @coefSummaryBoot seed B model@ で
 --   B 回の case bootstrap による係数表を得る。
+--   [English]: A model with a bootstrap coefficient summary. Call
+--   @coefSummaryBoot seed B model@ to get a coefficient table from B rounds
+--   of case bootstrap.
 class HasCoefBoot m where
-  -- | @coefSummaryBoot seed B model@: 乱数 seed と replicate 回数 B からサマリ。
+  -- | [日本語]: @coefSummaryBoot seed B model@: 乱数 seed と replicate 回数 B からサマリ。
+  --   [English]: @coefSummaryBoot seed B model@: a summary from the random
+  --   seed and the replicate count B.
   coefSummaryBoot :: Word32 -> Int -> m -> [CoefRow]
 
--- | seed → B → n から、 B 個の「@n@ 個の @[0, n)@ 一様乱数 index リスト」 を作る。
+-- | [日本語]: seed → B → n から、 B 個の「@n@ 個の @[0, n)@ 一様乱数 index リスト」 を作る。
 --   case (行) bootstrap の再標本化 index。 ST + MWC で純粋・seed 固定で再現可能。
+--   [English]: From seed → B → n, builds B lists of "n uniform random
+--   indices in @[0, n)@". These are the resampling indices for case (row)
+--   bootstrap. Pure and reproducible for a fixed seed, via ST + MWC.
 resampleRows :: Word32 -> Int -> Int -> [[Int]]
 resampleRows seed b n
   | n <= 0 || b <= 0 = []
@@ -249,7 +299,9 @@
       g <- initialize (V.singleton seed)
       replicateM b (replicateM n (uniformR (0, n - 1) g))
 
--- | numpy.percentile (type-7・線形補間) 互換。 @q@ は 0〜100。
+-- | [日本語]: numpy.percentile (type-7・線形補間) 互換。 @q@ は 0〜100。
+--   [English]: Compatible with numpy.percentile (type-7, linear
+--   interpolation). @q@ ranges over 0-100.
 percentileT7 :: [Double] -> Double -> Double
 percentileT7 xs q =
   let sorted = sort xs
@@ -266,13 +318,17 @@
 clamp01 :: Double -> Double
 clamp01 = max 0 . min 1
 
--- | (係数名, 点推定 β̂, B 個の replicate β) から係数表を作る。
+-- | [日本語]: (係数名, 点推定 β̂, B 個の replicate β) から係数表を作る。
 --   各係数 j について SE = 標本 SD (n−1 除算・要素 1 未満は 0)、
 --   p 値 = @clamp01 (2 · min(#{<0}\/B, #{>0}\/B))@、 CI = percentile [2.5, 97.5]。
+--   [English]: Builds a coefficient table from (coefficient names, point
+--   estimates β̂, B replicate βs). For each coefficient j: SE = sample SD
+--   (n−1 divisor; 0 for fewer than 1 element), p value =
+--   @clamp01 (2 · min(#{<0}\/B, #{>0}\/B))@, CI = percentile [2.5, 97.5].
 bootCoefRows
-  :: [Text]      -- ^ 係数名。
-  -> [Double]    -- ^ 点推定 β̂ (長さ = 係数数)。
-  -> [[Double]]  -- ^ B 個の replicate β (各長さ = 係数数)。
+  :: [Text]      -- ^ [日本語]: 係数名。 [English]: Coefficient names.
+  -> [Double]    -- ^ [日本語]: 点推定 β̂ (長さ = 係数数)。 [English]: Point estimates β̂ (length = number of coefficients).
+  -> [[Double]]  -- ^ [日本語]: B 個の replicate β (各長さ = 係数数)。 [English]: B replicate βs (each of length = number of coefficients).
   -> [CoefRow]
 bootCoefRows names point reps =
   [ let ests   = [ rep !! j | rep <- reps ]
@@ -301,11 +357,11 @@
 -- τ リスト・係数名 (intercept 込み) から、 τ ごとに行 (係数 × τ) を並べた表。
 quantileBootRows
   :: Word32 -> Int
-  -> LA.Matrix Double          -- ^ 設計行列 X (= [1, x..])。
-  -> LA.Vector Double          -- ^ 応答 y。
-  -> [Double]                  -- ^ τ リスト。
-  -> [(Double, LA.Vector Double)]  -- ^ (τ, 点推定 qfBeta)。
-  -> [Text]                    -- ^ 係数名 (intercept 込み)。
+  -> LA.Matrix Double          -- ^ [日本語]: 設計行列 X (= [1, x..])。 [English]: Design matrix X (= [1, x..]).
+  -> LA.Vector Double          -- ^ [日本語]: 応答 y。 [English]: Response y.
+  -> [Double]                  -- ^ [日本語]: τ リスト。 [English]: List of τ.
+  -> [(Double, LA.Vector Double)]  -- ^ [日本語]: (τ, 点推定 qfBeta)。 [English]: (τ, point estimate qfBeta).
+  -> [Text]                    -- ^ [日本語]: 係数名 (intercept 込み)。 [English]: Coefficient names (including intercept).
   -> [CoefRow]
 quantileBootRows seed b xMat y taus pointBetas names =
   let n        = LA.rows xMat
@@ -357,28 +413,46 @@
 --   * Spline (罰則なし OLS) は **厳密** nested F (曲線 vs 定数)。
 -- ===========================================================================
 
--- | 平滑項 1 つの近似有意性。 'teEdf' は有効自由度、 'teStat' は近似 F、
+-- | [日本語]: 平滑項 1 つの近似有意性。 'teEdf' は有効自由度、 'teStat' は近似 F、
 --   'tePValue' は上側 @F(r, dfRes)@ の確率。
 --   注: フィールド prefix は @te@ (term)。 @tr@ は 'Hanalyze.Stat.Test'
 --   の @TestResult@ が占有しており、 plot umbrella での再 export 衝突を避けるため。
+--   [English]: Approximate significance of one smooth term. 'teEdf' is the
+--   effective degrees of freedom, 'teStat' the approximate F, and
+--   'tePValue' the upper-tail probability of @F(r, dfRes)@.
+--   Note: the field prefix is @te@ (term); @tr@ is already taken by
+--   'Hanalyze.Stat.Test''s @TestResult@, and this avoids a
+--   re-export clash in the plot umbrella.
 data TermRow = TermRow
-  { teTerm   :: !Text    -- ^ 平滑項名 (@\"s(x)\"@ / @\"s(<name>)\"@)。
-  , teEdf    :: !Double  -- ^ 有効自由度 edf。
-  , teStat   :: !Double  -- ^ 近似 F 統計量。
-  , tePValue :: !Double  -- ^ 上側確率 (近似 p 値)。
+  { teTerm   :: !Text    -- ^ [日本語]: 平滑項名 (@\"s(x)\"@ / @\"s(<name>)\"@)。 [English]: Smooth term name (@\"s(x)\"@ \/ @\"s(<name>)\"@).
+  , teEdf    :: !Double  -- ^ [日本語]: 有効自由度 edf。 [English]: Effective degrees of freedom (edf).
+  , teStat   :: !Double  -- ^ [日本語]: 近似 F 統計量。 [English]: Approximate F statistic.
+  , tePValue :: !Double  -- ^ [日本語]: 上側確率 (近似 p 値)。 [English]: Upper-tail probability (approximate p value).
   } deriving (Show, Eq)
 
--- | fit 済の平滑モデルの「項単位」 近似有意性。 @termSummary model@ で各平滑項の
+-- | [日本語]: fit 済の平滑モデルの「項単位」 近似有意性。 @termSummary model@ で各平滑項の
 --   edf + 近似 F + p 値の表を得る。
+--   [English]: Approximate "per-term" significance of a fitted smooth model.
+--   Call @termSummary model@ to get a table of edf + approximate F + p value
+--   for each smooth term.
 class HasTermSummary m where
   termSummary :: m -> [TermRow]
 
--- | GAM の項単位サマリ (mgcv 流 edf + rank-r 擬似逆 Wald 近似 F)。
+-- | [日本語]: GAM の項単位サマリ (mgcv 流 edf + rank-r 擬似逆 Wald 近似 F)。
 --   名前は呼び出し側が与える (@length = length gamBetas@)。
 --
 --   設計列レイアウト: intercept=0、 項 j は @starts!!j .. starts!!j+mSizes!!j-1@。
 --   ★基底の再評価は不要 — 'GAMFit' に格納済の @gamBetas \/ gamCov \/ gamEdf \/
 --   gamLambda \/ gamResid@ のみから算出する。
+--   [English]: GAM's per-term summary (mgcv-style edf + rank-r
+--   pseudoinverse Wald approximate F). Names are supplied by the caller
+--   (@length = length gamBetas@).
+--
+--   Design column layout: intercept=0, term j is
+--   @starts!!j .. starts!!j+mSizes!!j-1@.
+--   Note: no need to re-evaluate the basis — computed solely from what's
+--   already stored in 'GAMFit' (@gamBetas \/ gamCov \/ gamEdf \/
+--   gamLambda \/ gamResid@).
 gamTermRows :: GAMFit -> [Text] -> [TermRow]
 gamTermRows fit names =
   let resid  = gamResid fit
@@ -404,9 +478,13 @@
            edfJ  = sum [ fDiag !! k | k <- cols ]
            vBlock = (cov LA.¿ cols) LA.? cols ]   -- cols×cols 共分散ブロック
 
--- | 1 項の rank-r 擬似逆 Wald 統計量から 'TermRow' を作る (mgcv Wood 2013 流)。
+-- | [日本語]: 1 項の rank-r 擬似逆 Wald 統計量から 'TermRow' を作る (mgcv Wood 2013 流)。
 --   r = clamp (round edf) 1 (dim β_j)。 V_j を対称固有分解し上位 r 固有対で
 --   Vr⁻ = Σ_{i≤r} (u_i u_iᵀ)/λ_i。 Tr = β_jᵀ Vr⁻ β_j、 F = Tr / r。
+--   [English]: Builds a 'TermRow' from one term's rank-r pseudoinverse Wald
+--   statistic (mgcv Wood 2013 style). r = clamp (round edf) 1 (dim β_j).
+--   V_j is symmetrically eigendecomposed and, using the top r eigenpairs,
+--   Vr⁻ = Σ_{i≤r} (u_i u_iᵀ)/λ_i. Tr = β_jᵀ Vr⁻ β_j, F = Tr / r.
 termRowFromBlock :: Text -> LA.Vector Double -> LA.Matrix Double -> Double -> Int -> TermRow
 termRowFromBlock nm beta vBlock edf dfResI =
   let mj   = LA.size beta
@@ -460,22 +538,27 @@
 -- ===========================================================================
 -- 統一玄関 (.summary() 風) — Phase 72.3
 --
--- 既存の 'coefSummary' (Wald・'HasCoefSummary')・'coefSummaryBoot' (bootstrap・
+-- 既存の @coefSummary@ (Wald・'HasCoefSummary')・@coefSummaryBoot@ (bootstrap・
 -- 'HasCoefBoot')・'termSummary' (項有意性・'HasTermSummary') を **1 つのタグ付き
 -- 直和** 'ModelReport' でラップし、 モデル型ごとに適切な診断へディスパッチする
 -- 玄関。 statsmodels の @.summary()@ に相当する「とりあえずこれを呼べば要約が出る」
 -- 入口を提供する (どの診断が該当するかをモデル型側が知っている)。
 -- ===========================================================================
 
--- | モデル要約レポート。 係数表 (Wald も bootstrap も同じ箱) / 平滑項有意性 /
+-- | [日本語]: モデル要約レポート。 係数表 (Wald も bootstrap も同じ箱) / 平滑項有意性 /
 --   該当なし (理由文付き) のタグ付き直和。
+--   [English]: Model summary report. A tagged sum of: a coefficient table
+--   (Wald and bootstrap share the same box) \/ smooth-term significance \/
+--   not applicable (with a reason string).
 data ModelReport
-  = CoefReport [CoefRow]   -- ^ 係数表 (Wald 'coefSummary' か bootstrap 'coefSummaryBoot')。
-  | TermReport [TermRow]   -- ^ GAM\/spline の平滑項有意性 'termSummary'。
-  | NoReport   Text        -- ^ 係数診断が非該当 (理由文)。
+  = CoefReport [CoefRow]   -- ^ [日本語]: 係数表 (Wald 'coefSummary' か bootstrap 'coefSummaryBoot')。 [English]: Coefficient table (either Wald 'coefSummary' or bootstrap 'coefSummaryBoot').
+  | TermReport [TermRow]   -- ^ [日本語]: GAM\/spline の平滑項有意性 'termSummary'。 [English]: GAM\/spline smooth-term significance 'termSummary'.
+  | NoReport   Text        -- ^ [日本語]: 係数診断が非該当 (理由文)。 [English]: Coefficient diagnostics not applicable (reason string).
   deriving (Show, Eq)
 
--- | fit 済モデルの統一要約玄関。 @modelReport model@ で型に応じた 'ModelReport' を得る。
+-- | [日本語]: fit 済モデルの統一要約玄関。 @modelReport model@ で型に応じた 'ModelReport' を得る。
+--   [English]: Unified summary entry point for a fitted model. Call
+--   @modelReport model@ to get a 'ModelReport' appropriate to the type.
 class HasReport m where
   modelReport :: m -> ModelReport
 
@@ -491,12 +574,18 @@
 padL :: Int -> String -> Text
 padL w s = T.pack (s <> replicate (max 0 (w - length s)) ' ')
 
--- | 'ModelReport' を @.summary()@ 風のテキスト表へ整形する。
+-- | [日本語]: 'ModelReport' を @.summary()@ 風のテキスト表へ整形する。
 --
 --   * 'CoefReport': ヘッダ @term \/ estimate \/ std.err \/ stat \/ p.value \/ [2.5%, 97.5%]@
 --     + 各 'CoefRow' を固定幅で整列 (4 桁)。
 --   * 'TermReport': ヘッダ @term \/ edf \/ F \/ p.value@ + 各 'TermRow'。
 --   * 'NoReport': 理由文をそのまま返す。
+--   [English]: Formats a 'ModelReport' into a @.summary()@-style text table.
+--
+--   * 'CoefReport': header @term \/ estimate \/ std.err \/ stat \/ p.value \/
+--     [2.5%, 97.5%]@ + each 'CoefRow' aligned at fixed width (4 digits).
+--   * 'TermReport': header @term \/ edf \/ F \/ p.value@ + each 'TermRow'.
+--   * 'NoReport': returns the reason string as-is.
 showReport :: ModelReport -> Text
 showReport (NoReport msg) = msg
 showReport (CoefReport rows) =
diff --git a/src/Hanalyze/Fit.hs b/src/Hanalyze/Fit.hs
--- a/src/Hanalyze/Fit.hs
+++ b/src/Hanalyze/Fit.hs
@@ -10,17 +10,33 @@
 -- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
 -- License     : BSD-3-Clause
 --
--- データ源 → モデル学習層 (= @df |-> spec@ の plot 非依存部分)。
+-- [日本語]: データ源 → モデル学習層 (= @df |-> spec@ の plot 非依存部分)。
 --
--- 'Hanalyze.Plot' から **図 ('VisualSpec') に依存しない**「データ源から
--- モデルを当てはめる」部分 (各 @*Spec@ 型・'Fit' instance・'(|->)' / '(|->!)'
+-- 'Hanalyze.Plot' から __図 (@VisualSpec@) に依存しない__「データ源から
+-- モデルを当てはめる」部分 (各 @*Spec@ 型・'Fit' instance・@(|->)@ / @(|->!)@
 -- 演算子・それらの smart ctor) をここへ切り出した非ゲートモジュール (常時 build)。
--- これにより @df |-> lm "x" "y"@ 等が cabal flag @plot-integration@ を on にせず
--- とも使える (Phase 71.3)。
+-- これにより @df |-> lm "x" "y"@ 等が別パッケージ @hanalyze-plot@
+-- (@cabal build --project-file=cabal.project.plot@) を build せずとも使える。
 --
--- ⚠ 本モジュールは 'Hgg.Plot' を一切 import しない (= 図に依存する toPlot /
+-- ⚠ 本モジュールは 'Graphics.Hgg' を一切 import しない (= 図に依存する toPlot /
 -- Plottable instance 等は 'Hanalyze.Plot' 側に残す)。 'Fit' クラス本体・
 -- ラッパ型・reqColV/reqColsM 等は 'Hanalyze.Model.Wrappers' から取り込む。
+--
+-- [English]: The data source → model training layer (the plot-independent
+-- part of @df |-> spec@).
+--
+-- An ungated module (always built) carving out from 'Hanalyze.Plot'
+-- the __figure-independent (@VisualSpec@)__ part of "fitting a model from a
+-- data source" — each @*Spec@ type, the 'Fit' instances, the
+-- @(|->)@ \/ @(|->!)@ operators, and their smart constructors. This lets
+-- @df |-> lm \"x\" \"y\"@ etc. be used without building the separate
+-- @hanalyze-plot@ package (@cabal build
+-- --project-file=cabal.project.plot@).
+--
+-- Warning: this module never imports 'Graphics.Hgg' at all (figure-dependent
+-- things like toPlot \/ Plottable instances stay on the
+-- 'Hanalyze.Plot' side). The 'Fit' class body, wrapper types,
+-- reqColV\/reqColsM, etc. are pulled in from 'Hanalyze.Model.Wrappers'.
 module Hanalyze.Fit
   ( -- * df |-> spec 統一 fit 演算子 (Phase 51)
     (|->)
@@ -348,24 +364,34 @@
 -- 軸の平滑曲線 (多予測子では他を訓練平均に固定した偏依存曲線・band 非提供は GAM 共通)。
 -- ---------------------------------------------------------------------------
 
--- | GAM の設定。 全項共通の基底 'gcBasis' と λ 方略 'gcLambda'。
+-- | [日本語]: GAM の設定。 全項共通の基底 'gcBasis' と λ 方略 'gcLambda'。
+--   [English]: GAM configuration. The basis 'gcBasis' shared by all terms
+--   and the λ strategy 'gcLambda'.
 data GAMConfig = GAMConfig
-  { gcBasis  :: GAMBasis   -- ^ 各平滑項に使う基底 (全項共通)。
-  , gcLambda :: GAMLambda  -- ^ ridge λ の決め方 ('FixedL' / 'GCV')。
+  { gcBasis  :: GAMBasis   -- ^ [日本語]: 各平滑項に使う基底 (全項共通)。 [English]: Basis used for each smooth term (shared across all terms).
+  , gcLambda :: GAMLambda  -- ^ [日本語]: ridge λ の決め方 ('FixedL' / 'GCV')。 [English]: How the ridge λ is decided ('FixedL' \/ 'GCV').
   } deriving (Show)
 
--- | 既定設定: 3 次 B-spline・内部 6 ノット・λ は GCV 自動選択。
+-- | [日本語]: 既定設定: 3 次 B-spline・内部 6 ノット・λ は GCV 自動選択。
+--   [English]: Default configuration: cubic B-spline, 6 interior knots, λ
+--   auto-selected via GCV.
 defaultGAMConfig :: GAMConfig
 defaultGAMConfig = GAMConfig (BSplineB 3 6) GCV
 
--- | GAM spec (単/多予測子)。 @gam cfg "x" "y"@ または @gamMulti cfg ["x1","x2"] "y"@。
+-- | [日本語]: GAM spec (単/多予測子)。 @gam cfg "x" "y"@ または @gamMulti cfg ["x1","x2"] "y"@。
+--   [English]: GAM spec (single or multiple predictors).
+--   @gam cfg \"x\" \"y\"@ or @gamMulti cfg [\"x1\",\"x2\"] \"y\"@.
 data GAMSpec = GAMSpec !GAMConfig ![Text] !Text
 
--- | @gam cfg xCol yCol@ — 単一予測子 GAM ('lm' と対)。
+-- | [日本語]: @gam cfg xCol yCol@ — 単一予測子 GAM ('lm' と対)。
+--   [English]: @gam cfg xCol yCol@ — a single-predictor GAM (paired with
+--   'lm').
 gam :: GAMConfig -> Text -> Text -> GAMSpec
 gam cfg xn yn = GAMSpec cfg [xn] yn
 
--- | @gamMulti cfg xCols yCol@ — 多予測子 GAM ('lmMulti' と対)。 第1予測子を描画軸にする。
+-- | [日本語]: @gamMulti cfg xCols yCol@ — 多予測子 GAM ('lmMulti' と対)。 第1予測子を描画軸にする。
+--   [English]: @gamMulti cfg xCols yCol@ — a multi-predictor GAM (paired
+--   with 'lmMulti'). The first predictor is used as the plotting axis.
 gamMulti :: GAMConfig -> [Text] -> Text -> GAMSpec
 gamMulti = GAMSpec
 
@@ -392,49 +418,72 @@
 -- * formula 多変量 (R 流): @lmF "y ~ x1 + x2"@ 等 (Phase 51.3)。
 -- * HBM (手書き model): @hbm cfg model@ (Phase 51.4)。
 
--- | @df |-> spec = fitWith spec df@。 plot の @|>>@ と同列 (@infixl 1@)。
+-- | [日本語]: @df |-> spec = fitWith spec df@。 plot の @|>>@ と同列 (@infixl 1@)。
+--   [English]: @df |-> spec = fitWith spec df@. On par with plot's @|>>@
+--   (@infixl 1@).
 infixl 1 |->
 (|->) :: (ColumnSource d, Fit spec) => d -> spec -> Fitted spec
 d |-> spec = fitWith spec d
 
--- | @df |->! spec = fitIO spec df@ (進捗つき IO 学習動詞・Phase 61.4)。
--- 純粋動詞 '(|->)' との違いは**副作用 (進捗表示) の有無が動詞の選択に現れる**
+-- | [日本語]: @df |->! spec = fitIO spec df@ (進捗つき IO 学習動詞)。
+-- 純粋動詞 @(|->)@ との違いは__副作用 (進捗表示) の有無が動詞の選択に現れる__
 -- こと: HBM のような重い学習は @|->!@ で stderr に進捗 1 行が出る。
--- 結果は同 cfg の @|->@ と**ビット一致** (seed 規約共有)。
+-- 結果は同 cfg の @|->@ と__ビット一致__ (seed 規約共有)。
+--   [English]: @df |->! spec = fitIO spec df@ (an IO training verb with
+--   progress display). The difference from the pure verb @(|->)@ is that
+--   __the choice of verb reflects whether there's a side effect__ (progress
+--   display): heavy training like HBM emits one line of progress
+--   to stderr via @|->!@. The result is __bit-identical__ to @|->@ with the
+--   same cfg (they share the seed convention).
 infixl 1 |->!
 (|->!) :: (ColumnSource d, Fit spec) => d -> spec -> IO (Fitted spec)
 d |->! spec = fitIO spec d
 
 -- --- 二変量近道 spec (列名 x, y) — Phase 51.2 -------------------------------
 
--- | 単回帰 spec。 @lm "x" "y"@。
+-- | [日本語]: 単回帰 spec。 @lm "x" "y"@。
+--   [English]: Simple regression spec. @lm \"x\" \"y\"@.
 data LMSpec       = LMSpec       !Text !Text
--- | 二変量 GLM spec。 @glm fam link "x" "y"@。
+-- | [日本語]: 二変量 GLM spec。 @glm fam link "x" "y"@。
+--   [English]: Bivariate GLM spec. @glm fam link \"x\" \"y\"@.
 data GLMSpec      = GLMSpec      !Family !LinkFn !Text !Text
--- | 二変量 spline spec。 @spline kind knots "x" "y"@。
+-- | [日本語]: 二変量 spline spec。 @spline kind knots "x" "y"@。
+--   [English]: Bivariate spline spec. @spline kind knots \"x\" \"y\"@.
 data SplineSpec   = SplineSpec   !SplineKind ![Double] !Text !Text
--- | 二変量 robust spec。 @robust est "x" "y"@。
+-- | [日本語]: 二変量 robust spec。 @robust est "x" "y"@。
+--   [English]: Bivariate robust regression spec. @robust est \"x\" \"y\"@.
 data RobustSpec   = RobustSpec   !RobustEstimator !Text !Text
--- | 分位点回帰 spec。 @quantile taus "x" "y"@。
+-- | [日本語]: 分位点回帰 spec。 @quantile taus "x" "y"@。
+--   [English]: Quantile regression spec. @quantile taus \"x\" \"y\"@.
 data QuantileSpec = QuantileSpec ![Double] !Text !Text
 
--- | @lm xCol yCol@ — 単回帰 ('LMModel') を当てはめる spec。
+-- | [日本語]: @lm xCol yCol@ — 単回帰 ('LMModel') を当てはめる spec。
+--   [English]: @lm xCol yCol@ — a spec that fits a simple regression
+--   ('LMModel').
 lm :: Text -> Text -> LMSpec
 lm = LMSpec
 
--- | @glm fam link xCol yCol@ — 二変量 GLM ('GLMModel') の spec。
+-- | [日本語]: @glm fam link xCol yCol@ — 二変量 GLM ('GLMModel') の spec。
+--   [English]: @glm fam link xCol yCol@ — a spec for a bivariate GLM
+--   ('GLMModel').
 glm :: Family -> LinkFn -> Text -> Text -> GLMSpec
 glm = GLMSpec
 
--- | @spline kind knots xCol yCol@ — spline 回帰 ('SplineModel') の spec。
+-- | [日本語]: @spline kind knots xCol yCol@ — spline 回帰 ('SplineModel') の spec。
+--   [English]: @spline kind knots xCol yCol@ — a spec for spline regression
+--   ('SplineModel').
 spline :: SplineKind -> [Double] -> Text -> Text -> SplineSpec
 spline = SplineSpec
 
--- | @rlm est xCol yCol@ — ロバスト回帰 ('RobustModel') の spec (R @MASS::rlm@)。
+-- | [日本語]: @rlm est xCol yCol@ — ロバスト回帰 ('RobustModel') の spec (R @MASS::rlm@)。
+--   [English]: @rlm est xCol yCol@ — a spec for robust regression
+--   ('RobustModel'; R's @MASS::rlm@).
 rlm :: RobustEstimator -> Text -> Text -> RobustSpec
 rlm = RobustSpec
 
--- | @rq taus xCol yCol@ — 分位点回帰 ('QuantileModel') の spec (R @quantreg::rq@)。
+-- | [日本語]: @rq taus xCol yCol@ — 分位点回帰 ('QuantileModel') の spec (R @quantreg::rq@)。
+--   [English]: @rq taus xCol yCol@ — a spec for quantile regression
+--   ('QuantileModel'; R's @quantreg::rq@).
 rq :: [Double] -> Text -> Text -> QuantileSpec
 rq = QuantileSpec
 
@@ -452,19 +501,27 @@
 -- 数値核 = √w スケール OLS: @X_w = diag(√w)·X@, @y_w = diag(√w)·y@ を OLS すると
 -- β̂ = (XᵀWX)⁻¹XᵀWy・s² = Σ wᵢ(yᵢ−x̂ᵢ)²/(n−p) が得られる。
 --
--- ★描画の整合: grid 評価 ('svGrid' → 'confidenceBandAt') は評価点を**非スケール**
+-- ★描画の整合: grid 評価 ('svGrid' → @confidenceBandAt@) は評価点を**非スケール**
 -- @[1, gx]@ で渡すので、 設計行列を**スケール済** ('lmDesign'=X_w) にしておけば
 -- @se = t·√(s²·x₀ᵀ(XᵀWX)⁻¹x₀)@ ＝ 正しい WLS pointwise CI が元 x スケールで出る。
--- 一方 'LMModel' の素の 'toPlot' (訓練点経路 'confidenceBand') は評価点がスケール済
+-- 一方 'LMModel' の素の @toPlot@ (訓練点経路 @confidenceBand@) は評価点がスケール済
 -- 行になり中心も se も √wᵢ 倍に壊れる。 ゆえに専用ラッパ 'WeightedLMModel' を設け
--- 'toPlot' を **grid 経路 ('statModel')** に固定する (= 元データ散布図と整合)。
+-- @toPlot@ を **grid 経路 ('statModel')** に固定する (= 元データ散布図と整合)。
 
--- | 重み付き最小二乗 spec。 @weighted "w" (lm "x" "y")@ (重みは **df の列名**・全て @≥ 0@)。
+-- | [日本語]: 重み付き最小二乗 spec。 @weighted "w" (lm "x" "y")@ (重みは __df の列名__・全て @≥ 0@)。
+--   [English]: Weighted least squares spec. @weighted \"w\" (lm \"x\" \"y\")@
+--   (the weight is __a df column name__; all values must be @≥ 0@).
 data WeightedLMSpec = WeightedLMSpec !Text !LMSpec
 
--- | @weighted wCol (lm xCol yCol)@ — 重み**列** @wCol@ で WLS を当てはめる spec ラッパ。
+-- | [日本語]: @weighted wCol (lm xCol yCol)@ — 重み__列__ @wCol@ で WLS を当てはめる spec ラッパ。
 --   重みは x / y と同一 'ColumnSource' の列名で受ける (statsmodels @wls(…, weights=col)@ /
 --   R @weights=@ と同型)。 現状 'LMSpec' 専用 (WLS は LM 固有。 GLM の重みは別軸ゆえ対象外)。
+--   [English]: @weighted wCol (lm xCol yCol)@ — a spec wrapper that fits WLS
+--   using the weight __column__ @wCol@. The weight is looked up by column
+--   name in the same 'ColumnSource' as x \/ y (the same shape as
+--   statsmodels's @wls(…, weights=col)@ \/ R's @weights=@). Currently
+--   'LMSpec'-only (WLS is specific to LM; GLM weights are a different axis
+--   and out of scope).
 weighted :: Text -> LMSpec -> WeightedLMSpec
 weighted = WeightedLMSpec
 
@@ -493,9 +550,14 @@
                , wlmY       = LA.toList ys
                }
 
--- | 重み付き R²。 statsmodels WLS @rsquared@ と一致: @1 − Σwᵢ(yᵢ−ŷᵢ)² / Σwᵢ(yᵢ−ȳ_w)²@
+-- | [日本語]: 重み付き R²。 statsmodels WLS @rsquared@ と一致: @1 − Σwᵢ(yᵢ−ŷᵢ)² / Σwᵢ(yᵢ−ȳ_w)²@
 --   (中心化は重み付き平均 @ȳ_w = Σwᵢyᵢ/Σwᵢ@)。 ★スケール空間の素朴な R² とは中心化が
---   異なる (= 内側 LM の 'svCoefR2' をそのまま使うと不一致になる罠)。
+--   異なる (= 内側 LM の @svCoefR2@ をそのまま使うと不一致になる罠)。
+--   [English]: Weighted R². Matches statsmodels WLS's @rsquared@:
+--   @1 − Σwᵢ(yᵢ−ŷᵢ)² / Σwᵢ(yᵢ−ȳ_w)²@ (centering uses the weighted mean
+--   @ȳ_w = Σwᵢyᵢ/Σwᵢ@). Note: this centering differs from the naive R² in
+--   scaled space — a trap where using the inner LM's @svCoefR2@ directly
+--   gives a mismatched value.
 weightedR2 :: [Double] -> [Double] -> [Double] -> Double
 weightedR2 ws ys yhats =
   let sw   = sum ws
@@ -507,31 +569,53 @@
 -- --- 透過標準化ラッパ (自動逆変換) — Phase 70.3 項目 C ----------------------
 --
 -- スケール敏感なモデル (とくに距離ベース kNN) は予測子の z-score 標準化が要る。
--- 'standardized' / 'standardizedY' は spec を**透過的に**ラップし、 学習は標準化空間で
+-- 'standardized' / @standardizedY@ は spec を**透過的に**ラップし、 学習は標準化空間で
 -- 行いつつ図・予測は元スケールへ自動逆変換する (tidymodels @step_normalize@ /
 -- sklearn @Pipeline@ 相当)。 標準化対象列は spec 自身が 'predictorCols' / 'responseCol'
 -- で返すので**列名を二重に書かない**。 内部標準化済 (GP/Reg/PCA/PLS) や木系は
 -- @predictorCols = []@ ゆえラッパが 'Left' で誤用を弾く (= 二重標準化バグ回避)。
 
--- | spec を透過標準化でラップする (@Bool@ = 応答 y も標準化するか)。
---   構成子は直接使わず 'standardized' / 'standardizedY' から作る。
+-- | [日本語]: spec を透過標準化でラップする (@Bool@ = 応答 y も標準化するか)。
+--   構成子は直接使わず 'standardized' / @standardizedY@ から作る。
+--   [English]: Wraps a spec with transparent standardization (@Bool@ =
+--   whether to also standardize the response y). Don't use the constructor
+--   directly; build via 'standardized' \/ @standardizedY@.
 data StandardizedSpec spec = StandardizedSpec !Bool !spec
 
--- | @standardized spec@ — **予測子列のみ** z-score 標準化して内側 @spec@ を学習し、
---   描画・予測は**元スケール**で返す透過ラッパ。 距離ベース kNN
+-- | [日本語]: @standardized spec@ — __予測子列のみ__ z-score 標準化して内側 @spec@ を学習し、
+--   描画・予測は__元スケール__で返す透過ラッパ。 距離ベース kNN
 --   (@knnReg@ / @knnCls@) が本命。 標準化対象列は spec の 'predictorCols' から
 --   取得する (列名を二重に書かない)。 被せる意味の無い spec
 --   (内部標準化済 GP\/Reg\/PCA\/PLS・スケール不変な木系) は @predictorCols = []@
 --   ゆえ 'fitEither' が 'Left' で誤用を弾く。
 --
 --   @df |-> standardized (knnReg 5 [\"x1\",\"x2\"] \"y\")@
+--   [English]: @standardized spec@ — a transparent wrapper that
+--   z-score-standardizes __only the predictor columns__ before training the
+--   inner @spec@, returning plots \/ predictions on the __original scale__.
+--   Aimed mainly at distance-based kNN (@knnReg@ \/ @knnCls@). The columns
+--   to standardize are obtained from the spec's 'predictorCols' (no
+--   duplicate column-name listing). Specs for which wrapping is meaningless
+--   (already internally standardized GP\/Reg\/PCA\/PLS; scale-invariant
+--   tree-based ones) have @predictorCols = []@, so 'fitEither' rejects the
+--   misuse with 'Left'.
+--
+--   @df |-> standardized (knnReg 5 [\"x1\",\"x2\"] \"y\")@
 standardized :: spec -> StandardizedSpec spec
 standardized = StandardizedSpec False
 
--- | @standardizedY spec@ — 予測子 **+ 応答 y** を標準化する版 (多目的最適化向け・
+-- | [日本語]: @standardizedY spec@ — 予測子 __+ 応答 y__ を標準化する版 (多目的最適化向け・
 --   opt-in)。 y 列名は spec の 'responseCol' から取得し、 予測・図は元スケールへ
 --   逆変換する。 応答がクラスラベル / family・link 拘束で標準化不可な spec
 --   (@responseCol = Nothing@: 分類・GLM) に付けると 'fitEither' が 'Left' (誤用ガード)。
+--   [English]: @standardizedY spec@ — a version that also standardizes the
+--   response y in addition to the predictors (opt-in, aimed at
+--   multi-objective optimization). The y column name is obtained from the
+--   spec's 'responseCol'; predictions \/ plots are inverse-transformed back
+--   to the original scale. Applying it to a spec whose response can't be
+--   standardized (class labels, or a GLM constrained by family\/link —
+--   @responseCol = Nothing@) makes 'fitEither' return 'Left' (a misuse
+--   guard).
 standardizedY :: spec -> StandardizedSpec spec
 standardizedY = StandardizedSpec True
 
@@ -543,13 +627,19 @@
   predictorCols _ = []
   responseCol   _ = Nothing
 
--- | 透過標準化の本体: 予測子列 (と opt-in で応答 y) を z-score 標準化した派生
+-- | [日本語]: 透過標準化の本体: 予測子列 (と opt-in で応答 y) を z-score 標準化した派生
 --   'ColumnSource' を作り、 内側 spec をそれに当てはめる。 逆変換用の (μ,σ) を保持。
 --   失敗 (空 predictorCols / 標準化不能応答への @standardizedY@ / 列欠落) は 'Left'。
+--   [English]: The core of transparent standardization: builds a derived
+--   'ColumnSource' with the predictor columns (and, opt-in, the response y)
+--   z-score-standardized, and fits the inner spec to it. Keeps the (μ,σ)
+--   needed for the inverse transform. Failure (empty predictorCols,
+--   @standardizedY@ on a non-standardizable response, missing columns)
+--   gives 'Left'.
 fitStd :: (ColumnSource d, Fit spec)
-       => [Text]       -- ^ 標準化対象の予測子列 (spec の 'predictorCols')。
-       -> Maybe Text   -- ^ 応答列 (spec の 'responseCol')。 散布用 + y 標準化に使う。
-       -> Bool         -- ^ y も標準化するか ('standardizedY' なら True)。
+       => [Text]       -- ^ [日本語]: 標準化対象の予測子列 (spec の 'predictorCols')。 [English]: Predictor columns to standardize (the spec's 'predictorCols').
+       -> Maybe Text   -- ^ [日本語]: 応答列 (spec の 'responseCol')。 散布用 + y 標準化に使う。 [English]: Response column (the spec's 'responseCol'). Used for the scatter plot + y standardization.
+       -> Bool         -- ^ [日本語]: y も標準化するか (@standardizedY@ なら True)。 [English]: Whether to also standardize y (True for @standardizedY@).
        -> spec
        -> d
        -> Either String (StandardizedModel (Fitted spec))
@@ -589,8 +679,12 @@
       Right StandardizedModel { smInner = innerM, smXStd = xStdr
                               , smYStd = yStd,   smTrain = mTrain }
 
--- | assoc を別 assoc で同名上書き (上書きキーは元の位置・値だけ差替え、 元に
+-- | [日本語]: assoc を別 assoc で同名上書き (上書きキーは元の位置・値だけ差替え、 元に
 --   無いキーは末尾追加)。 透過標準化の派生 'ColumnSource' 生成に使う。
+--   [English]: Overrides one assoc list with another, matching by key
+--   (overridden keys keep their original position with only the value
+--   replaced; keys absent from the original are appended at the end). Used
+--   to build the derived 'ColumnSource' for transparent standardization.
 overrideAssoc :: [(Text, [Double])] -> [(Text, [Double])] -> [(Text, [Double])]
 overrideAssoc base ovr =
   let ovrMap   = Map.fromList ovr
@@ -598,8 +692,11 @@
   in [ (k, Map.findWithDefault v k ovrMap) | (k, v) <- base ]
      ++ [ kv | kv@(k, _) <- ovr, k `notElem` baseKeys ]
 
--- | 標本平均と (n-1) 標準偏差。 σ≈0 (定数列) は 1 に丸めて 0 割を回避
+-- | [日本語]: 標本平均と (n-1) 標準偏差。 σ≈0 (定数列) は 1 に丸めて 0 割を回避
 --   ('Stat.Standardize' の 'fitStandardizer' と同流儀)。
+--   [English]: Sample mean and (n-1) standard deviation. σ≈0 (a constant
+--   column) is rounded to 1 to avoid division by zero (the same convention
+--   as 'Stat.Standardize''s 'fitStandardizer').
 meanSdSafe :: [Double] -> (Double, Double)
 meanSdSafe xs =
   let n = length xs
@@ -649,53 +746,74 @@
 -- 'gp'/'gpMulti' (gpMulti は E3)。
 -- ===========================================================================
 
--- | 'gp' / 'gpMulti' の設定。 カーネル種 × 象限 × ハイパラ方略。
+-- | [日本語]: 'gp' / 'gpMulti' の設定。 カーネル種 × 象限 × ハイパラ方略。
+--   [English]: Configuration for 'gp' \/ 'gpMulti'. Kernel kind × quadrant ×
+--   hyperparameter strategy.
 data GPConfig = GPConfig
-  { gpcKernel :: !Kernel        -- ^ RBF / Matern52 / Periodic (GP.hs 由来・新型は作らない)。
-  , gpcMethod :: !GPMethod      -- ^ 象限 + (RFF 時) 近似次元・seed。
-  , gpcHyper  :: !HyperStrategy -- ^ ハイパラの決め方 + 固定時の値。
+  { gpcKernel :: !Kernel        -- ^ [日本語]: RBF / Matern52 / Periodic (GP.hs 由来・新型は作らない)。 [English]: RBF \/ Matern52 \/ Periodic (from GP.hs; no new kind is created here).
+  , gpcMethod :: !GPMethod      -- ^ [日本語]: 象限 + (RFF 時) 近似次元・seed。 [English]: Quadrant + (for RFF) approximation dimension and seed.
+  , gpcHyper  :: !HyperStrategy -- ^ [日本語]: ハイパラの決め方 + 固定時の値。 [English]: How the hyperparameters are decided + the value when fixed.
   }
 
--- | 既定: 厳密 GP・RBF・周辺尤度自動。 @df |-> gp defaultGP "x" "y"@。
+-- | [日本語]: 既定: 厳密 GP・RBF・周辺尤度自動。 @df |-> gp defaultGP "x" "y"@。
+--   [English]: Default: exact GP, RBF, automatic via marginal likelihood.
+--   @df |-> gp defaultGP \"x\" \"y\"@.
 defaultGP :: GPConfig
 defaultGP = GPConfig RBF Gp AutoMarginalLik
 
--- | 二変量カーネル回帰 spec。 @gp cfg "x" "y"@ ('lm' と対称)。
+-- | [日本語]: 二変量カーネル回帰 spec。 @gp cfg "x" "y"@ ('lm' と対称)。
+--   [English]: Bivariate kernel regression spec. @gp cfg \"x\" \"y\"@
+--   (symmetric with 'lm').
 data GPSpec = GPSpec !GPConfig !Text !Text
 
--- | @gp cfg xCol yCol@ — 統合カーネル回帰 ('GPRegModel') を当てはめる spec。
+-- | [日本語]: @gp cfg xCol yCol@ — 統合カーネル回帰 ('GPRegModel') を当てはめる spec。
+--   [English]: @gp cfg xCol yCol@ — a spec that fits unified kernel
+--   regression ('GPRegModel').
 gp :: GPConfig -> Text -> Text -> GPSpec
 gp = GPSpec
 
--- | RFF 非対応カーネルのフォールバック通知 (RFF は定常カーネル限定 = Bochner)。
+-- | [日本語]: RFF 非対応カーネルのフォールバック通知 (RFF は定常カーネル限定 = Bochner)。
 -- Periodic は定常だがスペクトル密度未実装、 Linear/Poly は非定常ゆえ RFF 不可。
+--   [English]: Fallback notice for kernels that don't support RFF (RFF is
+--   limited to stationary kernels, via Bochner's theorem). Periodic is
+--   stationary but its spectral density isn't implemented; Linear\/Poly are
+--   non-stationary, so RFF isn't possible for them.
 gpNonRffMsg :: Kernel -> String
 gpNonRffMsg ker =
   "[gp] " <> show ker <> " カーネルは RFF 近似不可"
   <> " (Periodic はスペクトル密度未実装・Linear/Poly は非定常 = Bochner)。"
   <> " 厳密象限へフォールバックします。"
 
--- | RFF 近似象限 + RFF 非対応カーネル (Periodic/Linear/Poly) は厳密象限へ
+-- | [日本語]: RFF 近似象限 + RFF 非対応カーネル (Periodic/Linear/Poly) は厳密象限へ
 -- フォールバック ('log' 通知)。 定常カーネル (RBF/Matern52) はそのまま。
+--   [English]: An RFF-approximation quadrant combined with an RFF-unsupported
+--   kernel (Periodic\/Linear\/Poly) falls back to the exact quadrant (with a
+--   'log' notice). Stationary kernels (RBF\/Matern52) are left unchanged.
 gpResolveMethod :: Kernel -> GPMethod -> GPMethod
 gpResolveMethod ker (GpRff  _ _) | not (gpRffSupported ker) = Trace.trace (gpNonRffMsg ker) Gp
 gpResolveMethod ker (KrrRff _ _) | not (gpRffSupported ker) = Trace.trace (gpNonRffMsg ker) Krr
 gpResolveMethod _   m            = m
 
--- | RFF (Random Fourier Features) が近似可能なカーネルか (= 定常 + スペクトル密度実装済)。
+-- | [日本語]: RFF (Random Fourier Features) が近似可能なカーネルか (= 定常 + スペクトル密度実装済)。
+--   [English]: Whether a kernel can be approximated by RFF (Random Fourier
+--   Features) — i.e. stationary and with the spectral density implemented.
 gpRffSupported :: Kernel -> Bool
 gpRffSupported RBF      = True
 gpRffSupported Matern52 = True
 gpRffSupported _        = False   -- Periodic/Linear/Poly
 
--- | ハイパラ方略を解決して 'GPParams' を得る。
+-- | [日本語]: ハイパラ方略を解決して 'GPParams' を得る。
+--   [English]: Resolves the hyperparameter strategy to obtain 'GPParams'.
 gpResolveHyper :: HyperStrategy -> Kernel -> [Double] -> [Double] -> GPParams
 gpResolveHyper (FixedHyper p)  _   _  _  = p
 gpResolveHyper AutoMarginalLik ker xs ys = GP.optimizeGP ker xs ys (GP.initParamsFromData xs ys)
 gpResolveHyper AutoCV          ker xs ys = GP.autoCVHyperGP ker xs ys
 
--- | 象限ごとの予測子を組む。 渡される @method@ は Periodic フォールバック後なので、
+-- | [日本語]: 象限ごとの予測子を組む。 渡される @method@ は Periodic フォールバック後なので、
 -- RFF 象限 (GpRff/RidgeRff) に来るカーネルは RBF / Matern52 に限られる。
+--   [English]: Builds the predictor for each quadrant. Since the @method@
+--   passed in is already past the Periodic fallback, kernels reaching the
+--   RFF quadrants (GpRff\/RidgeRff) are limited to RBF \/ Matern52.
 gpBuildPredict
   :: GPMethod -> Kernel -> GPParams -> [Double] -> [Double]
   -> ([Double] -> ([Double], Maybe [Double]))
@@ -741,24 +859,33 @@
 
 -- ---------------------------------------------------------------------------
 -- 多変量 gpMulti (E3)。 'gamMulti' / 'GAMModelN' と同型 = 第1予測子を描画軸に、
--- 他予測子を訓練平均に固定した偏依存曲線 ('SingleVarModel' として扱う)。 4 象限の
+-- 他予測子を訓練平均に固定した偏依存曲線 (@SingleVarModel@ として扱う)。 4 象限の
 -- MV 実装: Gp/Ridge=fitGPMV (Ridge は mean のみ)、 GpRff=rffGPMV、 RidgeRff=rffRidgeMV。
 -- ---------------------------------------------------------------------------
 
--- | 多変量カーネル回帰 spec。 @gpMulti cfg ["x1","x2"] "y"@ ('lmMulti' と対称)。
+-- | [日本語]: 多変量カーネル回帰 spec。 @gpMulti cfg ["x1","x2"] "y"@ ('lmMulti' と対称)。
+--   [English]: Multivariate kernel regression spec.
+--   @gpMulti cfg [\"x1\",\"x2\"] \"y\"@ (symmetric with 'lmMulti').
 data GPMultiSpec = GPMultiSpec !GPConfig ![Text] !Text
 
--- | @gpMulti cfg xCols yCol@ — 多予測子カーネル回帰 ('lmMulti' と対)。 第1予測子を描画軸に。
+-- | [日本語]: @gpMulti cfg xCols yCol@ — 多予測子カーネル回帰 ('lmMulti' と対)。 第1予測子を描画軸に。
+--   [English]: @gpMulti cfg xCols yCol@ — multi-predictor kernel regression
+--   (paired with 'lmMulti'). The first predictor is used as the plotting
+--   axis.
 gpMulti :: GPConfig -> [Text] -> Text -> GPMultiSpec
 gpMulti = GPMultiSpec
 
--- | MV ハイパラ方略を解決 ('gpResolveHyper' の行列版)。
+-- | [日本語]: MV ハイパラ方略を解決 ('gpResolveHyper' の行列版)。
+--   [English]: Resolves the MV hyperparameter strategy (the matrix version
+--   of 'gpResolveHyper').
 gpResolveHyperMV :: HyperStrategy -> Kernel -> LA.Matrix Double -> LA.Vector Double -> GPParams
 gpResolveHyperMV (FixedHyper p)  _   _ _ = p
 gpResolveHyperMV AutoMarginalLik ker x y = GP.optimizeGPMV ker x y (GP.initParamsFromDataMV x y)
 gpResolveHyperMV AutoCV          ker x y = GP.autoCVHyperGPMV ker x y
 
--- | 象限ごとの MV 予測子。 渡される @method@ は Periodic フォールバック後。
+-- | [日本語]: 象限ごとの MV 予測子。 渡される @method@ は Periodic フォールバック後。
+--   [English]: The MV predictor for each quadrant. The @method@ passed in
+--   is already past the Periodic fallback.
 gpBuildPredictMV
   :: GPMethod -> Kernel -> GPParams -> LA.Matrix Double -> [Double]
   -> (LA.Matrix Double -> ([Double], Maybe [Double]))
@@ -815,38 +942,47 @@
 -- ・glmnet 慣例)。 [[selectLambdaCV]] の純粋化 (`selectLambdaCVPure`/`HCV.kFold` PrimMonad 化) を土台にする。
 -- ===========================================================================
 
--- | λ の決め方 (GP の 'HyperStrategy' と同型)。
+-- | [日本語]: λ の決め方 (GP の 'HyperStrategy' と同型)。
+--   [English]: How λ is decided (the same shape as GP's 'HyperStrategy').
 data LambdaStrat
-  = FixedLambda !Double        -- ^ 固定 λ。
-  | LambdaLOOCV                -- ^ 閉形式 LOOCV (★Ridge 等の線形平滑器専用・他は Left)。
-  | LambdaCV    !Int !Word32   -- ^ k-fold CV (k, seed)・best (CV-MSE 最小)。
-  | LambdaCV1SE !Int !Word32   -- ^ k-fold CV + 1-SE rule (R glmnet 既定推奨)。
+  = FixedLambda !Double        -- ^ [日本語]: 固定 λ。 [English]: A fixed λ.
+  | LambdaLOOCV                -- ^ [日本語]: 閉形式 LOOCV (★Ridge 等の線形平滑器専用・他は Left)。 [English]: Closed-form LOOCV (only for linear smoothers such as Ridge; 'Left' for others).
+  | LambdaCV    !Int !Word32   -- ^ [日本語]: k-fold CV (k, seed)・best (CV-MSE 最小)。 [English]: k-fold CV (k, seed); picks the best (minimum CV-MSE).
+  | LambdaCV1SE !Int !Word32   -- ^ [日本語]: k-fold CV + 1-SE rule (R glmnet 既定推奨)。 [English]: k-fold CV + the 1-SE rule (R glmnet's recommended default).
   deriving (Eq, Show)
 
--- | 罰則回帰の設定。
+-- | [日本語]: 罰則回帰の設定。
+--   [English]: Configuration for penalized regression.
 data RegConfig = RegConfig { rcMethod :: !RegMethod, rcLambda :: !LambdaStrat }
 
--- | 既定 (CV で λ 選択・5-fold・seed 42)。
+-- | [日本語]: 既定 (CV で λ 選択・5-fold・seed 42)。
+--   [English]: Defaults (λ selected via CV; 5-fold; seed 42).
 defaultRidge, defaultLasso :: RegConfig
 defaultRidge = RegConfig Ridge (LambdaCV 5 42)
 defaultLasso = RegConfig Lasso (LambdaCV 5 42)
 
--- | 罰則回帰 spec。 @regularized cfg ["x1","x2"] "y"@。
+-- | [日本語]: 罰則回帰 spec。 @regularized cfg ["x1","x2"] "y"@。
+--   [English]: Penalized regression spec. @regularized cfg [\"x1\",\"x2\"] \"y\"@.
 data RegSpec = RegSpec !RegConfig ![Text] !Text
 
--- | @regularized cfg xCols yCol@。 多予測子のみ。
+-- | [日本語]: @regularized cfg xCols yCol@。 多予測子のみ。
+--   [English]: @regularized cfg xCols yCol@. Multi-predictor only.
 regularized, regularizedMulti :: RegConfig -> [Text] -> Text -> RegSpec
 regularized      = RegSpec
 regularizedMulti = RegSpec               -- 同一関数の別名 (Q4)
 
--- | 近道 (λ 方略は既定 CV)。 bare = 多変量、 @*Multi@ は同一関数の別名。
+-- | [日本語]: 近道 (λ 方略は既定 CV)。 bare = 多変量、 @*Multi@ は同一関数の別名。
+--   [English]: Shortcuts (the λ strategy defaults to CV). The bare name is
+--   multivariate; @*Multi@ is an alias for the same function.
 ridge, ridgeMulti, lasso, lassoMulti :: [Text] -> Text -> RegSpec
 ridge      = RegSpec defaultRidge
 ridgeMulti = ridge
 lasso      = RegSpec defaultLasso
 lassoMulti = lasso
 
--- | Elastic Net 近道 (α 指定・λ 方略は既定 CV)。
+-- | [日本語]: Elastic Net 近道 (α 指定・λ 方略は既定 CV)。
+--   [English]: Elastic Net shortcut (α specified; the λ strategy defaults
+--   to CV).
 elasticNet, elasticNetMulti :: Double -> [Text] -> Text -> RegSpec
 elasticNet a    = RegSpec (RegConfig (ElasticNet a) (LambdaCV 5 42))
 elasticNetMulti = elasticNet
@@ -983,16 +1119,31 @@
       , rmgYraw      = yv
       }
 
--- | 罰則化回帰 ('RegModel') の case (行) bootstrap 係数サマリ。
+-- | [日本語]: 罰則化回帰 ('RegModel') の case (行) bootstrap 係数サマリ。
 --
--- 各 replicate で行を再標本化し、 **λ は full-fit で選択済の 'rmgLambda' に固定**
+-- 各 replicate で行を再標本化し、 __λ は full-fit で選択済の 'rmgLambda' に固定__
 -- (CV を再実行しない) して 'regFitAt' で refit、 元スケール係数を集めて
 -- 'bootCoefRows' で要約する。 標準化・中心化は replicate ごとに再計算する。
 --
--- ⚠ これは **penalized の bootstrap percentile 区間であって有意性検定ではない**。
+-- ⚠ これは __penalized の bootstrap percentile 区間であって有意性検定ではない__。
 -- Lasso 等の変数選択を伴う罰則化では post-selection inference が別問題として
 -- 存在し、 単純な percentile 区間はカバレッジを保証しない (selection の不確実性は
 -- λ 固定 bootstrap だけでは捉えきれない)。 探索的な不確実性目安として使うこと。
+--   [English]: Case (row) bootstrap coefficient summary for penalized
+--   regression ('RegModel').
+--
+-- Resamples rows for each replicate, refits via 'regFitAt' with
+-- __λ fixed at the 'rmgLambda' already selected by the full fit__ (CV is
+-- not re-run), and summarizes the collected original-scale coefficients via
+-- 'bootCoefRows'. Standardization\/centering is recomputed for each
+-- replicate.
+--
+-- Warning: this is __a penalized bootstrap percentile interval, not a__
+-- __significance test__. For penalization involving variable selection (e.g.
+-- Lasso), post-selection inference is a separate problem, and a naive
+-- percentile interval doesn't guarantee coverage (the uncertainty from
+-- selection isn't fully captured by a λ-fixed bootstrap alone). Use this as
+-- an exploratory measure of uncertainty.
 instance HasCoefBoot RegModel where
   coefSummaryBoot seed b m =
     let xRaw    = rmgXraw m
@@ -1010,11 +1161,19 @@
         reps = map repBeta idxSets
     in bootCoefRows (rmgNames m) (rmgCoefs m) reps
 
--- | 罰則化回帰 ('RegModel') の統一要約玄関。
+-- | [日本語]: 罰則化回帰 ('RegModel') の統一要約玄関。
 --
--- ⚠ これは bootstrap percentile (seed=42 / B=2000) であって**有意性検定ではない**
+-- ⚠ これは bootstrap percentile (seed=42 / B=2000) であって__有意性検定ではない__
 -- (post-selection inference は別問題。 'HasCoefBoot' RegModel の注記参照)。
 -- 'HasCoefBoot' RegModel が Fit 層にあるため、 'HasReport' instance もここに置く。
+--   [English]: Unified summary entry point for penalized regression
+--   ('RegModel').
+--
+-- Warning: this is a bootstrap percentile (seed=42, B=2000), __not a__
+-- __significance test__ (post-selection inference is a separate problem; see
+-- the note on the 'HasCoefBoot' RegModel instance). Since 'HasCoefBoot'
+-- RegModel lives in the Fit layer, the 'HasReport' instance is placed here
+-- too.
 instance HasReport RegModel where
   modelReport m = CoefReport (coefSummaryBoot 42 2000 m)
 
@@ -1024,11 +1183,15 @@
 -- 取る spec + 'Fit' instance を設ける ('reqColsM' で列名 → 行列)。 行列直叩き
 -- ('pca' / 'fitPLS') は低レベル退避。 結果型 ('PCAResult' / 'PLSFit') は既存のまま。
 
--- | PCA spec。 @pca std mK ["x1","x2",…]@ (std = 標準化方針・mK = 保持成分数)。
+-- | [日本語]: PCA spec。 @pca std mK ["x1","x2",…]@ (std = 標準化方針・mK = 保持成分数)。
+--   [English]: PCA spec. @pca std mK [\"x1\",\"x2\",…]@ (std = standardization
+--   policy, mK = number of components to keep).
 data PCASpec = PCASpec !PCAStandardize !(Maybe Int) ![Text]
 
--- | @pca std mK cols@ — 列 @cols@ で PCA ('PCAResult') を当てる spec。
+-- | [日本語]: @pca std mK cols@ — 列 @cols@ で PCA ('PCAResult') を当てる spec。
 --   @df |-> pca CenterScale (Just 3) ["x1","x2","x3"]@。
+--   [English]: @pca std mK cols@ — a spec that fits PCA ('PCAResult') on
+--   columns @cols@. @df |-> pca CenterScale (Just 3) [\"x1\",\"x2\",\"x3\"]@.
 pca :: PCAStandardize -> Maybe Int -> [Text] -> PCASpec
 pca = PCASpec
 
@@ -1041,9 +1204,13 @@
 -- 'toFrame' で **元データ (factor 温存)** を保持する (数値源なら数値列を再構築)。
 data MDSSpec = MDSSpec !MDSConfig ![Text]
 
--- | @mds cfg cols@ — 列 @cols@ で MDS ('MDSResult') を当てる spec。
+-- | [日本語]: @mds cfg cols@ — 列 @cols@ で MDS ('MDSResult') を当てる spec。
 --   @m = df |-> mds defaultMDS ["x1","x2","x3"]@ → @noDf |>> toPlot m@ (単色散布)。
 --   Sammon は @mds defaultMDS { mdsMethod = MDSSammon } cols@。
+--   [English]: @mds cfg cols@ — a spec that fits MDS ('MDSResult') on
+--   columns @cols@. @m = df |-> mds defaultMDS [\"x1\",\"x2\",\"x3\"]@ →
+--   @noDf |>> toPlot m@ (a monochrome scatter). Sammon is
+--   @mds defaultMDS { mdsMethod = MDSSammon } cols@.
 mds :: MDSConfig -> [Text] -> MDSSpec
 mds = MDSSpec
 
@@ -1051,11 +1218,16 @@
   type Fitted MDSSpec = MDSResult
   fitEither (MDSSpec cfg cols) d = runMDS cfg (toFrame d) cols
 
--- | PLS spec。 @pls cfg ["x1","x2"] ["y1","y2"]@ (X 列・Y 列を分けて指定)。
+-- | [日本語]: PLS spec。 @pls cfg ["x1","x2"] ["y1","y2"]@ (X 列・Y 列を分けて指定)。
+--   [English]: PLS spec. @pls cfg [\"x1\",\"x2\"] [\"y1\",\"y2\"]@ (X columns and
+--   Y columns specified separately).
 data PLSSpec = PLSSpec !PLSConfig ![Text] ![Text]
 
--- | @pls cfg xcols ycols@ — X 列・Y 列で PLS ('PLSFit') を当てる spec。
+-- | [日本語]: @pls cfg xcols ycols@ — X 列・Y 列で PLS ('PLSFit') を当てる spec。
 --   @df |-> pls defaultPLS ["x1","x2"] ["y1","y2"]@。
+--   [English]: @pls cfg xcols ycols@ — a spec that fits PLS ('PLSFit') using
+--   X columns and Y columns.
+--   @df |-> pls defaultPLS [\"x1\",\"x2\"] [\"y1\",\"y2\"]@.
 pls :: PLSConfig -> [Text] -> [Text] -> PLSSpec
 pls = PLSSpec
 
@@ -1066,11 +1238,16 @@
     y <- reqColsM ycols d
     either (Left . T.unpack) Right (fitPLS cfg x y)
 
--- | 判別分析 (LDA) spec。 @lda ["x1","x2"] "class"@ (特徴列 + クラス列・クラスは整数化)。
+-- | [日本語]: 判別分析 (LDA) spec。 @lda ["x1","x2"] "class"@ (特徴列 + クラス列・クラスは整数化)。
+--   [English]: Discriminant analysis (LDA) spec. @lda [\"x1\",\"x2\"] \"class\"@
+--   (feature columns + class column; the class is turned into integers).
 data LDASpec = LDASpec ![Text] !Text
 
--- | @lda featureCols classCol@ — LDA ('DiscriminantFit') を当てる spec。
+-- | [日本語]: @lda featureCols classCol@ — LDA ('DiscriminantFit') を当てる spec。
 --   クラス列は数値を 'round' で整数ラベル化する。
+--   [English]: @lda featureCols classCol@ — a spec that fits LDA
+--   ('DiscriminantFit'). The class column's numeric values are turned into
+--   integer labels via 'round'.
 lda :: [Text] -> Text -> LDASpec
 lda = LDASpec
 
@@ -1083,10 +1260,14 @@
     either (Left . T.unpack) Right (fitLDA x yInt)
   predictorCols (LDASpec feats _) = feats   -- 特徴標準化は可 (応答=クラスゆえ y は標準化せず)
 
--- | 正準相関分析 (CCA) spec。 @ccaOf ["x1","x2"] ["y1","y2"]@ (2 ブロックの列)。
+-- | [日本語]: 正準相関分析 (CCA) spec。 @ccaOf ["x1","x2"] ["y1","y2"]@ (2 ブロックの列)。
+--   [English]: Canonical correlation analysis (CCA) spec.
+--   @ccaOf [\"x1\",\"x2\"] [\"y1\",\"y2\"]@ (columns for the two blocks).
 data CCASpec = CCASpec ![Text] ![Text]
 
--- | @ccaOf xcols ycols@ — CCA ('CCAFit') を当てる spec (CCAFit は現状 toPlot 非対象)。
+-- | [日本語]: @ccaOf xcols ycols@ — CCA ('CCAFit') を当てる spec (CCAFit は現状 toPlot 非対象)。
+--   [English]: @ccaOf xcols ycols@ — a spec that fits CCA ('CCAFit'; CCAFit
+--   isn't currently a toPlot target).
 ccaOf :: [Text] -> [Text] -> CCASpec
 ccaOf = CCASpec
 
@@ -1094,11 +1275,15 @@
   type Fitted CCASpec = CCAFit
   fitEither (CCASpec xcols ycols) d = cca <$> reqColsM xcols d <*> reqColsM ycols d
 
--- | ラベル列を整数 ('round') の 'VU.Vector Int' で引く (分類器の y)。
+-- | [日本語]: ラベル列を整数 ('round') の 'VU.Vector Int' で引く (分類器の y)。
+--   [English]: Looks up a label column as a 'VU.Vector Int' via 'round'
+--   (the y of a classifier).
 reqLabelI :: ColumnSource d => Text -> d -> Either String (VU.Vector Int)
 reqLabelI n d = VU.fromList . map round . LA.toList <$> reqColV n d
 
--- | ラベル列を 'VU.Vector Double' で引く (回帰の y)。
+-- | [日本語]: ラベル列を 'VU.Vector Double' で引く (回帰の y)。
+--   [English]: Looks up a label column as a 'VU.Vector Double' (the y of a
+--   regression).
 reqLabelD :: ColumnSource d => Text -> d -> Either String (VU.Vector Double)
 reqLabelD n d = VU.fromList . LA.toList <$> reqColV n d
 
@@ -1106,29 +1291,43 @@
 --
 -- いずれも純粋 fit (RNG なし)。 特徴は 'reqColsM' で行列化、 ラベルは整数 / 実数で引く。
 
--- | 勾配ブースティング回帰 spec。 @gbmReg cfg ["x1","x2"] "y"@。
+-- | [日本語]: 勾配ブースティング回帰 spec。 @gbmReg cfg ["x1","x2"] "y"@。
+--   [English]: Gradient boosting regression spec.
+--   @gbmReg cfg [\"x1\",\"x2\"] \"y\"@.
 data GBRSpec = GBRSpec !GBConfig ![Text] !Text
--- | @gbmReg cfg featCols yCol@ — GBM 回帰 ('GBRegressor')。
+-- | [日本語]: @gbmReg cfg featCols yCol@ — GBM 回帰 ('GBRegressor')。
+--   [English]: @gbmReg cfg featCols yCol@ — GBM regression ('GBRegressor').
 gbmReg :: GBConfig -> [Text] -> Text -> GBRSpec
 gbmReg = GBRSpec
 instance Fit GBRSpec where
   type Fitted GBRSpec = GBRegressor
   fitEither (GBRSpec cfg feats yc) d = fitGBRegressor cfg <$> reqColsM feats d <*> reqLabelD yc d
 
--- | 勾配ブースティング分類 spec。 @gbmCls cfg ["x1","x2"] "cls"@ (ラベルは {0,1})。
+-- | [日本語]: 勾配ブースティング分類 spec。 @gbmCls cfg ["x1","x2"] "cls"@ (ラベルは {0,1})。
+--   [English]: Gradient boosting classification spec.
+--   @gbmCls cfg [\"x1\",\"x2\"] \"cls\"@ (labels are {0,1}).
 data GBCSpec = GBCSpec !GBConfig ![Text] !Text
--- | @gbmCls cfg featCols clsCol@ — GBM 分類 ('GBClassifier')。
+-- | [日本語]: @gbmCls cfg featCols clsCol@ — GBM 分類 ('GBClassifier')。
+--   [English]: @gbmCls cfg featCols clsCol@ — GBM classification
+--   ('GBClassifier').
 gbmCls :: GBConfig -> [Text] -> Text -> GBCSpec
 gbmCls = GBCSpec
 instance Fit GBCSpec where
   type Fitted GBCSpec = GBClassifier
   fitEither (GBCSpec cfg feats cc) d = fitGBClassifier cfg <$> reqColsM feats d <*> reqLabelI cc d
 
--- | 決定木 spec。 @decisionTree cfg ["x1","x2"] "cls"@。
+-- | [日本語]: 決定木 spec。 @decisionTree cfg ["x1","x2"] "cls"@。
+--   [English]: Decision tree spec. @decisionTree cfg [\"x1\",\"x2\"] \"cls\"@.
 data DTSpec = DTSpec !DTConfig ![Text] !Text
--- | @decisionTree cfg featCols clsCol@ — 決定木 ('DTFit'・行列版 'fitDTV')。 fit 時に
---   実列名 (feats) とクラス列の levels を載せて返すので 'treePlot'/'printRpart' が
+-- | [日本語]: @decisionTree cfg featCols clsCol@ — 決定木 ('DTFit'・行列版 'fitDTV')。 fit 時に
+--   実列名 (feats) とクラス列の levels を載せて返すので @treePlot@/@printRpart@ が
 --   名前を手渡し不要 ('RFClassifierFit' と同型)。 クラス列は factor(text)/数値の両対応。
+--   [English]: @decisionTree cfg featCols clsCol@ — a decision tree ('DTFit';
+--   the matrix version 'fitDTV'). At fit time, the actual column names
+--   (feats) and the class column's levels are attached to the result, so
+--   @treePlot@\/@printRpart@ don't need names passed in manually (the same
+--   shape as 'RFClassifierFit'). The class column supports both
+--   factor(text) and numeric.
 decisionTree :: DTConfig -> [Text] -> Text -> DTSpec
 decisionTree = DTSpec
 instance Fit DTSpec where
@@ -1138,9 +1337,12 @@
     (y, classes) <- reqLabelWithLevels cc d
     pure (DTFit (fitDTV cfg x y) feats classes)
 
--- | k-NN 分類 spec。 @knnCls 5 ["x1","x2"] "cls"@。
+-- | [日本語]: k-NN 分類 spec。 @knnCls 5 ["x1","x2"] "cls"@。
+--   [English]: k-NN classification spec. @knnCls 5 [\"x1\",\"x2\"] \"cls\"@.
 data KNNCSpec = KNNCSpec !Int ![Text] !Text
--- | @knnCls k featCols clsCol@ — k-NN 分類 ('KNNClassifier')。
+-- | [日本語]: @knnCls k featCols clsCol@ — k-NN 分類 ('KNNClassifier')。
+--   [English]: @knnCls k featCols clsCol@ — k-NN classification
+--   ('KNNClassifier').
 knnCls :: Int -> [Text] -> Text -> KNNCSpec
 knnCls = KNNCSpec
 instance Fit KNNCSpec where
@@ -1153,9 +1355,11 @@
   predictorCols (KNNCSpec _ feats _) = feats   -- ★距離ベース・標準化の本命
   -- responseCol = Nothing (既定)。 分類の応答はクラスラベルゆえ標準化しない。
 
--- | k-NN 回帰 spec。 @knnReg 5 ["x1","x2"] "y"@。
+-- | [日本語]: k-NN 回帰 spec。 @knnReg 5 ["x1","x2"] "y"@。
+--   [English]: k-NN regression spec. @knnReg 5 [\"x1\",\"x2\"] \"y\"@.
 data KNNRSpec = KNNRSpec !Int ![Text] !Text
--- | @knnReg k featCols yCol@ — k-NN 回帰 ('KNNRegressor')。
+-- | [日本語]: @knnReg k featCols yCol@ — k-NN 回帰 ('KNNRegressor')。
+--   [English]: @knnReg k featCols yCol@ — k-NN regression ('KNNRegressor').
 knnReg :: Int -> [Text] -> Text -> KNNRSpec
 knnReg = KNNRSpec
 instance Fit KNNRSpec where
@@ -1164,9 +1368,13 @@
   predictorCols (KNNRSpec _ feats _) = feats   -- ★距離ベース・標準化の本命
   responseCol   (KNNRSpec _ _ yc)    = Just yc
 
--- | Naive Bayes (Gaussian) spec。 @naiveBayes ["x1","x2"] "cls"@ (連続特徴)。
+-- | [日本語]: Naive Bayes (Gaussian) spec。 @naiveBayes ["x1","x2"] "cls"@ (連続特徴)。
+--   [English]: Naive Bayes (Gaussian) spec.
+--   @naiveBayes [\"x1\",\"x2\"] \"cls\"@ (continuous features).
 data NBSpec = NBSpec ![Text] !Text
--- | @naiveBayes featCols clsCol@ — Gaussian NB ('NBModel' = 'NBGaussian')。
+-- | [日本語]: @naiveBayes featCols clsCol@ — Gaussian NB ('NBModel' = 'NBGaussian')。
+--   [English]: @naiveBayes featCols clsCol@ — Gaussian NB ('NBModel' =
+--   'NBGaussian').
 naiveBayes :: [Text] -> Text -> NBSpec
 naiveBayes = NBSpec
 instance Fit NBSpec where
@@ -1181,24 +1389,36 @@
 --
 -- KMeans / RandomForest は RNG を使う ('MWC.GenIO -> IO')。 'df |->' (純粋 'fitEither')
 -- に載せるため、 サンプラ本体を seed 純粋化 (Phase 50 と同方針) した変種
--- 'kMeansPure' / 'fitRFVPure' を呼ぶ。 spec は @seed :: Word32@ を取り、 同 seed →
+-- @kMeansPure@ / @fitRFVPure@ を呼ぶ。 spec は @seed :: Word32@ を取り、 同 seed →
 -- ビット同一の決定的結果を返す。
 
--- | KMeans クラスタリング spec。 @kmeans cfg seed ["x1","x2"]@ (cfg = k 等・seed = 乱数種)。
+-- | [日本語]: KMeans クラスタリング spec。 @kmeans cfg seed ["x1","x2"]@ (cfg = k 等・seed = 乱数種)。
+--   [English]: KMeans clustering spec. @kmeans cfg seed [\"x1\",\"x2\"]@
+--   (cfg = k etc.; seed = random seed).
 data KMeansSpec = KMeansSpec !KMeansConfig !Word32 ![Text]
--- | @kmeans cfg seed featCols@ — KMeans ('KMeansResult'・toPlot = centroid 散布)。
+-- | [日本語]: @kmeans cfg seed featCols@ — KMeans ('KMeansResult'・toPlot = centroid 散布)。
 --   @df |-> kmeans (defaultKMeans 3) 42 ["x1","x2"]@。
+--   [English]: @kmeans cfg seed featCols@ — KMeans ('KMeansResult'; toPlot =
+--   a centroid scatter). @df |-> kmeans (defaultKMeans 3) 42 [\"x1\",\"x2\"]@.
 kmeans :: KMeansConfig -> Word32 -> [Text] -> KMeansSpec
 kmeans = KMeansSpec
 instance Fit KMeansSpec where
   type Fitted KMeansSpec = KMeansResult
   fitEither (KMeansSpec cfg seed cols) d = (\x -> kMeansPure cfg x seed) <$> reqColsM cols d
 
--- | ランダムフォレスト回帰 spec。 @randomForestReg cfg seed ["x1","x2"] "y"@ (cfg = 木数等・seed = 乱数種)。
+-- | [日本語]: ランダムフォレスト回帰 spec。 @randomForestReg cfg seed ["x1","x2"] "y"@ (cfg = 木数等・seed = 乱数種)。
+--   [English]: Random forest regression spec.
+--   @randomForestReg cfg seed [\"x1\",\"x2\"] \"y\"@ (cfg = number of trees
+--   etc.; seed = random seed).
 data RFSpec = RFSpec !RFConfig !Word32 ![Text] !Text
--- | @randomForestReg cfg seed featCols yCol@ — RF 回帰 ('RandomForest'・toPlot = 特徴重要度 bar)。
+-- | [日本語]: @randomForestReg cfg seed featCols yCol@ — RF 回帰 ('RandomForest'・toPlot = 特徴重要度 bar)。
 --   分類 'randomForestCls' と対の命名 (無印 randomForest は廃止)。
 --   @df |-> randomForestReg defaultRandomForest 42 ["x1","x2"] "y"@。
+--   [English]: @randomForestReg cfg seed featCols yCol@ — RF regression
+--   ('RandomForest'; toPlot = a feature-importance bar chart). Named to pair
+--   with the classification 'randomForestCls' (the bare @randomForest@ is
+--   deprecated).
+--   @df |-> randomForestReg defaultRandomForest 42 [\"x1\",\"x2\"] \"y\"@.
 randomForestReg :: RFConfig -> Word32 -> [Text] -> Text -> RFSpec
 randomForestReg = RFSpec
 instance Fit RFSpec where
@@ -1208,10 +1428,16 @@
     (\x y -> (fitRFVPure cfg x y seed) { rfFeatureNames = feats })
       <$> reqColsM feats d <*> reqLabelD yc d
 
--- | DirectLiNGAM (因果探索) の高レベル spec (Phase 77.A)。 @directLingam cfg cols@ =
+-- | [日本語]: DirectLiNGAM (因果探索) の高レベル spec。 @directLingam cfg cols@ =
 --   列名で n×p 行列を組み因果構造を推定する (教師なし・y 無し)。 結果は変数名を添えた
---   'LiNGAMFitted' で、 @toPlot@ が**実変数名の DAG** を描く (低レベルは @x0..@)。
+--   'LiNGAMFitted' で、 @toPlot@ が__実変数名の DAG__ を描く (低レベルは @x0..@)。
 --   @df |-> directLingam defaultDirectLiNGAMConfig ["smoking","tar","cancer"]@。
+--   [English]: High-level spec for DirectLiNGAM (causal discovery).
+--   @directLingam cfg cols@ builds an n×p matrix from column names and
+--   estimates the causal structure (unsupervised, no y). The result is a
+--   'LiNGAMFitted' carrying the variable names, so @toPlot@ draws a
+--   __DAG with real variable names__ (the low-level version uses @x0..@).
+--   @df |-> directLingam defaultDirectLiNGAMConfig [\"smoking\",\"tar\",\"cancer\"]@.
 data DirectLiNGAMSpec = DirectLiNGAMSpec !DirectLiNGAMConfig ![Text]
 
 directLingam :: DirectLiNGAMConfig -> [Text] -> DirectLiNGAMSpec
@@ -1222,8 +1448,11 @@
   fitEither (DirectLiNGAMSpec cfg cols) d =
     (\x -> LiNGAMFitted (fitDirectLiNGAM cfg x) cols) <$> reqColsM cols d
 
--- | ParceLiNGAM (bottom-up sink 探索・潜在交絡に頑健) の高レベル spec (Phase 77.B)。
+-- | [日本語]: ParceLiNGAM (bottom-up sink 探索・潜在交絡に頑健) の高レベル spec。
 --   @df |-> parceLingam defaultParceConfig cols@。 DAG は Direct と同型 (名前付き)。
+--   [English]: High-level spec for ParceLiNGAM (bottom-up sink search;
+--   robust to latent confounders). @df |-> parceLingam defaultParceConfig
+--   cols@. The DAG has the same shape as Direct's (named).
 data ParceLiNGAMSpec = ParceLiNGAMSpec !ParceConfig ![Text]
 
 parceLingam :: ParceConfig -> [Text] -> ParceLiNGAMSpec
@@ -1234,9 +1463,13 @@
   fitEither (ParceLiNGAMSpec cfg cols) d =
     (\x -> LiNGAMFitted (fitParceLiNGAM cfg x) cols) <$> reqColsM cols d
 
--- | MultiGroupLiNGAM (複数群で共通 DAG 構造・Shimizu 2012) の高レベル spec (Phase 77.B)。
---   @df |-> multiGroupLingam defaultMultiGroupConfig cols groupCol@。 @groupCol@ = **数値の
---   群コード列**で行を分割し各群を fit する。 DAG は多数決の共通構造 (名前付き)。
+-- | [日本語]: MultiGroupLiNGAM (複数群で共通 DAG 構造・Shimizu 2012) の高レベル spec。
+--   @df |-> multiGroupLingam defaultMultiGroupConfig cols groupCol@。 @groupCol@ = __数値の群コード列__ で行を分割し各群を fit する。 DAG は多数決の共通構造 (名前付き)。
+--   [English]: High-level spec for MultiGroupLiNGAM (a common DAG structure
+--   across multiple groups; Shimizu 2012).
+--   @df |-> multiGroupLingam defaultMultiGroupConfig cols groupCol@.
+--   @groupCol@ splits rows by __a numeric group-code column__ and fits each
+--   group. The DAG is the majority-vote common structure (named).
 data MultiGroupLiNGAMSpec = MultiGroupLiNGAMSpec !MultiGroupConfig ![Text] !Text
 
 multiGroupLingam :: MultiGroupConfig -> [Text] -> Text -> MultiGroupLiNGAMSpec
@@ -1255,9 +1488,13 @@
       then Left "multiGroupLingam: group 列が空です"
       else Right (LiNGAMFitted (fitMultiGroupLiNGAM cfg groups) cols)
 
--- | VARLiNGAM (時系列因果・同時刻 + 時間ラグ) の高レベル spec (Phase 77.B)。
+-- | [日本語]: VARLiNGAM (時系列因果・同時刻 + 時間ラグ) の高レベル spec。
 --   @df |-> varLingam defaultVARLiNGAMConfig cols@。 行 = 時刻順。 DAG は同時刻辺 + ラグ辺
---   (@x_j[t-l] → x_i[t]@) の**時間ラグ DAG**。
+--   (@x_j[t-l] → x_i[t]@) の__時間ラグ DAG__。
+--   [English]: High-level spec for VARLiNGAM (time-series causality;
+--   contemporaneous + time-lagged). @df |-> varLingam defaultVARLiNGAMConfig
+--   cols@. Rows are in time order. The DAG is __a time-lagged DAG__ with
+--   contemporaneous edges + lag edges (@x_j[t-l] → x_i[t]@).
 data VARLiNGAMSpec = VARLiNGAMSpec !VARLiNGAMConfig ![Text]
 
 varLingam :: VARLiNGAMConfig -> [Text] -> VARLiNGAMSpec
@@ -1268,9 +1505,13 @@
   fitEither (VARLiNGAMSpec cfg cols) d =
     (\x -> LiNGAMFitted (fitVARLiNGAM cfg x) cols) <$> reqColsM cols d
 
--- | PairwiseLiNGAM (2 変数の因果向き判定) の高レベル spec (Phase 77.B)。
+-- | [日本語]: PairwiseLiNGAM (2 変数の因果向き判定) の高レベル spec。
 --   @df |-> pairwiseLingam 0.0 "x" "y"@ (thr = |score| 閾値・0 で符号のみ)。 DAG でなく
 --   2 ノード + 検出向きの矢印 (Inconclusive は無向)。
+--   [English]: High-level spec for PairwiseLiNGAM (determining the causal
+--   direction between two variables). @df |-> pairwiseLingam 0.0 \"x\" \"y\"@
+--   (thr = |score| threshold; 0 uses the sign only). Not a DAG — 2 nodes plus
+--   the detected direction's arrow (undirected if Inconclusive).
 data PairwiseLiNGAMSpec = PairwiseLiNGAMSpec !Double !Text !Text
 
 pairwiseLingam :: Double -> Text -> Text -> PairwiseLiNGAMSpec
@@ -1282,8 +1523,12 @@
     (\x y -> LiNGAMFitted (pairwiseLiNGAM thr x y) [xc, yc])
       <$> reqColV xc d <*> reqColV yc d
 
--- | BootstrapLiNGAM (エッジ確信度) の高レベル spec (Phase 77.C)。 @df |-> bootstrapLingam cfg cols@。
+-- | [日本語]: BootstrapLiNGAM (エッジ確信度) の高レベル spec。 @df |-> bootstrapLingam cfg cols@。
 --   B 回リサンプルして各エッジの出現確率を出す (seed 純粋・決定的)。 toPlot = 確信度 DAG。
+--   [English]: High-level spec for BootstrapLiNGAM (edge confidence).
+--   @df |-> bootstrapLingam cfg cols@. Resamples B times to produce each
+--   edge's occurrence probability (pure via seed, deterministic).
+--   toPlot = a confidence DAG.
 data BootstrapLiNGAMSpec = BootstrapLiNGAMSpec !BootstrapConfig ![Text]
 
 bootstrapLingam :: BootstrapConfig -> [Text] -> BootstrapLiNGAMSpec
@@ -1294,8 +1539,11 @@
   fitEither (BootstrapLiNGAMSpec cfg cols) d =
     (\x -> LiNGAMFitted (fitBootstrapLiNGAMPure cfg x) cols) <$> reqColsM cols d
 
--- | ICA-LiNGAM (Shimizu 2006・FastICA + Hungarian) の高レベル spec (Phase 77.C)。
+-- | [日本語]: ICA-LiNGAM (Shimizu 2006・FastICA + Hungarian) の高レベル spec。
 --   @df |-> icaLingam cfg cols@ (seed 純粋・決定的)。 DAG は名前付き (ilAdjacency)。
+--   [English]: High-level spec for ICA-LiNGAM (Shimizu 2006; FastICA +
+--   Hungarian algorithm). @df |-> icaLingam cfg cols@ (pure via seed,
+--   deterministic). The DAG is named (ilAdjacency).
 data ICALiNGAMSpec = ICALiNGAMSpec !ICALiNGAMConfig ![Text]
 
 icaLingam :: ICALiNGAMConfig -> [Text] -> ICALiNGAMSpec
@@ -1306,9 +1554,15 @@
   fitEither (ICALiNGAMSpec cfg cols) d =
     (\x -> LiNGAMFitted (fitICALiNGAMPure cfg x) cols) <$> reqColsM cols d
 
--- | Pearson 相関ネットワーク (Phase 77)。 @df |-> correlationOf thr cols@ で相関行列を求め、
---   @toPlot@ で @|r| > thr@ の対を辺にしたグラフを描く (因果でなく**周辺相関**)。 LiNGAM DAG
+-- | [日本語]: Pearson 相関ネットワーク。 @df |-> correlationOf thr cols@ で相関行列を求め、
+--   @toPlot@ で @|r| > thr@ の対を辺にしたグラフを描く (因果でなく__周辺相関__)。 LiNGAM DAG
 --   と対比すると、 相関は間接・交絡も辺にして過剰に密になり、 LiNGAM が直接因果に削減するのが分かる。
+--   [English]: A Pearson correlation network. @df |-> correlationOf thr
+--   cols@ computes the correlation matrix, and @toPlot@ draws a graph with
+--   an edge for every pair where @|r| > thr@ (__marginal correlation__, not
+--   causation). Contrasted with a LiNGAM DAG, correlation also edges
+--   indirect \/ confounded relationships and becomes overly dense — which
+--   shows how LiNGAM reduces this down to direct causation.
 data CorrelationSpec = CorrelationSpec !Double ![Text]
 
 correlationOf :: Double -> [Text] -> CorrelationSpec
@@ -1319,11 +1573,19 @@
   fitEither (CorrelationSpec thr cols) d =
     (\x -> CorrelationGraph (correlationMatrix x) cols thr) <$> reqColsM cols d
 
--- | ランダムフォレスト分類 spec。 @randomForestCls cfg seed ["x1","x2"] "cls"@ (cfg = 木数等・seed = 乱数種)。
+-- | [日本語]: ランダムフォレスト分類 spec。 @randomForestCls cfg seed ["x1","x2"] "cls"@ (cfg = 木数等・seed = 乱数種)。
+--   [English]: Random forest classification spec.
+--   @randomForestCls cfg seed [\"x1\",\"x2\"] \"cls\"@ (cfg = number of trees
+--   etc.; seed = random seed).
 data RFCSpec = RFCSpec !RFCConfig !Word32 ![Text] !Text
--- | @randomForestCls cfg seed featCols clsCol@ — RF 分類 ('RFClassifierFit'・toPlot = 重要度 2 パネル
+-- | [日本語]: @randomForestCls cfg seed featCols clsCol@ — RF 分類 ('RFClassifierFit'・toPlot = 重要度 2 パネル
 --   permutation/gini)。 回帰 'randomForestReg' と対の命名 (旧名 rfClassifier)。
 --   @df |-> randomForestCls defaultRFCConfig 42 ["x1","x2"] "cls"@。
+--   [English]: @randomForestCls cfg seed featCols clsCol@ — RF
+--   classification ('RFClassifierFit'; toPlot = a two-panel
+--   permutation\/gini importance chart). Named to pair with the regression
+--   'randomForestReg' (formerly named rfClassifier).
+--   @df |-> randomForestCls defaultRFCConfig 42 [\"x1\",\"x2\"] \"cls\"@.
 randomForestCls :: RFCConfig -> Word32 -> [Text] -> Text -> RFCSpec
 randomForestCls = RFCSpec
 instance Fit RFCSpec where
@@ -1336,10 +1598,17 @@
 
 -- Phase 75.12: カーネル SVM 分類 (多クラス one-vs-rest・SMO は乱数不使用ゆえ純粋) 高レベル spec。
 data SVMSpec = SVMSpec !SVMConfig ![Text] !Text
--- | @svmCls cfg featCols clsCol@ — カーネル SVM 分類 ('SVMMulti'・RBF/poly で
---   非線形境界・真の SV)。 決定境界/SV 可視化は 'decisionBoundaryOf'/'svmSupportVectorsOf'。
+-- | [日本語]: @svmCls cfg featCols clsCol@ — カーネル SVM 分類 ('SVMMulti'・RBF/poly で
+--   非線形境界・真の SV)。 決定境界/SV 可視化は @decisionBoundaryOf@/@svmSupportVectorsOf@。
 --   ハイパラ調整は @svmHyper@ ('SVMConfig') に畳む (GP と同型): 'SVMFixed' で固定 fit、
 --   'SVMTuneCV' grid で k-fold CV 探索 → 最良ハイパラで再学習 (別動詞は作らない)。
+--   [English]: @svmCls cfg featCols clsCol@ — kernel SVM classification
+--   ('SVMMulti'; nonlinear boundaries with RBF\/poly, true SVs). Decision
+--   boundary\/SV visualization via @decisionBoundaryOf@\/@svmSupportVectorsOf@.
+--   Hyperparameter tuning is folded into @svmHyper@ ('SVMConfig'), the same
+--   shape as GP: 'SVMFixed' for a fixed fit, 'SVMTuneCV' grid for a k-fold
+--   CV search → refit with the best hyperparameters (no separate verb is
+--   created).
 svmCls :: SVMConfig -> [Text] -> Text -> SVMSpec
 svmCls = SVMSpec
 instance Fit SVMSpec where
@@ -1354,7 +1623,10 @@
 
 -- Phase 75.9: 古典 MLP 分類 (seed 純粋 'fitMLPClassifierPure') 高レベル spec。
 data MLPClsSpec = MLPClsSpec !MLPConfig !Word32 ![Text] !Text
--- | @mlpCls cfg seed featCols clsCol@ — 古典 MLP 分類 ('MLPFit')。 決定境界/混同行列対応。
+-- | [日本語]: @mlpCls cfg seed featCols clsCol@ — 古典 MLP 分類 ('MLPFit')。 決定境界/混同行列対応。
+--   [English]: @mlpCls cfg seed featCols clsCol@ — classic MLP
+--   classification ('MLPFit'). Supports decision boundary \/ confusion
+--   matrix.
 mlpCls :: MLPConfig -> Word32 -> [Text] -> Text -> MLPClsSpec
 mlpCls = MLPClsSpec
 instance Fit MLPClsSpec where
@@ -1366,7 +1638,9 @@
 
 -- Phase 75.9: 古典 MLP 回帰 (seed 純粋 'fitMLPRegressorPure') 高レベル spec。
 data MLPRegSpec = MLPRegSpec !MLPConfig !Word32 ![Text] !Text
--- | @mlpReg cfg seed featCols yCol@ — 古典 MLP 回帰 ('MLPFit'・応答は実数列)。
+-- | [日本語]: @mlpReg cfg seed featCols yCol@ — 古典 MLP 回帰 ('MLPFit'・応答は実数列)。
+--   [English]: @mlpReg cfg seed featCols yCol@ — classic MLP regression
+--   ('MLPFit'; the response is a real-valued column).
 mlpReg :: MLPConfig -> Word32 -> [Text] -> Text -> MLPRegSpec
 mlpReg = MLPRegSpec
 instance Fit MLPRegSpec where
@@ -1380,25 +1654,35 @@
 -- 群カテゴリ列で行を分割し、 各群に同じ spec を当てはめて N 個のモデルを得る。
 -- ★HBM 整合 = fit 結果 ('GroupedFit') が各群の 'Fitted spec' を保持し、
 -- 'groupModels' で取り出せる (各群 'LMModel' 等に既存診断 = 'Hanalyze.Model.LM.Diagnostics'
--- がそのまま適用でき、 群間で傾きが本当に違うかを診断できる)。 描画は 'Plottable'
--- で N 曲線を群色 + 凡例 ('ColorByCol' + 'scaleColorManual') で重畳する (A3 機構の一般化)。
+-- がそのまま適用でき、 群間で傾きが本当に違うかを診断できる)。 描画は @Plottable@
+-- で N 曲線を群色 + 凡例 (@ColorByCol@ + 'scaleColorManual') で重畳する (A3 機構の一般化)。
 -- 群列は factor (文字) でも数値でも可 (factor は 'toFrame'+'getTextVec'、 数値は
 -- 'lookupCol' を show でラベル化)。 主経路 = 二変量近道、 formula spec も副で許す。
 
--- | spec を「群別フィット」 でラップする。 @grouped "g" (lm "x" "y")@。
---   既存 spec ('lm'\/'glm'\/'spline'\/'robust'\/'quantile'、 formula も可) は不変。
+-- | [日本語]: spec を「群別フィット」 でラップする。 @grouped "g" (lm "x" "y")@。
+--   既存 spec ('lm'\/'glm'\/'spline'\/@robust@\/@quantile@、 formula も可) は不変。
+--   [English]: Wraps a spec as a "per-group fit". @grouped \"g\" (lm \"x\" \"y\")@.
+--   Existing specs ('lm'\/'glm'\/'spline'\/@robust@\/@quantile@; formula specs
+--   too) are unchanged.
 data GroupedSpec spec = GroupedSpec !Text spec
 
--- | @grouped "g" spec@ — 群列 @g@ で行を分け別々に @spec@ を当てはめる spec ラッパ。
+-- | [日本語]: @grouped "g" spec@ — 群列 @g@ で行を分け別々に @spec@ を当てはめる spec ラッパ。
+--   [English]: @grouped \"g\" spec@ — a spec wrapper that splits rows by
+--   group column @g@ and fits @spec@ to each separately.
 grouped :: Text -> spec -> GroupedSpec spec
 grouped = GroupedSpec
 
--- | 各群の (ラベル, fit 済みモデル) を取り出す (hbmDraws\/forestOf 流アクセサ)。
+-- | [日本語]: 各群の (ラベル, fit 済みモデル) を取り出す (hbmDraws\/forestOf 流アクセサ)。
 --   各群モデルに既存の診断 (例 LM なら 'Hanalyze.Model.LM.Diagnostics') がそのまま使える。
+--   [English]: Extracts each group's (label, fitted model) pair (an
+--   accessor in the style of hbmDraws\/forestOf). Existing diagnostics
+--   (e.g. 'Hanalyze.Model.LM.Diagnostics' for LM) apply directly to
+--   each group's model.
 groupModels :: GroupedFit spec -> [(Text, Fitted spec)]
 groupModels = gfGroups
 
--- | 群ラベルの一覧 (出現順)。
+-- | [日本語]: 群ラベルの一覧 (出現順)。
+--   [English]: The list of group labels (in order of appearance).
 groupLabels :: GroupedFit spec -> [Text]
 groupLabels = map fst . gfGroups
 
@@ -1413,8 +1697,11 @@
     fits <- mapM (\k -> (,) k <$> fitEither sp (subOf (rowsOf k))) ordered
     pure (GroupedFit fits)
 
--- | 群列を行ごとのラベル ('Text') 列に正規化する。 factor (文字) 列は
+-- | [日本語]: 群列を行ごとのラベル ('Text') 列に正規化する。 factor (文字) 列は
 --   'toFrame'+'getTextVec'、 数値列は 'lookupCol' を show でラベル化。
+--   [English]: Normalizes the group column into a per-row label ('Text')
+--   column. Factor (text) columns go through 'toFrame'+'getTextVec';
+--   numeric columns are labeled by 'show'-ing the result of 'lookupCol'.
 groupKeysCS :: ColumnSource d => Text -> d -> Either String [Text]
 groupKeysCS gcol d =
   case getTextVec gcol (toFrame d) of
@@ -1423,11 +1710,19 @@
       Just vs -> Right (map showGroupLabel vs)
       Nothing -> Left ("grouped: 群列が見つかりません: " <> T.unpack gcol)
 
--- | クラス列を (整数ラベル列 0..K-1, levels 名) に正規化する。 factor(text) 列は
---   'toFrame'+'getTextVec' で **辞書順 unique** を levels とし、 数値列は round して
---   **昇順 unique** を levels とする (それぞれ show)。 いずれも各行を levels 中の
+-- | [日本語]: クラス列を (整数ラベル列 0..K-1, levels 名) に正規化する。 factor(text) 列は
+--   'toFrame'+'getTextVec' で __辞書順 unique__ を levels とし、 数値列は round して
+--   __昇順 unique__ を levels とする (それぞれ show)。 いずれも各行を levels 中の
 --   index (0..K-1) に符号化するので、 @levels !! label@ でクラス名が引ける。
---   ('reqLabelI' の levels 付き版・'treePlot' 等でクラス名を出すのに使う。)
+--   ('reqLabelI' の levels 付き版・@treePlot@ 等でクラス名を出すのに使う。)
+--   [English]: Normalizes a class column into (integer label column
+--   0..K-1, level names). For factor(text) columns, levels are the
+--   __lexicographically unique values__ via 'toFrame'+'getTextVec'; for
+--   numeric columns, values are rounded and levels are the
+--   __ascending unique values__ (each shown as text). Either way, each row
+--   is encoded as its index (0..K-1) within levels, so @levels !! label@
+--   retrieves the class name. (A levels-carrying version of 'reqLabelI',
+--   used to display class names in @treePlot@ etc.)
 reqLabelWithLevels :: ColumnSource d => Text -> d -> Either String (VU.Vector Int, [Text])
 reqLabelWithLevels n d =
   case getTextVec n (toFrame d) of
@@ -1444,7 +1739,9 @@
         in Right (VU.fromList (map ix ints), map (T.pack . show) levels)
       Nothing -> Left ("label 列が見つかりません: " <> T.unpack n)
 
--- | 数値群ラベルの表示 (整数値は小数点を出さない)。
+-- | [日本語]: 数値群ラベルの表示 (整数値は小数点を出さない)。
+--   [English]: Displays a numeric group label (no decimal point for
+--   integer values).
 showGroupLabel :: Double -> Text
 showGroupLabel v
   | fromIntegral (round v :: Integer) == v = T.pack (show (round v :: Integer))
@@ -1454,34 +1751,51 @@
 --
 -- 二変量近道と違い、 formula 文字列で多変量を表す (R の @lm(y ~ x1 + x2, df)@)。
 -- 既存 'multiLMModel' / 'multiGLMModel' / 'fitMixedLME' を配線するだけ。
--- データ源は 'toFrame' で 'DX.DataFrame' に変換し **Phase 47 経路**
--- (MissingPolicy / contrast / 応答列判定) を通す (再実装しない)。 'DX.DataFrame'
+-- データ源は 'toFrame' で @DX.DataFrame@ に変換し **Phase 47 経路**
+-- (MissingPolicy / contrast / 応答列判定) を通す (再実装しない)。 @DX.DataFrame@
 -- 源は 'toFrame'=id ゆえ factor/NA を温存。
 
--- | formula LM spec。 @lmF "y ~ x1 + x2"@。
+-- | [日本語]: formula LM spec。 @lmF "y ~ x1 + x2"@。
+--   [English]: Formula LM spec. @lmF \"y ~ x1 + x2\"@.
 data LMFormulaSpec   = LMFormulaSpec   !Text
--- | formula GLM spec。 @glmF fam link "y ~ x1 + x2"@。
+-- | [日本語]: formula GLM spec。 @glmF fam link "y ~ x1 + x2"@。
+--   [English]: Formula GLM spec. @glmF fam link \"y ~ x1 + x2\"@.
 data GLMFormulaSpec  = GLMFormulaSpec  !Family !LinkFn !Text
--- | formula 混合モデル spec (random effects)。 @glmmF "y ~ x + (1|g)"@。
+-- | [日本語]: formula 混合モデル spec (random effects)。 @glmmF "y ~ x + (1|g)"@。
+--   [English]: Formula mixed-model spec (random effects).
+--   @glmmF \"y ~ x + (1|g)\"@.
 data GLMMFormulaSpec = GLMMFormulaSpec !Text
 
--- | @lmF "y ~ x1 + x2"@ — formula 多変量 LM ('MultiLMModel') の spec。
+-- | [日本語]: @lmF "y ~ x1 + x2"@ — formula 多変量 LM ('MultiLMModel') の spec。
+--   [English]: @lmF \"y ~ x1 + x2\"@ — a spec for a formula-based
+--   multivariate LM ('MultiLMModel').
 lmF :: Text -> LMFormulaSpec
 lmF = LMFormulaSpec
 
--- | @glmF fam link "y ~ x1 + x2"@ — formula 多変量 GLM ('MultiGLMModel') の spec。
+-- | [日本語]: @glmF fam link "y ~ x1 + x2"@ — formula 多変量 GLM ('MultiGLMModel') の spec。
+--   [English]: @glmF fam link \"y ~ x1 + x2\"@ — a spec for a formula-based
+--   multivariate GLM ('MultiGLMModel').
 glmF :: Family -> LinkFn -> Text -> GLMFormulaSpec
 glmF = GLMFormulaSpec
 
--- | @glmmF "y ~ x + (1|g)"@ — 線形混合モデル (Phase 48 'fitMixedLME') の spec。
+-- | [日本語]: @glmmF "y ~ x + (1|g)"@ — 線形混合モデル ('fitMixedLME') の spec。
 --   返り型は @('GLMMResultRE', [Text])@ (固定効果係数名つき)。
+--   [English]: @glmmF \"y ~ x + (1|g)\"@ — a spec for a linear mixed model
+--   ('fitMixedLME'). The return type is @('GLMMResultRE', [Text])@ (with
+--   fixed-effect coefficient names).
 glmmF :: Text -> GLMMFormulaSpec
 glmmF = GLMMFormulaSpec
 
--- | DOE 設計 (`Design`) の解析 spec (Phase 78.B)。 設計が含意する formula
---   (要因計画=全交互作用 / RSM=2 次) で **LM を当てる** (`MultiLMModel`)。 同じ @plan@ を
+-- | [日本語]: DOE 設計 (`Design`) の解析 spec。 設計が含意する formula
+--   (要因計画=全交互作用 / RSM=2 次) で __LM を当てる__ (`MultiLMModel`)。 同じ @plan@ を
 --   sim データ・実物データに使い回せる (formula は plan 由来・データは任意)。
 --   @filledDf |-> designModel plan "y"@。
+--   [English]: An analysis spec for a DOE design (`Design`). __Fits an LM__
+--   (`MultiLMModel`) using the formula implied by the design (full
+--   interactions for factorial designs, second order for RSM). The same
+--   @plan@ can be reused across simulated and real data (the formula comes
+--   from the plan; the data is arbitrary).
+--   @filledDf |-> designModel plan \"y\"@.
 data DesignModelSpec = DesignModelSpec !Design !Text
 
 designModel :: Design -> Text -> DesignModelSpec
@@ -1491,13 +1805,24 @@
   type Fitted DesignModelSpec = MultiLMModel
   fitEither (DesignModelSpec plan y) d = multiLMModel (designFormula plan y) (toFrame d)
 
--- | DOE 設計 (`Design`) の **GP/RFF 解析** spec (Phase 78.G-e)。`designModel` (LM) の
---   **非 LM 版**で、 plan の因子名で `gpMulti` を当てる (formula 不要 = kernel が非線形を
+-- | [日本語]: DOE 設計 (`Design`) の __GP/RFF 解析__ spec。@designModel@ (LM) の
+--   __非 LM 版__で、 plan の因子名で `gpMulti` を当てる (formula 不要 = kernel が非線形を
 --   吸収するので 2 次項展開が要らない)。 同じ @plan@ を sim/実物データに使い回せる
---   (`designModel` と対称)。 結果 (`GPRegModelN`) は `MultiVarModel` ゆえ profiler/contour に
---   **GP 事後予測帯**を出せる。 ★**連続因子専用** — 非連続因子 (`Num`/`Cat`) 混在は error
+--   (@designModel@ と対称)。 結果 (`GPRegModelN`) は @MultiVarModel@ ゆえ profiler/contour に
+--   __GP 事後予測帯__を出せる。 ★__連続因子専用__ — 非連続因子 (`Num`/`Cat`) 混在は error
 --   (GP kernel はカテゴリ距離を扱えない・`centralCompositeDesign` と同方針)。
 --   @filledDf |-> designModelGP defaultGP plan "y"@。
+--   [English]: A __GP\/RFF analysis__ spec for a DOE design (`Design`). The
+--   __non-LM version__ of @designModel@, fitting `gpMulti` on the plan's
+--   factor names (no formula needed — the kernel absorbs nonlinearity, so
+--   no second-order term expansion is required). The same @plan@ can be
+--   reused across sim\/real data (symmetric with @designModel@). Since the
+--   result (`GPRegModelN`) is a @MultiVarModel@, the profiler\/contour can
+--   show a __GP posterior predictive band__. Note: __continuous factors only__
+--   — mixing in non-continuous factors (`Num`\/`Cat`) is an error
+--   (the GP kernel can't handle categorical distances; the same policy as
+--   `centralCompositeDesign`).
+--   @filledDf |-> designModelGP defaultGP plan \"y\"@.
 data DesignModelGPSpec = DesignModelGPSpec !GPConfig !Design !Text
 
 designModelGP :: GPConfig -> Design -> Text -> DesignModelGPSpec
@@ -1513,34 +1838,64 @@
     where notCont (Cont _ _ _) = False
           notCont _            = True
 
--- | 型付きランダム効果項 (Phase 78.G-f)。lme4 の @(1|g)@ / @(1+s|g)@ を型で表す。
+-- | [日本語]: 型付きランダム効果項。lme4 の @(1|g)@ / @(1+s|g)@ を型で表す。
 --   文字列 formula を経由せず 'designModelHBM' に渡す。
+--   [English]: Typed random-effect terms. Represents lme4's @(1|g)@ \/
+--   @(1+s|g)@ in the type system. Passed to 'designModelHBM' without going
+--   through a string formula.
 ranIntercept :: Text -> RandomSpec
 ranIntercept g = RandomSpec True [] g
 
 ranSlope :: [Text] -> Text -> RandomSpec
 ranSlope slopes g = RandomSpec True slopes g
 
--- | 前処理済みランダム効果 (Phase 78.G-f / G-f2)。
+-- | [日本語]: 前処理済みランダム効果。
 --   @(群 idx (post-drop 行ごと), 群数, 傾き共変量列)@。 傾き列が空 = 切片のみ
 --   (@(1|g)@)、 非空 = 相関ランダム傾き (@(1+s|g)@) で各列が観測ごとの共変量値
 --   (長さ n・designX/ys と同じ post-drop 行整合)。
+--   [English]: A preprocessed random effect.
+--   @(group idx (per post-drop row), group count, slope covariate
+--   columns)@. Empty slope columns = intercept only (@(1|g)@); non-empty =
+--   correlated random slopes (@(1+s|g)@), where each column is the
+--   per-observation covariate value (length n, aligned with the post-drop
+--   rows of designX\/ys).
 type PreparedRE = ([Int], Int, [[Double]])
 
--- | DOE 階層モデルの手書き 'ModelP' (Phase 78.G-f・核心 / G-f2 で相関傾きを高速化)。
+-- | [日本語]: DOE 階層モデルの手書き 'ModelP' (核心・相関傾きを高速化)。
 --   固定効果 = designX·β (β に弱情報 prior)、 観測ノイズ = σ。
 --   ★HBM に formula 文字列を食わせる経路は無い (Fit.hs の方針) ため手書きで組む。
 --
 --   ランダム効果は 2 経路:
---   * **切片のみ** (全 RE の傾き列が空): 'reNormal' + 'observeLMR' の解析勾配 REff 経路
+--   * __切片のみ__ (全 RE の傾き列が空): 'reNormal' + 'observeLMR' の解析勾配 REff 経路
 --     (観測は affine = compiled 高速経路)。
---   * **相関ランダム傾き** (いずれかの RE に傾き列): lme4 @(1+s|g)@。 Phase 80.2b で
---     **非中心化** 化。 群成分 raw latent @z_g^c ~ N(0,1)@、 スケール @τ_c ~ HalfNormal@、
+--   * __相関ランダム傾き__ (いずれかの RE に傾き列): lme4 @(1+s|g)@。 __非中心化__ 化
+--     済み。 群成分 raw latent @z_g^c ~ N(0,1)@、 スケール @τ_c ~ HalfNormal@、
 --     相関 @Lcorr = LKJ Cholesky@ から @b_g^c = τ_c·Σ_{j≤c} Lcorr[c][j]·z_g^j@ を組み、
 --     観測 μ = β·X + Σ_c b_g^c·x_c を per-obs scalar 'observe' に載せる。 μ は τ·z / L·z の
---     latent×latent 積を含む非 affine ゆえ (b) 閉形式には載らないが、 'synthVecIR' が
---     (a) source-to-source AD (vecIR) に載せる (Phase 80.2a spike で per-eval 5×・funnel
---     消滅を実測)。 centered の @potential@ 相関 prior + funnel (160×) は撤去。
+--     latent×latent 積を含む非 affine ゆえ (b) 閉形式には載らないが、 @synthVecIR@ が
+--     (a) source-to-source AD (vecIR) に載せる (per-eval 5×・funnel 消滅を実測)。
+--     centered の @potential@ 相関 prior + funnel (160×) は撤去済み。
+--   [English]: A hand-written 'ModelP' for the DOE hierarchical model (the
+--   core; correlated slopes are sped up). Fixed effects = designX·β (with a
+--   weakly informative prior on β), observation noise = σ.
+--   Note: there's no path for feeding a formula string into HBM (a Fit.hs
+--   policy decision), so this is hand-assembled.
+--
+--   Random effects take 2 paths:
+--   * __Intercept only__ (slope columns empty for all REs): the analytic
+--     gradient REff path via 'reNormal' + 'observeLMR' (observations are
+--     affine — the compiled fast path).
+--   * __Correlated random slopes__ (any RE has slope columns): lme4's
+--     @(1+s|g)@. __Non-centered__. Builds
+--     @b_g^c = τ_c·Σ_{j≤c} Lcorr[c][j]·z_g^j@ from the group component's raw
+--     latent @z_g^c ~ N(0,1)@, scale @τ_c ~ HalfNormal@, and correlation
+--     @Lcorr = LKJ Cholesky@, feeding the observation
+--     μ = β·X + Σ_c b_g^c·x_c into a per-obs scalar 'observe'. Since μ
+--     includes a latent×latent product of τ·z \/ L·z, it's non-affine and
+--     doesn't fit the (b) closed-form path, but @synthVecIR@ puts it on the
+--     (a) source-to-source AD (vecIR) path (measured 5× per-eval with the
+--     funnel eliminated). The centered @potential@ correlation prior +
+--     funnel (160×) has been removed.
 designHBMProgram :: [[Double]] -> [Text] -> [PreparedRE] -> [Double] -> ModelP ()
 designHBMProgram designX betaNames res ys
   | all (\(_, _, sc) -> null sc) res = do
@@ -1553,7 +1908,7 @@
       -- === 相関ランダム傾き (非中心化): μ に τ·L·z (latent×latent) → (a) vecIR ===
       -- Phase 80.2b: centered (potential 相関 prior + observeLMR + funnel 160×) を撤去し、
       -- 非中心化 b_g^c = τ_c·Σ_{j≤c} Lcorr[c][j]·z_g^j を per-obs scalar 'observe' に載せる。
-      -- μ が latent×latent 非 affine ゆえ (b) 閉形式には載らないが、 'synthVecIR' が
+      -- μ が latent×latent 非 affine ゆえ (b) 閉形式には載らないが、 @synthVecIR@ が
       -- (a) source-to-source AD に載せる (Phase 80.2a spike で per-eval 5×・funnel 消滅を実測)。
       betas <- mapM (\nm -> sample nm (Normal 0 10)) betaNames
       sigma <- sample "sigma" (HalfNormal 5)
@@ -1602,21 +1957,34 @@
       pure $ \i -> let g = idxRow !! i
                    in sum [ ((bByComp !! c) !! g) * wOf c i | c <- [0 .. k - 1] ]
 
--- | DOE 設計の **階層ベイズ (mixed-effects)** fit spec (Phase 78.G-f)。 固定効果 =
+-- | [日本語]: DOE 設計の __階層ベイズ (mixed-effects)__ fit spec。 固定効果 =
 --   'designFormula' (factorial=交互作用 / RSM=2次) を LM 経路 (`modelFrame`/`designMatrixF`)
 --   で設計行列化し、 ランダム効果 = 'RandomSpec' の群 (v1 = random intercept のみ) を
 --   'designHBMProgram' で手書き 'ModelP' に組み、 'hbm' (NUTS) で学習する。 事後 draw を
 --   'DesignHBMFit' に格納する。 同じ @plan@ を sim/実物データに使い回せる
---   ('designModel' / 'designModelGP' と対称)。
+--   (@designModel@ / 'designModelGP' と対称)。
 --   @filledDf |-> designModelHBM defaultHBM plan [ranIntercept "lot"] "y"@。
+--   [English]: A __hierarchical Bayesian (mixed-effects)__ fit spec for a
+--   DOE design. Fixed effects: builds the design matrix via
+--   'designFormula' (factorial=interactions \/ RSM=second order) through
+--   the LM path (`modelFrame`\/`designMatrixF`). Random effects: assembles
+--   the groups from 'RandomSpec' (v1 = random intercept only) into a
+--   hand-written 'ModelP' via 'designHBMProgram', trained with 'hbm'
+--   (NUTS). Stores the posterior draws in 'DesignHBMFit'. The same @plan@
+--   can be reused across sim\/real data (symmetric with @designModel@ \/
+--   'designModelGP').
+--   @filledDf |-> designModelHBM defaultHBM plan [ranIntercept \"lot\"] \"y\"@.
 data DesignHBMFit = DesignHBMFit
-  { dhfFormula    :: !Formula      -- ^ 固定効果 formula ('designFormula' plan y の parse 結果)。
-  , dhfBetaNames  :: ![Text]       -- ^ 固定効果係数名 (設計列順)。
-  , dhfBetaDraws  :: ![[Double]]   -- ^ draws × p ('dhfBetaNames' 列順)。
-  , dhfSigmaDraws :: ![Double]     -- ^ 観測ノイズ σ の事後 draw。
-  , dhfFrame      :: !ModelFrame   -- ^ 訓練 frame (mvFrame 用)。
-  , dhfModel      :: !HBMModel     -- ^ 学習済 HBM 本体。 診断抽出子 ('dagOf' / 'tracesOf' /
-                                   --   'ppcOf' / 'energyOf' 等) に @dhfModel fit@ で渡せる。
+  { dhfFormula    :: !Formula      -- ^ [日本語]: 固定効果 formula ('designFormula' plan y の parse 結果)。 [English]: Fixed-effect formula (the parse result of 'designFormula' plan y).
+  , dhfBetaNames  :: ![Text]       -- ^ [日本語]: 固定効果係数名 (設計列順)。 [English]: Fixed-effect coefficient names (in design-column order).
+  , dhfBetaDraws  :: ![[Double]]   -- ^ [日本語]: draws × p ('dhfBetaNames' 列順)。 [English]: draws × p (in 'dhfBetaNames' column order).
+  , dhfSigmaDraws :: ![Double]     -- ^ [日本語]: 観測ノイズ σ の事後 draw。 [English]: Posterior draws of the observation noise σ.
+  , dhfFrame      :: !ModelFrame   -- ^ [日本語]: 訓練 frame (mvFrame 用)。 [English]: Training frame (for mvFrame).
+  , dhfModel      :: !HBMModel     -- ^ [日本語]: 学習済 HBM 本体。 診断抽出子 (@dagOf@ / @tracesOf@ /
+                                   --   @ppcOf@ / @energyOf@ 等) に @dhfModel fit@ で渡せる。
+                                   --   [English]: The trained HBM body. Can be passed to diagnostic
+                                   --   extractors (@dagOf@ \/ @tracesOf@ \/ @ppcOf@ \/ @energyOf@ etc.)
+                                   --   via @dhfModel fit@.
   }
 
 data DesignModelHBMSpec = DesignModelHBMSpec !HBMConfig !Design ![RandomSpec] !Text
@@ -1667,8 +2035,8 @@
         Nothing  -> Left ("designModelHBM: 傾き共変量列 '" <> T.unpack s
                           <> "' が見つかりません (数値列である必要があります)")
 
--- | 多出力 (複数応答) fit コンビネータ (Phase 78.F)。 応答名のリストと **応答名から spec を作る
---   関数**を受け、 各応答を同じデータ源で当てはめて @[(応答名, Fitted spec)]@ を返す。
+-- | [日本語]: 多出力 (複数応答) fit コンビネータ。 応答名のリストと __応答名から spec を作る関数__
+--   を受け、 各応答を同じデータ源で当てはめて @[(応答名, Fitted spec)]@ を返す。
 --   @designModel plan@ が既にカレー化 (@Text -> DesignModelSpec@) なので接着剤なしで slot に嵌る:
 --
 --   > let model = filledDf |-> multiOutput ["strength","yield"] (designModel plan)
@@ -1676,19 +2044,45 @@
 --
 --   designModel 専用でなく汎用 (@multiOutput ys (lmF . mkFormula)@ 等も可)。 結果は profiler
 --   ('Hanalyze.Plot.ML.profiler') が「行=応答 × 列=因子」 のグリッドに描ける。
+--   [English]: A multi-output (multiple-response) fit combinator. Takes a
+--   list of response names and __a response-name-to-spec function__, fits
+--   each response on the same data source, and
+--   returns @[(response name, Fitted spec)]@. Since @designModel plan@ is
+--   already curried (@Text -> DesignModelSpec@), it slots in with no glue
+--   code:
+--
+--   > let model = filledDf |-> multiOutput ["strength","yield"] (designModel plan)
+--   >     -- model :: [(Text, MultiLMModel)]
+--
+--   Not specific to designModel — generic (e.g.
+--   @multiOutput ys (lmF . mkFormula)@ also works). The result lets the
+--   profiler ('Hanalyze.Plot.ML.profiler') draw a "rows = responses
+--   × columns = factors" grid.
 data MultiOutputSpec spec = MultiOutputSpec ![Text] (Text -> spec)
 
--- | @multiOutput responseNames mkSpec@ — 各応答名に @mkSpec@ を適用して当てはめる。
+-- | [日本語]: @multiOutput responseNames mkSpec@ — 各応答名に @mkSpec@ を適用して当てはめる。
+--   [English]: @multiOutput responseNames mkSpec@ — fits by applying
+--   @mkSpec@ to each response name.
 multiOutput :: [Text] -> (Text -> spec) -> MultiOutputSpec spec
 multiOutput = MultiOutputSpec
 
--- | @multiOutput@ の結果 @[(応答名, model)]@ から**応答名でモデルを 1 つ取り出す**。 単一応答の
+-- | [日本語]: @multiOutput@ の結果 @[(応答名, model)]@ から__応答名でモデルを 1 つ取り出す__。 単一応答の
 --   可視化 (@contourOf@ / @surfaceOf@) に multiOutput の結果を渡すとき @snd (head model)@ の代わりに使う。
 --
 --   > let models = filledDf |-> multiOutput ["strength","yield"] (designModel plan)
 --   > noDf |>> contourOf (modelFor "strength" models) "temp" "time"
 --
 --   応答名が無ければ利用可能な名前を添えて error (対話で気付ける)。
+--   [English]: __Extracts one model by response name__ from a
+--   @multiOutput@ result @[(response name, model)]@. Used in place of
+--   @snd (head model)@ when passing a multiOutput result to a single-response
+--   visualization (@contourOf@ \/ @surfaceOf@).
+--
+--   > let models = filledDf |-> multiOutput ["strength","yield"] (designModel plan)
+--   > noDf |>> contourOf (modelFor "strength" models) "temp" "time"
+--
+--   Raises an error listing the available names if the response name isn't
+--   found (so it's noticed interactively).
 modelFor :: Text -> [(Text, m)] -> m
 modelFor r models = case lookup r models of
   Just m  -> m
@@ -1718,26 +2112,39 @@
 -- 重回帰 = 説明変数の**列名リスト**で多変量回帰を当てる (formula 文字列を書かない)。
 -- 内部は 'additiveFormula' で設計行列 @[1, x1,…,xp]@ を直接合成し、 既存の
 -- 'multiLMModelF' / 'multiGLMModelF' / 'multiRobustModelF' を配線する。 返り値は
--- effect plot ('statModelMulti' / along / holdAt / byVar) と係数サマリ
--- ('coefSummary') の両方が即使える。 'lmF' / 'glmF' は formula 糖衣として併存。
+-- effect plot (@statModelMulti@ / along / holdAt / byVar) と係数サマリ
+-- (@coefSummary@) の両方が即使える。 'lmF' / 'glmF' は formula 糖衣として併存。
 
--- | 重回帰 (多変量 LM) spec。 @lmMulti [\"x1\",\"x2\",\"x3\"] \"y\"@。
+-- | [日本語]: 重回帰 (多変量 LM) spec。 @lmMulti [\"x1\",\"x2\",\"x3\"] \"y\"@。
+--   [English]: Multiple-regression (multivariate LM) spec.
+--   @lmMulti [\"x1\",\"x2\",\"x3\"] \"y\"@.
 data LMMultiSpec     = LMMultiSpec     ![Text] !Text
--- | 重回帰 (多変量 GLM) spec。 @glmMulti fam link [\"x1\",\"x2\"] \"y\"@。
+-- | [日本語]: 重回帰 (多変量 GLM) spec。 @glmMulti fam link [\"x1\",\"x2\"] \"y\"@。
+--   [English]: Multiple-regression (multivariate GLM) spec.
+--   @glmMulti fam link [\"x1\",\"x2\"] \"y\"@.
 data GLMMultiSpec    = GLMMultiSpec    !Family !LinkFn ![Text] !Text
--- | 重回帰 (多変量ロバスト) spec。 @robustMulti est [\"x1\",\"x2\"] \"y\"@。
+-- | [日本語]: 重回帰 (多変量ロバスト) spec。 @robustMulti est [\"x1\",\"x2\"] \"y\"@。
+--   [English]: Multiple-regression (multivariate robust) spec.
+--   @robustMulti est [\"x1\",\"x2\"] \"y\"@.
 data RobustMultiSpec = RobustMultiSpec !RobustEstimator ![Text] !Text
 
--- | @lmMulti predCols yCol@ — 列名リストで多変量線形回帰 ('MultiLMModel')。
+-- | [日本語]: @lmMulti predCols yCol@ — 列名リストで多変量線形回帰 ('MultiLMModel')。
 --   @df |-> lmMulti [\"age\",\"bmi\",\"bp\"] \"y\"@。
+--   [English]: @lmMulti predCols yCol@ — multivariate linear regression
+--   ('MultiLMModel') from a column-name list.
+--   @df |-> lmMulti [\"age\",\"bmi\",\"bp\"] \"y\"@.
 lmMulti :: [Text] -> Text -> LMMultiSpec
 lmMulti = LMMultiSpec
 
--- | @glmMulti fam link predCols yCol@ — 列名リストで多変量 GLM ('MultiGLMModel')。
+-- | [日本語]: @glmMulti fam link predCols yCol@ — 列名リストで多変量 GLM ('MultiGLMModel')。
+--   [English]: @glmMulti fam link predCols yCol@ — multivariate GLM
+--   ('MultiGLMModel') from a column-name list.
 glmMulti :: Family -> LinkFn -> [Text] -> Text -> GLMMultiSpec
 glmMulti = GLMMultiSpec
 
--- | @rlmMulti est predCols yCol@ — 列名リストで多変量ロバスト回帰 ('MultiRobustModel')。
+-- | [日本語]: @rlmMulti est predCols yCol@ — 列名リストで多変量ロバスト回帰 ('MultiRobustModel')。
+--   [English]: @rlmMulti est predCols yCol@ — multivariate robust
+--   regression ('MultiRobustModel') from a column-name list.
 rlmMulti :: RobustEstimator -> [Text] -> Text -> RobustMultiSpec
 rlmMulti = RobustMultiSpec
 
@@ -1761,12 +2168,20 @@
   predictorCols (RobustMultiSpec _ xs _) = xs
   responseCol   (RobustMultiSpec _ _ y)  = Just y
 
--- | 多変量 (重回帰) 分位点回帰 spec。 @quantileMulti [0.1,0.5,0.9] ["x1","x2"] "y"@。
---   単変量 'quantile' の多予測子版 (各 τ を設計行列 @[1,x₁..xₚ]@ に当てる・statsmodels
+-- | [日本語]: 多変量 (重回帰) 分位点回帰 spec。 @quantileMulti [0.1,0.5,0.9] ["x1","x2"] "y"@。
+--   単変量 @quantile@ の多予測子版 (各 τ を設計行列 @[1,x₁..xₚ]@ に当てる・statsmodels
 --   @QuantReg@ 多予測子と同型)。 予測子は数値列を 'reqColsM' で直接行列化する。
+--   [English]: Multivariate (multiple-regression) quantile regression spec.
+--   @quantileMulti [0.1,0.5,0.9] [\"x1\",\"x2\"] \"y\"@. The multi-predictor
+--   version of univariate @quantile@ (fits each τ to the design matrix
+--   @[1,x₁..xₚ]@ — the same shape as statsmodels's multi-predictor
+--   @QuantReg@). Predictors are turned directly into a matrix via numeric
+--   columns and 'reqColsM'.
 data QuantileMultiSpec = QuantileMultiSpec ![Double] ![Text] !Text
 
--- | @rqMulti taus predCols yCol@ — 多変量分位点回帰 ('MultiQuantileModel')。
+-- | [日本語]: @rqMulti taus predCols yCol@ — 多変量分位点回帰 ('MultiQuantileModel')。
+--   [English]: @rqMulti taus predCols yCol@ — multivariate quantile
+--   regression ('MultiQuantileModel').
 rqMulti :: [Double] -> [Text] -> Text -> QuantileMultiSpec
 rqMulti = QuantileMultiSpec
 
@@ -1786,16 +2201,22 @@
 --
 -- HBM は formula を取らず手書き 'ModelP' を学習する (brms 風 formula→HBM は別 Phase)。
 -- spec は既存 'hbmModelPure' (純粋・seed 決定的) を配線するだけ。 データ源の
--- 数値列をすべて取り出し列名 assoc にして渡す (HBM 側 'dataNamed' 名と突合)。
+-- 数値列をすべて取り出し列名 assoc にして渡す (HBM 側 @dataNamed@ 名と突合)。
 
--- | HBM spec。 @hbm cfg model@ (設定 + 手書き確率プログラム)。
+-- | [日本語]: HBM spec。 @hbm cfg model@ (設定 + 手書き確率プログラム)。
+--   [English]: HBM spec. @hbm cfg model@ (configuration + hand-written
+--   probabilistic program).
 data HBMSpec = HBMSpec HBMConfig (ModelP ())
 
--- | @hbm cfg model@ — HBM ('HBMModel') を学習する spec。 cfg の seed で決定的。
+-- | [日本語]: @hbm cfg model@ — HBM ('HBMModel') を学習する spec。 cfg の seed で決定的。
+--   [English]: @hbm cfg model@ — a spec that trains HBM ('HBMModel').
+--   Deterministic via cfg's seed.
 hbm :: HBMConfig -> ModelP () -> HBMSpec
 hbm = HBMSpec
 
--- | データ源の数値列をすべて列名 assoc に取り出す (HBM の入力形)。
+-- | [日本語]: データ源の数値列をすべて列名 assoc に取り出す (HBM の入力形)。
+--   [English]: Extracts all numeric columns of a data source into a
+--   column-name assoc list (HBM's input shape).
 numericCols :: ColumnSource d => d -> [(Text, [Double])]
 numericCols d = [ (n, vs) | n <- columnNames d, Just vs <- [lookupCol n d] ]
 
@@ -1808,7 +2229,7 @@
     checkDataSlots model d
     (ixCols, levels) <- resolveIxSlots model d
     Right (hbmModelPureWith cfg model (numericCols d) ixCols levels)
-  -- Phase 61.4: '(|->!)' 経路 = 同じ列解決 + 進捗表示つき IO 学習。
+  -- Phase 61.4: @(|->!)@ 経路 = 同じ列解決 + 進捗表示つき IO 学習。
   -- seed 規約は pure 経路と共有ゆえ結果はビット一致 (test 固定)。
   fitIO (HBMSpec cfg model) d =
     case (do checkDataSlots model d; resolveIxSlots model d) of
@@ -1816,9 +2237,14 @@
       Right (ixCols, levels) ->
         hbmModelIOWith cfg model (numericCols d) ixCols levels
 
--- | 'dataNamed' slot の突合検査 (Phase 60.3): **空 placeholder** (@dataNamed n []@)
+-- | [日本語]: @dataNamed@ slot の突合検査: __空 placeholder__ (@dataNamed n []@)
 -- なのに対応する数値列が無ければ loud error。 実値入り placeholder の列欠落は
 -- default 続行 (データ直書きの正当パターンを壊さない)。
+--   [English]: Cross-checks @dataNamed@ slots: a loud error if there's no
+--   matching numeric column for __an empty placeholder__
+--   (@dataNamed n []@). A missing column for a placeholder that already
+--   has real values falls through to the default (so it doesn't break the
+--   legitimate pattern of writing data directly inline).
 checkDataSlots :: ColumnSource d => ModelP () -> d -> Either String ()
 checkDataSlots model d =
   case [ n | (n, True) <- dataSlots model
@@ -1830,12 +2256,22 @@
                 <> T.unpack (T.intercalate ", " (columnNames d))
                 <> ")。 Integer/Text 数値文字列は許容、 factor 列は dataNamedIx で")
 
--- | 'dataNamedIx' slot を列から解決する (Phase 60.3)。
---   * Text factor 列 → **sort 順** (辞書順) levels に 0.. コード化
+-- | [日本語]: @dataNamedIx@ slot を列から解決する。
+--   * Text factor 列 → __sort 順__ (辞書順) levels に 0.. コード化
 --     (R @factor()@ / pandas parity・行順 shuffle に不変)
 --   * 数値列 (Int / Integer / 整数値の Double) → @round@ で [Int]
 --   * 空 placeholder で列なし / 非整数値 → loud error ('Left')
 --   * 実値入り placeholder で列なし → default 続行 (bind しない)
+--   [English]: Resolves @dataNamedIx@ slots from columns.
+--   * Text factor columns → coded 0.. against __sort-order__
+--     (lexicographic) levels (R's @factor()@ \/ pandas parity; invariant to
+--     row-order shuffling).
+--   * Numeric columns (Int \/ Integer \/ integer-valued Double) → [Int] via
+--     @round@.
+--   * No column for an empty placeholder \/ non-integer values → a loud
+--     error ('Left').
+--   * No column for a placeholder that already has real values → falls
+--     through to the default (not bound).
 resolveIxSlots :: ColumnSource d => ModelP () -> d
                -> Either String ([(Text, [Int])], [(Text, [Text])])
 resolveIxSlots model d = do
diff --git a/src/Hanalyze/MCMC/BayesianTest.hs b/src/Hanalyze/MCMC/BayesianTest.hs
deleted file mode 100644
--- a/src/Hanalyze/MCMC/BayesianTest.hs
+++ /dev/null
@@ -1,227 +0,0 @@
--- |
--- Module      : Hanalyze.MCMC.BayesianTest
--- Description : Bayesian A/B test — 2 群間の平均差を NUTS でサンプルし ROPE/HDI で判定
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Bayesian A/B test helper — 2 群間の平均差を NUTS でサンプル、
--- ROPE / HDI に基づき決定。
---
--- Spotfire 風 "Good vs Bad" の Bayesian 版。 既存の頻度論版
--- ('Hanalyze.Stat.GroupComparison.goodVsBad') が Welch t + Cohen's d で
--- 並列比較するのに対し、 本モジュールは **2 群の平均差の posterior** を
--- 得て、 HDI (highest density interval) + ROPE (region of practical
--- equivalence) で意思決定する。
---
--- モデル:
---
--- @
--- μ_A    ~ Normal(0, priorScale)
--- μ_B    ~ Normal(0, priorScale)
--- σ_A    ~ HalfNormal(sigmaScale)
--- σ_B    ~ HalfNormal(sigmaScale)
--- y_A    ~ Normal(μ_A, σ_A)
--- y_B    ~ Normal(μ_B, σ_B)
--- diff   = μ_B - μ_A
--- @
---
--- 決定ルール (`ROPEDecision lo hi`):
---
---   * HDI が ROPE [lo, hi] と **重ならず HDI 全体が ROPE の外** → 'RejectH0'
---   * HDI が ROPE 内に **完全に含まれる** → 'AcceptH0'
---   * それ以外 → 'Inconclusive'
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE RankNTypes #-}
-module Hanalyze.MCMC.BayesianTest
-  ( -- * 入力
-    BayesianABConfig (..)
-  , DecisionRule (..)
-  , defaultBayesianABConfig
-    -- * 出力
-  , BayesianABResult (..)
-  , ABDecision (..)
-    -- * 実行
-  , bayesianAB
-    -- * 補助
-  , highestDensityInterval
-  ) where
-
-import qualified Data.Map.Strict       as Map
-import           Data.List             (sort)
-import qualified System.Random.MWC     as MWC
-
-import qualified Hanalyze.MCMC.Core    as MC
-import qualified Hanalyze.MCMC.NUTS    as NUTS
-import qualified Hanalyze.Model.HBM    as HBM
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
--- | 意思決定ルール。
-data DecisionRule
-  = HDIOnly                     -- ^ HDI を計算するのみ、 自動判定しない
-  | ROPEDecision !Double !Double
-    -- ^ @ROPEDecision lo hi@ で「実用上 0 と区別不能な区間 @[lo, hi]@」 を指定
-  deriving (Show, Eq)
-
--- | A/B 試験の入力設定。
-data BayesianABConfig = BayesianABConfig
-  { babCredible   :: !Double         -- ^ HDI の信頼水準 (例 0.95)
-  , babRule       :: !DecisionRule
-  , babPriorScale :: !Double         -- ^ μ_A, μ_B の prior σ (default 10)
-  , babSigmaScale :: !Double         -- ^ HalfNormal σ の scale (default 5)
-  , babNUTS       :: !NUTS.NUTSConfig
-  } deriving (Show)
-
-defaultBayesianABConfig :: BayesianABConfig
-defaultBayesianABConfig = BayesianABConfig
-  { babCredible   = 0.95
-  , babRule       = HDIOnly
-  , babPriorScale = 10.0
-  , babSigmaScale = 5.0
-  , babNUTS       = NUTS.defaultNUTSConfig
-                      { NUTS.nutsIterations = 1000
-                      , NUTS.nutsBurnIn     = 500
-                      }
-  }
-
--- | 自動判定の結果。
-data ABDecision
-  = AcceptH0       -- ^ HDI が ROPE 内 → 「実用上 0」 と判定
-  | RejectH0       -- ^ HDI が ROPE の外 → 「明確に差がある」 と判定
-  | Inconclusive   -- ^ HDI が ROPE と部分的に重なる → 「データ不足」
-  | NoRuleApplied  -- ^ 'HDIOnly' 指定で判定なし
-  deriving (Show, Eq)
-
--- | A/B 試験の出力。
-data BayesianABResult = BayesianABResult
-  { babPosteriorDiff :: ![Double]
-    -- ^ 平均差 (μ_B − μ_A) の post-burn-in サンプル
-  , babMeanDiff      :: !Double
-    -- ^ posterior mean (μ_B − μ_A)
-  , babHDI           :: !(Double, Double)
-    -- ^ @babCredible@ 信頼水準の HDI
-  , babDecision      :: !ABDecision
-  , babProbDiffPos   :: !Double
-    -- ^ @P(μ_B > μ_A)@ の posterior 確率
-  , babChain         :: !MC.Chain
-    -- ^ 生 chain (μ_A / μ_B / σ_A / σ_B / diff の post-burn-in サンプル)
-  } deriving (Show)
-
--- ===========================================================================
--- 公開関数
--- ===========================================================================
-
--- | 2 群のデータから Bayesian A/B 試験を実行。
---
--- 内部で HBM モデルを組み立て、 NUTS で posterior をサンプル、
--- 平均差の HDI と決定を返す。
---
--- 失敗条件: いずれかの群が空 → @error@ (canvas backend では事前に検査)。
-bayesianAB
-  :: BayesianABConfig
-  -> [Double]         -- ^ 群 A の観測値
-  -> [Double]         -- ^ 群 B の観測値
-  -> MWC.GenIO
-  -> IO BayesianABResult
-bayesianAB cfg ysA ysB gen
-  | null ysA || null ysB =
-      error "Hanalyze.MCMC.BayesianTest.bayesianAB: both groups must be non-empty"
-  | otherwise = do
-      let priorScale_ = babPriorScale cfg
-          sigmaScale_ = babSigmaScale cfg
-          model :: HBM.ModelP ()
-          model = do
-            muA <- HBM.sample "mu_a"    (HBM.Normal 0 (realToFrac priorScale_))
-            muB <- HBM.sample "mu_b"    (HBM.Normal 0 (realToFrac priorScale_))
-            sA  <- HBM.sample "sigma_a" (HBM.HalfNormal (realToFrac sigmaScale_))
-            sB  <- HBM.sample "sigma_b" (HBM.HalfNormal (realToFrac sigmaScale_))
-            HBM.observe "ya" (HBM.Normal muA sA) ysA
-            HBM.observe "yb" (HBM.Normal muB sB) ysB
-            _   <- HBM.deterministic "diff" (muB - muA)
-            pure ()
-          initParams = Map.fromList
-            [ ("mu_a", mean ysA)
-            , ("mu_b", mean ysB)
-            , ("sigma_a", max 0.1 (stddev ysA))
-            , ("sigma_b", max 0.1 (stddev ysB))
-            ]
-      rawChain <- NUTS.nuts model (babNUTS cfg) initParams gen
-      -- deterministic 値 "diff" は raw chain に入っていないため augment で注入
-      let chain = HBM.augmentChainWithDeterministic model rawChain
-          diffs = MC.chainVals "diff" chain
-          n     = length diffs
-          mu    = if n == 0 then 0 else sum diffs / fromIntegral n
-          hdi   = highestDensityInterval (babCredible cfg) diffs
-          probP = if n == 0
-                    then 0
-                    else fromIntegral (length (filter (> 0) diffs))
-                       / fromIntegral n
-          decision = case babRule cfg of
-            HDIOnly -> NoRuleApplied
-            ROPEDecision lo hi -> classifyROPE hdi lo hi
-      pure BayesianABResult
-        { babPosteriorDiff = diffs
-        , babMeanDiff      = mu
-        , babHDI           = hdi
-        , babDecision      = decision
-        , babProbDiffPos   = probP
-        , babChain         = chain
-        }
-
--- ===========================================================================
--- 補助
--- ===========================================================================
-
--- | サンプル列の **highest density interval (HDI)**。
---
--- ソート後、 窓幅 @floor(n · level)@ で全 sliding window を試し、
--- 最も狭い窓を返す。 unimodal な posterior では HDI = 最短連続区間。
---
--- @level ∈ (0, 1)@、 例: 0.95 で 95% HDI。
-highestDensityInterval :: Double -> [Double] -> (Double, Double)
-highestDensityInterval level xs
-  | null xs = (0, 0)
-  | level <= 0 || level >= 1 = error "HDI: level must be in (0, 1)"
-  | otherwise =
-      let sorted = sort xs
-          n      = length sorted
-          k      = max 1 (floor (fromIntegral n * level :: Double))
-          -- 全 sliding windows (start = 0 .. n-k)
-          arr    = case sorted of
-                     [] -> []
-                     _  -> sorted
-          windows = [ (arr !! i, arr !! (i + k - 1))
-                    | i <- [0 .. n - k] ]
-          -- 最も狭い窓
-          best   = head $ foldr keepNarrower [head windows] (tail windows)
-      in best
-  where
-    keepNarrower w (b:_) =
-      if (snd w - fst w) < (snd b - fst b) then [w] else [b]
-    keepNarrower w []    = [w]
-
--- | HDI と ROPE [lo, hi] から ABDecision を分類。
-classifyROPE :: (Double, Double) -> Double -> Double -> ABDecision
-classifyROPE (hdiLo, hdiHi) ropeLo ropeHi
-  | hdiHi < ropeLo || hdiLo > ropeHi = RejectH0       -- HDI 全体が ROPE 外
-  | hdiLo >= ropeLo && hdiHi <= ropeHi = AcceptH0     -- HDI 全体が ROPE 内
-  | otherwise = Inconclusive                          -- 部分重複
-
--- ===========================================================================
--- 統計 helper
--- ===========================================================================
-
-mean :: [Double] -> Double
-mean [] = 0
-mean xs = sum xs / fromIntegral (length xs)
-
-stddev :: [Double] -> Double
-stddev xs
-  | length xs < 2 = 1
-  | otherwise =
-      let n = fromIntegral (length xs) :: Double
-          m = mean xs
-      in sqrt (sum [ (x - m) ** 2 | x <- xs ] / (n - 1))
diff --git a/src/Hanalyze/MCMC/Core.hs b/src/Hanalyze/MCMC/Core.hs
deleted file mode 100644
--- a/src/Hanalyze/MCMC/Core.hs
+++ /dev/null
@@ -1,118 +0,0 @@
--- |
--- Module      : Hanalyze.MCMC.Core
--- Description : MCMC 共通の Chain 型と事後統計量 (mean/SD/分位点)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Common MCMC types and posterior statistics.
---
--- Sampler-agnostic: this is the foundation when @MCMC.*@ is used as a
--- standalone sampling library.
-{-# LANGUAGE OverloadedStrings #-}
-module Hanalyze.MCMC.Core
-  ( -- * チェーン型
-    Chain (..)
-    -- * Posterior statistics
-  , acceptanceRate
-  , posteriorMean
-  , posteriorSD
-  , posteriorQuantile
-  , chainVals
-    -- * Utilities
-  , spawnGen
-  ) where
-
-import Data.List (sort)
-import qualified Data.Map.Strict as Map
-import Data.Text (Text)
-import Data.Word (Word32)
-import qualified Data.Vector as V
-import System.Random.MWC (Gen, GenIO, uniform, initialize)
-import Control.Monad.Primitive (PrimMonad, PrimState)
-import Control.DeepSeq (NFData (..))
-
--- ---------------------------------------------------------------------------
--- Chain
--- ---------------------------------------------------------------------------
-
--- | MCMC chain. Holds post-burn-in samples only.
-data Chain = Chain
-  { chainSamples  :: [Map.Map Text Double]  -- ^ Post-burn-in samples in draw order.
-  , chainAccepted :: Int                    -- ^ Accepted proposals (burn-in included).
-  , chainTotal    :: Int                    -- ^ Total proposals (burn-in included).
-  , chainEnergy   :: [Double]
-    -- ^ Hamiltonian energy @H = −log p(θ) + 0.5|p|²@ per post-burn-in
-    --   iteration. Only meaningful for HMC / NUTS; samplers like MH /
-    --   Gibbs leave it empty. Used by BFMI and the energy plot.
-  , chainDivergences :: [Int]
-    -- ^ Zero-origin iteration indices where NUTS reported a divergent
-    --   transition (post-burn-in). Following Stan, the criterion is
-    --   @|H_proposal − H_initial| > 1000@. Many divergences signal a
-    --   pathological posterior that needs reparameterization.
-  , chainTreeDepths :: [Int]
-    -- ^ Phase 85.3: NUTS の per-draw tree depth (実行された doubling 回数・
-    --   post-burn-in・draw 順)。 leapfrog 数 ≈ 2^depth ゆえ per-draw コストの
-    --   診断に使う (PyMC の tree_depth 相当)。 NUTS 以外のサンプラは []。
-  } deriving (Show)
-
--- | Phase 50: 純粋 multi-chain (`nutsChainsPure`) で @parList rdeepseq@ により
--- chain 横断を spark 並列評価するため、 'Chain' を完全評価できるようにする。
-instance NFData Chain where
-  rnf (Chain s a t e d td) =
-    rnf s `seq` rnf a `seq` rnf t `seq` rnf e `seq` rnf d `seq` rnf td
-
--- ---------------------------------------------------------------------------
--- Summary statistics
--- ---------------------------------------------------------------------------
-
--- | Overall acceptance rate (burn-in included).
-acceptanceRate :: Chain -> Double
-acceptanceRate ch =
-  fromIntegral (chainAccepted ch) / fromIntegral (chainTotal ch)
-
--- | Posterior mean for a given parameter, or 'Nothing' if absent.
-posteriorMean :: Text -> Chain -> Maybe Double
-posteriorMean name ch =
-  let vals = chainVals name ch
-  in if null vals then Nothing
-     else Just (sum vals / fromIntegral (length vals))
-
--- | Posterior standard deviation for a given parameter.
-posteriorSD :: Text -> Chain -> Maybe Double
-posteriorSD name ch =
-  case posteriorMean name ch of
-    Nothing -> Nothing
-    Just mu ->
-      let vals = chainVals name ch
-      in if null vals then Nothing
-         else Just (sqrt (sum (map (\x -> (x - mu) ^ (2 :: Int)) vals)
-                         / fromIntegral (length vals)))
-
--- | Empirical quantile of a parameter (@0 ≤ p ≤ 1@).
-posteriorQuantile :: Double -> Text -> Chain -> Maybe Double
-posteriorQuantile p name ch =
-  let vals = sort (chainVals name ch)
-      n    = length vals
-  in if null vals then Nothing
-     else
-       let idx = min (n - 1) (floor (p * fromIntegral n) :: Int)
-       in Just (vals !! idx)
-
--- | Extract the sample sequence for one parameter from a chain. Useful
--- when feeding 'Hanalyze.Stat.MCMC.rhat' and friends.
-chainVals :: Text -> Chain -> [Double]
-chainVals name ch = [v | Just v <- map (Map.lookup name) (chainSamples ch)]
-
--- ---------------------------------------------------------------------------
--- Utility
--- ---------------------------------------------------------------------------
-
--- | Spawn an independent child generator seeded from a parent generator.
--- Used to give each parallel chain a different seed.
---
--- Phase 50: 'PrimMonad' に一般化 (既存 IO 呼出は @m=IO@ で不変)。 これにより
--- @ST s@ でも同じ種まきができ、 純粋な multi-chain (runST + seed) に使える。
-spawnGen :: PrimMonad m => Gen (PrimState m) -> m (Gen (PrimState m))
-spawnGen base = do
-  seed <- uniform base
-  initialize (V.singleton (seed :: Word32))
diff --git a/src/Hanalyze/MCMC/Gibbs.hs b/src/Hanalyze/MCMC/Gibbs.hs
deleted file mode 100644
--- a/src/Hanalyze/MCMC/Gibbs.hs
+++ /dev/null
@@ -1,520 +0,0 @@
--- |
--- Module      : Hanalyze.MCMC.Gibbs
--- Description : 共役事前分布向け Gibbs sampler (解析的フル条件付きサンプリング)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Gibbs sampler — analytic full-conditional sampling for conjugate priors.
---
--- Each 'GibbsUpdate' draws a single parameter directly from its full
--- conditional distribution, so no Metropolis rejection step is needed and
--- every sample is accepted. When non-conjugate parameters are mixed in,
--- combine with Metropolis-Hastings ('gibbsMH').
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-module Hanalyze.MCMC.Gibbs
-  ( -- * 共役アップデートブロック
-    GibbsUpdate
-  , normalNormal
-  , betaBinomial
-  , gammaPoisson
-  , sampleBetaBB
-    -- * Samplers
-  , GibbsConfig (..)
-  , defaultGibbsConfig
-  , gibbs
-  , gibbsBetaBinomial
-  , gibbsChains
-  , gibbsPure
-  , gibbsChainsPure
-  , gibbsBetaBinomialPure
-    -- * HBM-DSL integration: conjugacy auto-detection
-  , gibbsFromModel
-    -- * Hybrid Gibbs+MH sampler
-  , gibbsMH
-  , gibbsMHChains
-  , gibbsMHPure
-  , gibbsMHChainsPure
-  ) where
-
-import Control.Concurrent.Async (mapConcurrently)
-import Control.Monad (foldM, replicateM, when)
-import Control.Monad.Primitive (PrimMonad, PrimState)
-import Control.Monad.ST (runST)
-import Control.Parallel.Strategies (parList, rdeepseq, using)
-import Data.Primitive.MutVar
-import Data.List (nub)
-import Data.Maybe (listToMaybe)
-import Data.Word (Word32)
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import Data.Text (Text)
-import qualified Data.Vector as V
-import qualified Data.Vector.Storable as VS
-import System.Random.MWC (Gen, GenIO, uniform, initialize)
-import System.Random.MWC.Distributions (gamma, normal)
-
-import Hanalyze.MCMC.Core (Chain (..), spawnGen)
-import Hanalyze.Model.HBM (ModelP, Params, Distribution (..),
-                  Node (..), NodeKind (..), collectNodes,
-                  logJoint, runObserveDists, priorList)
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
--- | A Gibbs update block. Receives the current parameter set and returns
--- a single fresh @(name, value)@ sampled from the assigned parameter's
--- full conditional distribution.
---
--- Phase 50: monad パラメタ化 (@m@) で 'IO' でも @ST s@ でも走らせる
--- (純粋 Gibbs に必要)。 rank-N alias にすると @Maybe@/list 構築が impredicative に
--- なるため、 alias を kind @* -> *@ にして関数側 ('gibbsFromModel' 等) を多相にする。
-type GibbsUpdate m = Params -> Gen (PrimState m) -> m (Text, Double)
-
--- ---------------------------------------------------------------------------
--- 共役アップデート (モデル非依存)
--- ---------------------------------------------------------------------------
-
--- | Conjugate update for a Normal prior × Normal likelihood with known
--- @σ@.
-normalNormal
-  :: PrimMonad m => Text -> Double -> Double -> [Double] -> Double -> GibbsUpdate m
-normalNormal paramName mu0 sig0 ys sigLik _ps gen = do
-  let n        = fromIntegral (length ys) :: Double
-      ybar     = if n == 0 then 0 else sum ys / n
-      prec0    = 1 / sig0    ^ (2::Int)
-      precLik  = 1 / sigLik  ^ (2::Int)
-      precPost = prec0 + n * precLik
-      sigPost  = sqrt (1 / precPost)
-      muPost   = (mu0 * prec0 + n * ybar * precLik) / precPost
-  val <- normal muPost sigPost gen
-  return (paramName, val)
-
--- | Conjugate update for a Beta prior × Binomial likelihood.
-betaBinomial
-  :: PrimMonad m => Text -> Double -> Double -> Int -> Int -> GibbsUpdate m
-betaBinomial paramName alpha0 beta0 n k _ps gen = do
-  val <- sampleBeta (alpha0 + fromIntegral k)
-                    (beta0  + fromIntegral (n - k))
-                    gen
-  return (paramName, val)
-
--- | Conjugate update for a Gamma prior × Poisson likelihood
--- (rate parameterization).
-gammaPoisson
-  :: PrimMonad m => Text -> Double -> Double -> [Double] -> GibbsUpdate m
-gammaPoisson paramName alpha0 beta0 ys _ps gen = do
-  let n     = fromIntegral (length ys) :: Double
-      aPost = alpha0 + sum ys
-      bPost = beta0 + n
-  val <- gamma aPost (1 / bPost) gen
-  return (paramName, val)
-
--- | Sample @Beta(a, b)@. Implemented as @X / (X + Y)@ with
--- @X ~ Gamma(a)@, @Y ~ Gamma(b)@, since @mwc-random@ has no Beta sampler.
-sampleBeta :: PrimMonad m => Double -> Double -> Gen (PrimState m) -> m Double
-sampleBeta a b gen
-  | a > 1 && b > 1 = sampleBetaBB a b gen   -- Cheng's BB, much faster
-  | otherwise      = sampleBetaGamma a b gen
-{-# INLINE sampleBeta #-}
-
--- | Generic fallback: @X / (X + Y)@ with @X ~ Gamma(a)@, @Y ~ Gamma(b)@.
--- Used when the Cheng-BB precondition @a, b > 1@ is violated; the BB
--- algorithm's @λ = √((α − 2) / (2 a b − α))@ becomes imaginary at
--- @a, b ≤ 1@ so a different branch (BC) would be required there.
-sampleBetaGamma :: PrimMonad m => Double -> Double -> Gen (PrimState m) -> m Double
-sampleBetaGamma a b gen = do
-  x <- gamma a 1 gen
-  y <- gamma b 1 gen
-  return (x / (x + y))
-
--- | R. C. H. Cheng's BB algorithm (1978), valid for @min(a, b) > 1@.
--- Direct Beta sampler that avoids the two Gamma calls + division
--- ("X / (X+Y)") used by @sampleBetaGamma@.
---
--- P37 (2026-05-07): the n=10000 Gibbs Beta-Binomial bench is 78%
--- @sampleBetaGamma@ (1.38 ms / 1.76 ms total). Each gamma call uses
--- mwc-random's Marsaglia-Tsang squeeze, which needs ~3 uniforms + log
--- + cube on average — heavier than Cheng-BB which is ~1.5 uniforms +
--- log + exp per accepted sample on this regime.
---
--- Reference: Cheng (1978), "Generating Beta variates with non-integral
--- shape parameters", CACM 21(4):317-322. Algorithm BB on p. 319.
-sampleBetaBB :: forall m. PrimMonad m => Double -> Double -> Gen (PrimState m) -> m Double
-sampleBetaBB a b gen = do
-  let !alpha    = a + b
-      !beta_    = sqrt ((alpha - 2) / (2 * a * b - alpha))
-      !gamma_   = a + 1 / beta_
-      !logFour  = log 4
-      !log5     = log 5
-      !logAlpha = log alpha
-  let loop = do
-        u1 <- uniform gen :: m Double
-        u2 <- uniform gen :: m Double
-        let !v = beta_ * log (u1 / (1 - u1))
-            !w = a * exp v
-            !z = u1 * u1 * u2
-            !r = gamma_ * v - logFour
-            !s = a + r - w
-            -- Cheng-BB's three accept tests (numpy randomkit.c):
-            --   Step 4 (squeeze):  s + 1 + log(5) ≥ 5 z
-            --   Step 5a:           s ≥ log z
-            --   Step 5b:           r + α · log(α / (b + w)) ≥ log z
-        if s + 1 + log5 >= 5 * z
-          then return (w / (b + w))
-          else do
-            let !t = log z
-            if s >= t
-              then return (w / (b + w))
-              else if r + alpha * (logAlpha - log (b + w)) >= t
-                     then return (w / (b + w))
-                     else loop
-  loop
-{-# INLINE sampleBetaBB #-}
-
--- ---------------------------------------------------------------------------
--- Gibbs サンプラー (汎用ランナー、モデル非依存)
--- ---------------------------------------------------------------------------
-
--- | Gibbs configuration.
-data GibbsConfig = GibbsConfig
-  { gibbsIterations :: Int   -- ^ Total iterations (burn-in included).
-  , gibbsBurnIn     :: Int   -- ^ Burn-in iterations to discard.
-  } deriving (Show)
-
--- | Default configuration: 2000 iterations, 500 burn-in.
-defaultGibbsConfig :: GibbsConfig
-defaultGibbsConfig = GibbsConfig
-  { gibbsIterations = 2000
-  , gibbsBurnIn     = 500
-  }
-
--- | Apply each update in @updates@ once per iteration, in order. Every
--- Gibbs step is accepted by construction, so @chainAccepted@ equals
--- @(length updates) × iterations@.
-gibbs :: PrimMonad m => [GibbsUpdate m] -> GibbsConfig -> Params -> Gen (PrimState m) -> m Chain
-gibbs updates cfg initP gen = do
-  let total = gibbsBurnIn cfg + gibbsIterations cfg
-      nUpd  = length updates
-  samplesRef  <- newMutVar []
-  acceptedRef <- newMutVar (0 :: Int)
-  let step current = foldM applyOne current updates
-        where
-          applyOne ps upd = do
-            (name, val) <- upd ps gen
-            return (Map.insert name val ps)
-  let loop 0 current = return current
-      loop i current = do
-        next <- step current
-        modifyMutVar' acceptedRef (+ nUpd)
-        when (i <= gibbsIterations cfg) $
-          modifyMutVar' samplesRef (next :)
-        loop (i - 1) next
-  _ <- loop total initP
-  samples  <- fmap reverse (readMutVar samplesRef)
-  accepted <- readMutVar acceptedRef
-  return Chain
-    { chainSamples  = samples
-    , chainAccepted = accepted
-    , chainTotal    = total * nUpd
-    , chainEnergy   = []
-    , chainDivergences = []
-    , chainTreeDepths  = []
-    }
-
--- | Specialised Beta-Binomial conjugate sampler. Equivalent to
--- @gibbs [betaBinomial p a0 b0 n k] cfg (Map.singleton p init) gen@
--- but bypasses the generic loop's per-iteration overhead.
---
--- P37 (2026-05-07): per-iteration profile of the n=10000 bench:
--- @sampleBeta@ itself (two @gamma@ draws + division) is ~80 ns —
--- well over half the 180 ns/iter budget. The remaining ~100 ns is
--- bookkeeping that the generic runner has to do because it doesn't
--- know whether updates depend on @ps@:
---
---   * @Map.insert paramName val ps@ — fresh Map allocation every iter
---     (size-1 Map but still a tree node + Text key + boxed Double)
---   * @modifyMutVar' acceptedRef (+ nUpd)@ — counter that's exactly
---     @total@ at the end (every Gibbs step is unconditionally accepted)
---   * @modifyMutVar' samplesRef (next :)@ + final @reverse@ — list cons
---     of every kept iteration plus a 10000-element reverse
---   * @foldM applyOne current updates@ — closure construction even
---     though the update list has length 1
---
--- For Beta-Binomial in isolation the conjugate posterior is
--- /independent/ of the previous draw, so we replace the entire loop
--- with @VS.replicateM total (sampleBeta postA postB gen)@. The
--- @Params@ Map is constructed only at the chain-construction
--- boundary (lazily, one entry per kept sample), avoiding the per-iter
--- allocation while keeping the public 'Chain' shape intact.
---
--- numpy.random.beta(a, b, size=10000) does the same thing in C with
--- SIMD; this brings the Haskell side to within ~2× of it without FFI.
-gibbsBetaBinomial
-  :: PrimMonad m
-  => Text     -- ^ Parameter name (= sample-Map key).
-  -> Double   -- ^ Beta prior @α@.
-  -> Double   -- ^ Beta prior @β@.
-  -> Int      -- ^ Binomial @n@.
-  -> Int      -- ^ Observed successes @k@.
-  -> GibbsConfig
-  -> Gen (PrimState m)
-  -> m Chain
-gibbsBetaBinomial paramName alpha0 beta0 n k cfg gen = do
-  let !total = gibbsBurnIn cfg + gibbsIterations cfg
-      !keep  = gibbsIterations cfg
-      !postA = alpha0 + fromIntegral k
-      !postB = beta0  + fromIntegral (n - k)
-  -- Storable Vector keeps the n=10000 doubles in 80 KB of contiguous
-  -- memory rather than as a linked list of boxed thunks, and avoids
-  -- the @reverse@ pass at the end of the generic loop.
-  vals <- VS.replicateM total (sampleBeta postA postB gen)
-  let kept    = VS.drop (total - keep) vals
-      samples = [Map.singleton paramName v | v <- VS.toList kept]
-  return Chain
-    { chainSamples     = samples
-    , chainAccepted    = total       -- every Gibbs step is accepted
-    , chainTotal       = total
-    , chainEnergy      = []
-    , chainDivergences = []
-    , chainTreeDepths  = []
-    }
-
--- | Run 'gibbs' on @numChains@ parallel chains.
-gibbsChains :: [GibbsUpdate IO] -> GibbsConfig -> Int -> Params -> GenIO -> IO [Chain]
-gibbsChains updates cfg numChains initP baseGen = do
-  gens <- replicateM numChains (spawnGen baseGen)
-  mapConcurrently (\g -> gibbs updates cfg initP g) gens
-
--- ---------------------------------------------------------------------------
--- HBM DSL 統合: 共役構造の自動検出
--- ---------------------------------------------------------------------------
-
-distParams :: Distribution Double -> [Double]
-distParams (Normal mu sig)    = [mu, sig]
-distParams (Binomial n p)     = [fromIntegral n, p]
-distParams (Poisson lam)      = [lam]
-distParams (Exponential r)    = [r]
-distParams (Gamma a b)        = [a, b]
-distParams (Beta a b)         = [a, b]
-distParams (Uniform lo hi)    = [lo, hi]
-distParams (StudentT df mu s) = [df, mu, s]
-distParams (Cauchy loc s)     = [loc, s]
-distParams (HalfNormal s)     = [s]
-distParams (HalfCauchy s)     = [s]
-distParams (LogNormal mu s)   = [mu, s]
-distParams (Bernoulli p)      = [p]
-distParams (Categorical ps)   = ps
-distParams (Mixture ws _)     = ws  -- 共役検出には使えない (重みのみ)
-distParams (Truncated _ _ _)  = []  -- 共役検出対象外
-distParams (Censored  _ _ _)  = []  -- 共役検出対象外
-distParams MvNormal{}         = []  -- 共役検出対象外 (観測専用)
-distParams MvNormalChol{}     = []  -- 共役検出対象外 (観測専用)
-distParams (NegativeBinomial mu a) = [mu, a]
-distParams (Multinomial _ ps)      = ps
-distParams (ZeroInflatedPoisson psi lam)  = [psi, lam]
-distParams (ZeroInflatedBinomial _ psi p) = [psi, p]
-distParams (InverseGamma a b)             = [a, b]
-distParams (Weibull k l)                  = [k, l]
-distParams (Pareto a xm)                  = [a, xm]
-distParams (BetaBinomial _ a b)           = [a, b]
-distParams (VonMises mu k)                = [mu, k]
-
--- 各潜在変数が Observe ノードのどの (obsIndex, slotIndex) に影響するかを検出。
-detectObsDeps :: ModelP r -> [Text] -> Map Text [(Int, Int)]
-detectObsDeps m latNames =
-  let baseline = map (\(_, d, _) -> distParams d) (runObserveDists m Map.empty)
-      perturb v = map (\(_, d, _) -> distParams d)
-                      (runObserveDists m (Map.singleton v 1.0))
-  in Map.fromList
-      [ (v, nub
-              [ (oi, si)
-              | let pp = perturb v
-              , (oi, (bp, pp')) <- zip [0..] (zip baseline pp)
-              , (si, (bv, pv))  <- zip [0..] (zip bp pp')
-              , bv /= pv
-              ])
-      | v <- latNames
-      ]
-
--- | Inspect an HBM model's structure and synthesise the conjugate
--- 'GibbsUpdate' steps automatically.
---
--- Detected conjugate pairs:
---
---   * @Gamma(α,β)@   + @Poisson(λ)@    → 'gammaPoisson'
---   * @Beta(α,β)@    + @Binomial(n,p)@ → 'betaBinomial'
---   * @Normal(μ₀,σ₀)@ + @Normal(μ,σ)@  → 'normalNormal'
---
--- Returns @(updates, remaining)@: the synthesised updates and the names
--- of parameters that still need an MH step.
-gibbsFromModel :: forall r m. PrimMonad m => ModelP r -> ([GibbsUpdate m], [Text])
-gibbsFromModel m =
-  let nodes    = collectNodes m
-      latNames = [ nodeName n | n <- nodes, nodeKind n == LatentN ]
-      priorMap = Map.fromList (priorList m)
-      obsList  = runObserveDists m Map.empty
-      indexedObs = zip [0 :: Int ..] obsList
-      deps     = detectObsDeps m latNames
-
-      obsAt i = listToMaybe [ (d, xs) | (j, (_, d, xs)) <- indexedObs, i == j ]
-
-      buildUpd v =
-        let priorD = Map.findWithDefault (Normal 0 1) v priorMap
-            vDeps  = Map.findWithDefault [] v deps
-        in case (priorD, vDeps) of
-          (Gamma a b, [(obsIdx, 0)]) ->
-            case obsAt obsIdx of
-              Just (Poisson _, xs) -> Just (gammaPoisson v a b xs)
-              _                    -> Nothing
-
-          (Beta a b, [(obsIdx, 1)]) ->
-            case obsAt obsIdx of
-              Just (Binomial nPerObs _, xs) ->
-                let k = round (sum xs) :: Int
-                    n = nPerObs * length xs
-                in Just (betaBinomial v a b n k)
-              _ -> Nothing
-
-          (Normal mu0 sig0, [(obsIdx, 0)]) ->
-            case obsAt obsIdx of
-              Just (Normal _ _, xs) ->
-                let sigmaVar = listToMaybe
-                      [ w | (w, wDeps) <- Map.toList deps
-                      , any (\(oi, si) -> oi == obsIdx && si == 1) wDeps
-                      , w /= v
-                      ]
-                in Just $ \ps gen ->
-                  let sigLik = maybe 1.0 (\sv -> Map.findWithDefault 1.0 sv ps) sigmaVar
-                  in normalNormal v mu0 sig0 xs sigLik ps gen
-              _ -> Nothing
-
-          _ -> Nothing
-
-      results   = map buildUpd latNames
-      updates   = [ u | Just u  <- results ]
-      remaining = [ v | (v, Nothing) <- zip latNames results ]
-  in (updates, remaining)
-
--- ---------------------------------------------------------------------------
--- ハイブリッド Gibbs+MH
--- ---------------------------------------------------------------------------
-
-hybridStep
-  :: PrimMonad m
-  => [GibbsUpdate m]
-  -> [Text]
-  -> Map Text Double
-  -> ModelP r
-  -> Params -> Gen (PrimState m)
-  -> m (Params, Bool)
-hybridStep gibbsUpds mhNames mhSteps model current gen = do
-  afterGibbs <- foldM (\ps upd -> do
-    (name, val) <- upd ps gen
-    return (Map.insert name val ps)) current gibbsUpds
-  if null mhNames
-    then return (afterGibbs, True)
-    else do
-      proposed <- foldM (\ps n -> do
-        let s  = Map.findWithDefault 1.0 n mhSteps
-            cv = Map.findWithDefault 0.0 n ps
-        eps <- normal 0 s gen
-        return (Map.insert n (cv + eps) ps)) afterGibbs mhNames
-      let logA = logJoint model proposed - logJoint model afterGibbs
-      u <- uniform gen
-      let accepted = log (u :: Double) < logA
-      return (if accepted then proposed else afterGibbs, accepted)
-
--- | Hybrid sampler: Gibbs-update conjugate parameters and use Random-Walk
--- Metropolis on the rest.
-gibbsMH
-  :: PrimMonad m
-  => ModelP r
-  -> GibbsConfig
-  -> Map Text Double   -- ^ MH step size per non-conjugate parameter.
-  -> Params
-  -> Gen (PrimState m)
-  -> m Chain
-gibbsMH model cfg mhSteps initP gen = do
-  let (gibbsUpds, mhNames) = gibbsFromModel model
-      total = gibbsBurnIn cfg + gibbsIterations cfg
-  samplesRef  <- newMutVar []
-  acceptedRef <- newMutVar (0 :: Int)
-  let loop 0 current = return current
-      loop i current = do
-        (next, acc) <- hybridStep gibbsUpds mhNames mhSteps model current gen
-        when acc $ modifyMutVar' acceptedRef (+1)
-        when (i <= gibbsIterations cfg) $
-          modifyMutVar' samplesRef (next :)
-        loop (i - 1) next
-  _ <- loop total initP
-  samples  <- fmap reverse (readMutVar samplesRef)
-  accepted <- readMutVar acceptedRef
-  return Chain
-    { chainSamples  = samples
-    , chainAccepted = accepted
-    , chainTotal    = total
-    , chainEnergy   = []
-    , chainDivergences = []
-    , chainTreeDepths  = []
-    }
-
-gibbsMHChains
-  :: ModelP r
-  -> GibbsConfig
-  -> Map Text Double
-  -> Int
-  -> Params
-  -> GenIO
-  -> IO [Chain]
-gibbsMHChains model cfg mhSteps numChains initP baseGen = do
-  gens <- replicateM numChains (spawnGen baseGen)
-  mapConcurrently (\g -> gibbsMH model cfg mhSteps initP g) gens
-
--- ---------------------------------------------------------------------------
--- Phase 50: 純粋 (ST + seed) ラッパ
--- ---------------------------------------------------------------------------
-
--- | 純粋・決定的な hybrid Gibbs+MH (モデルから共役 update を内部導出)。 seed → 確定 Chain。
-gibbsMHPure :: ModelP r -> GibbsConfig -> Map Text Double -> Params -> Word32 -> Chain
-gibbsMHPure model cfg mhSteps initP seed =
-  runST (initialize (V.singleton seed) >>= gibbsMH model cfg mhSteps initP)
-
--- | 純粋・決定的な multi-chain hybrid Gibbs+MH。 子 seed を純粋導出し @parList rdeepseq@ で並列。
-gibbsMHChainsPure :: ModelP r -> GibbsConfig -> Map Text Double -> Int -> Params -> Word32 -> [Chain]
-gibbsMHChainsPure model cfg mhSteps numChains initP seed =
-  let childSeeds :: [Word32]
-      childSeeds = runST $ do
-        g <- initialize (V.singleton seed)
-        replicateM numChains (uniform g)
-      chains = [ gibbsMHPure model cfg mhSteps initP s | s <- childSeeds ]
-  in chains `using` parList rdeepseq
-
--- | 純粋・決定的な Beta-Binomial 共役 Gibbs (seed → 確定 Chain)。
-gibbsBetaBinomialPure
-  :: Text -> Double -> Double -> Int -> Int -> GibbsConfig -> Word32 -> Chain
-gibbsBetaBinomialPure paramName alpha0 beta0 n k cfg seed =
-  runST (initialize (V.singleton seed)
-           >>= gibbsBetaBinomial paramName alpha0 beta0 n k cfg)
-
--- | 純粋・決定的な汎用 Gibbs (seed → 確定 Chain)。 update 群は **rank-N**
--- (@forall m. PrimMonad m => [GibbsUpdate m]@) で渡す = リストリテラルを直接渡せば
--- 多相のまま通る (@let updates = …@ で束縛すると単相化するので注意・直接渡しが楽)。
-gibbsPure
-  :: (forall m. PrimMonad m => [GibbsUpdate m])
-  -> GibbsConfig -> Params -> Word32 -> Chain
-gibbsPure updates cfg initP seed =
-  runST (initialize (V.singleton seed) >>= gibbs updates cfg initP)
-
--- | 純粋・決定的な汎用 multi-chain Gibbs。 子 seed を純粋導出し @parList rdeepseq@ で並列。
-gibbsChainsPure
-  :: (forall m. PrimMonad m => [GibbsUpdate m])
-  -> GibbsConfig -> Int -> Params -> Word32 -> [Chain]
-gibbsChainsPure updates cfg numChains initP seed =
-  let childSeeds :: [Word32]
-      childSeeds = runST $ do
-        g <- initialize (V.singleton seed)
-        replicateM numChains (uniform g)
-      chains = [ gibbsPure updates cfg initP s | s <- childSeeds ]
-  in chains `using` parList rdeepseq
diff --git a/src/Hanalyze/MCMC/HMC.hs b/src/Hanalyze/MCMC/HMC.hs
deleted file mode 100644
--- a/src/Hanalyze/MCMC/HMC.hs
+++ /dev/null
@@ -1,337 +0,0 @@
--- |
--- Module      : Hanalyze.MCMC.HMC
--- Description : Hamiltonian Monte Carlo (HMC) サンプラー
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Hamiltonian Monte Carlo (HMC) sampler.
---
--- Computes exact gradients of polymorphic 'Hanalyze.Model.HBM' models ('ModelP') via
--- 'Numeric.AD.Mode.Reverse.Double' (Phase 53). Constrained parameters (@PositiveT@,
--- @UnitIntervalT@) are detected automatically from the prior distribution.
---
--- @
--- import Hanalyze.Model.HBM
--- import Hanalyze.MCMC.HMC
---
--- myModel :: ModelP ()
--- myModel = do
---   mu    <- sample "mu"    (Normal 0 10)
---   sigma <- sample "sigma" (Exponential 1)
---   observe "y" (Normal mu sigma) [1.5, 2.0, 1.8]
---
--- chain <- hmc myModel defaultHMCConfig (Map.fromList [("mu",0),("sigma",1)]) gen
--- @
-{-# LANGUAGE StrictData #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-module Hanalyze.MCMC.HMC
-  ( -- * Configuration
-    HMCConfig (..)
-  , defaultHMCConfig
-    -- * Constraint-transform helpers
-  , toUnconstrainedParams
-  , fromUnconstrainedParams
-  , logJointU
-  , leapfrogWith
-  , leapfrogWithM
-  , leapfrogWithMVS
-    -- * Basic utilities
-  , kinetic
-  , kineticM
-  , kineticMVS
-  , paramsToVec
-  , vecToParams
-    -- * Sampler
-  , hmc
-  , hmcChains
-  , hmcPure
-  , hmcChainsPure
-  ) where
-
-import Control.Concurrent.Async (mapConcurrently)
-import Control.Monad (forM, replicateM, when)
-import Control.Monad.Primitive (PrimMonad, PrimState)
-import Control.Monad.ST (runST)
-import Control.Parallel.Strategies (parList, rdeepseq, using)
-import Data.Primitive.MutVar
-import Data.Word (Word32)
-import qualified Data.Vector as V
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import Data.Text (Text)
-import qualified Data.Vector.Storable         as VS
-import System.Random.MWC (Gen, GenIO, uniform, initialize)
-import System.Random.MWC.Distributions (standard)
-
-import Hanalyze.Model.HBM (ModelP, Params, sampleNames, getTransforms,
-                  logJointUnconstrained, gradADU)
-import Hanalyze.MCMC.Core (Chain (..), spawnGen)
-import Hanalyze.Stat.Distribution (Transform, toUnconstrained, fromUnconstrained)
-
--- ---------------------------------------------------------------------------
--- Configuration
--- ---------------------------------------------------------------------------
-
--- | HMC configuration.
-data HMCConfig = HMCConfig
-  { hmcIterations    :: Int     -- ^ Total iterations (burn-in included).
-  , hmcBurnIn        :: Int     -- ^ Burn-in iterations to discard.
-  , hmcStepSize      :: Double  -- ^ Leapfrog step size @ε@.
-  , hmcLeapfrogSteps :: Int     -- ^ Number of leapfrog steps per HMC iteration.
-  } deriving (Show)
-
--- | Default HMC configuration: 2000 iterations, 500 burn-in,
--- @ε = 0.1@, 10 leapfrog steps.
-defaultHMCConfig :: HMCConfig
-defaultHMCConfig = HMCConfig
-  { hmcIterations    = 2000
-  , hmcBurnIn        = 500
-  , hmcStepSize      = 0.1
-  , hmcLeapfrogSteps = 10
-  }
-
--- ---------------------------------------------------------------------------
--- パラメータ変換ユーティリティ
--- ---------------------------------------------------------------------------
-
--- | Pack parameters into a flat vector in the given name order.
-paramsToVec :: [Text] -> Params -> [Double]
-paramsToVec names params = map (\n -> Map.findWithDefault 0.0 n params) names
-
--- | Inverse of 'paramsToVec': pair names with values.
-vecToParams :: [Text] -> [Double] -> Params
-vecToParams names vals = Map.fromList (zip names vals)
-
--- | Apply 'toUnconstrained' to every named parameter; unmapped names are
--- left untouched.
-toUnconstrainedParams :: Map Text Transform -> Params -> Params
-toUnconstrainedParams transforms =
-  Map.mapWithKey (\k v -> maybe v (`toUnconstrained` v) (Map.lookup k transforms))
-
--- | Apply 'fromUnconstrained' to every named parameter.
-fromUnconstrainedParams :: Map Text Transform -> Params -> Params
-fromUnconstrainedParams transforms =
-  Map.mapWithKey (\k u -> maybe u (`fromUnconstrained` u) (Map.lookup k transforms))
-
--- ---------------------------------------------------------------------------
--- unconstrained 空間での log-joint (Jacobian 補正付き)
--- ---------------------------------------------------------------------------
-
--- | Log-joint of a polymorphic model in the unconstrained space (shared
--- with VI and NUTS).
-logJointU :: ModelP r -> Map Text Transform -> Params -> Double
-logJointU model transforms paramsU =
-  let names     = sampleNames model
-      transList = [Map.findWithDefault errT n transforms | n <- names]
-      errT      = error "logJointU: transform missing"
-  in logJointUnconstrained model names transList paramsU
-
--- ---------------------------------------------------------------------------
--- リープフロッグ積分
--- ---------------------------------------------------------------------------
-
--- | Kinetic energy @0.5 ‖r‖²@ for unit-mass momentum @r@.
-kinetic :: [Double] -> Double
-kinetic r = 0.5 * sum (map (^ (2 :: Int)) r)
-
--- | Kinetic energy with a diagonal mass matrix:
--- @½ rᵀ M⁻¹ r = ½ Σ M⁻¹_ii · r_i²@.
---
--- Used by NUTS (B11) when running with diagonal mass-matrix adaptation.
--- @kinetic = kineticM (repeat 1)@ recovers the identity-mass case.
-kineticM :: [Double] -> [Double] -> Double
-kineticM mInv r = 0.5 * sum (zipWith (\m_inv ri -> m_inv * ri * ri) mInv r)
-
--- | Storable-Vector variant of 'kineticM'.
-kineticMVS :: VS.Vector Double -> VS.Vector Double -> Double
-kineticMVS mInv r =
-  0.5 * VS.sum (VS.zipWith (\m_inv ri -> m_inv * ri * ri) mInv r)
-{-# INLINE kineticMVS #-}
-
--- | Leapfrog integrator with a user-supplied gradient function. Takes
--- the gradient function, parameter names, step size @ε@, number of
--- steps, initial @θ@ and momentum @r@, and returns the updated pair.
-leapfrogWith
-  :: ([Text] -> Params -> [Double])
-  -> [Text]
-  -> Double
-  -> Int
-  -> Params
-  -> [Double]
-  -> (Params, [Double])
-leapfrogWith gradFn names eps steps theta0 r0 = go steps theta0 r0
-  where
-    go 0 theta r = (theta, r)
-    go n theta r =
-      let g      = gradFn names theta
-          rHalf  = zipWith (\ri gi -> ri - (eps / 2) * gi) r g
-          tVec'  = zipWith (\ti ri -> ti + eps * ri) (paramsToVec names theta) rHalf
-          theta' = vecToParams names tVec'
-          g'     = gradFn names theta'
-          r'     = zipWith (\ri gi -> ri - (eps / 2) * gi) rHalf g'
-      in go (n - 1) theta' r'
-
--- | Leapfrog integrator with a diagonal mass matrix.
---
---   * Position update: @θ' = θ + ε · M⁻¹ · r@ (so smaller @M_ii@
---     ⇒ slower per-step move along that coordinate, matching the
---     intent that posterior-narrow directions get smaller steps).
---   * Momentum update: @r' = r − (ε/2) · ∇U(θ)@ (unchanged).
---
--- @leapfrogWith = leapfrogWithM (repeat 1)@.
-leapfrogWithM
-  :: ([Text] -> Params -> [Double])
-  -> [Text]
-  -> [Double]                      -- ^ Diagonal @M⁻¹@ (length = number of params).
-  -> Double                        -- ^ Step size @ε@.
-  -> Int                           -- ^ Number of leapfrog steps.
-  -> Params
-  -> [Double]
-  -> (Params, [Double])
-leapfrogWithM gradFn names mInv eps steps theta0 r0 = go steps theta0 r0
-  where
-    go 0 theta r = (theta, r)
-    go n theta r =
-      let g      = gradFn names theta
-          rHalf  = zipWith (\ri gi -> ri - (eps / 2) * gi) r g
-          -- θ' = θ + ε · M⁻¹ · r
-          tVec'  = zipWith3 (\ti m_inv ri -> ti + eps * m_inv * ri)
-                            (paramsToVec names theta) mInv rHalf
-          theta' = vecToParams names tVec'
-          g'     = gradFn names theta'
-          r'     = zipWith (\ri gi -> ri - (eps / 2) * gi) rHalf g'
-      in go (n - 1) theta' r'
-
--- | Storable-Vector–native variant of 'leapfrogWithM'. Position,
--- momentum, gradient, and the diagonal @M⁻¹@ all live on
--- @VS.Vector Double@ throughout the integration; no @Map@ or
--- @[Double]@ traversal occurs in the inner loop.
---
--- Used by 'Hanalyze.MCMC.NUTS' where each leapfrog step is invoked up to
--- @2¹⁰@ times per iteration: the previous form went
--- @[Double] → Map → [Double]@ at every step (per-name @Map.lookup@
--- ×p plus list cell allocation for @zipWith3@), which dominated the
--- profile after the algorithmic improvements were in place.
-leapfrogWithMVS
-  :: (VS.Vector Double -> VS.Vector Double)   -- ^ Gradient (Vector → Vector).
-  -> VS.Vector Double                         -- ^ Diagonal @M⁻¹@.
-  -> Double                                   -- ^ Step size @ε@.
-  -> Int                                      -- ^ Steps.
-  -> VS.Vector Double                         -- ^ Initial @θ@.
-  -> VS.Vector Double                         -- ^ Initial @r@.
-  -> (VS.Vector Double, VS.Vector Double)
-leapfrogWithMVS gradFn mInv eps steps theta0 r0 = go steps theta0 r0
-  where
-    !halfEps = eps * 0.5
-    -- Phase 54.7b: SCC で勾配呼出 (= compiled カーネル) と VS 更新を分離計測。
-    go !n theta r
-      | n <= 0    = (theta, r)
-      | otherwise =
-          let g      = {-# SCC "leapfrog_grad1" #-} gradFn theta
-              rHalf  = {-# SCC "leapfrog_vec_rhalf" #-}
-                       VS.zipWith (\ri gi -> ri - halfEps * gi) r g
-              theta' = {-# SCC "leapfrog_vec_pos" #-}
-                       VS.zipWith3 (\ti m_inv ri -> ti + eps * m_inv * ri)
-                                   theta mInv rHalf
-              g'     = {-# SCC "leapfrog_grad2" #-} gradFn theta'
-              r'     = {-# SCC "leapfrog_vec_rfull" #-}
-                       VS.zipWith (\ri gi -> ri - halfEps * gi) rHalf g'
-          in go (n - 1) theta' r'
-
--- ---------------------------------------------------------------------------
--- HMC サンプラー (AD 勾配版)
--- ---------------------------------------------------------------------------
-
--- | HMC sampler for a polymorphic HBM model ('ModelP').
---
--- Uses AD gradients ('Numeric.AD.Mode.Reverse.Double'), so it is more accurate
--- and faster than numeric differentiation. Constraint transforms are
--- detected automatically from the priors via 'getTransforms'.
-hmc :: PrimMonad m => ModelP r -> HMCConfig -> Params -> Gen (PrimState m) -> m Chain
-hmc m cfg initC gen = do
-  let names      = sampleNames m
-      trMap      = getTransforms m
-      transList  = [Map.findWithDefault errT n trMap | n <- names]
-      errT       = error "hmc: missing transform (should not happen)"
-
-      initU = Map.fromList
-        [ (n, toUnconstrained t v)
-        | (n, t) <- zip names transList
-        , Just v <- [Map.lookup n initC] ]
-
-      total = hmcBurnIn cfg + hmcIterations cfg
-
-      logJU :: Params -> Double
-      logJU paramsU = logJointUnconstrained m names transList paramsU
-
-      gradFn :: [Text] -> Params -> [Double]
-      gradFn ns paramsU =
-        let xs = [Map.findWithDefault 0 n paramsU | n <- ns]
-        in map negate (gradADU m names transList xs)
-
-  samplesRef  <- newMutVar []
-  energyRef   <- newMutVar ([] :: [Double])
-  acceptedRef <- newMutVar (0 :: Int)
-
-  let step currentU = do
-        r <- forM names (\_ -> standard gen)
-        let h0 = -(logJU currentU) + kinetic r
-            (proposedU, rFinal) =
-              leapfrogWith gradFn names
-                           (hmcStepSize cfg) (hmcLeapfrogSteps cfg)
-                           currentU r
-            logAlpha = (logJU proposedU - kinetic rFinal)
-                     - (logJU currentU  - kinetic r)
-        u <- uniform gen
-        nextU <- if log (u :: Double) < logAlpha
-          then do modifyMutVar' acceptedRef (+1); return proposedU
-          else return currentU
-        return (nextU, h0)
-
-  let toConstrained pu = Map.fromList
-        [ (n, fromUnconstrained t (Map.findWithDefault 0 n pu))
-        | (n, t) <- zip names transList ]
-
-  let loop 0 currentU = return currentU
-      loop i currentU = do
-        (nextU, h0) <- step currentU
-        when (i <= hmcIterations cfg) $ do
-          modifyMutVar' samplesRef (toConstrained nextU :)
-          modifyMutVar' energyRef  (h0 :)
-        loop (i - 1) nextU
-
-  _ <- loop total initU
-  samples  <- fmap reverse (readMutVar samplesRef)
-  energies <- fmap reverse (readMutVar energyRef)
-  accepted <- readMutVar acceptedRef
-  return Chain
-    { chainSamples  = samples
-    , chainAccepted = accepted
-    , chainTotal    = total
-    , chainEnergy   = energies
-    , chainDivergences = []
-    , chainTreeDepths  = []
-    }
-
--- | Run 'hmc' on @numChains@ parallel chains (use @+RTS -N@ for CPU
--- parallelism).
-hmcChains :: ModelP r -> HMCConfig -> Int -> Params -> GenIO -> IO [Chain]
-hmcChains m cfg numChains initC baseGen = do
-  gens <- replicateM numChains (spawnGen baseGen)
-  mapConcurrently (\g -> hmc m cfg initC g) gens
-
--- | Phase 50: 純粋・決定的な HMC (seed → 確定 Chain)。
-hmcPure :: ModelP r -> HMCConfig -> Params -> Word32 -> Chain
-hmcPure m cfg initC seed =
-  runST (initialize (V.singleton seed) >>= hmc m cfg initC)
-
--- | Phase 50: 純粋・決定的な multi-chain HMC。 子 seed を純粋導出し @parList rdeepseq@ で並列。
-hmcChainsPure :: ModelP r -> HMCConfig -> Int -> Params -> Word32 -> [Chain]
-hmcChainsPure m cfg numChains initC seed =
-  let childSeeds :: [Word32]
-      childSeeds = runST $ do
-        g <- initialize (V.singleton seed)
-        replicateM numChains (uniform g)
-      chains = [ hmcPure m cfg initC s | s <- childSeeds ]
-  in chains `using` parList rdeepseq
diff --git a/src/Hanalyze/MCMC/MH.hs b/src/Hanalyze/MCMC/MH.hs
deleted file mode 100644
--- a/src/Hanalyze/MCMC/MH.hs
+++ /dev/null
@@ -1,128 +0,0 @@
--- |
--- Module      : Hanalyze.MCMC.MH
--- Description : Random-Walk Metropolis-Hastings サンプラー
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Random-Walk Metropolis-Hastings sampler.
---
--- Tune the per-parameter step sizes ('mcmcStepSizes') so the acceptance rate
--- lands in the 20-50% range. Pair 'Hanalyze.MCMC.Core.Chain' with
--- 'Hanalyze.Viz.Report.renderReport' to produce diagnostic plots.
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-module Hanalyze.MCMC.MH
-  ( MCMCConfig (..)
-  , defaultMCMCConfig
-  , metropolis
-  , metropolisChains
-  , metropolisPure
-  , metropolisChainsPure
-  ) where
-
-import Control.Concurrent.Async (mapConcurrently)
-import Control.Monad (forM, replicateM)
-import Control.Monad.Primitive (PrimMonad, PrimState)
-import Control.Monad.ST (runST)
-import Control.Parallel.Strategies (parList, rdeepseq, using)
-import Data.Primitive.MutVar
-import Data.Word (Word32)
-import qualified Data.Map.Strict as Map
-import qualified Data.Vector as V
-import Data.Text (Text)
-import System.Random.MWC (Gen, GenIO, uniform, initialize)
-import System.Random.MWC.Distributions (normal)
-
-import Hanalyze.Model.HBM (ModelP, Params, logJoint, sampleNames)
-import Hanalyze.MCMC.Core (Chain (..), spawnGen)
-
--- ---------------------------------------------------------------------------
--- Configuration
--- ---------------------------------------------------------------------------
-
--- | Random-Walk Metropolis configuration.
-data MCMCConfig = MCMCConfig
-  { mcmcIterations :: Int                   -- ^ Total iterations (burn-in included).
-  , mcmcBurnIn     :: Int                   -- ^ Burn-in iterations to discard.
-  , mcmcStepSizes  :: Map.Map Text Double   -- ^ Per-parameter proposal step.
-  } deriving (Show)
-
--- | Default configuration: 2000 iterations, 500 burn-in, step size 1.0
--- for every parameter.
-defaultMCMCConfig :: [Text] -> MCMCConfig
-defaultMCMCConfig names = MCMCConfig
-  { mcmcIterations = 2000
-  , mcmcBurnIn     = 500
-  , mcmcStepSizes  = Map.fromList [(n, 1.0) | n <- names]
-  }
-
--- ---------------------------------------------------------------------------
--- Random Walk Metropolis
--- ---------------------------------------------------------------------------
-
--- | Run Random-Walk Metropolis. Uses a joint proposal that updates all
--- latent variables simultaneously.
-metropolis :: PrimMonad m => ModelP r -> MCMCConfig -> Params -> Gen (PrimState m) -> m Chain
-metropolis model cfg init_ gen = do
-  let names = sampleNames model
-      total = mcmcBurnIn cfg + mcmcIterations cfg
-      steps = mcmcStepSizes cfg
-
-  samplesRef  <- newMutVar []
-  acceptedRef <- newMutVar (0 :: Int)
-
-  let step current = do
-        proposed <- fmap Map.fromList $ forM names $ \n -> do
-          let s   = Map.findWithDefault 1.0 n steps
-              cur = Map.findWithDefault 0.0 n current
-          eps <- normal 0 s gen
-          return (n, cur + eps)
-        let logA = logJoint model proposed - logJoint model current
-        u <- uniform gen
-        if log (u :: Double) < logA
-          then do modifyMutVar' acceptedRef (+1)
-                  return proposed
-          else return current
-
-  let loop 0 current = return current
-      loop i current = do
-        next <- step current
-        if i <= mcmcIterations cfg
-          then modifyMutVar' samplesRef (next :)
-          else return ()
-        loop (i - 1) next
-
-  _ <- loop total init_
-  samples  <- fmap reverse (readMutVar samplesRef)
-  accepted <- readMutVar acceptedRef
-  return Chain
-    { chainSamples  = samples
-    , chainAccepted = accepted
-    , chainTotal    = total
-    , chainEnergy   = []
-    , chainDivergences = []
-    , chainTreeDepths  = []
-    }
-
--- | Run 'metropolis' on @numChains@ parallel chains, each with an
--- independent RNG (use @+RTS -N@ to run on multiple cores).
-metropolisChains :: ModelP r -> MCMCConfig -> Int -> Params -> GenIO -> IO [Chain]
-metropolisChains model cfg numChains initP baseGen = do
-  gens <- replicateM numChains (spawnGen baseGen)
-  mapConcurrently (\g -> metropolis model cfg initP g) gens
-
--- | Phase 50: 純粋・決定的な Metropolis (seed → 確定 Chain・IO 不要)。
-metropolisPure :: ModelP r -> MCMCConfig -> Params -> Word32 -> Chain
-metropolisPure model cfg initP seed =
-  runST (initialize (V.singleton seed) >>= metropolis model cfg initP)
-
--- | Phase 50: 純粋・決定的な multi-chain Metropolis。 親 seed から子 seed を純粋導出し
--- 各 chain 別 'runST' → @parList rdeepseq@ で chain 横断を並列評価 (決定性は seed 由来)。
-metropolisChainsPure :: ModelP r -> MCMCConfig -> Int -> Params -> Word32 -> [Chain]
-metropolisChainsPure model cfg numChains initP seed =
-  let childSeeds :: [Word32]
-      childSeeds = runST $ do
-        g <- initialize (V.singleton seed)
-        replicateM numChains (uniform g)
-      chains = [ metropolisPure model cfg initP s | s <- childSeeds ]
-  in chains `using` parList rdeepseq
diff --git a/src/Hanalyze/MCMC/NUTS.hs b/src/Hanalyze/MCMC/NUTS.hs
deleted file mode 100644
--- a/src/Hanalyze/MCMC/NUTS.hs
+++ /dev/null
@@ -1,846 +0,0 @@
--- |
--- Module      : Hanalyze.MCMC.NUTS
--- Description : No-U-Turn Sampler (NUTS) — Hoffman & Gelman (2014) Algorithm 3 実装
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- No-U-Turn Sampler (NUTS).
---
--- Implements Hoffman & Gelman (2014) Algorithm 3, with Nesterov dual
--- averaging for step-size adaptation (Stan's strategy). Gradients are
--- exact, computed via 'Numeric.AD.Mode.Reverse.Double' (Phase 53: reverse モードで
--- 勾配を latent 数非依存の ~1 sweep に。 旧 forward は O(p) だった).
---
--- Constrained parameters (@PositiveT@, @UnitIntervalT@) are detected
--- automatically from the prior distribution.
---
--- @
--- import Hanalyze.Model.HBM
--- import Hanalyze.MCMC.NUTS
---
--- chain <- nuts myModel defaultNUTSConfig
---                (Map.fromList [("mu",0),("sigma",1)]) gen
--- @
-{-# LANGUAGE StrictData #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-module Hanalyze.MCMC.NUTS
-  ( NUTSConfig (..)
-  , defaultNUTSConfig
-  , nuts
-  , nutsStream
-  , nutsChains
-  , nutsPure
-  , nutsChainsPure
-  , nutsChainsStream
-  , chainSeeds
-  , SampleEvent (..)
-  ) where
-
-import Control.Concurrent.Async (mapConcurrently)
-import Control.Monad (foldM, replicateM, when)
-import Control.Monad.ST (ST, runST)
-import Control.Parallel.Strategies (parList, rdeepseq, using)
-import Data.Primitive.MutVar
-import Data.Word (Word32)
-import qualified Data.Map.Strict as Map
-import qualified Data.Vector as V
-import qualified Data.Vector.Storable as VS
-import System.Random.MWC (Gen, GenIO, uniform, initialize)
-import Control.Monad.Primitive (PrimMonad, PrimState, RealWorld)
-import System.Random.MWC.Distributions (standard)
-
-import Hanalyze.MCMC.Core (Chain (..), spawnGen)
-import Hanalyze.MCMC.HMC  (kineticMVS, leapfrogWithMVS)
-import Hanalyze.Model.HBM (ModelP, Params, sampleNames, getTransforms,
-                  compileGradUV, compileGradValUVM, compileLogPUV)
-import Hanalyze.Stat.Distribution (toUnconstrained, fromUnconstrained)
-
--- ---------------------------------------------------------------------------
--- Configuration
--- ---------------------------------------------------------------------------
-
--- | NUTS configuration.
-data NUTSConfig = NUTSConfig
-  { nutsIterations    :: Int     -- ^ Post-burn-in draws to keep (the loop runs
-                                 --   @nutsBurnIn + nutsIterations@ total).
-  , nutsBurnIn        :: Int     -- ^ Burn-in iterations to discard.
-  , nutsStepSize      :: Double  -- ^ Initial leapfrog step size @ε@.
-  , nutsMaxDepth      :: Int     -- ^ Maximum tree depth (typically 10).
-  , nutsAdaptStepSize :: Bool    -- ^ Enable Nesterov dual-averaging step-size adaptation.
-  , nutsTargetAccept  :: Double  -- ^ Target acceptance rate (0.8 typical, 0.95 for hard problems).
-  , nutsWarmupInitMaxDepth :: Maybe Int
-                                 -- ^ Phase 85.6: 質量行列の**初回更新前** (init
-                                 --   buffer + 第 1 window・M=I 期間) に適用する
-                                 --   tree depth 上限 (opt-in・既定 'Nothing' =
-                                 --   無効)。 M=I では幾何が合わず dual averaging
-                                 --   の ε 鋸歯で depth 7-10 の木を掘り radon 実測
-                                 --   で warmup leapfrog の 68% を浪費するため、
-                                 --   'Just 6' 等で抑制できる。 ただし参照実装
-                                 --   (Stan/PyMC) に無いヒューリスティックゆえ
-                                 --   既定 OFF — 原理側の対策は 'nutsInitEpsSearch'。
-                                 --   'nutsAdaptMass' が False のときは不適用。
-  , nutsInitEpsSearch :: Bool    -- ^ Phase 85.6c/86: Stan (Hoffman–Gelman
-                                 --   Algorithm 4) の ε 倍加探索を (i) サンプリング
-                                 --   開始前と (ii) 質量行列の各 window 末更新直後
-                                 --   (Phase 86・Stan adapt_diag_e_nuts の
-                                 --   init_stepsize+restart と同順) に行う (既定
-                                 --   True)。 DA anchor (μ = log 10ε) が幾何と
-                                 --   乖離すると ε が鋸歯振動して深い木を掘るため、
-                                 --   ε を 1 step leapfrog の受容率 ~1/2 になる値へ
-                                 --   都度較正する (Stan と同じ標準機構)。
-                                 --   'nutsAdaptStepSize' が True のときのみ有効。
-  , nutsAdaptMass     :: Bool    -- ^ Enable diagonal mass-matrix adaptation (B11).
-                                 --   Stan-style multi-window: init buffer (15% /
-                                 --   ≥75 iter, M=I) → doubling windows
-                                 --   25→50→100→200→… (M updated + dual avg
-                                 --   restarted at each window end) → term buffer
-                                 --   (10% / ≥50 iter, M frozen, ε converges).
-                                 --   Recommended for posteriors with strongly
-                                 --   varying scales across parameters.
-  , nutsInitJitter    :: Double  -- ^ Phase 94 A4-2: 各 chain の初期位置 (unconstrained)
-                                 --   に加える一様 jitter 半幅 (PyMC jitter+adapt_diag
-                                 --   相当)。 chain ごとに独立に @U(-j, +j)@ を各成分へ
-                                 --   加算し、 funnel 首での whole-chain 崩壊 (全 chain
-                                 --   同一 init 由来) を減らす。 @0@ = 無操作 (= 従来
-                                 --   挙動・単一 chain 再現性テスト非影響)。 多 chain
-                                 --   経路 ('hbmNutsConfig') で 1.0 を設定。
-  } deriving (Show)
-
--- | Default NUTS configuration: 2000 post-burn-in draws, 500 burn-in
--- (2500 total), @ε = 0.1@,
--- max depth 10, dual averaging enabled, target acceptance 0.8,
--- diagonal mass-matrix adaptation off (opt-in via 'nutsAdaptMass').
-defaultNUTSConfig :: NUTSConfig
-defaultNUTSConfig = NUTSConfig
-  { nutsIterations    = 2000
-  , nutsBurnIn        = 500
-  , nutsStepSize      = 0.1
-  , nutsMaxDepth      = 10
-  , nutsAdaptStepSize = True
-  , nutsTargetAccept  = 0.8
-  , nutsWarmupInitMaxDepth = Nothing
-  , nutsInitEpsSearch = True
-  , nutsAdaptMass     = False
-  , nutsInitJitter    = 0.0
-  }
-
--- ---------------------------------------------------------------------------
--- Dual averaging
--- ---------------------------------------------------------------------------
-
--- | Internal state for Nesterov's dual-averaging step-size adaptation.
-data DualAvgState = DualAvgState
-  { daLogEps     :: Double   -- ^ Current @log ε@ used for sampling.
-  , daLogEpsBar  :: Double   -- ^ Running smoothed @log ε̄@ (post-adaptation value).
-  , daH          :: Double   -- ^ Running average of (target − accept-stat).
-  , daMu         :: Double   -- ^ Anchor @μ = log(10 ε₀)@.
-  , daM          :: Int      -- ^ Iteration counter.
-  }
-
--- | Initialize 'DualAvgState' from an initial step size @ε₀@.
-initDualAvg :: Double -> DualAvgState
-initDualAvg eps0 = DualAvgState
-  { daLogEps    = log eps0
-  , daLogEpsBar = log eps0
-  , daH         = 0.0
-  , daMu        = log (10 * eps0)
-  , daM         = 0
-  }
-
--- | Phase 85.6c/86: Stan (@base_hmc::init_stepsize@) 準拠の ε 探索。
--- 与えられた ε を起点に、 1 step leapfrog の受容比が 0.8 を跨ぐまで倍加/半減
--- する (運動量は試行ごとに再サンプル)。 dual averaging の anchor μ = log(10 ε₀)
--- が幾何に合った値になり、 ε 鋸歯振動 (radon で depth 7-10 の深掘り) を防ぐ。
--- ★Hoffman–Gelman 2014 Alg.4 (起点 1.0・閾値 1/2・運動量 1 本固定) でなく
--- Stan 実装 (起点 = 現在 ε・閾値 0.8・毎試行再サンプル) に合わせる —
--- window 末の再較正 (Phase 86) では既適応の ε 近傍から保守的に探す必要がある
--- (起点 1.0/閾値 0.5 は radon 実測で新 M 下の trajectory が支えない大きな ε を
--- 返し、 次 window の深掘りを招いた)。 非有限 (発散) は比 −∞ 扱い = 半減方向。
--- 反復と ε は安全側に有界。
-findReasonableEpsilon
-  :: PrimMonad m
-  => (VS.Vector Double -> VS.Vector Double)   -- ^ gradFn (−∇ logπ・NUTS と同じ向き)
-  -> (VS.Vector Double -> Double)             -- ^ logπ (unconstrained)
-  -> VS.Vector Double                          -- ^ M⁻¹ 対角
-  -> Double                                    -- ^ 探索起点 ε (現在の nominal ε)
-  -> VS.Vector Double                          -- ^ 初期位置 θ (unconstrained)
-  -> Gen (PrimState m)
-  -> m Double
-findReasonableEpsilon gradFn logPiFn mInv eps0 theta gen = do
-    dH0 <- trial epsInit
-    let dir = if dH0 > thresh then 1 else -1 :: Int
-    loop dir epsInit (50 :: Int)
-  where
-    epsInit = max 1e-10 (min 1e7 eps0)
-    thresh  = log 0.8
-    -- 1 step leapfrog の log 受容比 (Stan と同じく運動量を都度引き直す)。
-    trial eps = do
-      r0 <- sampleMomentum mInv gen
-      let h0        = negate (logPiFn theta) + kineticMVS mInv r0
-          (th', r') = leapfrogWithMVS gradFn mInv eps 1 theta r0
-          h'        = negate (logPiFn th') + kineticMVS mInv r'
-      pure (if isNaN h' || isInfinite h' then (-1) / 0 else h0 - h')
-    loop dir !eps !k
-      | k <= 0 = pure eps
-      | otherwise = do
-          dH <- trial eps
-          let keepGoing = if dir == 1 then dH > thresh else dH < thresh
-              eps'      = if dir == 1 then eps * 2 else eps / 2
-          if not keepGoing then pure eps
-          else if eps' > 1e7 || eps' < 1e-10 then pure eps
-          else loop dir eps' (k - 1)
-
--- | Apply one dual-averaging update given the target acceptance @δ@ and
--- the observed acceptance statistic @α@ for the iteration.
-updateDualAvg :: Double -> Double -> DualAvgState -> DualAvgState
-updateDualAvg delta alpha da =
-  let m      = daM da + 1
-      gamma  = 0.05
-      t0     = 10.0
-      kappa  = 0.75
-      hNew   = (1 - 1 / (fromIntegral m + t0)) * daH da
-             + (1 / (fromIntegral m + t0)) * (delta - alpha)
-      logEps = daMu da - sqrt (fromIntegral m) / gamma * hNew
-      logEpsClip = max (-7) (min 5 logEps)
-      logEpsBar = (fromIntegral m ** (-kappa)) * logEpsClip
-                + (1 - fromIntegral m ** (-kappa)) * daLogEpsBar da
-  in da { daLogEps = logEpsClip, daLogEpsBar = logEpsBar, daH = hNew, daM = m }
-
--- ---------------------------------------------------------------------------
--- 内部ツリー
--- ---------------------------------------------------------------------------
-
--- | Internal NUTS tree node. All position/momentum are 'VS.Vector
--- Double' rather than 'Params' (= @Map@) / @[Double]@: the
--- @doubleTree@ recursion creates up to @2¹⁰@ intermediate trees per
--- iteration, and the previous Map / list representation paid an
--- order-of-magnitude in allocation that swamped the actual leapfrog
--- arithmetic.
-data NUTSTree = NUTSTree
-  { ntThMinus :: VS.Vector Double
-  , ntRMinus  :: VS.Vector Double
-  , ntGMinus  :: VS.Vector Double
-    -- ^ Phase 87.2b: minus 端点の ∇U = −∇logπ (leapfrog 勾配キャッシュ)。
-    --   同方向の次の葉が始点勾配を再計算せずに済む (Stan の z_.g と同じ)。
-  , ntThPlus  :: VS.Vector Double
-  , ntRPlus   :: VS.Vector Double
-  , ntGPlus   :: VS.Vector Double
-    -- ^ Phase 87.2b: plus 端点の ∇U (同上)。
-  , ntThPrime :: VS.Vector Double
-  , ntN       :: Int
-  , ntS       :: Bool
-  , ntDiv     :: Bool
-    -- ^ サブツリー中で divergent (|ΔH| > deltaMax) が発生したか
-  , ntASum    :: !Double
-    -- ^ Phase 87.2: Σ min(1, exp(H0 − H_leaf)) — Stan の accept_stat 蓄積。
-    --   dual averaging はこの平均 ᾱ を学習する (旧: 1-step probe = 毎 draw
-    --   余分な leapfrog+エネルギー評価を払う非標準の独自実装だった)。
-  , ntANum    :: !Int
-    -- ^ Phase 87.2: ᾱ の分母 (サブツリーの葉数・棄却葉も含む)。
-  }
-
-deltaMax :: Double
-deltaMax = 1000.0
-
--- | U-turn check on Storable Vectors. @(θ⁺ − θ⁻) · r⁻ < 0@ or
--- @(θ⁺ − θ⁻) · r⁺ < 0@ ⇒ trajectory has begun to retrace itself.
---
--- Phase 90 A11-4①: 旧実装は @delta@ の共有 binding で stream fusion が切れ
--- delta ベクトルを毎回実体化していた (prof 実測: nuts_uturn が総 alloc の
--- 23.5%)。 2 つの内積を単一パス・確保なしで融合する。 加算順序は旧
--- 'VS.sum' (左畳み込み) と同一 = ビット同一。
-uTurnVS
-  :: VS.Vector Double -> VS.Vector Double
-  -> VS.Vector Double -> VS.Vector Double -> Bool
-uTurnVS thMinus rMinus thPlus rPlus = go 0 0 0
-  where
-    !n = VS.length thMinus
-    go !d1 !d2 !j
-      | j >= n    = d1 < 0 || d2 < 0
-      | otherwise =
-          let d = thPlus `VS.unsafeIndex` j - thMinus `VS.unsafeIndex` j
-          in go (d1 + d * (rMinus `VS.unsafeIndex` j))
-                (d2 + d * (rPlus  `VS.unsafeIndex` j))
-                (j + 1)
-{-# INLINE uTurnVS #-}
-
--- | Sample momentum @r ~ N(0, M)@ from the diagonal mass matrix
--- represented as @M⁻¹@. Per coordinate: @r_i = z / sqrt(M⁻¹_i)@,
--- @z ~ N(0,1)@. Storable-vector tight loop, no list allocation.
-sampleMomentum :: PrimMonad m => VS.Vector Double -> Gen (PrimState m) -> m (VS.Vector Double)
-sampleMomentum mInv gen = do
-  let n = VS.length mInv
-  VS.generateM n $ \i -> do
-    z <- standard gen
-    return (z / sqrt (mInv `VS.unsafeIndex` i))
-{-# INLINE sampleMomentum #-}
-
--- ---------------------------------------------------------------------------
--- ツリービルダー
--- ---------------------------------------------------------------------------
-
-buildTree
-  :: forall m. PrimMonad m
-  => (VS.Vector Double -> m (Double, VS.Vector Double))
-     -- ^ 融合評価 (Phase 87.2b): θ ↦ (logπ(θ), ∇U(θ) = −∇logπ(θ))。 Phase 90
-     --   A11-4①: chain 閉包に確保した arena/adj を再利用するため monadic。
-  -> VS.Vector Double                         -- ^ Diagonal M⁻¹.
-  -> Double                                   -- ^ Step size @ε@.
-  -> VS.Vector Double                         -- ^ Position.
-  -> VS.Vector Double                         -- ^ Momentum.
-  -> VS.Vector Double                         -- ^ ∇U at position (キャッシュ)。
-  -> Double                                   -- ^ @log u@ slice.
-  -> Double                                   -- ^ 初期エネルギー @H0@ (ᾱ 用)。
-  -> Int                                      -- ^ Direction (±1).
-  -> Int                                      -- ^ Recursion depth.
-  -> Gen (PrimState m)
-  -> m NUTSTree
--- Phase 54.7b: PrimMonad 多相 (Phase 50) は SPECIALIZE が無いと dictionary 渡しで
--- mwc の uniform/standard が unbox されない (prof 実測で RNG 系 11%/alloc 25%)。
--- IO / ST の両具体型に特殊化して Phase 50 以前の機械語品質に戻す。
-{-# SPECIALIZE buildTree
-  :: (VS.Vector Double -> IO (Double, VS.Vector Double))
-  -> VS.Vector Double -> Double -> VS.Vector Double -> VS.Vector Double
-  -> VS.Vector Double
-  -> Double -> Double -> Int -> Int -> Gen RealWorld -> IO NUTSTree #-}
-{-# SPECIALIZE buildTree
-  :: (VS.Vector Double -> ST s (Double, VS.Vector Double))
-  -> VS.Vector Double -> Double -> VS.Vector Double -> VS.Vector Double
-  -> VS.Vector Double
-  -> Double -> Double -> Int -> Int -> Gen s -> ST s NUTSTree #-}
-buildTree gradValU mInv eps theta r gU logU h0 dir depth gen
-  | depth == 0 = do
-      -- Phase 87.2b: 1-step leapfrog を融合評価でインライン化。 始点勾配は
-      -- 端点キャッシュ (gU) を使い、 終点は (logπ, ∇U) を 1 回の融合評価で
-      -- 取得 (旧: 葉ごとに grad 2 回 + logπ 1 回 = 始点勾配の再計算と
-      -- エネルギー用 forward の重複を払っていた)。
-      let !epsD    = fromIntegral dir * eps
-          !halfEps = 0.5 * epsD
-          rHalf  = {-# SCC "nuts_leapfrog_kick1" #-}
-                   VS.zipWith (\ri gi -> ri - halfEps * gi) r gU
-          theta' = {-# SCC "nuts_leapfrog_drift" #-}
-                   VS.zipWith3 (\ti m_inv ri -> ti + epsD * m_inv * ri)
-                               theta mInv rHalf
-      (v', g') <- {-# SCC "nuts_gradval" #-} gradValU theta'
-      let r'     = {-# SCC "nuts_leapfrog_kick2" #-}
-                   VS.zipWith (\ri gi -> ri - halfEps * gi) rHalf g'
-          h'  = {-# SCC "nuts_energy" #-} (negate v' + kineticMVS mInv r')
-          n'  = if logU <= -h' then 1 else 0
-          s'  = logU < deltaMax - h'
-          divergent = not s'
-          -- Phase 87.2: Stan の accept_stat = min(1, exp(H0 − H')) を葉ごとに
-          -- 蓄積 (非有限は 0 = 棄却扱い)。
-          a'  = let d = h0 - h'
-                in if isNaN d then 0 else min 1 (exp (min 0 d))
-      return NUTSTree
-        { ntThMinus = theta', ntRMinus = r', ntGMinus = g'
-        , ntThPlus  = theta', ntRPlus  = r', ntGPlus  = g'
-        , ntThPrime = theta', ntN = n', ntS = s'
-        , ntDiv = divergent
-        , ntASum = a', ntANum = 1
-        }
-  | otherwise = do
-      t1 <- buildTree gradValU mInv eps theta r gU logU h0 dir (depth - 1) gen
-      if not (ntS t1) then return t1
-      else do
-        let (th0, r0, g0) = if dir == -1
-              then (ntThMinus t1, ntRMinus t1, ntGMinus t1)
-              else (ntThPlus  t1, ntRPlus  t1, ntGPlus  t1)
-        t2 <- buildTree gradValU mInv eps th0 r0 g0 logU h0 dir (depth - 1) gen
-        let n1 = ntN t1; n2 = ntN t2
-        thPrime' <-
-          if n1 == 0 then return (ntThPrime t2)
-          else if n2 == 0 then return (ntThPrime t1)
-          else do
-            u <- {-# SCC "nuts_rng_uniform" #-} (uniform gen :: m Double)
-            return $ if u < min 1.0 (fromIntegral n2 / fromIntegral n1)
-                     then ntThPrime t2
-                     else ntThPrime t1
-        let (minus', rMinus', gMinus', plus', rPlus', gPlus') = if dir == -1
-              then (ntThMinus t2, ntRMinus t2, ntGMinus t2,
-                    ntThPlus t1, ntRPlus t1, ntGPlus t1)
-              else (ntThMinus t1, ntRMinus t1, ntGMinus t1,
-                    ntThPlus t2, ntRPlus t2, ntGPlus t2)
-            s' = ntS t2 && not ({-# SCC "nuts_uturn" #-} uTurnVS minus' rMinus' plus' rPlus')
-        return NUTSTree
-          { ntThMinus = minus', ntRMinus = rMinus', ntGMinus = gMinus'
-          , ntThPlus  = plus',  ntRPlus  = rPlus',  ntGPlus  = gPlus'
-          , ntThPrime = thPrime', ntN = n1 + n2, ntS = s'
-          , ntDiv = ntDiv t1 || ntDiv t2
-          , ntASum = ntASum t1 + ntASum t2
-          , ntANum = ntANum t1 + ntANum t2
-          }
-
--- ---------------------------------------------------------------------------
--- Streaming hook
--- ---------------------------------------------------------------------------
-
--- | Per-iteration sample event emitted by 'nutsStream'.
---
--- Used by callers that want to observe MCMC progress as it happens
--- (e.g. live trace plots, real-time R-hat / ESS updates over the wire).
--- The callback receives one event per iteration of the outer loop,
--- including burn-in iterations (distinguished by 'seIsBurnIn').
---
--- The 'seParams' values are in the **constrained** parameter space,
--- matching the convention used in 'chainSamples'. Burn-in events are
--- /not/ included in 'chainSamples', but are still streamed via the
--- callback so the UI can show warmup progress and adaptation.
-data SampleEvent = SampleEvent
-  { seIter      :: !Int      -- ^ 0-based iteration index (burn-in inclusive).
-                              --   Ranges over @[0 .. nutsBurnIn + nutsIterations - 1]@.
-  , seIsBurnIn  :: !Bool     -- ^ True if @seIter < nutsBurnIn@.
-  , seParams    :: !Params   -- ^ Current sample (constrained space).
-  , seEnergy    :: !Double   -- ^ Hamiltonian H0 at the start of this iteration.
-  , seDivergent :: !Bool     -- ^ Whether this iteration's trajectory diverged.
-  , seAccepted  :: !Bool     -- ^ Whether the proposal was accepted
-                              --   (@proposedU /= currentU@).
-  , seStepSize  :: !Double   -- ^ Current ε (after this iteration's adaptation).
-  , seTreeDepth :: !Int      -- ^ Phase 85.6: この draw で実行された doubling 回数
-                              --   (leapfrog 数 ≈ 2^depth・warmup 固定費の診断用)。
-  , seAcceptStat :: !Double  -- ^ Phase 87.1: この draw の mean accept-stat α
-                              --   (dual averaging が target と比較する統計・
-                              --   'seAccepted' の bool とは別物)。ε̄ 収束診断用。
-  }
-
--- ---------------------------------------------------------------------------
--- NUTS サンプラー
--- ---------------------------------------------------------------------------
-
--- | NUTS sampler for a polymorphic HBM model ('ModelP').
--- 軌道長は U-Turn 判定で自動決定。
---
--- This is a thin wrapper around 'nutsStream' with a no-op callback.
--- Use 'nutsStream' directly if you want per-iteration progress
--- (e.g. for live UI updates over a WebSocket / SSE channel).
-nuts :: PrimMonad m => ModelP r -> NUTSConfig -> Params -> Gen (PrimState m) -> m Chain
-{-# SPECIALIZE nuts :: ModelP r -> NUTSConfig -> Params -> Gen RealWorld -> IO Chain #-}
-{-# SPECIALIZE nuts :: ModelP r -> NUTSConfig -> Params -> Gen s -> ST s Chain #-}
-nuts m cfg initC gen = nutsStream m cfg initC gen (\_ -> pure ())
-
--- | NUTS sampler with a per-iteration callback. Identical to 'nuts'
--- semantically; in addition, calls @onSample event@ once per outer
--- loop iteration (burn-in inclusive). The callback runs synchronously
--- inside the sampler loop, so it should return quickly (push events to
--- a queue rather than do IO of unbounded latency).
-nutsStream :: forall r m. PrimMonad m
-           => ModelP r -> NUTSConfig -> Params -> Gen (PrimState m)
-           -> (SampleEvent -> m ())
-           -> m Chain
-{-# SPECIALIZE nutsStream
-  :: ModelP r -> NUTSConfig -> Params -> Gen RealWorld
-  -> (SampleEvent -> IO ()) -> IO Chain #-}
-{-# SPECIALIZE nutsStream
-  :: ModelP r -> NUTSConfig -> Params -> Gen s
-  -> (SampleEvent -> ST s ()) -> ST s Chain #-}
-nutsStream m cfg initC gen onSample = do
-  let names      = sampleNames m
-      trMap      = getTransforms m
-      transList  = [Map.findWithDefault errT n trMap | n <- names]
-      errT       = error "nuts: missing transform"
-
-      -- Initial unconstrained position as a Storable Vector. The hot
-      -- loop never touches 'Params' (= Map); we only convert at the
-      -- boundary to record samples.
-      initUV0 :: VS.Vector Double
-      initUV0 = VS.fromList
-        [ toUnconstrained t (Map.findWithDefault 0 n initC)
-        | (n, t) <- zip names transList ]
-
-      total   = nutsBurnIn cfg + nutsIterations cfg
-      doAdapt = nutsAdaptStepSize cfg && nutsBurnIn cfg > 0
-
-      -- Vector-native log target density. Phase 54.4d/54.6: エネルギー評価も
-      -- 'compileLogPUV' で静的部分 (名前→index 解決込み) を 1 度だけ前処理した
-      -- compiled closure を全 tree node で再利用する (旧: 毎回
-      -- 'logJointUnconstrained' の Free walk + per-obs スカラ logDensityObs)。
-      logPiFn :: VS.Vector Double -> Double
-      logPiFn = compileLogPUV m names transList
-
-      -- Vector-native gradient. Phase 54.4b/54.6: モデル構造は draw 間で不変
-      -- ゆえ 'compileGradUV' で静的部分を **1 度だけ**前処理し、 返った
-      -- vector-native クロージャを全 leapfrog で再利用する (VS↔list 変換なし)。
-      gradV :: VS.Vector Double -> VS.Vector Double
-      gradV = compileGradUV m names transList
-      gradFn :: VS.Vector Double -> VS.Vector Double
-      gradFn uv = VS.map negate (gradV uv)
-
-      toConstrained :: VS.Vector Double -> Params
-      toConstrained uv = Map.fromList
-        [ (n, fromUnconstrained t (uv `VS.unsafeIndex` i))
-        | (i, (n, t)) <- zip [0..] (zip names transList) ]
-
-  -- Phase 87.2b: 値+勾配の融合評価 (JAX value_and_grad 相当)。 tree の葉が
-  -- leapfrog 最終勾配とエネルギーを同一点で二重評価していた重複を除去。
-  -- Phase 90 A11-4①: 'compileGradValUVM' は forward/随伴 arena を **この chain
-  -- 閉包生成時に 1 度だけ**確保して全 leapfrog で再利用する (per-call 34k×2
-  -- セル確保 + GC churn を除去)。 chain ごとに別 'nutsStream' 呼出 = 別バッファ
-  -- ゆえ chain 横断並列 ('nutsChainsPure'/'nutsChainsStream') と非干渉。
-  -- Phase 94 A4-2: 各 chain の初期位置に一様 jitter (funnel 首の whole-chain 崩壊対策)。
-  -- j=0 なら initUV0 をそのまま (従来挙動)。 gen は chain 固有ゆえ chain ごと独立。
-  initUV <- let j = nutsInitJitter cfg
-            in if j <= 0 then pure initUV0
-               else VS.mapM (\x -> do u <- uniform gen
-                                      pure (x + (u * 2 - 1) * j)) initUV0
-  gradValV <- compileGradValUVM m names transList
-  let gradValU :: VS.Vector Double -> m (Double, VS.Vector Double)
-      gradValU uv = do
-        (v, g) <- gradValV uv
-        pure (v, VS.map negate g)
-
-  samplesRef    <- newMutVar []
-  energyRef     <- newMutVar ([] :: [Double])
-  divergenceRef <- newMutVar ([] :: [Int])
-  depthRef      <- newMutVar ([] :: [Int])   -- Phase 85.3: per-draw tree depth
-  acceptedRef   <- newMutVar (0 :: Int)
-  -- Phase 85.6c: 初期 ε の較正 (Stan Algorithm 4・doAdapt 時のみ)。
-  eps0 <- if doAdapt && nutsInitEpsSearch cfg
-            then findReasonableEpsilon gradFn logPiFn
-                   (VS.replicate (length names) 1.0) (nutsStepSize cfg) initUV gen
-            else pure (nutsStepSize cfg)
-  daRef         <- newMutVar (initDualAvg eps0)
-
-  -- B11: Stan-style multi-window diagonal mass-matrix adaptation.
-  --
-  -- Schedule (warmup W):
-  --   * init buffer  (max 75 / W÷7 iters): step-size adapt only, M = I
-  --   * window phase: doubling windows 25 → 50 → 100 → 200 → ...
-  --       At the end of each window: update M⁻¹ from window's
-  --       Welford-accumulated diagonal variance, restart dual averaging.
-  --   * term buffer  (max 50 / W÷10 iters): M frozen, step-size adapt
-  --       continues to converge ε under the final geometry.
-  let nParams       = length names
-      adaptM        = nutsAdaptMass cfg && nutsBurnIn cfg > 0
-      (windowEnds, initBuf, _termBuf) = stanWindows (nutsBurnIn cfg)
-      windowPhaseEnd = if null windowEnds then 0 else last windowEnds
-  mInvRef     <- newMutVar (VS.replicate nParams 1.0)
-  welfordRef  <- newMutVar (emptyWelford nParams)
-
-  let step :: VS.Vector Double -> Double -> Int -> VS.Vector Double
-           -> m (VS.Vector Double, Double, Double, Bool, Int)
-      step mInv eps maxDep currentU = do
-        -- r ~ N(0, M)  ⇔  r_i = sqrt(M_ii) * z = z / sqrt(M⁻¹_ii)
-        r0 <- {-# SCC "nuts_sampleMomentum" #-} sampleMomentum mInv gen
-        u0 <- {-# SCC "nuts_rng_uniform" #-} (uniform gen :: m Double)
-        -- Phase 87.2b: 始点の (logπ, ∇U) を融合評価 1 回で取得。 値は H0 に、
-        -- 勾配は両方向の最初の葉の始点キャッシュに使う。
-        (v0, gU0) <- {-# SCC "nuts_gradval0" #-} gradValU currentU
-        let h0   = negate v0 + kineticMVS mInv r0
-            logU = log u0 - h0
-        let tree0 = NUTSTree
-              { ntThMinus = currentU, ntRMinus = r0, ntGMinus = gU0
-              , ntThPlus  = currentU, ntRPlus  = r0, ntGPlus  = gU0
-              , ntThPrime = currentU, ntN = 1, ntS = True
-              , ntDiv = False
-              , ntASum = 0, ntANum = 0
-              }
-        let doubleTree tree j =
-              if not (ntS tree) then return tree
-              else do
-                u <- {-# SCC "nuts_rng_uniform" #-} (uniform gen :: m Double)
-                let dir = if u < 0.5 then -1 else 1 :: Int
-                    (th0, r0', g0') = if dir == -1
-                      then (ntThMinus tree, ntRMinus tree, ntGMinus tree)
-                      else (ntThPlus  tree, ntRPlus  tree, ntGPlus  tree)
-                subtree <- {-# SCC "nuts_buildTree" #-}
-                  buildTree gradValU mInv eps th0 r0' g0' logU h0 dir j gen
-                let n1 = ntN tree; n2 = ntN subtree
-                thPrime' <-
-                  if not (ntS subtree) || n2 == 0
-                  then return (ntThPrime tree)
-                  else do
-                    u2 <- {-# SCC "nuts_rng_uniform" #-} (uniform gen :: m Double)
-                    return $ if u2 < min 1.0 (fromIntegral n2 / fromIntegral n1)
-                             then ntThPrime subtree
-                             else ntThPrime tree
-                let (minus', rMinus', gMinus', plus', rPlus', gPlus') = if dir == -1
-                      then (ntThMinus subtree, ntRMinus subtree, ntGMinus subtree,
-                            ntThPlus  tree,    ntRPlus  tree,    ntGPlus  tree)
-                      else (ntThMinus tree,    ntRMinus tree,    ntGMinus tree,
-                            ntThPlus  subtree, ntRPlus  subtree, ntGPlus  subtree)
-                    s' = ntS subtree && not ({-# SCC "nuts_uturn" #-} uTurnVS minus' rMinus' plus' rPlus')
-                return NUTSTree
-                  { ntThMinus = minus', ntRMinus = rMinus', ntGMinus = gMinus'
-                  , ntThPlus  = plus',  ntRPlus  = rPlus',  ntGPlus  = gPlus'
-                  , ntThPrime = thPrime', ntN = n1 + n2, ntS = s'
-                  , ntDiv = ntDiv tree || ntDiv subtree
-                  , ntASum = ntASum tree + ntASum subtree
-                  , ntANum = ntANum tree + ntANum subtree
-                  }
-        -- Phase 85.3: 実行された doubling 回数 = tree depth (PyMC の
-        -- tree_depth 相当・leapfrog 数 ≈ 2^depth) を数える。
-        let doubleTreeD (tree, !dep) j =
-              if not (ntS tree) then return (tree, dep)
-              else do
-                t' <- doubleTree tree j
-                return (t', dep + 1 :: Int)
-        (finalTree, treeDepth) <-
-          foldM doubleTreeD (tree0, 0) [0 .. maxDep - 1]
-        -- Phase 87.2: alpha は Stan の accept_stat = tree 全葉の
-        -- min(1, exp(H0−H')) 平均 (buildTree で蓄積)。 旧 1-step probe
-        -- (毎 draw 余分な leapfrog + エネルギー評価・非標準の独自実装) を廃止。
-        let proposedU = ntThPrime finalTree
-            alpha     = if ntANum finalTree > 0
-                          then ntASum finalTree / fromIntegral (ntANum finalTree)
-                          else 0
-        when (proposedU /= currentU) $ modifyMutVar' acceptedRef (+1)
-        return (proposedU, alpha, h0, ntDiv finalTree, treeDepth)
-
-  let loop 0 currentU _eps = return currentU
-      loop i currentU eps = do
-        mInv <- readMutVar mInvRef
-        let isBurnIn   = i > nutsIterations cfg
-            -- iteration index from start (1-based); total counts down.
-            iterIdx    = total - i + 1
-            -- Phase 85.6: M 初回更新前 (M=I) は深い木を掘らない (init 期の
-            -- draw は捨てる区間・radon で warmup leapfrog の 68% を占めた)。
-            firstMUpd  = case windowEnds of { (w : _) -> w; [] -> 0 }
-            maxDep
-              | adaptM && isBurnIn && iterIdx <= firstMUpd
-              , Just cap <- nutsWarmupInitMaxDepth cfg =
-                  min (nutsMaxDepth cfg) cap
-              | otherwise = nutsMaxDepth cfg
-            -- Inside the window phase: collect samples for Welford.
-            inWindowPhase = adaptM && isBurnIn
-                            && iterIdx > initBuf
-                            && iterIdx <= windowPhaseEnd
-            -- This iteration ends a window: update M, restart DA.
-            isWindowEnd   = adaptM && isBurnIn && iterIdx `elem` windowEnds
-        (nextU, alpha, h0, divergent, treeDepth) <- step mInv eps maxDep currentU
-        when inWindowPhase $
-          modifyMutVar' welfordRef (\w -> {-# SCC "nuts_welford" #-} welfordAddVS w nextU)
-        -- Phase 86: window 末に M を更新したら、 Stan (adapt_diag_e_nuts の
-        -- init_stepsize + restart) と同じく**新 metric の下で ε を再較正**
-        -- (Algorithm 4) して DA を restart する。 旧実装は鋸歯振動中の瞬間値
-        -- ε を anchor (μ = log 10ε) にしており、 M 更新直後に ε が幾何と桁で
-        -- 乖離すると次 window 丸ごと深掘りする (radon seed=1 実測で window
-        -- [150,250) が depth 9.9・101k leapfrog = warmup 全体の 76%)。
-        recalEps <- if isWindowEnd
-          then do
-            w <- readMutVar welfordRef
-            -- Reset Welford for the next window (window-local variance).
-            writeMutVar welfordRef (emptyWelford nParams)
-            if wN w >= 5  -- need a few samples to be meaningful
-              then do
-                let mInv' = welfordMInvVS w
-                writeMutVar mInvRef mInv'
-                if doAdapt && nutsInitEpsSearch cfg
-                  then
-                    -- Phase 87.1: **最終 window 末 (term buffer 直前) は restart
-                    -- しない** (M のみ更新・DA 継続 = PyMC の連続 DA と同じ挙動)。
-                    -- restart すると DA が m=1 の暴れ期からやり直しになり、 50
-                    -- draw の term buffer では ε̄ が鋸歯の暴れを拾って過小に着地
-                    -- する (radon 実測: ε 振動 0.035-1.37・ε̄=0.18・sampling α
-                    -- 0.95/depth 5。 PyMC は restart なしで振動 0.14-0.50・
-                    -- ε 0.23-0.34・depth 4/α 0.80-0.88)。 中間 window 末の
-                    -- recal+restart (Phase 86・爆発対策) は維持 — この時点まで
-                    -- に M はほぼ収束しており継続 DA の ε がそのまま通用する。
-                    if iterIdx == windowPhaseEnd
-                      then pure Nothing
-                      else do
-                        epsNew <- findReasonableEpsilon gradFn logPiFn mInv' eps nextU gen
-                        writeMutVar daRef (initDualAvg epsNew)
-                        pure (Just epsNew)
-                  else do
-                    -- 旧挙動 (opt-out 時): 現 ε anchor で restart。
-                    writeMutVar daRef (initDualAvg eps)
-                    pure Nothing
-              else pure Nothing
-          else pure Nothing
-        eps' <- case recalEps of
-          -- Stan と同じく restart 直後はこの draw の accept 統計を学習しない
-          -- (旧 metric 下の α で較正済 anchor を汚さない)。
-          Just epsNew -> pure epsNew
-          Nothing
-            | doAdapt && isBurnIn -> do
-                da <- readMutVar daRef
-                let da' = {-# SCC "nuts_dualavg" #-} updateDualAvg (nutsTargetAccept cfg) alpha da
-                writeMutVar daRef da'
-                return (exp (daLogEps da'))
-            | otherwise -> do
-                da <- readMutVar daRef
-                let epsBar = if doAdapt && not isBurnIn && i == nutsIterations cfg
-                             then exp (daLogEpsBar da)
-                             else eps
-                return epsBar
-        let nextParams = {-# SCC "nuts_toConstrained" #-} toConstrained nextU
-        if not isBurnIn
-          then do
-            modifyMutVar' samplesRef (nextParams :)
-            modifyMutVar' energyRef  (h0 :)
-            modifyMutVar' depthRef   (treeDepth :)
-            when divergent $
-              modifyMutVar' divergenceRef
-                ((nutsIterations cfg - i) :)
-          else return ()
-        -- Phase 9.1a: per-iteration callback for streaming UIs.
-        -- 0-based iter index running 0 .. total-1; isBurnIn for first nutsBurnIn.
-        onSample SampleEvent
-          { seIter      = total - i
-          , seIsBurnIn  = isBurnIn
-          , seParams    = nextParams
-          , seEnergy    = h0
-          , seDivergent = divergent
-          , seAccepted  = nextU /= currentU
-          , seStepSize  = eps'
-          , seTreeDepth = treeDepth
-          , seAcceptStat = alpha
-          }
-        loop (i - 1) nextU eps'
-
-  _ <- loop total initUV eps0
-  samples  <- fmap reverse (readMutVar samplesRef)
-  energies <- fmap reverse (readMutVar energyRef)
-  divs     <- fmap reverse (readMutVar divergenceRef)
-  depths   <- fmap reverse (readMutVar depthRef)
-  accepted <- readMutVar acceptedRef
-  return Chain
-    { chainSamples     = samples
-    , chainAccepted    = accepted
-    , chainTotal       = total
-    , chainEnergy      = energies
-    , chainDivergences = divs
-    , chainTreeDepths  = depths
-    }
-
--- ---------------------------------------------------------------------------
--- B11: Mass-matrix adaptation helpers
--- ---------------------------------------------------------------------------
-
--- | Welford online accumulator for diagonal sample variance.
---
--- Per-coordinate one-pass mean / M2; variance = M2 / (n − 1).
--- Used by Stan-style window adaptation to estimate posterior variance
--- without keeping the raw samples around.
--- | Plain (non-record) constructor: @Welford n mean m2@. Kept positional
--- because the @m2@ field is only ever pattern-matched, never read via a
--- selector (record syntax would generate an unused-binding warning).
--- | Storable-Vector Welford. The previous list-based form allocated
--- four @[Double]@ vectors per add (warmup ~500 iters × 4 cells = 10K
--- list cells per fit) and was hot during the mass-matrix adaptation
--- window phase.
-data Welford = Welford !Int !(VS.Vector Double) !(VS.Vector Double)
-
-wN :: Welford -> Int
-wN (Welford n _ _) = n
-
-emptyWelford :: Int -> Welford
-emptyWelford p = Welford 0 (VS.replicate p 0) (VS.replicate p 0)
-
-welfordAddVS :: Welford -> VS.Vector Double -> Welford
-welfordAddVS (Welford n mean m2) x =
-  let !n'   = n + 1
-      !nD   = fromIntegral n' :: Double
-      !d    = VS.zipWith (-) x mean
-      !mean' = VS.zipWith (\me di -> me + di / nD) mean d
-      !d2   = VS.zipWith (-) x mean'
-      !m2'  = VS.zipWith3 (\m2i d1 d22 -> m2i + d1 * d22) m2 d d2
-  in Welford n' mean' m2'
-
--- | Stan-regularised diagonal @M⁻¹@ from a Welford accumulator.
---
--- @σ̂² = (n / (n+5)) · sample_var + 1e-3 · (5 / (n+5))@.
--- The 1e-3 shrinkage target keeps the estimator non-degenerate when
--- @n@ is tiny; for moderate @n@ it reduces to the sample variance.
---
--- /Convention/: following Stan/blackjax, @M⁻¹@ stores the posterior
--- covariance directly (so @M⁻¹_ii = σ̂²_i@). With kinetic energy
--- @½ rᵀ M⁻¹ r@ and @r ~ N(0, M)@, this gives a per-leapfrog position
--- step @ε · σ̂_i@ in absolute units (i.e. @ε@ in posterior-sd units),
--- which is what NUTS needs for tree depth ~ @1/ε@.
-welfordMInvVS :: Welford -> VS.Vector Double
-welfordMInvVS (Welford n _ m2)
-  | n < 2     = VS.replicate (VS.length m2) 1.0
-  | otherwise =
-      let nD     = fromIntegral n :: Double
-          k      = 5.0 :: Double
-          weight = nD / (nD + k)
-          target = 1e-3
-      in VS.map (\v -> let raw = v / (nD - 1)
-                       in max 1e-12 (weight * raw + (1 - weight) * target))
-                m2
-
--- | Stan-style adaptation schedule for a warmup of @W@ iterations.
---
--- Returns @(windowEndIters, initBuffer, termBuffer)@ where
--- @windowEndIters@ are 1-based iteration indices at which to update the
--- mass matrix, and @initBuffer@ / @termBuffer@ are the no-update
--- prefix / suffix lengths (Stan defaults: 15% / 10%, with floors of 75
--- and 50 iters respectively). Windows double in size starting from 25;
--- the last window absorbs any remainder.
---
--- For @W = 500@: @initBuffer = 75@, @termBuffer = 50@, middle = 375,
--- windows = @[100, 150, 250, 450]@.
-stanWindows :: Int -> ([Int], Int, Int)
-stanWindows w
-  | w < 50    = ([], w, 0)
-  | otherwise =
-      let initB  = max 75 (w `div` 7)
-          termB  = max 50 (w `div` 10)
-          midLen = w - initB - termB
-      in if midLen < 25
-         then ([], w, 0)
-         else (genW (initB + 1) midLen 25, initB, termB)
-  where
-    genW _     0    _     = []
-    genW start rest wsize
-      | wsize * 2 > rest =
-          -- Next doubled window wouldn't fit; absorb the remainder.
-          [start + rest - 1]
-      | otherwise =
-          let endIter = start + wsize - 1
-          in endIter : genW (endIter + 1) (rest - wsize) (wsize * 2)
-
--- | Run 'nuts' on @numChains@ parallel chains.
-nutsChains :: ModelP r -> NUTSConfig -> Int -> Params -> GenIO -> IO [Chain]
-nutsChains m cfg numChains initC baseGen = do
-  gens <- replicateM numChains (spawnGen baseGen)
-  mapConcurrently (\g -> nuts m cfg initC g) gens
-
--- ---------------------------------------------------------------------------
--- Phase 50: 純粋 (ST + seed) ラッパ
---
--- 'nuts' を 'ST' で走らせ 'runST' で閉じることで、 **seed → 確定 'Chain'** の
--- 純粋関数にする (同 seed → ビット同一・IO 不要)。 mwc は 'PrimMonad' 汎用ゆえ
--- ロジックは 50.2 で一般化した 'nuts' をそのまま使う。
--- ---------------------------------------------------------------------------
-
--- | 純粋・決定的な単一 NUTS chain。 同じ @seed@ なら必ず同じ 'Chain' を返す。
-nutsPure :: ModelP r -> NUTSConfig -> Params -> Word32 -> Chain
-nutsPure m cfg initC seed =
-  runST (initialize (V.singleton seed) >>= nuts m cfg initC)
-
--- | 親 @seed@ から chain ごとの child seed 列を純粋に導出する (Phase 61.1 で
--- 'nutsChainsPure' から抽出)。 pure 経路と IO 経路 ('nutsChainsStream') が
--- **同じ seed 列**を共有することで両経路のビット一致を保証する (複製すると drift)。
-chainSeeds :: Word32 -> Int -> [Word32]
-chainSeeds seed numChains = runST $ do
-  g <- initialize (V.singleton seed)
-  replicateM numChains (uniform g)
-
--- | 純粋・決定的な multi-chain。 親 @seed@ から子 seed を純粋に導出 (各 chain は
--- 別 'runST') し、 chain 横断を @parList rdeepseq@ で**最初から**並列評価する
--- (純粋性と並列性は直交。 @+RTS -N@ でマルチコア。 結果は spark/コア数に依らずビット同一)。
-nutsChainsPure :: ModelP r -> NUTSConfig -> Int -> Params -> Word32 -> [Chain]
-nutsChainsPure m cfg numChains initC seed =
-  let chains = [ nutsPure m cfg initC s | s <- chainSeeds seed numChains ]
-  in chains `using` parList rdeepseq
-
--- | 'nutsChainsPure' の IO 版 (Phase 61.1): 同じ child seed 規約
--- ('chainSeeds') で chain ごとに 'nutsStream' を回し、 chain index 付き
--- callback で進捗を観測できるようにする。 chain 横断は 'mapConcurrently'
--- (既存 'nutsChains' と同様・実 OS スレッド並列には @-threaded +RTS -N@)。
---
--- mwc の 'PrimMonad' 汎用性 + Phase 50 で実証済の ST/IO ビット同一により、
--- no-op callback なら結果は @nutsChainsPure m cfg n initC seed@ と
--- **ビット一致**する (回帰テストで固定)。
-nutsChainsStream :: ModelP r -> NUTSConfig -> Int -> Params -> Word32
-                 -> (Int -> SampleEvent -> IO ())
-                 -> IO [Chain]
-nutsChainsStream m cfg numChains initC seed onSample =
-  mapConcurrently
-    (\(i, s) -> do
-        g <- initialize (V.singleton s)
-        nutsStream m cfg initC g (onSample i))
-    (zip [0 ..] (chainSeeds seed numChains))
diff --git a/src/Hanalyze/MCMC/Progress.hs b/src/Hanalyze/MCMC/Progress.hs
deleted file mode 100644
--- a/src/Hanalyze/MCMC/Progress.hs
+++ /dev/null
@@ -1,160 +0,0 @@
--- |
--- Module      : Hanalyze.MCMC.Progress
--- Description : MCMC サンプリングの進捗表示 (全 chain 集計を stderr に描画)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- MCMC サンプリングの進捗表示 (Phase 61.2)。
---
--- 'Hanalyze.MCMC.NUTS.nutsChainsStream' の chain index 付き callback に
--- 接続して、 全 chain 集計の進捗 1 行を stderr に描画する:
---
--- > chains 2/4 done | draw 3400/8000 (warmup) | div 12 | 380.0 it/s
---
--- 設計 (phase-61 計画の柱):
---
--- * 表示は「現在の chain」 でなく**全 chain 集計** (chain は mapConcurrently
---   並列で同時進行するため「現在」 が無い)。
--- * callback はサンプラループ内で**同期実行**される ('nutsStream' doc 明記)
---   ので、 描画はカウンタ先行の間引き (全体の ~0.5% 刻み) を通過した時だけ
---   時刻取得 + 描画する。 ホットパスに乗るのはカウンタ更新のみ。
--- * TTY (対話端末) では @\\r@ 上書きの 1 行、 非 TTY (CI ログ等) では
---   10% 刻みの行出力。
--- * 並列 chain からの stderr 競合は 'MVar' の単一描画権で回避
---   (取れなければ描画 skip = 次の間引き通過で追いつく)。
-{-# LANGUAGE StrictData #-}
-{-# LANGUAGE OverloadedStrings #-}
-module Hanalyze.MCMC.Progress
-  ( ProgressSnapshot (..)
-  , formatProgress
-  , newProgressRenderer
-  ) where
-
-import Control.Concurrent.MVar (newMVar, tryTakeMVar, putMVar)
-import Control.Monad (when)
-import Data.IORef (IORef, newIORef, readIORef, atomicModifyIORef')
-import Data.Text (Text)
-import qualified Data.IntMap.Strict as IM
-import qualified Data.Text as T
-import qualified Data.Text.IO as TIO
-import GHC.Clock (getMonotonicTime)
-import Numeric (showFFloat)
-import System.IO (stderr, hIsTerminalDevice, hFlush)
-
-import Hanalyze.MCMC.NUTS (SampleEvent (..))
-
--- ===========================================================================
--- スナップショット + 純粋フォーマッタ
--- ===========================================================================
-
--- | 全 chain 集計の進捗スナップショット (描画と独立な純粋データ)。
-data ProgressSnapshot = ProgressSnapshot
-  { psChains      :: Int     -- ^ 総 chain 数。
-  , psChainsDone  :: Int     -- ^ 完了 chain 数。
-  , psDraw        :: Int     -- ^ 全 chain 合算の消化 iteration 数 (burn-in 込み)。
-  , psTotal       :: Int     -- ^ 全 chain 合算の総 iteration 数。
-  , psWarmup      :: Bool    -- ^ いずれかの chain が warmup (burn-in) 中か。
-  , psDivergent   :: Int     -- ^ divergence 累計 (全 chain)。
-  , psItersPerSec :: Double  -- ^ 開始からの平均スループット (iteration/s)。
-  } deriving (Show, Eq)
-
--- | 進捗 1 行の純粋フォーマッタ。 例:
---
--- @
--- formatProgress (ProgressSnapshot 4 2 3400 8000 True 12 380.0)
---   == "chains 2\/4 done | draw 3400\/8000 (warmup) | div 12 | 380.0 it\/s"
--- @
-formatProgress :: ProgressSnapshot -> Text
-formatProgress ps = T.intercalate " | "
-  [ "chains " <> tshow (psChainsDone ps) <> "/" <> tshow (psChains ps) <> " done"
-  , "draw " <> tshow (psDraw ps) <> "/" <> tshow (psTotal ps)
-      <> (if psWarmup ps then " (warmup)" else "")
-  , "div " <> tshow (psDivergent ps)
-  , T.pack (showFFloat (Just 1) (psItersPerSec ps) "") <> " it/s"
-  ]
-  where tshow = T.pack . show
-
--- ===========================================================================
--- stderr レンダラ
--- ===========================================================================
-
--- | レンダラ内部の可変状態 (chain ごとの消化数 / warmup フラグ / div 累計)。
-data RState = RState
-  { rsDraws :: !(IM.IntMap Int)   -- ^ chain index → 消化 iteration 数。
-  , rsWarm  :: !(IM.IntMap Bool)  -- ^ chain index → 直近 event が burn-in か。
-  , rsDiv   :: !Int               -- ^ divergence 累計。
-  }
-
--- | stderr 進捗レンダラを作る。 返り値 = (chain index 付き callback, 終了処理)。
---
--- 終了処理は最終スナップショットを描画して行を閉じる (TTY では改行を補う)。
--- 'Hanalyze.MCMC.NUTS.nutsChainsStream' に渡す想定:
---
--- @
--- (onSample, finish) <- newProgressRenderer chains (burnIn + iters)
--- chains <- nutsChainsStream m cfg chains initC seed onSample
--- finish
--- @
-newProgressRenderer :: Int   -- ^ 総 chain 数
-                    -> Int   -- ^ chain あたりの総 iteration 数 (burn-in 込み)
-                    -> IO (Int -> SampleEvent -> IO (), IO ())
-newProgressRenderer nChains perChain = do
-  isTTY    <- hIsTerminalDevice stderr
-  t0       <- getMonotonicTime
-  stRef    <- newIORef (RState IM.empty IM.empty 0)
-  lastPct  <- newIORef (-1 :: Int)   -- 非 TTY の 10% 刻み判定
-  drawLock <- newMVar ()             -- 単一描画権
-  let totalAll = nChains * perChain
-      stride   = max 1 (totalAll `div` 200)   -- ~0.5% 刻みで描画候補
-
-      snapshot :: RState -> Double -> ProgressSnapshot
-      snapshot st now =
-        let drawn = sum (IM.elems (rsDraws st))
-            done  = IM.size (IM.filter (>= perChain) (rsDraws st))
-            warm  = or (IM.elems (rsWarm st))
-            dt    = max 1e-9 (now - t0)
-        in ProgressSnapshot
-             { psChains = nChains, psChainsDone = done
-             , psDraw = drawn, psTotal = totalAll
-             , psWarmup = warm, psDivergent = rsDiv st
-             , psItersPerSec = fromIntegral drawn / dt
-             }
-
-      -- 描画権が取れた時だけ描画 (競合時は skip・次の間引きで追いつく)。
-      render :: Bool -> IO ()
-      render final = do
-        got <- tryTakeMVar drawLock
-        case got of
-          Nothing -> pure ()
-          Just () -> do
-            st  <- readIORef stRef
-            now <- getMonotonicTime
-            let snap = snapshot st now
-                line = formatProgress snap
-            if isTTY
-              then do
-                TIO.hPutStr stderr ("\r" <> line)
-                when final (TIO.hPutStr stderr "\n")
-                hFlush stderr
-              else do
-                -- 非 TTY: 10% 境界を跨いだ時 (or 終了時) だけ 1 行出す。
-                let pct10 = (10 * psDraw snap) `div` max 1 totalAll
-                prev <- readIORef lastPct
-                when (pct10 > prev || final) $ do
-                  atomicModifyIORef' lastPct (\p -> (max p pct10, ()))
-                  TIO.hPutStrLn stderr line
-                  hFlush stderr
-            putMVar drawLock ()
-
-      onSample :: Int -> SampleEvent -> IO ()
-      onSample i ev = do
-        n <- atomicModifyIORef' stRef $ \st ->
-          let st' = RState
-                { rsDraws = IM.insertWith (+) i 1 (rsDraws st)
-                , rsWarm  = IM.insert i (seIsBurnIn ev) (rsWarm st)
-                , rsDiv   = rsDiv st + (if seDivergent ev then 1 else 0)
-                }
-          in (st', sum (IM.elems (rsDraws st')))
-        when (n `mod` stride == 0) (render False)
-
-  pure (onSample, render True)
diff --git a/src/Hanalyze/MCMC/SMC.hs b/src/Hanalyze/MCMC/SMC.hs
deleted file mode 100644
--- a/src/Hanalyze/MCMC/SMC.hs
+++ /dev/null
@@ -1,266 +0,0 @@
--- |
--- Module      : Hanalyze.MCMC.SMC
--- Description : Tempered target による Sequential Monte Carlo (SMC) サンプラー
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Sequential Monte Carlo (SMC) sampler with tempered targets.
---
--- Implements a particle-based sampler that bridges from a broad initial
--- distribution to the full posterior @π(θ) ∝ p(θ) · L(θ)@ via a sequence
--- of intermediate targets @π_t(θ) ∝ p(θ) · L(θ)^β_t@, where
--- @β_0 = 0 → β_T = 1@.
---
--- Reference: Del Moral, Doucet, Jasra (2006) "Sequential Monte Carlo
--- samplers". JRSSB 68:411-436.
---
--- ## アルゴリズム概要 (Phase 29-A1)
---
--- 1. **Init**: N 個の粒子を @init_@ を中心とする広い Gaussian cloud から
---    サンプル (= 近似 prior)
--- 2. **Tempering loop** (t = 1..T):
---    a. **Weight**: 重み更新 @w_i ∝ exp((β_t − β_{t-1}) · logL(θ_i))@
---    b. **log marginal contribution**: @log(mean w_i)@ を累積
---    c. **Resample**: ESS = @(Σw)² / Σw²@ が閾値以下なら systematic resampling
---    d. **Move**: 各粒子に対し K 回の MH 移動 (target = π_t、 random walk
---       proposal)
--- 3. **Output**: 最終粒子集合を 'Chain' として返す + log marginal likelihood
---    の推定値
---
--- ## NUTS / MH との位置付け
---
--- SMC の advantage:
---
---   * 並列性が高い (粒子間は独立、 移動が並列化可能)
---   * 多峰分布で chain がはまりにくい (= temperature annealing)
---   * **log marginal likelihood の副産物推定**: Bridge Sampling より
---     軽量で取れる (= Bayes Factor / BMA の前処理に使える)
---
--- SMC の disadvantage:
---
---   * 単峰分布なら NUTS の方が effective sample size / 時間 で有利
---   * temperature schedule の選択が結果に影響
---
--- Phase 29-A2 Bridge Sampling は本 SMC の log marginal 推定の **独立な
--- 検証手段** として使う (両者で 5% 以内一致なら確からしい)。
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes        #-}
-{-# LANGUAGE BangPatterns      #-}
-module Hanalyze.MCMC.SMC
-  ( SMCConfig (..)
-  , defaultSMCConfig
-  , SMCResult (..)
-  , smc
-  , smcPure
-  ) where
-
-import           Control.Monad             (forM, replicateM, foldM)
-import           Control.Monad.Primitive   (PrimMonad, PrimState)
-import           Control.Monad.ST          (runST)
-import qualified Data.Map.Strict           as Map
-import           Data.List                 (sort)
-import           Data.Text                 (Text)
-import           Data.Word                 (Word32)
-import qualified Data.Vector               as V
-import qualified Data.Vector.Unboxed       as VU
-import           System.Random.MWC         (Gen, uniform, initialize)
-import           System.Random.MWC.Distributions (normal)
-
-import           Hanalyze.Model.HBM        (ModelP, Params, logPrior, logLikelihood, sampleNames)
-import           Hanalyze.MCMC.Core        (Chain (..))
-
--- ---------------------------------------------------------------------------
--- Configuration
--- ---------------------------------------------------------------------------
-
--- | SMC configuration.
-data SMCConfig = SMCConfig
-  { smcNParticles   :: !Int     -- ^ N: 粒子数 (典型 500-2000)
-  , smcNSteps       :: !Int     -- ^ T: temperature step 数 (典型 10-50)
-  , smcMHIterations :: !Int     -- ^ K: 各 temperature 内の MH 移動 回数 (典型 5-20)
-  , smcMHStepSize   :: !(Map.Map Text Double)  -- ^ Random walk MH の per-param std
-  , smcInitJitter   :: !Double  -- ^ 初期粒子を init_ から散らす Gaussian σ (typical 2-5)
-  , smcESSThreshold :: !Double  -- ^ 0..1、 ESS < N · threshold で resample (typical 0.5)
-  } deriving (Show)
-
--- | Default: N=500、 T=20、 K=10、 step=0.5、 jitter σ=3、 ESS threshold=0.5。
-defaultSMCConfig :: [Text] -> SMCConfig
-defaultSMCConfig names = SMCConfig
-  { smcNParticles   = 500
-  , smcNSteps       = 20
-  , smcMHIterations = 10
-  , smcMHStepSize   = Map.fromList [(n, 0.5) | n <- names]
-  , smcInitJitter   = 3.0
-  , smcESSThreshold = 0.5
-  }
-
--- | SMC の結果。 粒子を Chain 形に詰めた posterior 推定 + log marginal +
--- temperature step ごとの ESS 履歴。
---
--- **重要 (Phase 29-A1)**: 'smcLogMarginal' は **初期粒子が prior から
--- サンプルされていることを仮定** した推定値。 本実装は init_ を中心とする
--- jittered Gaussian から初期粒子を作るため、 prior が広いと bias する。
--- 厳密な log marginal が必要な場合は Phase 29-A2 'Hanalyze.Stat.BridgeSampling.bridgeSampling'
--- を使用すること (SMC chain を入力に独立に推定する)。 SMC の primary 用途は
--- **多峰 posterior の効率的なサンプリング**。
-data SMCResult = SMCResult
-  { smcChain        :: !Chain
-  , smcLogMarginal  :: !Double
-  , smcESSHistory   :: ![Double]
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- 公開 API
--- ---------------------------------------------------------------------------
-
--- | SMC を実行。 'init_' を中心に initial particles を散らし、 linear
--- temperature schedule (β_t = t/T) で posterior に温めていく。
-smc :: forall r m. PrimMonad m => ModelP r -> SMCConfig -> Params -> Gen (PrimState m) -> m SMCResult
-smc model cfg init_ gen = do
-  let n      = smcNParticles cfg
-      tT     = smcNSteps cfg
-      names  = sampleNames model
-      steps  = smcMHStepSize cfg
-      jitter = smcInitJitter cfg
-  -- 1. Init particles: init_ + N(0, jitter · stepSizes_i)
-  particles0 <- replicateM n (jitterInit names jitter steps init_ gen)
-  let betas = [ fromIntegral t / fromIntegral tT | t <- [0 .. tT] ]   -- [0, 1/T, .., 1]
-      betaSteps = zip betas (tail betas)                              -- [(β_{t-1}, β_t)]
-
-  -- 2. Tempering loop
-  (finalParticles, logMarg, essHist) <-
-    foldM (stepTemper model steps (smcMHIterations cfg) (smcESSThreshold cfg) gen n)
-          (particles0, 0.0 :: Double, [])
-          betaSteps
-
-  let accepted = chainAcceptedAcc (length finalParticles * tT * smcMHIterations cfg)
-      total    = length finalParticles * tT * smcMHIterations cfg
-  pure SMCResult
-    { smcChain = Chain
-        { chainSamples     = finalParticles
-        , chainAccepted    = accepted
-        , chainTotal       = total
-        , chainEnergy      = []
-        , chainDivergences = []
-        , chainTreeDepths  = []
-        }
-    , smcLogMarginal = logMarg
-    , smcESSHistory  = reverse essHist
-    }
-  where
-    -- 受理数は本実装では追跡しない (= 0 を入れて acceptanceRate は意味なし)
-    chainAcceptedAcc _ = 0
-
--- | Phase 50: 純粋・決定的な SMC (seed → 確定 SMCResult・IO 不要)。 'smc' の ST/seed 版。
-smcPure :: ModelP r -> SMCConfig -> Params -> Word32 -> SMCResult
-smcPure model cfg initP seed =
-  runST (initialize (V.singleton seed) >>= smc model cfg initP)
-
--- | 1 ステップの tempering:
---   * 重み計算 + log marginal 累積
---   * ESS 判定して resample
---   * K 回の MH 移動 (target = π_t = p(θ) · L(θ)^β_t)
-stepTemper
-  :: forall r m. PrimMonad m => ModelP r
-  -> Map.Map Text Double         -- ^ step sizes
-  -> Int                         -- ^ K
-  -> Double                      -- ^ ESS threshold
-  -> Gen (PrimState m)
-  -> Int                         -- ^ N (元の粒子数、 resample で N keep)
-  -> ([Params], Double, [Double]) -- ^ (粒子、 累積 log marginal、 ESS 履歴)
-  -> (Double, Double)            -- ^ (β_{t-1}, β_t)
-  -> m ([Params], Double, [Double])
-stepTemper model steps k essThr gen n (particles, logMarg, essHist) (b0, b1) = do
-  let dbeta   = b1 - b0
-      logLs   = map (logLikelihood model) particles
-      logWs   = map (dbeta *) logLs               -- log incremental weights
-      logSumW = logSumExp logWs
-      logMean = logSumW - log (fromIntegral (length particles))
-      ws      = map (\lw -> exp (lw - logSumW)) logWs   -- normalized weights
-      ess     = if sum (map (** 2) ws) == 0 then 0
-                  else 1 / sum (map (** 2) ws)
-      logMarg' = logMarg + logMean
-
-  -- Resample if ESS < threshold · N
-  resampled <-
-    if ess < essThr * fromIntegral n
-      then systematicResample particles ws n gen
-      else pure particles
-
-  -- Move with K MH iterations
-  moved <- moveK model steps b1 k resampled gen
-  pure (moved, logMarg', ess : essHist)
-
--- | systematic resampling (= particle filter standard)。
-systematicResample
-  :: forall m. PrimMonad m => [Params] -> [Double] -> Int -> Gen (PrimState m) -> m [Params]
-systematicResample particles ws n gen = do
-  u0 <- uniform gen :: m Double
-  let total = sum ws
-      ws' = map (/ total) ws  -- normalize
-      cdf = scanl1 (+) ws'
-      ps  = [ (fromIntegral i + u0) / fromIntegral n | i <- [0 .. n - 1] ]
-      pick p = pickAt p cdf particles
-  pure (map pick ps)
-  where
-    pickAt p (c : cs) (x : xs)
-      | p <= c    = x
-      | otherwise = pickAt p cs xs
-    pickAt _ _ (x : _) = x  -- fallback (numeric edge)
-    pickAt _ _ []      = error "systematicResample: empty particle list"
-
--- | K 回の Random Walk MH 移動。 target は @log π_t = logPrior + β · logLik@。
-moveK
-  :: forall r m. PrimMonad m => ModelP r
-  -> Map.Map Text Double
-  -> Double          -- ^ β
-  -> Int
-  -> [Params]
-  -> Gen (PrimState m)
-  -> m [Params]
-moveK model steps beta k particles gen =
-  mapM (mhKSteps model steps beta k gen) particles
-
-mhKSteps
-  :: forall r m. PrimMonad m => ModelP r
-  -> Map.Map Text Double
-  -> Double
-  -> Int
-  -> Gen (PrimState m)
-  -> Params
-  -> m Params
-mhKSteps model steps beta k gen p0 = go k p0
-  where
-    target p = logPrior model p + beta * logLikelihood model p
-    go 0 p = pure p
-    go i p = do
-      let names = Map.keys p
-      proposed <- fmap Map.fromList $ forM names $ \n -> do
-        let s   = Map.findWithDefault 1.0 n steps
-            cur = Map.findWithDefault 0.0 n p
-        eps <- normal 0 s gen
-        pure (n, cur + eps)
-      let logA = target proposed - target p
-      u <- uniform gen :: m Double
-      let !next = if log u < logA then proposed else p
-      go (i - 1) next
-
--- ---------------------------------------------------------------------------
--- Helpers
--- ---------------------------------------------------------------------------
-
-jitterInit
-  :: forall m. PrimMonad m => [Text] -> Double -> Map.Map Text Double -> Params -> Gen (PrimState m) -> m Params
-jitterInit names jitter steps init_ gen =
-  fmap Map.fromList $ forM names $ \n -> do
-    let s   = jitter * Map.findWithDefault 1.0 n steps
-        cur = Map.findWithDefault 0.0 n init_
-    eps <- normal 0 s gen
-    pure (n, cur + eps)
-
--- | Numerically stable log-sum-exp.
-logSumExp :: [Double] -> Double
-logSumExp [] = -1 / 0
-logSumExp xs =
-  let m = maximum xs
-  in m + log (sum [ exp (x - m) | x <- xs ])
diff --git a/src/Hanalyze/MCMC/Slice.hs b/src/Hanalyze/MCMC/Slice.hs
deleted file mode 100644
--- a/src/Hanalyze/MCMC/Slice.hs
+++ /dev/null
@@ -1,166 +0,0 @@
--- |
--- Module      : Hanalyze.MCMC.Slice
--- Description : Slice sampler (Neal 2003) — 受理率調整不要な単変量サンプリング法
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Slice sampler (Neal 2003) — a univariate method with no acceptance-rate
--- tuning.
---
--- Each iteration:
---
---   1. Draw @log_y = log p(θ) − Exp(1)@ from the current log-density.
---   2. Build a horizontal slice @[L, R]@ along each axis via stepping-out.
---   3. Shrink: draw @θ_i'@ uniformly from @[L, R]@ and accept when
---      @log p > log_y@.
---
--- One iteration is a Gibbs-style sweep over every coordinate. Like
--- HMC/NUTS no gradient is required, but each sweep involves many
--- log-density evaluations.
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-module Hanalyze.MCMC.Slice
-  ( SliceConfig (..)
-  , defaultSliceConfig
-  , slice
-  , sliceChains
-  , slicePure
-  , sliceChainsPure
-  ) where
-
-import Control.Concurrent.Async (mapConcurrently)
-import Control.Monad (forM, replicateM, when)
-import Control.Monad.Primitive (PrimMonad, PrimState)
-import Control.Monad.ST (runST)
-import Control.Parallel.Strategies (parList, rdeepseq, using)
-import Data.Primitive.MutVar
-import Data.Word (Word32)
-import qualified Data.Map.Strict as Map
-import qualified Data.Vector as V
-import Data.Map.Strict (Map)
-import Data.Text (Text)
-import System.Random.MWC (Gen, GenIO, uniform, initialize)
-import System.Random.MWC.Distributions (exponential)
-
-import Hanalyze.Model.HBM (ModelP, Params, logJoint, sampleNames)
-import Hanalyze.MCMC.Core (Chain (..), spawnGen)
-
--- | Slice-sampler configuration.
-data SliceConfig = SliceConfig
-  { sliceIterations :: Int                -- ^ Total iterations (burn-in included).
-  , sliceBurnIn     :: Int                -- ^ Burn-in iterations to discard.
-  , sliceWidths     :: Map Text Double    -- ^ Initial stepping-out width @w@
-                                          --   per coordinate (default 1.0).
-  , sliceMaxSteps   :: Int                -- ^ Maximum number of stepping-out
-                                          --   steps (safety bound).
-  } deriving (Show)
-
--- | Default configuration: 1000 iterations, 200 burn-in, width 1.0 per
--- parameter, max stepping-out 50.
-defaultSliceConfig :: [Text] -> SliceConfig
-defaultSliceConfig names = SliceConfig
-  { sliceIterations = 1000
-  , sliceBurnIn     = 200
-  , sliceWidths     = Map.fromList [(n, 1.0) | n <- names]
-  , sliceMaxSteps   = 50
-  }
-
--- | Run the slice sampler. One iteration updates every coordinate in
--- turn (Gibbs-style sweep).
-slice :: forall r m. PrimMonad m => ModelP r -> SliceConfig -> Params -> Gen (PrimState m) -> m Chain
-slice model cfg init_ gen = do
-  let names    = sampleNames model
-      total    = sliceBurnIn cfg + sliceIterations cfg
-      widths   = sliceWidths cfg
-      maxStep  = sliceMaxSteps cfg
-
-      logP :: Params -> Double
-      logP = logJoint model
-
-  samplesRef  <- newMutVar []
-  acceptedRef <- newMutVar (0 :: Int)
-
-  -- 1 coordinate 更新 (slice sampling on one axis)
-  let updateOne :: Text -> Params -> m Params
-      updateOne nm cur = do
-        let w   = Map.findWithDefault 1.0 nm widths
-            x0  = Map.findWithDefault 0.0 nm cur
-            pAt v = logP (Map.insert nm v cur)
-        -- 水平スライス: log_y = log p(θ) - Exp(1)
-        e <- exponential 1.0 gen
-        let logY = pAt x0 - e
-        -- Stepping out
-        u <- uniform gen
-        let l0   = x0 - w * (u :: Double)
-            r0   = l0 + w
-        u2 <- uniform gen
-        let kL    = floor (fromIntegral maxStep * (u2 :: Double)) :: Int
-            kR    = maxStep - 1 - kL
-            expandLeft k l
-              | k <= 0 || pAt l <= logY = return l
-              | otherwise = expandLeft (k - 1) (l - w)
-            expandRight k r
-              | k <= 0 || pAt r <= logY = return r
-              | otherwise = expandRight (k - 1) (r + w)
-        l1 <- expandLeft  kL l0
-        r1 <- expandRight kR r0
-        -- Shrinkage
-        let shrink l r = do
-              uS <- uniform gen
-              let xNew = l + (uS :: Double) * (r - l)
-              if pAt xNew > logY
-                then return xNew
-                else
-                  if xNew < x0
-                    then shrink xNew r
-                    else shrink l xNew
-        xNew <- shrink l1 r1
-        modifyMutVar' acceptedRef (+1)
-        return (Map.insert nm xNew cur)
-
-  let sweep current = foldr (\_ _ -> id) id [] `seq`
-                       sweepGo names current
-        where
-          sweepGo []     c = return c
-          sweepGo (n:ns) c = do c' <- updateOne n c
-                                sweepGo ns c'
-
-  let loop 0 current = return current
-      loop i current = do
-        next <- sweep current
-        when (i <= sliceIterations cfg) $
-          modifyMutVar' samplesRef (next :)
-        loop (i - 1) next
-
-  _ <- loop total init_
-  samples  <- fmap reverse (readMutVar samplesRef)
-  accepted <- readMutVar acceptedRef
-  return Chain
-    { chainSamples     = samples
-    , chainAccepted    = accepted
-    , chainTotal       = total * length names
-    , chainEnergy      = []
-    , chainDivergences = []
-    , chainTreeDepths  = []
-    }
-
--- | Run 'slice' on @numChains@ parallel chains.
-sliceChains :: ModelP r -> SliceConfig -> Int -> Params -> GenIO -> IO [Chain]
-sliceChains model cfg numChains initP baseGen = do
-  gens <- replicateM numChains (spawnGen baseGen)
-  mapConcurrently (\g -> slice model cfg initP g) gens
-
--- | Phase 50: 純粋・決定的な slice sampler (seed → 確定 Chain)。
-slicePure :: ModelP r -> SliceConfig -> Params -> Word32 -> Chain
-slicePure model cfg initP seed =
-  runST (initialize (V.singleton seed) >>= slice model cfg initP)
-
--- | Phase 50: 純粋・決定的な multi-chain slice。 子 seed を純粋導出し @parList rdeepseq@ で並列。
-sliceChainsPure :: ModelP r -> SliceConfig -> Int -> Params -> Word32 -> [Chain]
-sliceChainsPure model cfg numChains initP seed =
-  let childSeeds :: [Word32]
-      childSeeds = runST $ do
-        g <- initialize (V.singleton seed)
-        replicateM numChains (uniform g)
-      chains = [ slicePure model cfg initP s | s <- childSeeds ]
-  in chains `using` parList rdeepseq
diff --git a/src/Hanalyze/Math/HSIC.hs b/src/Hanalyze/Math/HSIC.hs
deleted file mode 100644
--- a/src/Hanalyze/Math/HSIC.hs
+++ /dev/null
@@ -1,121 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Math.HSIC
--- Description : Hilbert-Schmidt Independence Criterion による kernel 法ベースの独立性検定統計量
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Hilbert-Schmidt Independence Criterion (HSIC、 Gretton et al. 2005)。
---
--- ## モチベーション
---
--- 確率変数 X, Y の独立性を測る kernel 法ベースの統計量。 線形相関や
--- partial correlation と違い、 非線形依存も検出できる。 LiNGAM 系統
--- (特に ParceLiNGAM bottom-up 探索) で「残差と他変数の独立性」 を判定する
--- 中核ツール。
---
--- ## 統計量 (biased empirical estimator)
---
--- > HSIC_b(X, Y) = (1 / n²) · tr(K_X · H · K_Y · H)
---
--- ここで K_X[i,j] = k(x_i, x_j) は RBF kernel、 H = I − (1/n) · 1 1ᵀ は
--- 中心化行列。 X ⊥ Y の下で HSIC_b → 0、 強依存で正値。
---
--- ## bandwidth の決め方
---
--- median heuristic: σ = median(‖x_i − x_j‖) (i ≠ j、 サンプル間距離の中央値)。
--- cdt15/lingam を含む慣用設定で、 サンプル数のオーダー依存が小さく robust。
---
--- ## 集約 (ParceLiNGAM での使い方)
---
--- 多次元 X (列が変数) と単変量残差 R の依存判定は、 各列 X_i ごとに
--- HSIC(X_i, R) を計算して **総和 (= aggregate)** を取る。 cdt15/lingam の
--- 内部実装は Fisher 法で p 値を合成するが、 v0.2 では p 値を使わず統計量の
--- 総和で相対比較する (実用上は relative scoring が機能する)。
---
--- ## リファレンス
---
--- Gretton et al. (2005) "Measuring statistical dependence with Hilbert-Schmidt
--- norms", ALT 2005. cdt15/lingam の `lingam/hsic.py`。
-module Hanalyze.Math.HSIC
-  ( hsicBiased
-  , hsicRBF
-  , medianBandwidth
-  , hsicAggregate
-  ) where
-
-import qualified Numeric.LinearAlgebra      as LA
-import qualified Hanalyze.Stat.KernelDist   as KD
-import           Data.List                  (sort)
-
--- ===========================================================================
--- カーネル行列構築
--- ===========================================================================
-
--- | RBF (Gaussian) カーネル行列 K[i, j] = exp(−‖x_i − x_j‖² / (2σ²))。
---   入力 @x@ は @n × p@ (行がサンプル、 列が変数)。
-rbfKernelMatrix :: Double -> LA.Matrix Double -> LA.Matrix Double
-rbfKernelMatrix sigma x =
-  let !twoSig2 = 2 * sigma * sigma
-      !d2      = KD.pairwiseSqDist x
-  in LA.cmap (\v -> exp (negate v / twoSig2)) d2
-
--- | サンプル間距離の中央値 (median heuristic for kernel bandwidth)。
---   対角 (距離 0) は除外し、 上三角の値だけを集めて中央値を取る。
---   退化 (median = 0) の場合は 1.0 にフォールバック。
-medianBandwidth :: LA.Matrix Double -> Double
-medianBandwidth x =
-  let !d2    = KD.pairwiseSqDist x
-      !n     = LA.rows d2
-      vals   = [ LA.atIndex d2 (i, j)
-               | i <- [0 .. n - 1], j <- [i + 1 .. n - 1] ]
-      sorted = sort vals
-      med    = case sorted of
-                 [] -> 1.0
-                 _  -> let !m = length sorted `div` 2
-                       in sorted !! m
-      sig    = sqrt (max med 1.0e-12)
-  in if sig > 0 then sig else 1.0
-
--- ===========================================================================
--- HSIC 統計量
--- ===========================================================================
-
--- | biased empirical HSIC を K, L から計算: (1/n²) · tr(K_c · L_c)。
---   K_c = H K H、 L_c = H L H、 H = I − (1/n) · 1 1ᵀ。
---   ※ tr(K_c L_c) = tr(K_c L) (中心化の冪等性により) なので片側中心化で済む。
-hsicWithKernels :: LA.Matrix Double -> LA.Matrix Double -> Double
-hsicWithKernels k l =
-  let !n     = LA.rows k
-      !nD    = fromIntegral n
-      !h     = LA.ident n - LA.scale (1.0 / nD)
-                   (LA.konst 1.0 (n, n))
-      !kc    = h LA.<> k LA.<> h
-      !prod  = kc LA.<> l
-      !tr    = sum [ LA.atIndex prod (i, i) | i <- [0 .. n - 1] ]
-  in tr / (nD * nD)
-
--- | RBF kernel + median bandwidth で biased HSIC を計算。
---   入力 @x@, @y@ は @n × p@ / @n × q@ (行が共通サンプル、 列が変数)。
-hsicRBF :: LA.Matrix Double -> LA.Matrix Double -> Double
-hsicRBF x y =
-  let !sx = medianBandwidth x
-      !sy = medianBandwidth y
-      !k  = rbfKernelMatrix sx x
-      !l  = rbfKernelMatrix sy y
-  in hsicWithKernels k l
-
--- | bias HSIC を @hsicRBF@ で計算する公開エイリアス。
-hsicBiased :: LA.Matrix Double -> LA.Matrix Double -> Double
-hsicBiased = hsicRBF
-
--- | 多次元 @X@ (n × p) と単変量 @r@ (長さ n) の依存度を、
---   各列ごとの HSIC を **総和** して集約する。 ParceLiNGAM bottom-up の
---   exogenous 判定に使う (cdt15/lingam の Fisher 法と同趣旨、 ただし p 値
---   合成ではなく統計量の総和)。
-hsicAggregate :: LA.Matrix Double -> LA.Vector Double -> Double
-hsicAggregate x r =
-  let !p    = LA.cols x
-      !rMat = LA.asColumn r
-  in sum [ hsicRBF (LA.asColumn (LA.flatten (x LA.¿ [j]))) rMat
-         | j <- [0 .. p - 1] ]
diff --git a/src/Hanalyze/Math/Hungarian.hs b/src/Hanalyze/Math/Hungarian.hs
deleted file mode 100644
--- a/src/Hanalyze/Math/Hungarian.hs
+++ /dev/null
@@ -1,132 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Math.Hungarian
--- Description : Hungarian (Kuhn-Munkres) 法による正方割当問題の最小コスト解 (O(n³))
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Hungarian (Kuhn-Munkres) アルゴリズムによる正方割当問題の最小コスト解。
---
--- ## 入出力
---
--- 入力: コスト行列 C (n × n、 各成分は実数、 inf 不可)。
--- 出力: 行 i に割当てる列 j からなる長さ n のベクトル @assignment[i] = j@。
--- 目的: Σᵢ C[i, assignment[i]] を最小化、 かつ assignment が **全単射**。
---
--- ## 実装
---
--- e-maxx の "Hungarian algorithm in O(V³)" 系統 (Jonker-Volgenant の
--- shortest augmenting path 方式)。 双対変数 u, v と potential を保持して
--- 1 行ずつ augment する。 ST + mutable Vector で内部状態を管理し、 純関数
--- 'hungarianMin' として API 公開する。
---
--- ## 用途
---
--- ICA-LiNGAM (Shimizu 2006) の行/列順列下三角化で、 W 行列の対角成分絶対値
--- を最大化する割当を求めるのに使う。 コスト C[i, j] = 1 / (|W[i, j]| + ε)
--- で 'hungarianMin' を呼ぶと、 グリーディと違って大域最適解が得られる。
--- p > 10 でグリーディが劣化するケースを救う。
---
--- ## 計算量
---
--- O(n³)。 n ≤ 200 程度では実用上問題なし (測定: n=100 で数十 ms オーダー、
--- 計測値ではなく目安)。
-module Hanalyze.Math.Hungarian
-  ( hungarianMin
-  ) where
-
-import           Control.Monad               (forM_, unless, when)
-import           Control.Monad.ST            (ST, runST)
-import           Data.STRef
-import qualified Data.Vector.Unboxed         as VU
-import qualified Data.Vector.Unboxed.Mutable as MV
-import qualified Numeric.LinearAlgebra       as LA
-
--- ===========================================================================
--- 公開 API
--- ===========================================================================
-
--- | 正方コスト行列 C (n × n) に対する最小コスト割当。
---   戻り値 @v@ は @v VU.! i = j@ で「行 i が列 j に割当てられる」 意味。
-hungarianMin :: LA.Matrix Double -> VU.Vector Int
-hungarianMin cost
-  | n == 0    = VU.empty
-  | otherwise = runST (runHungarian n cost)
-  where
-    n = LA.rows cost
-
--- ===========================================================================
--- 内部実装 (ST monad、 1-indexed の慣例で size n+1 配列を確保)
--- ===========================================================================
-
-runHungarian :: Int -> LA.Matrix Double -> ST s (VU.Vector Int)
-runHungarian n cost = do
-  let !inf = 1.0e300 :: Double
-  u   <- MV.replicate (n + 1) (0 :: Double)
-  v   <- MV.replicate (n + 1) (0 :: Double)
-  p   <- MV.replicate (n + 1) (0 :: Int)     -- p[j] = 列 j に割当てた行
-  way <- MV.replicate (n + 1) (0 :: Int)
-
-  forM_ [1 .. n] $ \i -> do
-    MV.write p 0 i
-    j0Ref <- newSTRef (0 :: Int)
-    minv  <- MV.replicate (n + 1) inf
-    used  <- MV.replicate (n + 1) False
-
-    let -- shortest-path-tree 拡張 1 ステップ
-        step = do
-          j0 <- readSTRef j0Ref
-          MV.write used j0 True
-          i0 <- MV.read p j0
-          deltaRef <- newSTRef inf
-          j1Ref    <- newSTRef (0 :: Int)
-          forM_ [1 .. n] $ \j -> do
-            isU <- MV.read used j
-            unless isU $ do
-              ui0 <- MV.read u i0
-              vj  <- MV.read v j
-              let !cur = LA.atIndex cost (i0 - 1, j - 1) - ui0 - vj
-              mj <- MV.read minv j
-              when (cur < mj) $ do
-                MV.write minv j cur
-                MV.write way  j j0
-              mj' <- MV.read minv j
-              d   <- readSTRef deltaRef
-              when (mj' < d) $ do
-                writeSTRef deltaRef mj'
-                writeSTRef j1Ref j
-          delta <- readSTRef deltaRef
-          forM_ [0 .. n] $ \j -> do
-            isU <- MV.read used j
-            if isU
-              then do
-                pj <- MV.read p j
-                upj <- MV.read u pj
-                MV.write u pj (upj + delta)
-                vj <- MV.read v j
-                MV.write v j (vj - delta)
-              else do
-                mj <- MV.read minv j
-                MV.write minv j (mj - delta)
-          j1 <- readSTRef j1Ref
-          writeSTRef j0Ref j1
-          pj1 <- MV.read p j1
-          when (pj1 /= 0) step
-    step
-
-    -- augmenting path に沿って割当を更新
-    let aug = do
-          j0 <- readSTRef j0Ref
-          j1 <- MV.read way j0
-          pj1 <- MV.read p j1
-          MV.write p j0 pj1
-          writeSTRef j0Ref j1
-          when (j1 /= 0) aug
-    aug
-
-  -- 結果ベクトルを構築: assignment[i-1] = j-1 (p[j] = i ⇒ row i → col j)
-  result <- MV.replicate n (0 :: Int)
-  forM_ [1 .. n] $ \j -> do
-    pj <- MV.read p j
-    when (pj >= 1) $ MV.write result (pj - 1) (j - 1)
-  VU.freeze result
diff --git a/src/Hanalyze/Math/ICA.hs b/src/Hanalyze/Math/ICA.hs
deleted file mode 100644
--- a/src/Hanalyze/Math/ICA.hs
+++ /dev/null
@@ -1,180 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns      #-}
--- |
--- Module      : Hanalyze.Math.ICA
--- Description : FastICA (Hyvärinen 1999) による独立成分分析 (whitening + fixed-point iteration)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- FastICA (Hyvärinen 1999) による独立成分分析。
---
--- 観測 X = A · S (n_samples × p)、 S が互いに独立な非ガウシアン成分のとき、
--- A を推定して S = A⁻¹ · X を抽出する。 ICA-LiNGAM (Shimizu 2006) の前段
--- および信号分離一般に使う。
---
--- ## アルゴリズム
---
--- 1. **Centering**: X の各列を中心化
--- 2. **Whitening**: X の covariance を eigen 分解して
---    @Z = E · D^(-1/2) · Eᵀ · X@ を作る (Z の cov = I)
--- 3. **Fixed-point iteration** (per component): 任意の w から始めて
---    @w⁺ = E[Z · g(wᵀZ)] - E[g'(wᵀZ)] · w@、 正規化、 直交化 (デフレーション)、
---    収束 (|wᵀwᵒˡᵈ| ≈ 1) まで繰返し
--- 4. **回収**: 全成分の row 構成 W に対し、 S = W · Z、 A = pinv(W) (whitened
---    座標から元座標への戻し変換は別途)
---
--- non-linearity g としては logcosh (Hyvärinen 標準) を採用:
--- g(u) = tanh(a·u)、 g'(u) = a·(1 - tanh²(a·u))、 a = 1.0
---
--- ## 出力
---
--- 'ICAResult' は分離行列 W (p × p, whitened 座標)、 mixing 行列 A (元座標、
--- W · whiten から逆算)、 推定独立成分 S (n × p)、 収束情報を持つ。
-module Hanalyze.Math.ICA
-  ( ICAConfig (..)
-  , ICAResult (..)
-  , defaultICAConfig
-  , fitICA
-  , fitICAGen
-  , fitICAPure
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import qualified Data.Vector           as V
-import qualified System.Random.MWC     as MWC
-import           Control.Monad         (forM_, when)
-import           Control.Monad.Primitive (PrimMonad, PrimState)
-import           Control.Monad.ST      (runST)
-import           Data.Primitive.MutVar (newMutVar, readMutVar, writeMutVar)
-import           System.Random.MWC.Distributions (standard)
-
--- ===========================================================================
--- 設定
--- ===========================================================================
-
-data ICAConfig = ICAConfig
-  { icaMaxIter   :: !Int
-  , icaTol       :: !Double
-  , icaNumComp   :: !(Maybe Int)
-    -- ^ 抽出する成分数。 'Nothing' で全成分 (= p)
-  , icaSeed      :: !(Maybe Int)
-  } deriving (Show)
-
-defaultICAConfig :: ICAConfig
-defaultICAConfig = ICAConfig
-  { icaMaxIter = 200
-  , icaTol     = 1e-4
-  , icaNumComp = Nothing
-  , icaSeed    = Just 12345
-  }
-
-data ICAResult = ICAResult
-  { icaW           :: !(LA.Matrix Double)
-    -- ^ whitened 空間での分離行列 (p × p)
-  , icaA           :: !(LA.Matrix Double)
-    -- ^ 元 X 空間における推定 mixing 行列。 X ≈ S · Aᵀ + mean
-  , icaUnmixing    :: !(LA.Matrix Double)
-    -- ^ 元 X 空間における分離行列 (S = (X - mean) · unmixingᵀ)
-  , icaS           :: !(LA.Matrix Double)
-    -- ^ 推定独立成分 (n × k)
-  , icaMean        :: !(LA.Vector Double)
-    -- ^ 列平均 (centering 用)
-  , icaConverged   :: !Bool
-  , icaIterations  :: !Int
-  } deriving (Show)
-
--- ===========================================================================
--- 主実装
--- ===========================================================================
-
--- | FastICA 本体 (Phase 77.C で PrimMonad 一般化)。 Gen を受け取り ST/IO いずれでも動く
---   (IORef→MutVar)。 'fitICA' (IO) / 'fitICAPure' (ST・seed) が gen を作って呼ぶ。
-fitICAGen :: PrimMonad m => ICAConfig -> LA.Matrix Double -> MWC.Gen (PrimState m) -> m ICAResult
-fitICAGen cfg x gen = do
-  let !n  = LA.rows x
-      !p  = LA.cols x
-      !k  = maybe p id (icaNumComp cfg)
-      -- centering
-      means = LA.fromList
-                [ LA.sumElements (x LA.¿ [j]) / fromIntegral n
-                | j <- [0 .. p - 1] ]
-      meanMat = LA.fromRows (replicate n means)
-      xc      = x - meanMat
-      -- whitening: Z = E D^(-1/2) Eᵀ · Xᵀ をしたいが、 hmatrix は行ベクトル
-      -- 規約なので、 共分散行列を求めて eigen 分解する
-      cov     = (LA.tr xc LA.<> xc) / fromIntegral n
-      (d, e)  = LA.eigSH (LA.trustSym cov)
-      -- d : Vector Double, e : Matrix Double (columns are eigenvectors)
-      dInvSqrt = LA.cmap (\v -> if v > 1e-12 then 1 / sqrt v else 0) d
-      whitenMat = e LA.<> LA.diag dInvSqrt LA.<> LA.tr e   -- (p × p)
-      z         = xc LA.<> LA.tr whitenMat                -- (n × p)
-  -- FastICA loop (deflation) — p × p の分離行列 W を 1 行ずつ確定。 gen は引数。
-  wRowsRef <- newMutVar ([] :: [LA.Vector Double])
-  itersRef <- newMutVar (0 :: Int)
-  convRef  <- newMutVar True
-  forM_ [0 .. k - 1] $ \_compIdx -> do
-    -- 初期 w を gauss 乱数で
-    w0Raw <- V.replicateM p (standard gen)
-    let w0 = LA.fromList (V.toList w0Raw)
-    wsExisting <- readMutVar wRowsRef
-    -- 既存成分への直交化
-    let w0Ortho = deflate wsExisting w0
-        w0Norm  = LA.scale (1 / LA.norm_2 w0Ortho) w0Ortho
-    -- fixed point iteration
-    wRef <- newMutVar w0Norm
-    convergedThisRef <- newMutVar False
-    forM_ [1 .. icaMaxIter cfg] $ \iter -> do
-      wOld <- readMutVar wRef
-      isC  <- readMutVar convergedThisRef
-      when (not isC) $ do
-        let wu     = z LA.#> wOld          -- (n,)
-            gWu    = LA.cmap tanh wu
-            gpWu   = LA.cmap (\v -> 1 - tanh v ** 2) wu
-            wNew0  = LA.tr z LA.#> gWu / LA.scalar (fromIntegral n)
-                       - LA.scale (LA.sumElements gpWu / fromIntegral n) wOld
-            wDef   = deflate wsExisting wNew0
-            wNew   = LA.scale (1 / LA.norm_2 wDef) wDef
-            !diff  = abs (abs (wNew `LA.dot` wOld) - 1)
-        writeMutVar wRef wNew
-        writeMutVar itersRef iter
-        when (diff < icaTol cfg) $ writeMutVar convergedThisRef True
-    finalConv <- readMutVar convergedThisRef
-    when (not finalConv) $ writeMutVar convRef False
-    wFinal <- readMutVar wRef
-    writeMutVar wRowsRef (wsExisting ++ [wFinal])
-  ws <- readMutVar wRowsRef
-  let !wMat = LA.fromRows ws                    -- (k × p)、 whitened 空間
-      !sMat = z LA.<> LA.tr wMat                -- (n × k)、 独立成分
-      -- 元 X 空間: unmixing = wMat · whitenMat (k × p)
-      !unmixing = wMat LA.<> whitenMat
-      -- mixing = pseudo-inverse of unmixing  (p × k)
-      !mixing   = LA.pinv unmixing
-  iters <- readMutVar itersRef
-  conv  <- readMutVar convRef
-  pure ICAResult
-    { icaW           = wMat
-    , icaA           = mixing
-    , icaUnmixing    = unmixing
-    , icaS           = sMat
-    , icaMean        = means
-    , icaConverged   = conv
-    , icaIterations  = iters
-    }
-  where
-    deflate :: [LA.Vector Double] -> LA.Vector Double -> LA.Vector Double
-    deflate ws w = foldl (\acc wi -> acc - LA.scale (acc `LA.dot` wi) wi) w ws
-
--- | FastICA (IO)。 'icaSeed' が 'Just' なら決定的、 'Nothing' で system random。
-fitICA :: ICAConfig -> LA.Matrix Double -> IO ICAResult
-fitICA cfg x = do
-  gen <- case icaSeed cfg of
-    Just s  -> MWC.initialize (V.fromList [fromIntegral s])
-    Nothing -> MWC.createSystemRandom
-  fitICAGen cfg x gen
-
--- | FastICA の **seed 純粋版** (Phase 77.C・@df |->@ 用)。 'icaSeed' (既定 12345・'Nothing' は
---   12345 fallback) で 'runST'+MWC。 同 seed で IO 版とビット一致 (乱数列は monad 非依存)。
-fitICAPure :: ICAConfig -> LA.Matrix Double -> ICAResult
-fitICAPure cfg x = runST $ do
-  gen <- MWC.initialize (V.fromList [fromIntegral (maybe 12345 id (icaSeed cfg))])
-  fitICAGen cfg x gen
diff --git a/src/Hanalyze/Model/AFT.hs b/src/Hanalyze/Model/AFT.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/AFT.hs
+++ /dev/null
@@ -1,212 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Model.AFT
--- Description : Accelerated Failure Time (AFT) パラメトリック生存モデル
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Accelerated Failure Time (AFT) parametric survival model.
---
--- AFT は寿命 T の対数を共変量の線形関数として表現する:
---
--- @
--- log T_i = X_i β + σ ε_i
--- @
---
--- ε の分布で family が決まる:
---
---   * 'AFTWeibull'    : ε ~ Gumbel  (生存解析の Weibull AFT)
---   * 'AFTLogNormal'  : ε ~ Normal(0, 1)
---   * 'AFTLogLogistic': ε ~ Logistic(0, 1)
---   * 'AFTExponential': Weibull with σ = 1 を固定
---
--- 右側打ち切り (right censoring) 対応。 推定は対数尤度の最大化を
--- Nelder-Mead で行う (純粋関数のため runIdentity 経由)。
---
--- API:
---
--- > fitAFT     :: AFTDistribution -> Matrix Double -> Vector Double
--- >            -> Vector Bool -> IO (Either Text AFTFit)
--- > predictAFT :: AFTFit -> Matrix Double -> Vector Double  -- 期待寿命
-module Hanalyze.Model.AFT
-  ( AFTDistribution (..)
-  , AFTFit (..)
-  , fitAFT
-  , predictAFT
-  , logS          -- ^ 標準化誤差 z の log 生存関数 (= 生存曲線描画に使用・Phase 68 A5)
-  ) where
-
-import qualified Data.Vector                       as V
-import qualified Numeric.LinearAlgebra             as LA
-import           Data.Text                         (Text)
-import qualified Data.Text                         as T
-import qualified Statistics.Distribution           as SD
-import qualified Statistics.Distribution.Normal    as ND
-
-import           Hanalyze.Optim.NelderMead         (runNelderMeadWith, defaultNMConfig,
-                                                    NMConfig (..))
-import           Hanalyze.Optim.Common             (OptimResult (..), StopCriteria (..))
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
-data AFTDistribution
-  = AFTWeibull
-  | AFTLogNormal
-  | AFTLogLogistic
-  | AFTExponential
-  deriving (Show, Eq)
-
-data AFTFit = AFTFit
-  { aftBeta         :: !(LA.Vector Double)
-  , aftScale        :: !Double            -- ^ scale parameter σ
-  , aftLogLik       :: !Double
-  , aftDistribution :: !AFTDistribution
-  , aftIters        :: !Int
-  } deriving (Show)
-
--- ===========================================================================
--- fit
--- ===========================================================================
-
--- | AFT モデルを MLE で fit する。
---   X: n × p 共変量、 t: n 観測時間 (> 0)、 delta: n failure indicator
---   (True = 観測、 False = 右側打ち切り)。
-fitAFT
-  :: AFTDistribution
-  -> LA.Matrix Double
-  -> LA.Vector Double
-  -> V.Vector Bool
-  -> IO (Either Text AFTFit)
-fitAFT dist x t delta
-  | LA.rows x /= LA.size t || LA.rows x /= V.length delta =
-      pure (Left "fitAFT: input dimensions mismatch")
-  | LA.size t == 0 =
-      pure (Left "fitAFT: empty input")
-  | V.any (<= 0) (V.fromList (LA.toList t)) =
-      pure (Left "fitAFT: t must be > 0")
-  | otherwise = do
-      let p   = LA.cols x
-          -- intercept-only start: β_0 = mean(log t), β_j = 0 (j ≥ 1)
-          logT = LA.cmap log t
-          beta0 =
-            let mu = LA.sumElements logT / fromIntegral (LA.size logT)
-            in if p == 0
-                 then []
-                 else mu : replicate (p - 1) 0
-          -- log σ を最後に追加 (Exponential では 0 固定)
-          x0 = case dist of
-                 AFTExponential -> beta0
-                 _              -> beta0 ++ [0]   -- log σ = 0  → σ = 1 として開始
-          obj params =
-            let (betaPart, logSigma) = case dist of
-                  AFTExponential -> (params, 0)
-                  _              -> (init params, last params)
-                sigma = exp logSigma
-                betaV = LA.fromList betaPart
-            in negate (logLikAFT dist x t delta betaV sigma)
-          cfg = defaultNMConfig
-            { nmStop = StopCriteria
-                { stMaxIter = 2000
-                , stTolFun  = 1e-8
-                , stTolX    = 1e-8
-                }
-            }
-      res <- runNelderMeadWith cfg obj x0
-      let xs = orBest res
-          (betaPart, sigma) = case dist of
-            AFTExponential -> (xs, 1)
-            _              -> (init xs, exp (last xs))
-          betaV = LA.fromList betaPart
-          ll = logLikAFT dist x t delta betaV sigma
-      pure (Right AFTFit
-              { aftBeta         = betaV
-              , aftScale        = sigma
-              , aftLogLik       = ll
-              , aftDistribution = dist
-              , aftIters        = orIters res
-              })
-
--- | 期待寿命の予測 E[T | X] = exp(X β + σ² / 2) -- log-normal の場合
---   Weibull AFT: E[T] = exp(X β) · Γ(1 + σ)
---   LogLogistic: E[T] = exp(X β) · π σ / sin(π σ) (σ < 1)
---   Exponential: E[T] = exp(X β)
-predictAFT :: AFTFit -> LA.Matrix Double -> LA.Vector Double
-predictAFT fit xNew =
-  let linPred = xNew LA.#> aftBeta fit
-      sigma   = aftScale fit
-      adjust  = case aftDistribution fit of
-        AFTWeibull     -> gammaApprox (1 + sigma)
-        AFTLogNormal   -> exp (sigma * sigma / 2)
-        AFTLogLogistic ->
-          if sigma < 1 && sigma > 0
-            then pi * sigma / sin (pi * sigma)
-            else 1 / 0   -- 平均が発散
-        AFTExponential -> 1
-  in LA.cmap (\lp -> exp lp * adjust) linPred
-
--- ===========================================================================
--- 内部 helpers
--- ===========================================================================
-
--- | 対数尤度。 censored は log S(t)、 observed は log f(t)。
-logLikAFT
-  :: AFTDistribution
-  -> LA.Matrix Double -> LA.Vector Double -> V.Vector Bool
-  -> LA.Vector Double -> Double
-  -> Double
-logLikAFT dist x t delta beta sigma
-  | sigma <= 0 = -1e15
-  | otherwise =
-      let n = LA.rows x
-          eta = x LA.#> beta             -- length n
-          logT = LA.cmap log t           -- length n
-          zs = LA.cmap (/ sigma) (logT - eta)
-      in sum
-           [ let z   = LA.atIndex zs i
-                 lt  = LA.atIndex logT i
-                 obs = delta V.! i
-             in if obs
-                  then logPDF dist sigma lt z
-                  else logS  dist z
-           | i <- [0 .. n - 1] ]
-
--- | log f(t)  =  log f_ε(z) − log σ − log t
-logPDF :: AFTDistribution -> Double -> Double -> Double -> Double
-logPDF dist sigma logT z =
-  let body = case dist of
-        AFTWeibull     -> z - exp z
-        AFTExponential -> z - exp z
-        AFTLogNormal   -> -0.5 * z * z - 0.5 * log (2 * pi)
-        AFTLogLogistic -> z - 2 * log1p (exp z)
-  in body - log (max 1e-300 sigma) - logT
-
--- | log S(t)  =  log S_ε(z)
-logS :: AFTDistribution -> Double -> Double
-logS dist z = case dist of
-  AFTWeibull     -> -exp z
-  AFTExponential -> -exp z
-  AFTLogNormal   -> log (max 1e-300 (1 - SD.cumulative ND.standard z))
-  AFTLogLogistic -> -log1p (exp z)
-
-log1p :: Double -> Double
-log1p x
-  | abs x < 1e-4 = x - x * x / 2 + x * x * x / 3
-  | otherwise    = log (1 + x)
-
--- | Stirling 近似による Γ(x) (x > 0)。 AFT の平均補正で使うだけなので簡易版。
-gammaApprox :: Double -> Double
-gammaApprox x
-  | x <= 0 = 1 / 0
-  | x < 1  = gammaApprox (x + 1) / x
-  | otherwise =
-      let n = floor (x - 1) :: Int
-          frac = x - fromIntegral n - 1
-          base = gammaStirling (1 + frac)
-      in base * fromIntegral (product [1 .. n])
-  where
-    gammaStirling y =
-      sqrt (2 * pi / y) * (y / exp 1) ** y
-      * (1 + 1/(12*y) + 1/(288*y*y))
diff --git a/src/Hanalyze/Model/Cluster.hs b/src/Hanalyze/Model/Cluster.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Cluster.hs
+++ /dev/null
@@ -1,417 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Model.Cluster
--- Description : クラスタリングアルゴリズム (k-means / silhouette / inertia)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Clustering algorithms.
---
--- Implements:
---
---   * 'kMeans' (Lloyd / Forgy / k-means++ initialisation, multi-restart)
---   * 'silhouette' (cluster quality metric)
---   * 'inertia' (within-cluster sum of squared distances)
---
--- Hierarchical and DBSCAN are deferred to a follow-up phase.
-module Hanalyze.Model.Cluster
-  ( -- * K-means
-    KMeansConfig (..)
-  , KMeansInit (..)
-  , KMeansResult (..)
-  , defaultKMeans
-  , kMeans
-  , kMeansPure
-    -- * Quality metrics
-  , silhouette
-  , inertia
-    -- * Helpers (exposed for advanced use)
-  , assignLabels
-  , updateCentroids
-  ) where
-
-import qualified Numeric.LinearAlgebra        as LA
-import qualified Hanalyze.Stat.KernelDist              as KD
-import qualified System.Random.MWC            as MWC
-import           Control.Monad                (forM_, foldM)
-import           Control.Monad.Primitive      (PrimMonad, PrimState)
-import           Control.Monad.ST             (ST, runST)
-import qualified Data.Vector                  as V
-import qualified Data.Vector.Mutable          as VM
-import qualified Data.Vector.Unboxed          as VU
-import qualified Data.Vector.Unboxed.Mutable  as MVU
-import qualified Data.Vector.Storable         as VS
-import qualified Data.Vector.Storable.Mutable as VSM
-import           Data.List                    (minimumBy)
-import           Data.Ord                     (comparing)
-import           Data.Word                    (Word32)
-
--- ---------------------------------------------------------------------------
--- K-means
--- ---------------------------------------------------------------------------
-
--- | Initialisation strategy.
-data KMeansInit
-  = Forgy        -- ^ Pick k random data points.
-  | KMeansPlus   -- ^ k-means++ (Arthur & Vassilvitskii 2007).
-  deriving (Show, Eq)
-
--- | K-means configuration.
-data KMeansConfig = KMeansConfig
-  { kmK        :: !Int
-  , kmInit     :: !KMeansInit
-  , kmMaxIter  :: !Int
-  , kmTol      :: !Double
-  , kmRestarts :: !Int
-  } deriving (Show, Eq)
-
--- | Default: k-means++, 300 iters, tol 1e-4, 10 restarts.
-defaultKMeans :: Int -> KMeansConfig
-defaultKMeans k = KMeansConfig
-  { kmK        = k
-  , kmInit     = KMeansPlus
-  , kmMaxIter  = 300
-  , kmTol      = 1e-4
-  , kmRestarts = 10
-  }
-
--- | K-means result.
-data KMeansResult = KMeansResult
-  { kmrCentroids :: !(LA.Matrix Double)
-  , kmrLabels    :: ![Int]
-  , kmrInertia   :: !Double
-  , kmrIters     :: !Int
-  , kmrConverged :: !Bool
-  } deriving (Show)
-
--- | Fit K-means; runs 'kmRestarts' independent restarts and keeps
--- the lowest-inertia solution.
---
--- IO ラッパ。 ロジックは 'PrimMonad' 汎用の 'kMeansM' (mwc は 'PrimMonad'
--- 汎用ゆえ ST/IO で同コードを共有) をそのまま IO に特殊化したもの。
-kMeans :: KMeansConfig -> LA.Matrix Double -> MWC.GenIO -> IO KMeansResult
-kMeans = kMeansM
-
--- | 純粋・決定的な K-means。 同じ @seed@ なら必ず同じ 'KMeansResult' を返す
--- (同 seed → ビット同一・IO 不要)。 'kMeansM' を 'ST' で走らせ 'runST' で
--- 閉じる ([[phase-50-mcmc-purification-status]] の 'nutsPure' と同方針)。
-kMeansPure :: KMeansConfig -> LA.Matrix Double -> Word32 -> KMeansResult
-kMeansPure cfg x seed =
-  runST (MWC.initialize (V.singleton seed) >>= kMeansM cfg x)
-
--- | 'PrimMonad' 汎用の K-means 本体。 'kMeans' (IO) / 'kMeansPure' (ST) が共有。
-kMeansM :: PrimMonad m
-        => KMeansConfig -> LA.Matrix Double -> MWC.Gen (PrimState m)
-        -> m KMeansResult
-kMeansM cfg x gen = do
-  results <- mapM (\_ -> kMeansSingleRunM cfg x gen) [1 .. kmRestarts cfg]
-  pure (minimumBy (comparing kmrInertia) results)
-
-kMeansSingleRunM :: PrimMonad m
-                 => KMeansConfig -> LA.Matrix Double -> MWC.Gen (PrimState m)
-                 -> m KMeansResult
-kMeansSingleRunM cfg x gen = do
-  initC <- case kmInit cfg of
-    Forgy      -> forgyInitM (kmK cfg) x gen
-    KMeansPlus -> kmppInitM (kmK cfg) x gen
-  -- Hot loop: keep labels as 'VU.Vector Int' to avoid the per-iteration
-  -- list↔Vector roundtrip the previous version paid via 'assignLabels'
-  -- + 'updateCentroids' on @[Int]@.
-  let loop !iter !centroids
-        | iter >= kmMaxIter cfg = pure (centroids, iter, False)
-        | otherwise = do
-            let labelsV = assignLabelsV x centroids
-                newC    = updateCentroidsV x labelsV (kmK cfg)
-                shift   = LA.norm_2 (LA.flatten (newC - centroids))
-            if shift < kmTol cfg
-              then pure (newC, iter + 1, True)
-              else loop (iter + 1) newC
-  (finalC, iters, conv) <- loop 0 initC
-  let labelsV = assignLabelsV x finalC
-  pure KMeansResult
-    { kmrCentroids = finalC
-    , kmrLabels    = VU.toList labelsV
-    , kmrInertia   = inertiaV x finalC labelsV
-    , kmrIters     = iters
-    , kmrConverged = conv
-    }
-
--- | Forgy initialisation: pick k random rows.
-forgyInitM :: PrimMonad m
-           => Int -> LA.Matrix Double -> MWC.Gen (PrimState m)
-           -> m (LA.Matrix Double)
-forgyInitM k x gen = do
-  let n     = LA.rows x
-      xRowsV = V.fromList (LA.toRows x)   -- O(1) row access
-  idxs <- pickKDistinctM k n gen
-  pure (LA.fromRows [xRowsV V.! i | i <- idxs])
-
--- | k-means++ initialisation: 1st centroid uniform random, subsequent
--- centroids weighted by squared distance to nearest existing centroid.
---
--- /Implementation/. Maintain @bestDist[i] = min_c ‖x_i − c‖²@ across
--- the centroids picked so far. Adding a new centroid is __one BLAS
--- GEMV__ + element-wise min, not a per-row Vector subtract / dot.
---
--- The previous version paid @n@ separate @LA.Vector@ allocations and
--- @n@ BLAS @ddot@ dispatches per centroid update (e.g. for
--- @n = 2000, k = 5@ that was ~10 000 length-@p@ allocations and
--- ~10 000 BLAS calls per kMeans run, ×10 restarts ≈ 100 000 allocs).
--- The fused-BLAS form below uses pre-computed row sq-norms and a
--- single matrix-vector multiply per centroid — O(np) work for the
--- whole sweep instead of O(n) per row.
-kmppInitM :: PrimMonad m
-          => Int -> LA.Matrix Double -> MWC.Gen (PrimState m)
-          -> m (LA.Matrix Double)
-kmppInitM k x gen = do
-  let n        = LA.rows x
-      -- Pre-compute row squared norms once: ‖x_i‖² for all rows
-      -- (length-n vector via @(X ⊙ X) · 1@).
-      normsX   = KD.rowSqNorms x
-
-  -- Pick the first centroid.
-  i0 <- MWC.uniformR (0, n - 1) gen
-  -- bestDist[i] = ‖x_i − x_{i0}‖²  in BLAS form:
-  --   = ‖x_i‖² + ‖x_{i0}‖² − 2 x_iᵀ x_{i0}
-  -- via @cross = X · x_{i0}@ (one GEMV), reusing 'normsX'.
-  let initBest = sqDistsToRow x normsX i0
-
-      pickWeighted total bdv =
-        if total <= 0
-          then pure 0
-          else do
-            u <- MWC.uniformR (0, total) gen
-            -- Linear scan of the cumulative weights via VS.unsafeIndex.
-            let go !acc !i
-                  | i >= n - 1 = pure i
-                  | otherwise  = do
-                      let !nxt = acc + bdv `VS.unsafeIndex` i
-                      if u <= nxt
-                        then pure i
-                        else go nxt (i + 1)
-            go 0 0
-
-      -- IORef を foldM で純粋に畳む (純粋化のため・乱数列順は不変ゆえ
-      -- 旧 IORef 版とビット同一)。 state = (bestDist, 逆順 centroid idx)。
-      step (bd, acc) _ = do
-        let !total = VS.sum bd
-        pickIdx <- pickWeighted total bd
-        -- One GEMV → length-n @sq dist to new centroid@; element-wise
-        -- min with @bestDist@ in a single Storable Vector pass.
-        let !newDist = sqDistsToRow x normsX pickIdx
-            !updated = VS.zipWith min bd newDist
-        pure (updated, pickIdx : acc)
-
-  (_, idxsRev) <- foldM step (initBest, [i0]) [2 .. k]
-  -- Build the @k × p@ centroid matrix from row indices in one shot.
-  let xRowsV = V.fromList (LA.toRows x)
-  pure (LA.fromRows [xRowsV V.! i | i <- reverse idxsRev])
-
--- | Squared distance from every row of @X@ (n × p) to @X[i, :]@,
--- via the BLAS identity
--- @‖x_a − x_i‖² = ‖x_a‖² + ‖x_i‖² − 2 x_aᵀ x_i@.
---
--- Cost: 1 GEMV (@O(np)@) plus one length-@n@ element-wise pass.
--- Used by 'kmppInit' to avoid per-row Vector subtract/dot.
-sqDistsToRow
-  :: LA.Matrix Double      -- ^ Data matrix @X@ (@n × p@).
-  -> LA.Vector Double      -- ^ Pre-computed row squared norms.
-  -> Int                   -- ^ Reference row index @i@.
-  -> LA.Vector Double      -- ^ Length-@n@ squared distances.
-sqDistsToRow xMat normsX i =
-  let xi    = LA.flatten (xMat LA.?? (LA.Pos (LA.idxs [i]), LA.All))
-      ni    = normsX `LA.atIndex` i
-      cross = xMat LA.#> xi                          -- length n, GEMV
-      d     = normsX + LA.scalar ni - LA.scale 2 cross
-  in LA.cmap (max 0) d   -- numerical-noise floor at 0
-
--- | Pick k distinct indices in [0, n) via Fisher-Yates partial.
-pickKDistinctM :: PrimMonad m
-               => Int -> Int -> MWC.Gen (PrimState m) -> m [Int]
-pickKDistinctM k n gen = do
-  v <- V.thaw (V.fromList [0 .. n - 1])
-  forM_ [0 .. min k n - 1] $ \i -> do
-    j <- MWC.uniformR (i, n - 1) gen
-    a <- VM.read v i
-    b <- VM.read v j
-    VM.write v i b
-    VM.write v j a
-  V.toList . V.take k <$> V.freeze v
-
--- | Assign each row to its nearest centroid (Euclidean) — public API.
-assignLabels :: LA.Matrix Double -> LA.Matrix Double -> [Int]
-assignLabels x cs = VU.toList (assignLabelsV x cs)
-
--- | Vector version of 'assignLabels'. Internal hot path; the public
--- @assignLabels@ wraps with @VU.toList@ at the boundary.
---
--- /Implementation/. The full @n × k@ squared-distance matrix is
--- /not/ materialised. Instead we use the BLAS identity
---
--- @‖x_i − c_j‖² = ‖x_i‖² + ‖c_j‖² − 2 x_iᵀ c_j@
---
--- of which only the cross term @cross = X · Cᵀ@ depends on @j@
--- per-row, so the row-wise argmin is equivalent to
---
--- @argmin_j (‖c_j‖² − 2 cross[i, j])@
---
--- (the @‖x_i‖²@ term is constant across @j@). Replaces the previous
--- @KD.pairwiseSqDistXY x cs@ + scan pipeline, which built a full
--- @n × k@ Storable matrix only to read every cell once. Now: one
--- BLAS GEMM (@O(npk)@) plus a length-@nk@ argmin scan with a small
--- per-row constant — half the writes, lower cache pressure.
-assignLabelsV :: LA.Matrix Double -> LA.Matrix Double -> VU.Vector Int
-assignLabelsV x cs =
-  let n        = LA.rows x
-      k        = LA.rows cs
-      normsC   = KD.rowSqNorms cs               -- length k
-      cross    = x LA.<> LA.tr cs               -- n × k, single GEMM
-      flatXC   = LA.flatten cross
-  in runST $ do
-       lab <- MVU.new n
-       let scanRow !i
-             | i >= n    = pure ()
-             | otherwise = do
-                 let !base = i * k
-                     -- argmin_j of (‖c_j‖² − 2 X·Cᵀ[i, j]).
-                     pickArg !j !bestJ !bestVal
-                       | j >= k    = bestJ
-                       | otherwise =
-                           let !v = (normsC `VS.unsafeIndex` j)
-                                  - 2 * (flatXC `VS.unsafeIndex` (base + j))
-                           in if v < bestVal
-                                then pickArg (j + 1) j v
-                                else pickArg (j + 1) bestJ bestVal
-                     !v0      = (normsC `VS.unsafeIndex` 0)
-                              - 2 * (flatXC `VS.unsafeIndex` base)
-                     !bestJ0  = pickArg 1 0 v0
-                 MVU.unsafeWrite lab i bestJ0
-                 scanRow (i + 1)
-       scanRow 0
-       VU.unsafeFreeze lab
-
--- | Recompute centroids — public API. Wraps @updateCentroidsV@.
-updateCentroids :: LA.Matrix Double -> [Int] -> Int -> LA.Matrix Double
-updateCentroids x labels k = updateCentroidsV x (VU.fromList labels) k
-
--- | Vector version of 'updateCentroids'. Internal hot path.
---
--- Single-pass scatter-add: traverse the @n × p@ data matrix once,
--- accumulating each row into its assigned cluster's running sum and
--- bumping that cluster's count. Centroids are then @sum / count@.
--- Replaces the previous @[ [r | (r,l) ← zip rows labels, l == c]
--- | c ← [0..k-1] ]@ which scanned the whole label list once /per/
--- cluster — @O(n k)@ per call vs the new @O(n p)@.
-updateCentroidsV
-  :: LA.Matrix Double -> VU.Vector Int -> Int -> LA.Matrix Double
-updateCentroidsV x labels k =
-  let n    = LA.rows x
-      p    = LA.cols x
-      flat = LA.flatten x          -- length n*p, row-major
-      out  = runST $ do
-        -- VSM.replicate avoids the explicit init forM_ loops.
-        sumBuf <- VSM.replicate (k * p) (0 :: Double)
-        cntBuf <- MVU.replicate k     (0 :: Int)
-            :: ST s (MVU.STVector s Int)
-        -- Single pass over all rows. Tail-recursive Int loops keep the
-        -- whole pass list-free; the previous @forM_ [0..n-1]@ +
-        -- @forM_ [0..p-1]@ relied on GHC's list-fusion rewrite, which
-        -- adds Haskell-level monadic-bind overhead for very small
-        -- inner @p@.
-        let goRow !i
-              | i >= n    = pure ()
-              | otherwise = do
-                  let !l   = labels `VU.unsafeIndex` i
-                      !off = i * p
-                      !sof = l * p
-                      goCol !j
-                        | j >= p    = pure ()
-                        | otherwise = do
-                            old <- VSM.unsafeRead sumBuf (sof + j)
-                            VSM.unsafeWrite sumBuf (sof + j)
-                              (old + flat `VS.unsafeIndex` (off + j))
-                            goCol (j + 1)
-                  goCol 0
-                  c0 <- MVU.unsafeRead cntBuf l
-                  MVU.unsafeWrite cntBuf l (c0 + 1)
-                  goRow (i + 1)
-        goRow 0
-        -- Divide each cluster's sum by its count.
-        let goNorm !c
-              | c >= k    = pure ()
-              | otherwise = do
-                  cnt <- MVU.unsafeRead cntBuf c
-                  let !invN = if cnt == 0 then 0
-                                          else 1 / fromIntegral cnt
-                      !sof  = c * p
-                      goScale !j
-                        | j >= p    = pure ()
-                        | otherwise = do
-                            v <- VSM.unsafeRead sumBuf (sof + j)
-                            VSM.unsafeWrite sumBuf (sof + j) (v * invN)
-                            goScale (j + 1)
-                  goScale 0
-                  goNorm (c + 1)
-        goNorm 0
-        VS.unsafeFreeze sumBuf
-  in LA.reshape p out
-
--- | Sum of squared Euclidean distances — public API.
-inertia :: LA.Matrix Double -> LA.Matrix Double -> [Int] -> Double
-inertia x cs labels = inertiaV x cs (VU.fromList labels)
-
--- | Vector version. Single pass over the @n × p@ data matrix and the
--- @k × p@ centroid matrix, accumulating @‖x_i − c_{l_i}‖²@ via flat
--- indexing — no @LA.toRows@ list, no @cRows !! l@ list-index per row.
-inertiaV
-  :: LA.Matrix Double -> LA.Matrix Double -> VU.Vector Int -> Double
-inertiaV x cs labels =
-  let n     = LA.rows x
-      p     = LA.cols x
-      flatX = LA.flatten x
-      flatC = LA.flatten cs
-      go !i !acc
-        | i >= n    = acc
-        | otherwise =
-            let l    = labels VU.! i
-                !off = i * p
-                !cof = l * p
-                rowSq !j !s
-                  | j >= p    = s
-                  | otherwise =
-                      let !d = (flatX `VS.unsafeIndex` (off + j))
-                             - (flatC `VS.unsafeIndex` (cof + j))
-                      in rowSq (j + 1) (s + d * d)
-            in go (i + 1) (acc + rowSq 0 0)
-  in go 0 0
-
--- ---------------------------------------------------------------------------
--- Quality
--- ---------------------------------------------------------------------------
-
--- | Silhouette coefficient. Mean over samples of
--- @(b − a) / max(a, b)@ where @a@ is the mean distance to other points
--- in the same cluster and @b@ is the mean distance to the closest
--- other cluster. Range @[-1, 1]@; higher is better.
-silhouette :: LA.Matrix Double -> [Int] -> Double
-silhouette x labels =
-  let n     = LA.rows x
-      d2    = KD.pairwiseSqDist x
-      d     = LA.cmap sqrt d2
-      lvec  = V.fromList labels
-      uniqL = V.toList (V.fromList (foldr (\l acc ->
-        if l `elem` acc then acc else l:acc) [] labels))
-      meanD i js
-        | null js   = 0
-        | otherwise = sum [LA.atIndex d (i, j) | j <- js]
-                      / fromIntegral (length js)
-      sIof i =
-        let li = lvec V.! i
-            ai = meanD i [j | j <- [0..n-1], j /= i, lvec V.! j == li]
-            otherClusters = filter (/= li) uniqL
-            bi = if null otherClusters then 0
-                   else minimum [meanD i [j | j <- [0..n-1], lvec V.! j == c]
-                                | c <- otherClusters]
-        in if max ai bi == 0 then 0 else (bi - ai) / max ai bi
-  in if n == 0 then 0 else sum [sIof i | i <- [0..n-1]] / fromIntegral n
diff --git a/src/Hanalyze/Model/CompetingRisks.hs b/src/Hanalyze/Model/CompetingRisks.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/CompetingRisks.hs
+++ /dev/null
@@ -1,114 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Model.CompetingRisks
--- Description : 競合リスク生存解析 (累積発生関数 CIF 推定)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Competing-risks survival analysis.
---
--- Extends 'Hanalyze.Model.Survival' to settings with multiple, mutually
--- exclusive failure causes. Implements the non-parametric Cumulative
--- Incidence Function (CIF) estimator (Kalbfleisch & Prentice 1980):
---
--- @
---   F̂_k(t) = Σ_{t_i ≤ t}  Ŝ(t_i⁻) · (d_{k,i} / n_i)
--- @
---
--- where @Ŝ@ is the overall Kaplan-Meier survival treating *any* cause as
--- an event, @d_{k,i}@ is the number of failures from cause @k@ at time
--- @t_i@, and @n_i@ is the size of the risk set just before @t_i@.
---
--- The naïve approach of taking @1 - KM@ on cause-specific data ignores
--- competing events and biases the cumulative incidence upward; this
--- estimator is the canonical correction.
---
--- @
--- import Hanalyze.Model.CompetingRisks
---
--- let samples = [ CRSample 1.2 1, CRSample 2.5 2, CRSample 3.0 0, … ]
---     fit     = fitCompetingRisks samples
--- @
---
--- == Implemented
---
---   * 'fitCompetingRisks' (per-cause CIF on the distinct event grid)
-module Hanalyze.Model.CompetingRisks
-  ( CRSample (..)
-  , CRFit (..)
-  , fitCompetingRisks
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import           Data.List             (sort, nub, sortBy)
-import           Data.Ord              (comparing)
-
--- ---------------------------------------------------------------------------
--- Types
--- ---------------------------------------------------------------------------
-
--- | A single observation with cause-of-failure indicator.
--- @crCause = 0@ ↔ right-censored, @crCause ≥ 1@ ↔ failure from that cause.
-data CRSample = CRSample
-  { crTime  :: !Double
-  , crCause :: !Int
-  } deriving (Show, Eq)
-
--- | Fitted competing-risks estimator: cumulative incidence per cause,
--- evaluated on the distinct event times (causes 1, …, K combined).
-data CRFit = CRFit
-  { crfCauses          :: ![Int]                       -- ^ Cause labels (sorted).
-  , crfTimes           :: !(LA.Vector Double)          -- ^ Distinct event times.
-  , crfCIF             :: ![(Int, LA.Vector Double)]   -- ^ Per-cause CIF values
-                                                       --   on @crfTimes@.
-  , crfOverallSurvival :: !(LA.Vector Double)          -- ^ Overall KM survival
-                                                       --   on @crfTimes@.
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- Fitting
--- ---------------------------------------------------------------------------
-
--- | Estimate the cumulative incidence function for each observed cause.
--- Inputs need not be sorted; ties at the same time are handled jointly.
-fitCompetingRisks :: [CRSample] -> CRFit
-fitCompetingRisks samples =
-  let sorted     = sortBy (comparing crTime) samples
-      causes     = sort (nub [ c | CRSample _ c <- sorted, c > 0 ])
-      eventTimes = sort (nub [ crTime s | s <- sorted, crCause s > 0 ])
-      -- Number at risk just before time t: #{s | crTime s >= t}.
-      atRisk t   = length [ s | s <- sorted, crTime s >= t ]
-      atTime t   = [ s | s <- sorted, crTime s == t ]
-      -- Per-event-time row: (S(t⁻) before update, n at risk, total d, per-cause d)
-      step !sPrev t =
-        let here       = atTime t
-            events     = [ c | CRSample _ c <- here, c > 0 ]
-            dTot       = length events
-            n          = atRisk t
-            sNew       = sPrev * (1 - fromIntegral dTot / fromIntegral n)
-            incs       = [ ( k
-                           , sPrev * fromIntegral (length [ c | c <- events, c == k ])
-                                       / fromIntegral n )
-                         | k <- causes ]
-        in (sNew, incs)
-      walk _      []       = ([], [])
-      walk !sPrev (t : ts) =
-        let (sNew, incs)  = step sPrev t
-            (ss, incss)   = walk sNew ts
-        in (sNew : ss, incs : incss)
-      (survList, incList) = walk 1.0 eventTimes
-      sVec     = LA.fromList survList
-      -- Cumulate increments per cause across the event-time grid.
-      cumulate inc = scanl1 (+) inc
-      cifByCause k =
-        let perTimeInc = [ snd (head [ (k', v) | (k', v) <- row, k' == k ])
-                         | row <- incList ]
-        in LA.fromList (cumulate perTimeInc)
-      cifs = [ (k, cifByCause k) | k <- causes ]
-  in CRFit
-       { crfCauses          = causes
-       , crfTimes           = LA.fromList eventTimes
-       , crfCIF             = cifs
-       , crfOverallSurvival = sVec
-       }
diff --git a/src/Hanalyze/Model/Core.hs b/src/Hanalyze/Model/Core.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Core.hs
+++ /dev/null
@@ -1,176 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.Core
--- Description : 全回帰モデル共通の Result 型と Model 型クラス
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Result type and 'Model' class shared by every regression model.
---
--- For multi-output support, the principal fields of 'FitResult' are
--- generalized to @Matrix Double@ (@n × q@) or @Vector Double@ (@q@-vector).
--- Single-output (@q = 1@) models can keep using the convenience accessors
--- ('coefficientsV', 'fittedV', 'residualsV', 'rSquared1'), which return
--- @Vector@ / @Double@ as before.
---
--- Migrating a single-output model to multi-output is just a matter of
--- calling @fitLM@ with @Matrix × Matrix@ and interpreting the result like
--- a @MultiFitResult@.
-module Hanalyze.Model.Core
-  ( FitResult (..)
-  , Model (..)
-  , PredictiveModel (..)
-  , ResidualModel (..)
-  , Band (..)
-    -- * Vec / Scalar accessors (for @q = 1@)
-  , coefficientsV
-  , fittedV
-  , residualsV
-  , rSquared1
-    -- * List conversion
-  , fittedList
-  , coeffList
-    -- * Per-column access
-  , coefficientsCol
-  , fittedCol
-  , residualsCol
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-
--- | Multi-output regression fit result.
---
--- Shapes:
---
---   * 'coefficients' — @p × q@  (@p@ features × @q@ responses).
---   * 'fitted'       — @n × q@  (@n@ observations × @q@ responses).
---   * 'residuals'    — @n × q@.
---   * 'rSquared'     — vector of length @q@ (one R² per response).
---
--- Single-output models use @q = 1@ (a one-column matrix).
-data FitResult = FitResult
-  { coefficients :: LA.Matrix Double  -- ^ Coefficient matrix @p × q@.
-  , fitted       :: LA.Matrix Double  -- ^ Fitted values @n × q@.
-  , residuals    :: LA.Matrix Double  -- ^ Residuals @n × q@.
-  , rSquared     :: LA.Vector Double  -- ^ Per-response R² (length @q@).
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- Vec / Scalar アクセサ (q = 1 用)
--- ---------------------------------------------------------------------------
-
--- | Coefficients of a single-output fit as a @Vector@. For multi-output
--- fits this returns just the first column; use 'coefficients' to access
--- all columns.
-coefficientsV :: FitResult -> LA.Vector Double
-coefficientsV = LA.flatten . coefficients
-
--- | Fitted values @ŷ@ of a single-output fit as a @Vector@.
-fittedV :: FitResult -> LA.Vector Double
-fittedV = LA.flatten . fitted
-
--- | Residuals of a single-output fit as a @Vector@.
-residualsV :: FitResult -> LA.Vector Double
-residualsV = LA.flatten . residuals
-
--- | R² of a single-output fit as a scalar 'Double'. For multi-output
--- fits this returns the first component; use 'rSquared' for all
--- responses.
-rSquared1 :: FitResult -> Double
-rSquared1 r = case LA.toList (rSquared r) of
-  (h : _) -> h
-  []      -> 0
-
--- ---------------------------------------------------------------------------
--- 後方互換ヘルパ (旧 Vec API 利用者用)
--- ---------------------------------------------------------------------------
-
--- | Fitted values as @[Double]@ (single-output).
-fittedList :: FitResult -> [Double]
-fittedList = LA.toList . fittedV
-
--- | Coefficients as @[Double]@ (single-output).
-coeffList :: FitResult -> [Double]
-coeffList = LA.toList . coefficientsV
-
--- ---------------------------------------------------------------------------
--- 列単位アクセス (多出力時)
--- ---------------------------------------------------------------------------
-
--- | Coefficients for response @j@ as a @Vector@.
-coefficientsCol :: Int -> FitResult -> LA.Vector Double
-coefficientsCol j r = LA.flatten (coefficients r LA.¿ [j])
-
--- | Fitted values @ŷ@ for response @j@ as a @Vector@.
-fittedCol :: Int -> FitResult -> LA.Vector Double
-fittedCol j r = LA.flatten (fitted r LA.¿ [j])
-
--- | Residuals for response @j@ as a @Vector@.
-residualsCol :: Int -> FitResult -> LA.Vector Double
-residualsCol j r = LA.flatten (residuals r LA.¿ [j])
-
--- ---------------------------------------------------------------------------
--- 不確実性帯
--- ---------------------------------------------------------------------------
-
--- | Uncertainty band drawn around the mean response.
-data Band
-  = NoBand      -- ^ No band.
-  | CI Double   -- ^ Confidence interval at the given level (e.g. 0.95).
-  | PI Double   -- ^ Prediction interval (Gaussian models only).
-  deriving (Show, Eq)
-
--- ---------------------------------------------------------------------------
--- Model クラス (多出力に対応)
--- ---------------------------------------------------------------------------
-
--- | Common interface implemented by every regression model.
---
--- @
--- fit     m X Y        :: FitResult       -- X (n×p), Y (n×q)
--- predict m beta Xnew  :: Matrix          -- ŷ (m × q), m = rows Xnew
--- @
-class Model m where
-  fit     :: m -> LA.Matrix Double -> LA.Matrix Double -> FitResult
-  predict :: m
-          -> LA.Matrix Double  -- ^ Coefficients @β@ of shape @p × q@.
-          -> LA.Matrix Double  -- ^ Test input @X_new@ of shape @m × p@.
-          -> LA.Matrix Double  -- ^ Predictions @ŷ@ of shape @m × q@.
-
--- ---------------------------------------------------------------------------
--- 能力別 protocol (Phase 46 / plot Phase 15 = analyze 統合 A 先行)
---
--- モデルの「能力」 を細粒度 class に割り、 持てる能力だけ instance を生やす
--- (spec §2.3 = god class を避ける)。 数値核は hmatrix で完結 (list 操作で書かない)。
--- これらは plot 非依存 = hanalyze-portable (toPlot/Plottable は別途 Hanalyze.Plot)。
--- ===========================================================================
-
--- | 残差を取り出せるフィット結果。 'toPlot' の残差診断図 (残差 vs fitted / QQ)
--- が要求する最小能力。
-class ResidualModel r where
-  -- | 残差ベクトル (単出力 @q = 1@ を想定。 多出力は 'residualsCol' を使う)。
-  residualsOf :: r -> LA.Vector Double
-
--- | 新しい入力に対し予測できるフィット結果。 'toPlot' の回帰線・予測 band が
--- 要求する最小能力。
---
--- ⚠ 既定の意味は **線形予測子** @η = X_new · β@ (列 = 各応答)。 LM では平均応答に
--- 一致するが、 GLM の平均応答 @μ = g⁻¹(η)@ には逆リンクが要る (モデルタグ依存)
--- ため、 GLM は 'Model' の 'predict' を使うこと。 本 class は線形スケールの予測を
--- 与える低レベル能力と位置づける。
-class PredictiveModel r where
-  -- | @X_new (m×p)@ に対する線形予測子 @ŷ = X_new · β (m×q)@。
-  predictAt :: r -> LA.Matrix Double -> LA.Matrix Double
-
--- ---------------------------------------------------------------------------
--- FitResult instances
---
--- 'FitResult' は LM / GLM / GLMM が共有する数値核 (= 1 instance で 3 モデルを覆う)。
--- ===========================================================================
-
-instance ResidualModel FitResult where
-  residualsOf = residualsV
-
-instance PredictiveModel FitResult where
-  -- ŷ = X_new · β  (β = coefficients、 線形予測子)
-  predictAt res xNew = xNew LA.<> coefficients res
diff --git a/src/Hanalyze/Model/DAG.hs b/src/Hanalyze/Model/DAG.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/DAG.hs
+++ /dev/null
@@ -1,239 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns      #-}
--- |
--- Module      : Hanalyze.Model.DAG
--- Description : DAG (有向非巡回グラフ) の共通表現 (重み付き隣接行列)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Directed Acyclic Graph (DAG) の共通表現。
---
--- 因果探索 (LiNGAM 系) / 将来の SEM / Bayesian Network の出力型を統一する。
--- 内部表現は **重み付き隣接行列** で、 hmatrix の線形代数操作との親和性を保つ。
---
--- ## 規約
---
--- 重み行列 W (p × p) の要素 W[i, j] は **エッジ j → i の重み** を表す。
--- これは構造方程式 X_i = Σ_j W[i, j] · X_j + e_i に対応する自然な向きで、
--- LiNGAM の B 行列と完全一致する。 W[i, i] = 0 (self-loop 禁止)。
---
--- ## DAG 判定
---
--- 'isAcyclic' は W の非零パターンから到達可能性を見て循環を検出する。
--- 浮動小数閾値の影響を避けるため、 判定は 'dagW' の **絶対値 > 0** マスク
--- に対して実施。 ノイズで小さな非零が出る場合は事前に 'pruneByThreshold'
--- でクリーンナップする。
-module Hanalyze.Model.DAG
-  ( DAG (..)
-  , Edge (..)
-  -- 構築
-  , mkDAG
-  , fromAdjacency
-  , fromBMatrix
-  , withNames
-  -- 操作
-  , pruneByThreshold
-  -- 問合せ
-  , dagEdges
-  , dagParents
-  , dagChildren
-  , dagNodeName
-  , topoSort
-  , isAcyclic
-  , dagReachable
-  -- 出力
-  , toDOT
-  ) where
-
-import qualified Data.Set              as S
-import qualified Data.Text             as T
-import qualified Data.Vector           as V
-import qualified Numeric.LinearAlgebra as LA
-import           Data.Text             (Text)
-import           Data.List             (foldl')
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
-data DAG = DAG
-  { dagN     :: !Int
-    -- ^ ノード数
-  , dagNames :: !(Maybe (V.Vector Text))
-    -- ^ ノード名 (任意)。 'Nothing' なら "x0".."x(n-1)" を使う
-  , dagW     :: !(LA.Matrix Double)
-    -- ^ 重み付き隣接行列 (p × p)。 W[i, j] = エッジ j → i の重み
-  } deriving (Show)
-
-data Edge = Edge
-  { edgeFrom   :: !Int
-  , edgeTo     :: !Int
-  , edgeWeight :: !Double
-  } deriving (Show, Eq)
-
--- ===========================================================================
--- 構築
--- ===========================================================================
-
--- | 重み付き隣接行列から DAG を作る。 ノード数は W の行数。 W が
---   p × p でない場合は呼出側のバグ (here で error)。
-mkDAG :: LA.Matrix Double -> DAG
-mkDAG w
-  | LA.rows w /= LA.cols w =
-      error "Hanalyze.Model.DAG.mkDAG: W は p × p 正方行列でなければならない"
-  | otherwise = DAG
-      { dagN     = LA.rows w
-      , dagNames = Nothing
-      , dagW     = w
-      }
-
--- | 0/1 隣接行列から DAG。 重みはエッジ存在を 1 として保持。
-fromAdjacency :: LA.Matrix Double -> DAG
-fromAdjacency = mkDAG
-
--- | LiNGAM B 行列 + threshold から DAG を構築。 |B[i, j]| ≤ thr の
---   エッジは刈り取る。 対角要素は常に 0。
-fromBMatrix :: Double -> LA.Matrix Double -> DAG
-fromBMatrix thr b = mkDAG (pruned b)
-  where
-    pruned m =
-      let p = LA.rows m
-          f i j
-            | i == j                          = 0
-            | abs (LA.atIndex m (i, j)) <= thr = 0
-            | otherwise                       = LA.atIndex m (i, j)
-      in LA.build (p, p) (\i j -> f (round i) (round j) :: Double)
-
--- | ノード名を付与する (length 不一致は呼出側のバグ)。
-withNames :: V.Vector Text -> DAG -> DAG
-withNames ns g
-  | V.length ns /= dagN g =
-      error "Hanalyze.Model.DAG.withNames: ノード数と名前数が不一致"
-  | otherwise = g { dagNames = Just ns }
-
--- ===========================================================================
--- 操作
--- ===========================================================================
-
--- | |W[i, j]| ≤ thr のエッジを 0 に。 自己ループは常に 0。
-pruneByThreshold :: Double -> DAG -> DAG
-pruneByThreshold thr g = g { dagW = pruned }
-  where
-    p = dagN g
-    f i j
-      | i == j                                = 0
-      | abs (LA.atIndex (dagW g) (i, j)) <= thr = 0
-      | otherwise                             = LA.atIndex (dagW g) (i, j)
-    pruned = LA.build (p, p) (\i j -> f (round i) (round j) :: Double)
-
--- ===========================================================================
--- 問合せ
--- ===========================================================================
-
--- | 全エッジを (from, to, weight) のリストで返す (非零重みのみ)。
-dagEdges :: DAG -> [Edge]
-dagEdges g =
-  let p = dagN g
-      w = dagW g
-  in [ Edge j i (LA.atIndex w (i, j))
-     | i <- [0 .. p - 1]
-     , j <- [0 .. p - 1]
-     , i /= j
-     , LA.atIndex w (i, j) /= 0
-     ]
-
--- | ノード i に直接影響を与えるノード集合 (W[i, j] ≠ 0 となる j のリスト)。
-dagParents :: DAG -> Int -> [Int]
-dagParents g i =
-  [ j | j <- [0 .. dagN g - 1]
-      , j /= i
-      , LA.atIndex (dagW g) (i, j) /= 0 ]
-
--- | ノード i から直接影響を受けるノード集合 (W[k, i] ≠ 0 となる k のリスト)。
-dagChildren :: DAG -> Int -> [Int]
-dagChildren g i =
-  [ k | k <- [0 .. dagN g - 1]
-      , k /= i
-      , LA.atIndex (dagW g) (k, i) /= 0 ]
-
--- | ノード名取得 ('dagNames' が Nothing なら "x{idx}")。
-dagNodeName :: DAG -> Int -> Text
-dagNodeName g i = case dagNames g of
-  Just ns | i >= 0 && i < V.length ns -> ns V.! i
-  _                                   -> T.pack ("x" <> show i)
-
--- | 到達可能性: from から to へ DAG エッジを辿って到達可能か。
-dagReachable :: DAG -> Int -> Int -> Bool
-dagReachable g from to = go S.empty [from]
-  where
-    go _    []     = False
-    go seen (x:xs)
-      | x == to               = True
-      | x `S.member` seen     = go seen xs
-      | otherwise             =
-          let !seen' = S.insert x seen
-              kids   = dagChildren g x
-          in go seen' (kids ++ xs)
-
--- | 循環を含まないか。 全ノード対 (i, j) について 「j から i へ到達可能か
---   つ i → j のエッジが存在する」 ならば循環。
-isAcyclic :: DAG -> Bool
-isAcyclic g =
-  let !p = dagN g
-      cyclePair i j =
-            i /= j
-        &&  LA.atIndex (dagW g) (j, i) /= 0
-        &&  dagReachable g j i
-  in not $ or [ cyclePair i j | i <- [0 .. p - 1], j <- [0 .. p - 1] ]
-
--- | topological sort: 根 (parents なし) から葉までの並び。
---   循環を検出した場合は 'Nothing'。 Kahn のアルゴリズム (Pure 版)。
-topoSort :: DAG -> Maybe [Int]
-topoSort g =
-  let !p     = dagN g
-      inDeg0 = V.fromList [ length (dagParents g i) | i <- [0 .. p - 1] ]
-      go acc inDeg remaining
-        | null remaining = Just (reverse acc)
-        | otherwise =
-            case findRoot remaining inDeg of
-              Nothing -> Nothing   -- 循環
-              Just r  ->
-                let kids   = dagChildren g r
-                    inDegN = V.imap
-                      (\idx v -> if idx `elem` kids then v - 1 else v)
-                      inDeg
-                in go (r : acc) inDegN (filter (/= r) remaining)
-  in go [] inDeg0 [0 .. p - 1]
-  where
-    findRoot xs inDeg =
-      case filter (\i -> (inDeg V.! i) == 0) xs of
-        []    -> Nothing
-        (h:_) -> Just h
-
--- ===========================================================================
--- 出力
--- ===========================================================================
-
--- | Graphviz DOT 形式で出力。 シェル経由で
---   @echo "..." | dot -Tpng -o dag.png@ で可視化可能。
-toDOT :: DAG -> Text
-toDOT g =
-  let header = T.pack "digraph G {\n  rankdir=LR;\n"
-      footer = T.pack "}\n"
-      nodes  = T.concat
-        [ T.pack "  " <> sanitize (dagNodeName g i)
-          <> T.pack " [label=\"" <> dagNodeName g i <> T.pack "\"];\n"
-        | i <- [0 .. dagN g - 1] ]
-      edges  = T.concat
-        [ T.pack "  " <> sanitize (dagNodeName g (edgeFrom e))
-          <> T.pack " -> " <> sanitize (dagNodeName g (edgeTo e))
-          <> T.pack " [label=\""
-          <> T.pack (showWeight (edgeWeight e))
-          <> T.pack "\"];\n"
-        | e <- dagEdges g ]
-  in header <> nodes <> edges <> footer
-  where
-    sanitize = T.replace (T.pack " ") (T.pack "_")
-             . T.replace (T.pack "-") (T.pack "_")
-    showWeight w = let r = round (w * 1000) :: Int
-                   in show (fromIntegral r / 1000 :: Double)
diff --git a/src/Hanalyze/Model/DecisionTree.hs b/src/Hanalyze/Model/DecisionTree.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/DecisionTree.hs
+++ /dev/null
@@ -1,485 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Model.DecisionTree
--- Description : 決定木分類器 (CART, classification)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Decision tree classifier (CART, classification).
---
--- Pairs with the existing regression-oriented 'Hanalyze.Model.RandomForest';
--- this module focuses on classification. Splits use Gini impurity as
--- the criterion (matches sklearn default).
---
--- @
--- import Hanalyze.Model.DecisionTree
---
--- let cfg  = defaultDecisionTree
---     tree = fitDT cfg xs ys           -- xs :: [[Double]], ys :: [Int]
---     yhat = map (predictDT tree) xs
--- @
---
--- /Performance/: the primary fit API is now 'fitDTV', which takes a
--- contiguous 'LA.Matrix' of features and an unboxed 'VU.Vector' of
--- labels. The classic 'fitDT' over @[[Double]]@ / @[Int]@ is preserved
--- as a backwards-compatible wrapper that converts at the boundary.
--- The internal representation keeps a single shared feature matrix
--- and recurses on row-index permutations, so building a tree is
--- @O(p · n log n · depth)@ rather than the old @O(p · n² · depth)@.
-module Hanalyze.Model.DecisionTree
-  ( -- * Tree types
-    DTree (..)
-  , DTFit (..)
-  , DTConfig (..)
-  , defaultDecisionTree
-    -- * Fit / predict
-  , fitDT
-  , fitDTV
-  , predictDT
-  , predictDTProbs
-    -- * Text export (R @print.rpart@ 相当)
-  , printRpart
-  , printRpartRaw
-    -- * Helpers
-  , giniImpurity
-  ) where
-
-import qualified Data.Map.Strict             as Map
-import qualified Data.Vector                 as V
-import qualified Data.Vector.Unboxed         as VU
-import qualified Data.Vector.Unboxed.Mutable as VUM
-import qualified Data.Vector.Algorithms.Intro as Intro
-import qualified Numeric.LinearAlgebra       as LA
-import           Control.Monad.ST            (runST)
-import           Data.List                   (foldl')
-import           Data.Text                   (Text)
-import qualified Data.Text                   as T
-import           Numeric                     (showFFloat)
-
--- ---------------------------------------------------------------------------
--- Types
--- ---------------------------------------------------------------------------
-
--- | Classification decision tree node.
--- | 決定木。 Phase 75.23 で各ノードに **サンプル数 n / gini 不純度 / クラス分布 /
--- 多数決クラス** を保持するよう拡張 (rpart.plot / sklearn plot_tree 水準の樹形図・
--- ルールテキスト出力のため)。 予測 (predict) の数値は不変。
-data DTree
-  = DLeaf
-      { dlClassProbs :: !(Map.Map Int Double)  -- ^ クラス割合。
-      , dlMajority   :: !Int                   -- ^ 多数決クラス (予測)。
-      , dlN          :: !Int                   -- ^ このノードのサンプル数。
-      , dlImpurity   :: !Double                -- ^ gini 不純度。
-      }
-  | DNode
-      { dnFeature :: !Int
-      , dnThr     :: !Double
-      , dnLeft    :: !DTree
-      , dnRight   :: !DTree
-      , dnN        :: !Int                     -- ^ このノードのサンプル数。
-      , dnImpurity :: !Double                  -- ^ 分割前の gini 不純度。
-      , dnProbs    :: !(Map.Map Int Double)    -- ^ 分割前のクラス割合。
-      , dnMajority :: !Int                     -- ^ 分割前の多数決クラス。
-      }
-  deriving (Show)
-
--- | 学習済み決定木 + 表示メタ (特徴量名・クラス名)。 高レベル @df |-> decisionTree@
---   ('Hanalyze.Fit') が fit 時に手元の実列名とクラス列の levels を載せて返す
---   ('RandomForestClassifier.RFClassifierFit' と同型のラッパ)。 これにより 'treePlot' /
---   'printRpart' は名前を手渡しせず @DTFit@ 一つで済む。 クラス番号 (0..K-1) は
---   @dtClassNames !! k@ で名前が引ける。
-data DTFit = DTFit
-  { dtTree         :: !DTree    -- ^ 学習済み木。
-  , dtFeatureNames :: ![Text]   -- ^ 特徴量名 (fit に使った列順)。
-  , dtClassNames   :: ![Text]   -- ^ クラス名 (label 0..K-1 に対応する levels)。
-  } deriving (Show)
-
--- | Decision tree configuration.
-data DTConfig = DTConfig
-  { dtMaxDepth        :: !(Maybe Int)
-  , dtMinSamplesSplit :: !Int
-  , dtMinSamplesLeaf  :: !Int
-  , dtMinImpurity     :: !Double
-  } deriving (Show, Eq)
-
--- | Defaults (sklearn-compatible): unlimited depth, min split 2,
--- min leaf 1, min impurity 0.
-defaultDecisionTree :: DTConfig
-defaultDecisionTree = DTConfig
-  { dtMaxDepth        = Nothing
-  , dtMinSamplesSplit = 2
-  , dtMinSamplesLeaf  = 1
-  , dtMinImpurity     = 0
-  }
-
--- ---------------------------------------------------------------------------
--- Fit (Vector-based primary API)
--- ---------------------------------------------------------------------------
-
--- | Fit a decision tree from a row-major feature matrix and unboxed
--- label vector. This is the high-performance path; 'fitDT' is a
--- list-based backwards-compatibility wrapper.
-fitDTV :: DTConfig -> LA.Matrix Double -> VU.Vector Int -> DTree
-fitDTV cfg x y =
-  let !n   = VU.length y
-      !idx = VU.enumFromN 0 n
-  in buildNodeV cfg x y idx 0
-
--- | Backwards-compatible list-based fit.
-fitDT :: DTConfig -> [[Double]] -> [Int] -> DTree
-fitDT cfg xs ys
-  | null xs   = DLeaf Map.empty 0 0 0
-  | otherwise = fitDTV cfg (LA.fromLists xs) (VU.fromList ys)
-
--- ---------------------------------------------------------------------------
--- Recursive build over row-index permutations
--- ---------------------------------------------------------------------------
-
-buildNodeV
-  :: DTConfig
-  -> LA.Matrix Double      -- ^ Shared feature matrix (n × p).
-  -> VU.Vector Int         -- ^ Shared label vector (length n).
-  -> VU.Vector Int         -- ^ Row indices in this subtree.
-  -> Int                   -- ^ Current depth.
-  -> DTree
-buildNodeV cfg x y idx depth =
-  let !nIdx     = VU.length idx
-      !sublabs  = VU.map (y VU.!) idx
-      !probs    = classProbsV sublabs
-      !gini     = giniFromCounts probs
-      !majority = argMaxClass probs
-      leaf      = DLeaf probs majority nIdx gini
-
-      depthLimit = case dtMaxDepth cfg of
-                     Just d  -> depth >= d
-                     Nothing -> False
-      stop = depthLimit
-          || nIdx < dtMinSamplesSplit cfg
-          || gini < dtMinImpurity cfg
-          || allSameV sublabs
-  in if stop
-       then leaf
-       else case bestSplitV cfg x y idx of
-         Nothing -> leaf
-         Just (fIdx, thr, _gain) ->
-           let (lIdx, rIdx) = partitionVIdx x idx fIdx thr
-           in if VU.length lIdx < dtMinSamplesLeaf cfg
-                || VU.length rIdx < dtMinSamplesLeaf cfg
-                then leaf
-                else DNode
-                       { dnFeature = fIdx
-                       , dnThr     = thr
-                       , dnLeft    = buildNodeV cfg x y lIdx (depth + 1)
-                       , dnRight   = buildNodeV cfg x y rIdx (depth + 1)
-                       , dnN        = nIdx
-                       , dnImpurity = gini
-                       , dnProbs    = probs
-                       , dnMajority = majority
-                       }
-
--- | Partition row indices by a feature threshold.
-partitionVIdx
-  :: LA.Matrix Double
-  -> VU.Vector Int
-  -> Int
-  -> Double
-  -> (VU.Vector Int, VU.Vector Int)
-partitionVIdx x idx feat thr =
-  let pred_ i = LA.atIndex x (i, feat) <= thr
-  in VU.partition pred_ idx
-
--- ---------------------------------------------------------------------------
--- Class probabilities and Gini on subsets
--- ---------------------------------------------------------------------------
-
--- | Class probability map (class → fraction).
-classProbsV :: VU.Vector Int -> Map.Map Int Double
-classProbsV ys =
-  let !n     = fromIntegral (VU.length ys) :: Double
-      counts = VU.foldl'
-                 (\m c -> Map.insertWith (+) c (1 :: Double) m)
-                 Map.empty ys
-  in Map.map (/ n) counts
-
-allSameV :: VU.Vector Int -> Bool
-allSameV ys
-  | VU.null ys = True
-  | otherwise  =
-      let !y0 = VU.unsafeHead ys
-      in VU.all (== y0) (VU.unsafeTail ys)
-
-giniFromCounts :: Map.Map Int Double -> Double
-giniFromCounts ps = 1 - foldl' (\acc p -> acc + p * p) 0 (Map.elems ps)
-
--- | Backwards-compatible Gini on @[Int]@.
-giniImpurity :: [Int] -> Double
-giniImpurity []  = 0
-giniImpurity ys  =
-  let !n     = fromIntegral (length ys) :: Double
-      counts = foldl' (\m c -> Map.insertWith (+) c (1 :: Double) m)
-                      Map.empty ys
-  in 1 - foldl' (\acc c -> acc + (c / n) ^ (2 :: Int)) 0 (Map.elems counts)
-
--- | 多数決 (予測) クラス = 確率最大のクラス。 同点は **最小クラス index** を選ぶ
---   (rpart / sklearn 慣例)。 'Map.toList' は昇順 key なので、 @foldl'@ で「厳密に
---   大きい確率でだけ更新」すれば先勝ち = 最小 index の同点タイブレークになる。
---
---   ⚠ 旧 @sortByValDescV@ は名前に反して昇順を返し (@reverse . 降順ソート@)、
---   @head@ が **最小確率クラス (argmin)** を拾っていた。 深さ無制限で葉が純粋な間は
---   露見しないが、 depth/min_samples で止まった混在葉で予測が少数派に化ける実バグ
---   だった (Phase 75.26 で樹形図を目視して発覚・修正)。
-argMaxClass :: Map.Map Int Double -> Int
-argMaxClass m = case Map.toList m of
-  []       -> 0
-  (x : xs) -> fst (foldl' better x xs)
-  where
-    better acc@(_, av) cur@(_, cv)
-      | cv > av   = cur   -- 厳密に大きい確率のときだけ更新。
-      | otherwise = acc   -- 同点は据置き = 昇順 key で先に来た小さい index が勝つ。
-
--- ---------------------------------------------------------------------------
--- Best split: per-feature O(n log n) sweep with running counts
--- ---------------------------------------------------------------------------
-
-bestSplitV
-  :: DTConfig
-  -> LA.Matrix Double
-  -> VU.Vector Int
-  -> VU.Vector Int
-  -> Maybe (Int, Double, Double)
-bestSplitV _cfg x y idx
-  | VU.length idx < 2 = Nothing
-  | otherwise =
-      let !p = LA.cols x
-          best = foldr step Nothing [0 .. p - 1]
-          step i acc =
-            case bestSplitFeature x y idx i of
-              Nothing       -> acc
-              Just (thr, g) ->
-                case acc of
-                  Nothing                          -> Just (i, thr, g)
-                  Just (_, _, gPrev) | g > gPrev   -> Just (i, thr, g)
-                                     | otherwise   -> acc
-      in best
-
--- | Per-feature best split on the index subset. Returns @Just (thr,
--- gain)@ where @gain@ is the impurity reduction (parent − weighted
--- children); negative or zero means no useful split was found.
-bestSplitFeature
-  :: LA.Matrix Double
-  -> VU.Vector Int
-  -> VU.Vector Int
-  -> Int
-  -> Maybe (Double, Double)
-bestSplitFeature x y idx feat = runST $ do
-  let !n = VU.length idx
-  -- Build (value, label) pairs for this subset and sort by value.
-  let valOf i = LA.atIndex x (i, feat)
-      lab i   = y VU.! i
-  pairs <- VUM.new n
-  let fill !k
-        | k == n = pure ()
-        | otherwise = do
-            let !i = VU.unsafeIndex idx k
-            VUM.unsafeWrite pairs k (valOf i, lab i)
-            fill (k + 1)
-  fill 0
-  Intro.sortBy (\a b -> compare (fst a) (fst b)) pairs
-  pairsF <- VU.unsafeFreeze pairs
-
-  -- Determine the number of distinct classes within this subset.
-  let labels = VU.map snd pairsF
-  let !numClasses = 1 + VU.maximum labels  -- labels are non-negative
-
-  -- Right counts start with all labels.
-  rightCounts <- VUM.replicate numClasses (0 :: Int)
-  let initRight !k
-        | k == n = pure ()
-        | otherwise = do
-            let !c = VU.unsafeIndex labels k
-            old <- VUM.unsafeRead rightCounts c
-            VUM.unsafeWrite rightCounts c (old + 1)
-            initRight (k + 1)
-  initRight 0
-  leftCounts <- VUM.replicate numClasses (0 :: Int)
-
-  let parentImp = giniFromIntCountsRO numClasses (VU.toList (VU.map snd pairsF))
-
-  -- Sweep through sorted pairs, moving sample i to the left side and
-  -- evaluating split between i and i+1 only when value changes.
-  let sweep !k !bestThr !bestGain
-        | k >= n - 1 = pure (bestThr, bestGain)
-        | otherwise = do
-            let (v_k, c_k)  = VU.unsafeIndex pairsF k
-                (v_k1, _)   = VU.unsafeIndex pairsF (k + 1)
-            -- Move sample k to left.
-            lOld <- VUM.unsafeRead leftCounts c_k
-            VUM.unsafeWrite leftCounts c_k (lOld + 1)
-            rOld <- VUM.unsafeRead rightCounts c_k
-            VUM.unsafeWrite rightCounts c_k (rOld - 1)
-            -- Skip threshold if values equal — splitting equal
-            -- samples is meaningless.
-            if v_k == v_k1
-              then sweep (k + 1) bestThr bestGain
-              else do
-                let !thr = (v_k + v_k1) / 2
-                    !nL  = k + 1
-                    !nR  = n - nL
-                gL <- giniMutable leftCounts  numClasses nL
-                gR <- giniMutable rightCounts numClasses nR
-                let !nD    = fromIntegral n :: Double
-                    !child = (fromIntegral nL * gL + fromIntegral nR * gR) / nD
-                    !gain  = parentImp - child
-                if gain > bestGain
-                  then sweep (k + 1) thr  gain
-                  else sweep (k + 1) bestThr bestGain
-  (thr, gain) <- sweep 0 0 (negate (1.0 / 0.0))
-  pure $ if gain == negate (1.0 / 0.0)
-           then Nothing
-           else Just (thr, gain)
-  where
-    -- Compute Gini from a mutable Int counts vector + total n.
-    giniMutable counts numClasses nTot
-      | nTot == 0 = pure 0
-      | otherwise = do
-          let !nD = fromIntegral nTot :: Double
-              loop !i !acc
-                | i == numClasses = pure (1 - acc)
-                | otherwise = do
-                    c <- VUM.unsafeRead counts i
-                    let !p = fromIntegral c / nD
-                    loop (i + 1) (acc + p * p)
-          loop 0 0
-
--- | Read-only Gini from a list of class labels (used once per node
--- for the parent impurity baseline).
-giniFromIntCountsRO :: Int -> [Int] -> Double
-giniFromIntCountsRO numClasses labels =
-  let !n = fromIntegral (length labels) :: Double
-      counts = foldl' (\m c -> Map.insertWith (+) c (1 :: Double) m)
-                      Map.empty labels
-      _ = numClasses  -- silence unused
-  in 1 - sum [ (c / n) ^ (2 :: Int) | c <- Map.elems counts ]
-
--- ---------------------------------------------------------------------------
--- Predict
--- ---------------------------------------------------------------------------
-
--- | Predict the majority class label for one sample.
-predictDT :: DTree -> [Double] -> Int
-predictDT DLeaf{dlMajority = m} _ = m
-predictDT DNode{dnFeature = i, dnThr = thr, dnLeft = l, dnRight = r} x
-  | x !! i <= thr = predictDT l x
-  | otherwise     = predictDT r x
-
--- | Predict class probabilities for one sample.
-predictDTProbs :: DTree -> [Double] -> Map.Map Int Double
-predictDTProbs DLeaf{dlClassProbs = p} _ = p
-predictDTProbs DNode{dnFeature = i, dnThr = thr, dnLeft = l, dnRight = r} x
-  | x !! i <= thr = predictDTProbs l x
-  | otherwise     = predictDTProbs r x
-
--- ---------------------------------------------------------------------------
--- Text export (R print.rpart 相当)
--- ---------------------------------------------------------------------------
-
--- | 決定木のルールを R @print.rpart@ 形式のテキストで出力する。
---
--- R の rpart オブジェクトを @print@ したときと同じ体裁:
---
--- @
--- n= &lt;total&gt;
---
--- node), split, n, loss, yval, (yprob)
---       * denotes terminal node
---
--- 1) root 12 8 setosa (0.3333 0.3333 0.3333)
---   2) petal_width< 0.80 4 0 setosa (1.0000 0.0000 0.0000) *
---   3) petal_width>=0.80 8 4 versicolor (0.0000 0.5000 0.5000)
---     6) petal_width< 1.65 4 0 versicolor (0.0000 1.0000 0.0000) *
---     7) petal_width>=1.65 4 0 virginica (0.0000 0.0000 1.0000) *
--- @
---
--- 各行 = @&lt;node#&gt;) &lt;split&gt; &lt;n&gt; &lt;loss&gt; &lt;yval&gt; (&lt;yprob…&gt;) [*]@。
--- ノード番号は R 同様 root=1・子は @2k@/@2k+1@。 @loss@ = 誤分類数
--- (n − 多数決クラス件数)、 @yval@ = 予測クラス、 @yprob@ = 木に現れる全クラスの
--- 確率 (クラス index 昇順)、 @*@ = 終端 (葉)。 分岐は R の固定幅表記に忠実に
--- 左 = @name&lt; thr@ (≤・条件成立)、 右 = @name&gt;=thr@ とする (dtreeToDag と同じ
--- 左 ≤ / 右 > 慣例)。
---
--- 第 1 引数 = 特徴量名、 第 2 引数 = クラス名 (yval に使う factor 水準)。
--- いずれも index に対し長さ不足・空文字なら @f{i}@ / 生の整数へフォールバックする
--- (行列 fit で名無しの木でも動く)。 75.23 で各ノードに載せた n / gini / クラス分布
--- から純粋計算し、 予測 (predict) の数値には非依存。
--- | 高レベル版 — 'DTFit' からノード規則テキストを出す ('df |-> decisionTree' の返り値
---   をそのまま渡せる)。 名前を手渡ししたい低レベルは 'printRpartRaw'。
-printRpart :: DTFit -> Text
-printRpart (DTFit tree feats classes) = printRpartRaw feats classes tree
-
--- | 行列 fit 用の低レベル版 — 特徴量名・クラス名を明示的に渡す。
-printRpartRaw :: [Text] -> [Text] -> DTree -> Text
-printRpartRaw featNames classNames tree =
-  T.intercalate "\n" (header ++ go 1 0 "root" tree)
-  where
-    classes = Map.keys (labelSet tree)          -- 木に現れる全クラス (昇順)。
-    header =
-      [ "n= " <> tShow (nodeN tree)
-      , ""
-      , "node), split, n, loss, yval, (yprob)"
-      , "      * denotes terminal node"
-      , "" ]
-
-    go :: Int -> Int -> Text -> DTree -> [Text]
-    go num d split node =
-      let n     = nodeN node
-          probs = nodeProbs node
-          maj   = nodeMajority node
-          loss  = n - round (Map.findWithDefault 0 maj probs * fromIntegral n) :: Int
-          yprob = "(" <> T.intercalate " "
-                    [ fmt4 (Map.findWithDefault 0 c probs) | c <- classes ] <> ")"
-          term  = case node of DLeaf{} -> " *"; _ -> ""
-          line  = T.concat (replicate d "  ") <> tShow num <> ") " <> split
-                    <> " " <> tShow n <> " " <> tShow loss <> " " <> classLabel maj
-                    <> " " <> yprob <> term
-      in case node of
-           DLeaf{}                -> [line]
-           DNode f thr l r _ _ _ _ ->
-             let fn = featName f
-                 lb = fn <> "< "  <> fmt2 thr
-                 rb = fn <> ">="  <> fmt2 thr
-             in line : go (2 * num) (d + 1) lb l ++ go (2 * num + 1) (d + 1) rb r
-
-    featName i  = pick i featNames  ("f" <> tShow i)
-    classLabel i = pick i classNames (tShow i)
-    pick i xs dflt = case drop i xs of
-      (nm : _) | not (T.null nm) -> nm
-      _                          -> dflt
-
-    tShow  = T.pack . show
-    fmt2 x = T.pack (showFFloat (Just 2) x "")
-    fmt4 x = T.pack (showFFloat (Just 4) x "")
-
--- | 木に現れる全クラス label を集めた集合 (値は () のダミー)。 'Map.keys' で昇順。
-labelSet :: DTree -> Map.Map Int ()
-labelSet (DLeaf p m _ _)          = Map.insert m () (() <$ p)
-labelSet (DNode _ _ l r _ _ p m)  =
-  Map.unions [Map.insert m () (() <$ p), labelSet l, labelSet r]
-
--- ノードアクセサ (葉 / 分岐 共通)。
-nodeN :: DTree -> Int
-nodeN (DLeaf _ _ n _)         = n
-nodeN (DNode _ _ _ _ n _ _ _) = n
-
-nodeProbs :: DTree -> Map.Map Int Double
-nodeProbs (DLeaf p _ _ _)         = p
-nodeProbs (DNode _ _ _ _ _ _ p _) = p
-
-nodeMajority :: DTree -> Int
-nodeMajority (DLeaf _ m _ _)         = m
-nodeMajority (DNode _ _ _ _ _ _ _ m) = m
-
--- Silence unused-import warning for V (keeps import slot for future
--- variants without re-touching imports).
-_unused :: V.Vector Int -> Int
-_unused = V.length
diff --git a/src/Hanalyze/Model/Discriminant.hs b/src/Hanalyze/Model/Discriminant.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Discriminant.hs
+++ /dev/null
@@ -1,226 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Model.Discriminant
--- Description : 判別分析 (Linear / Quadratic Discriminant Analysis)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- 判別分析 (Linear / Quadratic Discriminant Analysis)。
---
--- 連続説明変数で複数クラスを判別する古典的手法。
---
---   * 'LDA': 全クラスで共分散行列を共通 (pooled) と仮定 → 線形決定境界
---   * 'QDA': クラスごとに共分散行列が異なる → 二次決定境界
---
--- 予測は class-conditional 密度 × prior の対数 (log-posterior) を比較。
--- 数値安定化のため Cholesky 分解経由で log-determinant + Mahalanobis 距離を
--- 計算する。 hmatrix Vector / Matrix 演算で完結 (list 化禁止)。
-module Hanalyze.Model.Discriminant
-  ( DiscriminantMethod (..)
-  , DiscriminantFit (..)
-  , fitLDA
-  , fitQDA
-  , predictDiscriminant
-  ) where
-
-import qualified Data.Vector           as V
-import qualified Numeric.LinearAlgebra as LA
-import           Data.List             (nub, sort)
-import           Data.Text             (Text)
-import qualified Data.Text             as T
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
-data DiscriminantMethod = LDA | QDA deriving (Show, Eq)
-
-data DiscriminantFit = DiscriminantFit
-  { dfMeans       :: !(LA.Matrix Double)
-    -- ^ K × p、 各クラスの平均ベクトル
-  , dfCovariance  :: !(LA.Matrix Double)
-    -- ^ LDA: pooled covariance (p × p)、 QDA: 空 (使わず、 dfCovariances を見る)
-  , dfCovariances :: ![LA.Matrix Double]
-    -- ^ QDA: クラス別 covariance (K matrices)、 LDA: 空
-  , dfPriors      :: !(LA.Vector Double)
-    -- ^ クラス事前確率 (length K、 sum = 1)
-  , dfClasses     :: !(LA.Vector Double)
-    -- ^ クラス label (sorted、 length K、 Int を Double で保持)
-  , dfMethod      :: !DiscriminantMethod
-  } deriving (Show)
-
--- ===========================================================================
--- 公開関数
--- ===========================================================================
-
--- | LDA fit: pooled covariance、 線形判別。
-fitLDA :: LA.Matrix Double  -- ^ X (n × p)
-       -> V.Vector Int      -- ^ y (n)、 整数クラスラベル
-       -> Either Text DiscriminantFit
-fitLDA x y
-  | LA.rows x /= V.length y =
-      Left "fitLDA: X rows and y length mismatch"
-  | LA.rows x < 2 =
-      Left "fitLDA: need at least 2 observations"
-  | length classIds < 2 =
-      Left "fitLDA: need at least 2 distinct classes"
-  | otherwise =
-      let (means, sigmaP, priors) = pooledStats x y classIds
-      in Right DiscriminantFit
-           { dfMeans       = means
-           , dfCovariance  = sigmaP
-           , dfCovariances = []
-           , dfPriors      = priors
-           , dfClasses     = LA.fromList (map fromIntegral classIds)
-           , dfMethod      = LDA
-           }
-  where
-    classIds = sort (nub (V.toList y))
-
--- | QDA fit: クラス別 covariance。
-fitQDA :: LA.Matrix Double -> V.Vector Int -> Either Text DiscriminantFit
-fitQDA x y
-  | LA.rows x /= V.length y =
-      Left "fitQDA: X rows and y length mismatch"
-  | LA.rows x < 2 =
-      Left "fitQDA: need at least 2 observations"
-  | length classIds < 2 =
-      Left "fitQDA: need at least 2 distinct classes"
-  | minimum classCounts < LA.cols x + 1 =
-      Left (T.pack ("fitQDA: each class needs ≥ p+1 = "
-                    <> show (LA.cols x + 1) <> " observations (got min "
-                    <> show (minimum classCounts) <> ")"))
-  | otherwise =
-      let (means, covs, priors) = perClassStats x y classIds
-      in Right DiscriminantFit
-           { dfMeans       = means
-           , dfCovariance  = LA.fromLists [[]]
-           , dfCovariances = covs
-           , dfPriors      = priors
-           , dfClasses     = LA.fromList (map fromIntegral classIds)
-           , dfMethod      = QDA
-           }
-  where
-    classIds = sort (nub (V.toList y))
-    classCounts = [length [i | i <- [0 .. V.length y - 1], y V.! i == c]
-                  | c <- classIds]
-
--- | 予測。 返り値 = (予測ラベル長 m, posterior 行列 m × K)。
-predictDiscriminant
-  :: DiscriminantFit
-  -> LA.Matrix Double      -- ^ X_new (m × p)
-  -> (V.Vector Int, LA.Matrix Double)
-predictDiscriminant fit xNew =
-  let m = LA.rows xNew
-      k = LA.size (dfPriors fit)
-      classLabels = LA.toList (dfClasses fit)
-      -- 各サンプル × 各クラスの log-posterior を計算
-      logPostMat = LA.fromLists
-        [ [ logPosterior fit (LA.flatten (xNew LA.? [i])) j
-          | j <- [0 .. k - 1] ]
-        | i <- [0 .. m - 1] ]
-      -- 各行で argmax → ラベル予測
-      predLabels = V.fromList
-        [ let row = LA.toList (logPostMat LA.! i)
-              maxIdx = snd (maximum (zip row [0 ..]))
-          in round (classLabels !! maxIdx :: Double) :: Int
-        | i <- [0 .. m - 1] ]
-      -- posterior = exp(log-post) / Σ exp(log-post) (各行で normalize)
-      posteriorMat = LA.fromLists
-        [ let row = LA.toList (logPostMat LA.! i)
-              maxLP = maximum row
-              expRow = map (\x -> exp (x - maxLP)) row
-              s = sum expRow
-          in if s > 0 then map (/ s) expRow else expRow
-        | i <- [0 .. m - 1] ]
-  in (predLabels, posteriorMat)
-
--- ===========================================================================
--- 内部 helper
--- ===========================================================================
-
--- | log p(class=j) + log f(x | class=j)
---   - LDA: − 0.5 (x − μ_j)ᵀ Σ_p⁻¹ (x − μ_j) + log π_j  (定数項を省略)
---   - QDA: − 0.5 log |Σ_j| − 0.5 (x − μ_j)ᵀ Σ_j⁻¹ (x − μ_j) + log π_j
-logPosterior :: DiscriminantFit -> LA.Vector Double -> Int -> Double
-logPosterior fit x j =
-  let mu_j = LA.flatten (dfMeans fit LA.? [j])
-      diff = x - mu_j
-      logPi = log (LA.atIndex (dfPriors fit) j)
-  in case dfMethod fit of
-       LDA ->
-         let sigInvDiff = case LA.linearSolve (dfCovariance fit)
-                                              (LA.asColumn diff) of
-               Just m  -> LA.flatten m
-               Nothing -> diff  -- singular fallback
-             mahal = LA.sumElements (diff * sigInvDiff)
-         in -0.5 * mahal + logPi
-       QDA ->
-         let sigma_j = dfCovariances fit !! j
-             logDet = log (max 1e-300 (LA.det sigma_j))
-             sigInvDiff = case LA.linearSolve sigma_j (LA.asColumn diff) of
-               Just m  -> LA.flatten m
-               Nothing -> diff
-             mahal = LA.sumElements (diff * sigInvDiff)
-         in -0.5 * logDet - 0.5 * mahal + logPi
-
--- | 各クラスの平均と pooled covariance + prior を計算。
-pooledStats
-  :: LA.Matrix Double -> V.Vector Int -> [Int]
-  -> (LA.Matrix Double, LA.Matrix Double, LA.Vector Double)
-pooledStats x y classIds =
-  let n  = LA.rows x
-      p  = LA.cols x
-      nD = fromIntegral n :: Double
-      classRows c = [i | i <- [0 .. n - 1], y V.! i == c]
-      classN c = fromIntegral (length (classRows c)) :: Double
-      means = LA.fromRows
-        [ let rs = classRows c
-              xc = x LA.? rs
-              n_c = fromIntegral (length rs) :: Double
-              colSum j = LA.sumElements (xc LA.¿ [j])
-          in LA.fromList [ colSum j / n_c | j <- [0 .. p - 1] ]
-        | c <- classIds ]
-      -- pooled covariance: Σ_p = Σ_c (n_c - 1) S_c / (n - K)
-      sigmaP =
-        let k = length classIds
-            sumS = foldr (+) (LA.konst 0 (p, p))
-              [ let rs = classRows c
-                    xc = x LA.? rs
-                    mu = LA.flatten (means LA.? [idx])
-                    centered = xc - LA.fromRows (replicate (length rs) mu)
-                in LA.tr centered LA.<> centered  -- (n_c - 1) S_c
-              | (idx, c) <- zip [0 ..] classIds ]
-        in LA.scale (1 / fromIntegral (n - k)) sumS
-      priors = LA.fromList [ classN c / nD | c <- classIds ]
-  in (means, sigmaP, priors)
-
--- | クラス別 mean + cov + prior。
-perClassStats
-  :: LA.Matrix Double -> V.Vector Int -> [Int]
-  -> (LA.Matrix Double, [LA.Matrix Double], LA.Vector Double)
-perClassStats x y classIds =
-  let n  = LA.rows x
-      p  = LA.cols x
-      nD = fromIntegral n :: Double
-      classRows c = [i | i <- [0 .. n - 1], y V.! i == c]
-      means = LA.fromRows
-        [ let rs = classRows c
-              xc = x LA.? rs
-              n_c = fromIntegral (length rs) :: Double
-              colSum j = LA.sumElements (xc LA.¿ [j])
-          in LA.fromList [ colSum j / n_c | j <- [0 .. p - 1] ]
-        | c <- classIds ]
-      covs =
-        [ let rs = classRows c
-              xc = x LA.? rs
-              n_c = fromIntegral (length rs) :: Double
-              mu = LA.flatten (means LA.? [idx])
-              centered = xc - LA.fromRows (replicate (length rs) mu)
-          in LA.scale (1 / (n_c - 1)) (LA.tr centered LA.<> centered)
-        | (idx, c) <- zip [0 ..] classIds ]
-      priors = LA.fromList
-        [ fromIntegral (length (classRows c)) / nD | c <- classIds ]
-      _ = p  -- silence
-  in (means, covs, priors)
diff --git a/src/Hanalyze/Model/FDA.hs b/src/Hanalyze/Model/FDA.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/FDA.hs
+++ /dev/null
@@ -1,240 +0,0 @@
--- |
--- Module      : Hanalyze.Model.FDA
--- Description : 関数データ解析 (Functional Data Analysis, FDA)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Functional Data Analysis (FDA) (Phase 33)。
---
--- センサ / プロセス時系列を **1 観測 = 1 関数**として扱う Ramsay-Silverman
--- FDA の基礎機能。 個別の生時系列ではなく、 関数空間上の主成分 / 回帰を
--- 直接扱う。
---
--- ## 構成
---
--- - 'smoothBasis': 各サンプルを B-spline basis + 二階差分 (P-spline) penalty
---   で smooth fit → 'FunctionalSample' (basis 係数表現)
--- - 'functionalPCA': basis 係数行列の covariance に PCA、 関数主成分
--- - 'fLM': functional linear regression @y_i = α + ∫ x_i(t) β(t) dt + ε@
---
--- 既存 'Hanalyze.Model.Spline' の `bsplineBasis` を basis 生成として再利用。
--- Fourier basis は将来拡張 (Phase 33 範囲外)。
---
--- Reference: Ramsay & Silverman (2005) "Functional Data Analysis" 2nd ed.
--- Eilers-Marx (1996) "Flexible smoothing with B-splines and penalties" —
--- P-spline 二階差分 penalty。
-module Hanalyze.Model.FDA
-  ( Basis (..)
-  , FunctionalSample (..)
-  , smoothBasis
-  , evalFunctional
-    -- * FPCA
-  , FunctionalPCA (..)
-  , functionalPCA
-    -- * Functional Linear Regression
-  , FLMResult (..)
-  , fLM
-  ) where
-
-import qualified Numeric.LinearAlgebra        as LA
-import qualified Data.Vector                  as V
-import qualified Hanalyze.Model.Spline        as Sp
-
--- ---------------------------------------------------------------------------
--- 基底
--- ---------------------------------------------------------------------------
-
--- | basis 種別。 現在は B-spline のみ実装、 Fourier は将来拡張。
-data Basis
-  = BSpline !Int ![Double]   -- ^ (degree, interior knots、 境界含む)
-  deriving (Show)
-
--- | smooth した関数表現 (basis 係数 + 元 grid)。
-data FunctionalSample = FunctionalSample
-  { fsCoef  :: !(LA.Vector Double)   -- ^ basis 係数
-  , fsBasis :: !Basis
-  , fsGrid  :: !(LA.Vector Double)   -- ^ 元の時間 grid (eval 用)
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- 33-A1: smoothBasis (P-spline)
--- ---------------------------------------------------------------------------
-
--- | 複数サンプルを basis + roughness penalty で smooth fit。
---
--- 解: @c = (BᵀB + λ DᵀD)⁻¹ Bᵀy@ (= P-spline、 D は二階差分作用素)。
--- @λ → 0@ で interpolate、 @λ → ∞@ で over-smooth (≈ 一次関数)。
---
--- 入力 @y@ は @n_samples × n_grid@、 各行が 1 サンプル。
-smoothBasis
-  :: Basis              -- ^ basis (B-spline)
-  -> Double             -- ^ roughness penalty @λ@
-  -> LA.Vector Double   -- ^ 時間 grid @t@ (長さ @n_grid@)
-  -> LA.Matrix Double   -- ^ 観測 @y@ (@n_samples × n_grid@)
-  -> [FunctionalSample]
-smoothBasis basis@(BSpline deg intKnots) lambda tGrid yMat =
-  let tV  = V.fromList (LA.toList tGrid)
-      bMat = Sp.bsplineBasis deg intKnots tV   -- n_grid × d
-      d   = LA.cols bMat
-      btb = LA.tr bMat LA.<> bMat
-      penalty = diff2Penalty d
-      reg = btb + LA.scale lambda penalty
-      -- 各行 (= 1 サンプル) について解く: c = (BᵀB+λΩ)⁻¹ Bᵀy_i
-      btY = LA.tr bMat LA.<> LA.tr yMat        -- d × n_samples
-      cMat = reg LA.<\> btY                     -- d × n_samples
-      n = LA.rows yMat
-  in [ FunctionalSample
-         { fsCoef  = LA.flatten (cMat LA.¿ [i])
-         , fsBasis = basis
-         , fsGrid  = tGrid
-         }
-     | i <- [0 .. n - 1] ]
-
--- | smooth した関数を任意 grid で評価。
-evalFunctional :: FunctionalSample -> LA.Vector Double -> LA.Vector Double
-evalFunctional fs tNew =
-  case fsBasis fs of
-    BSpline deg intKnots ->
-      let tV = V.fromList (LA.toList tNew)
-          bM = Sp.bsplineBasis deg intKnots tV
-      in bM LA.#> fsCoef fs
-
--- | 二階差分 penalty 行列 @DᵀD@ (= 連続二階微分の量を有限差分で近似)。
--- @D@ は @(d-2) × d@、 @D_{i,j} = 1 if j=i、 -2 if j=i+1、 1 if j=i+2@。
-diff2Penalty :: Int -> LA.Matrix Double
-diff2Penalty d
-  | d <= 2    = LA.konst 0 (d, d)
-  | otherwise =
-      let dM = LA.fromLists
-            [ [ if j == i then 1
-                else if j == i + 1 then -2
-                else if j == i + 2 then 1
-                else 0
-              | j <- [0 .. d - 1] ]
-            | i <- [0 .. d - 3] ]
-      in LA.tr dM LA.<> dM
-
--- ---------------------------------------------------------------------------
--- 33-A2: Functional PCA
--- ---------------------------------------------------------------------------
-
-data FunctionalPCA = FunctionalPCA
-  { fpcaScores      :: !(LA.Matrix Double)   -- ^ n × K (各サンプルの主成分得点)
-  , fpcaEigenfn     :: !(LA.Matrix Double)   -- ^ K × n_grid (主成分関数を grid 上で評価)
-  , fpcaEigenvalues :: !(LA.Vector Double)   -- ^ length K (降順)
-  , fpcaMeanFn      :: !(LA.Vector Double)   -- ^ length n_grid (平均関数)
-  } deriving (Show)
-
--- | basis 係数行列の covariance に PCA。 簡略実装として basis 係数空間で
--- PCA を行い、 主成分関数を grid 上で評価して返す (= basis が直交近似で
--- ある前提)。 厳密版は basis mass matrix @J = ∫ B B^T@ で重み付き SVD が
--- 必要だが、 B-spline + dense grid なら直交近似で十分実用に耐える。
-functionalPCA
-  :: Int                  -- ^ 主成分数 K
-  -> [FunctionalSample]
-  -> FunctionalPCA
-functionalPCA k samples =
-  let cMat = LA.fromColumns (map fsCoef samples)  -- d × n
-      n    = LA.cols cMat
-      d    = LA.rows cMat
-      mu   = LA.scale (1 / fromIntegral n)
-               (cMat LA.#> LA.konst 1 n)
-      cCentered = cMat - LA.asColumn mu  -- d × n
-      cov = LA.scale (1 / fromIntegral (max 1 (n - 1)))
-              (cCentered LA.<> LA.tr cCentered)  -- d × d
-      (eigVals, eigVecs) = LA.eigSH (LA.trustSym cov)
-      -- hmatrix eigSH は降順で返す
-      kEff = min k d
-      topVecs = eigVecs LA.¿ [0 .. kEff - 1]    -- d × K
-      topVals = LA.subVector 0 kEff eigVals
-      -- score: K × n、 各列 = 係数空間での座標
-      scoresT = LA.tr topVecs LA.<> cCentered
-      -- 主成分関数を grid 上で評価
-      sampleBasis = fsBasis (head samples)
-      tGrid = fsGrid (head samples)
-      eigFn = case sampleBasis of
-        BSpline deg intKnots ->
-          let bM = Sp.bsplineBasis deg intKnots
-                     (V.fromList (LA.toList tGrid))   -- n_grid × d
-          in LA.tr (bM LA.<> topVecs)  -- K × n_grid
-      meanFn = case sampleBasis of
-        BSpline deg intKnots ->
-          let bM = Sp.bsplineBasis deg intKnots
-                     (V.fromList (LA.toList tGrid))
-          in bM LA.#> mu
-  in FunctionalPCA
-       { fpcaScores      = LA.tr scoresT
-       , fpcaEigenfn     = eigFn
-       , fpcaEigenvalues = topVals
-       , fpcaMeanFn      = meanFn
-       }
-
--- ---------------------------------------------------------------------------
--- 33-A3: Functional Linear Regression
--- ---------------------------------------------------------------------------
-
-data FLMResult = FLMResult
-  { flmAlpha  :: !Double                  -- ^ intercept
-  , flmBetaFn :: !(LA.Vector Double)      -- ^ β(t) を共通 grid 上で評価
-  , flmFitted :: !(LA.Vector Double)      -- ^ ŷ_i (length n)
-  , flmR2     :: !Double
-  } deriving (Show)
-
--- | Functional linear regression: @y_i = α + ∫ x_i(t) β(t) dt + ε@.
---
--- @β(t)@ を同じ basis で展開: @β(t) = B(t)^T γ@。 すると
--- @∫ x_i(t) β(t) dt = c_i^T J γ@ ここで @J = ∫ B(t) B(t)^T dt@ (mass matrix)。
--- 設計行列 @[1, c_i^T J]@ で OLS + 任意の roughness penalty。
---
--- mass matrix @J@ は trapezoidal 積分で近似:
--- @J ≈ Δt · B^T diag(w) B@ where @w@ は等間隔積分重み (端点 0.5、 内点 1)。
-fLM
-  :: [FunctionalSample]   -- ^ X_i(t)
-  -> LA.Vector Double     -- ^ y (n samples)
-  -> Double               -- ^ λ (β(t) の二階差分 penalty)
-  -> FLMResult
-fLM samples y lambda =
-  let sample0 = head samples
-      basis@(BSpline deg intKnots) = fsBasis sample0
-      tGrid = fsGrid sample0
-      tV    = V.fromList (LA.toList tGrid)
-      bM    = Sp.bsplineBasis deg intKnots tV
-      nGrid = LA.size tGrid
-      -- trapezoidal 重み
-      dt    = if nGrid >= 2
-                then (LA.atIndex tGrid (nGrid - 1) - LA.atIndex tGrid 0)
-                       / fromIntegral (nGrid - 1)
-                else 1
-      wVec  = LA.fromList
-                ([0.5] ++ replicate (max 0 (nGrid - 2)) 1.0 ++ [0.5])
-      wScaled = LA.scale dt wVec
-      -- mass matrix J = B^T diag(w) B (d × d)
-      jMat  = LA.tr bM LA.<> (LA.asColumn wScaled * bM)
-      -- 設計行列: 各 i 行 = [1, c_i^T J] (length 1 + d)
-      cMat  = LA.fromRows (map fsCoef samples)    -- n × d
-      ciJ   = cMat LA.<> jMat                     -- n × d
-      n     = LA.rows cMat
-      xDes  = LA.fromColumns
-                (LA.konst 1 n : LA.toColumns ciJ)  -- n × (1 + d)
-      -- penalty: intercept は 0、 γ には二階差分 penalty
-      d     = LA.cols cMat
-      pen   = diff2Penalty d
-      penFull = LA.diagBlock [LA.scalar 0, LA.scale lambda pen]
-      reg   = LA.tr xDes LA.<> xDes + penFull
-      xty   = LA.tr xDes LA.#> y
-      coefs = LA.flatten (reg LA.<\> LA.asColumn xty)
-      alpha = LA.atIndex coefs 0
-      gamma = LA.subVector 1 d coefs
-      yHat  = xDes LA.#> coefs
-      resid = y - yHat
-      yMean = LA.sumElements y / fromIntegral n
-      ssTot = LA.sumElements ((y - LA.scalar yMean) ^ (2 :: Int))
-      ssRes = LA.sumElements (resid ^ (2 :: Int))
-      r2    = if ssTot == 0 then 0 else 1 - ssRes / ssTot
-      betaFn = bM LA.#> gamma
-  in FLMResult
-       { flmAlpha  = alpha
-       , flmBetaFn = betaFn
-       , flmFitted = yHat
-       , flmR2     = r2
-       }
diff --git a/src/Hanalyze/Model/FitYByX.hs b/src/Hanalyze/Model/FitYByX.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/FitYByX.hs
+++ /dev/null
@@ -1,116 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.FitYByX
--- Description : JMP "Fit Y by X" platform 相当の自動 dispatch wrapper
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- JMP \"Fit Y by X\" platform 相当の wrapper。
---
--- X / Y それぞれが連続 (Continuous) か カテゴリ (Categorical) かで
--- 適切な解析を自動 dispatch する:
---
--- @
---   X \\ Y  | Continuous           | Categorical
---   --------+----------------------+---------------------
---   Cont    | 単回帰 (LM)          | logistic GLM
---   Cat     | one-way ANOVA        | chi-square independence
--- @
---
--- canvas frontend で 「変数 2 つドラッグ → 自動分析」 を支える backend wrapper。
-module Hanalyze.Model.FitYByX
-  ( VarType (..)
-  , FitYByXResult (..)
-  , fitYByX
-  ) where
-
-import qualified Data.Vector             as V
-import qualified Numeric.LinearAlgebra   as LA
-import           Data.List               (nub, sort)
-import           Data.Text               (Text)
-
-import qualified Hanalyze.Model.Core     as Core
-import qualified Hanalyze.Model.LM       as LM
-import qualified Hanalyze.Model.GLM      as GLM
-import qualified Hanalyze.Stat.Test      as ST
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
-data VarType
-  = Continuous
-  | Categorical
-  deriving (Show, Eq)
-
-data FitYByXResult
-  = FitContCont !Core.FitResult
-    -- ^ 単回帰: y = β₀ + β₁ x
-  | FitCatCont  !ST.TestResult ![Double]
-    -- ^ one-way ANOVA + group means (group order = sort.nub of x)
-  | FitContCat  !Core.FitResult
-    -- ^ logistic GLM: P(Y=1) = sigmoid(β₀ + β₁ x)
-  | FitCatCat   !ST.TestResult
-    -- ^ chi-square independence
-  deriving (Show)
-
--- ===========================================================================
--- 公開 API
--- ===========================================================================
-
--- | X / Y の型に応じて適切な解析を dispatch する。
---   入力は両方とも Double Vector。 Categorical の場合は整数値を Double 化
---   して渡す前提 (例: 0, 1, 2, ...)。
-fitYByX
-  :: VarType -> VarType
-  -> LA.Vector Double      -- ^ X
-  -> LA.Vector Double      -- ^ Y
-  -> Either Text FitYByXResult
-fitYByX xt yt x y
-  | LA.size x /= LA.size y =
-      Left "fitYByX: X and Y must have the same length"
-  | LA.size x < 2 =
-      Left "fitYByX: need at least 2 observations"
-  | otherwise = case (xt, yt) of
-      (Continuous, Continuous) ->
-        let xMat = LA.fromColumns [LA.fromList (replicate (LA.size x) 1), x]
-        in Right (FitContCont (LM.fitLMVec xMat y))
-
-      (Categorical, Continuous) ->
-        let levels = sort (nub (LA.toList x))
-            groups = [ LA.fromList
-                        [ LA.atIndex y i
-                        | i <- [0 .. LA.size x - 1]
-                        , LA.atIndex x i == lvl ]
-                     | lvl <- levels ]
-            tr     = ST.anovaOneWay groups
-            means  = [ LA.sumElements g / fromIntegral (LA.size g)
-                     | g <- groups ]
-        in if any ((< 1) . LA.size) groups
-             then Left "fitYByX (cat × cont): some groups are empty"
-             else Right (FitCatCont tr means)
-
-      (Continuous, Categorical) ->
-        -- Y must be binary 0/1 for logistic
-        let ys = LA.toList y
-        in if not (all (\v -> v == 0 || v == 1) ys)
-             then Left "fitYByX (cont × cat): Y must be binary 0/1 for logistic GLM"
-             else
-               let xMat = LA.fromColumns
-                            [LA.fromList (replicate (LA.size x) 1), x]
-               in Right (FitContCat (GLM.fitGLM GLM.Binomial xMat y))
-
-      (Categorical, Categorical) ->
-        let xLevels = sort (nub (LA.toList x))
-            yLevels = sort (nub (LA.toList y))
-            cell xl yl = fromIntegral $ length
-              [ () | i <- [0 .. LA.size x - 1]
-                   , LA.atIndex x i == xl
-                   , LA.atIndex y i == yl ]
-            tbl = LA.fromLists
-              [ [ cell xl yl | yl <- yLevels ] | xl <- xLevels ]
-        in if length xLevels < 2 || length yLevels < 2
-             then Left "fitYByX (cat × cat): need at least 2 levels per axis"
-             else Right (FitCatCat (ST.chiSquareIndep tbl))
-  where
-    _ = V.length :: V.Vector Int -> Int  -- silence warn
diff --git a/src/Hanalyze/Model/Formula.hs b/src/Hanalyze/Model/Formula.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Formula.hs
+++ /dev/null
@@ -1,211 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
--- |
--- Module      : Hanalyze.Model.Formula
--- Description : Formula DSL 正本 front-end (独自・明示係数構文) の parser と AST
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Formula DSL — 正本 front-end (独自・明示係数構文) の parser と AST。
---
---   このモジュールの責務は「文字列 → 構文木 (Formula AST)」 のみ。
---   AST が真の正本で、 R/patsy front-end (A18) も同じ AST に落とす。
---   意味論的分類 (Ref がデータ変数かパラメータか・factor 添字・基底展開) は
---   data と突合する後段 (A16 ModelFrame / A17 designMatrixF) に委ねる。
---   ゆえに本モジュールは plot 非依存・portable (upstream hanalyze cherry-pick 候補)。
---
---   構文 (例): @"y x group = b0 + b1*x + b2*log x + bg ! group"@
---     - 左辺 @y x group@ で 応答=y / データ変数=x,group を宣言。
---     - 右辺の自由名 (左辺に無い名前) = 推定パラメータ。
---     - @+@ @-@ @*@ @/@ @^@ は常に本物の算術 (R formula の「項追加」 ではない)。
---     - 添字 @bg ! group@ = 係数ベクトル × factor 水準 (@!@ は Haskell 正規の添字演算子)。
---     - 交互作用は型で分解: 連続×連続 @b*x*z@ / factor×連続 @bg ! group * x@ /
---       factor×factor @b ! x ! z@ (@!@ 連鎖 = 2 次元添字)。
---     - 適用 @log x@ / @exp(-b*x)@ / @bspline(x,k)@ (空白並置・括弧引数どちらも App)。
-module Hanalyze.Model.Formula
-  ( -- * AST (真の正本)
-    Formula (..)
-  , Term (..)
-  , BinOp (..)
-    -- * Parse (正本 front-end = 独自構文)
-  , parseFormula
-    -- * Pretty (round-trip 検証用・正規形)
-  , prettyFormula
-  , prettyTerm
-  ) where
-
-import           Control.Monad.Combinators.Expr (Operator (..), makeExprParser)
-import           Data.Text                      (Text)
-import qualified Data.Text                      as T
-import           Data.Void                      (Void)
-import           Text.Megaparsec
-import           Text.Megaparsec.Char           (alphaNumChar, char, letterChar,
-                                                 space1)
-import qualified Text.Megaparsec.Char.Lexer     as L
-
--- ============================================================================
--- AST — parse 結果の構文木 (意味論的分類は後段)
--- ============================================================================
-
--- | 二項算術演算子 (すべて本物の算術)。
-data BinOp = Add | Sub | Mul | Div | Pow
-  deriving (Eq, Show)
-
--- | 右辺の式木。 Ref がデータ変数かパラメータかは 'Formula' の LHS 宣言で決まる。
-data Term
-  = Lit Double          -- ^ 数値リテラル (非負。 負号は 'Neg' が担う)
-  | Ref Text            -- ^ 識別子参照 (x / b1 / group)
-  | App Text [Term]     -- ^ 関数適用 log x / exp(-b*x) / bspline(x,k)
-  | Index Term Term     -- ^ 添字 bg ! group (連鎖 b!x!z = Index (Index (Ref b) (Ref x)) (Ref z))
-  | Neg Term            -- ^ 単項マイナス -x
-  | Bin BinOp Term Term -- ^ 二項算術
-  deriving (Eq, Show)
-
--- | formula 全体。 左辺で応答 + データ変数を宣言、 右辺が式。
-data Formula = Formula
-  { formResponse :: Text   -- ^ 応答変数 y
-  , formDataVars :: [Text] -- ^ データ変数宣言 (x, group, …)。 右辺の自由名でこれに無い名前 = パラメータ
-  , formRHS      :: Term   -- ^ 右辺式
-  }
-  deriving (Eq, Show)
-
--- ============================================================================
--- Parser (megaparsec) — 字句 / 優先順位 / formula 全体
--- ============================================================================
-
-type Parser = Parsec Void Text
-
--- | 空白消費 (コメントは持たない)。
-sc :: Parser ()
-sc = L.space space1 empty empty
-
-lexeme :: Parser a -> Parser a
-lexeme = L.lexeme sc
-
-symbol :: Text -> Parser Text
-symbol = L.symbol sc
-
--- | 識別子: 英字/_ 始まり、 英数/_ 継続。
-identifier :: Parser Text
-identifier = lexeme $ do
-  c  <- letterChar <|> char '_'
-  cs <- many (alphaNumChar <|> char '_')
-  pure (T.pack (c : cs))
-
--- | 数値リテラル (非負)。 float 優先 (0.5)、 無ければ整数 (2)。
-number :: Parser Double
-number = lexeme (try L.float <|> (fromIntegral <$> (L.decimal :: Parser Integer)))
-
--- | 括弧でくくった部分式 (grouping)。
-parens :: Parser a -> Parser a
-parens = between (symbol "(") (symbol ")")
-
--- | atom = 数値 | 括弧グループ | 識別子参照。
-pAtom :: Parser Term
-pAtom =
-      (Lit <$> number)
-  <|> parens pExpr
-  <|> (Ref <$> identifier)
-
--- | 適用項。 識別子の直後に
---     - 括弧引数 @f(a, b, …)@ が来れば多引数 App、
---     - 空白並置 atom @log x@ が来れば単/多引数 App、
---   どちらも無ければただの atom。
-pApp :: Parser Term
-pApp = do
-  h <- pAtom
-  case h of
-    Ref f -> do
-      mcall <- optional (parens (pExpr `sepBy1` symbol ","))
-      case mcall of
-        Just args -> pure (App f args)          -- f(a, b)
-        Nothing   -> do
-          xs <- many pAtom                       -- log x (空白並置)
-          pure (if null xs then h else App f xs)
-    _ -> pure h
-
--- | 式 (優先順位付き)。 高→低: @!@ 添字 > @^@ > 単項@-@ > @* /@ > @+ -@。
-pExpr :: Parser Term
-pExpr = makeExprParser pApp opTable
-
-opTable :: [[Operator Parser Term]]
-opTable =
-  [ [ InfixL (Index       <$ symbol "!") ]                  -- 添字 (左結合・最高位)
-  , [ InfixR (Bin Pow     <$ symbol "^") ]                  -- べき (右結合)
-  , [ Prefix (Neg         <$ symbol "-") ]                  -- 単項マイナス (^ より下)
-  , [ InfixL (Bin Mul     <$ symbol "*")
-    , InfixL (Bin Div     <$ symbol "/") ]
-  , [ InfixL (Bin Add     <$ symbol "+")
-    , InfixL (Bin Sub     <$ symbol "-") ]
-  ]
-
--- | formula 全体: @LHS変数列 = RHS式@。
-pFormula :: Parser Formula
-pFormula = do
-  sc
-  vars <- some identifier
-  _    <- symbol "="
-  rhs  <- pExpr
-  eof
-  case vars of
-    (y : ds) -> pure (Formula y ds rhs)
-    []       -> fail "左辺に応答変数がありません"
-
--- | 文字列 → 'Formula'。 失敗時は人間可読なエラーメッセージ。
-parseFormula :: Text -> Either String Formula
-parseFormula t =
-  case parse pFormula "<formula>" t of
-    Left err -> Left (errorBundlePretty err)
-    Right f  -> Right f
-
--- ============================================================================
--- Pretty — round-trip の正規形 (App は常に括弧形式で曖昧性ゼロ)
--- ============================================================================
-
--- | 'Formula' を正規形文字列に。 @parseFormula (prettyFormula f) == Right f@ を満たす。
-prettyFormula :: Formula -> Text
-prettyFormula (Formula y ds rhs) =
-  T.unwords (y : ds) <> " = " <> prettyTerm rhs
-
--- | 右辺式を正規形に (優先順位に応じ最小限の括弧)。
-prettyTerm :: Term -> Text
-prettyTerm = go 0
-  where
-    -- prec: 親文脈の結合度。 子の演算子優先度が親より緩ければ括弧。
-    go :: Int -> Term -> Text
-    go _ (Lit d)     = prettyNum d
-    go _ (Ref x)     = x
-    go _ (App f as)  = f <> "(" <> T.intercalate ", " (map (go 0) as) <> ")"
-    go p (Index a b) = paren (p > 6) (go 6 a <> " ! " <> go 7 b)
-    -- operand は prec 5 で描く: 連続前置 (Neg (Neg …) = "-(-…)") も括弧化され parse 可能に。
-    go p (Neg a)     = paren (p > 4) ("-" <> go 5 a)
-    go p (Bin op a b) =
-      let pr = binPrec op
-          (lp, rp) = case op of
-            Pow -> (pr + 1, pr)        -- 右結合
-            _   -> (pr, pr + 1)        -- 左結合
-      in paren (p > pr) (go lp a <> " " <> binSym op <> " " <> go rp b)
-
-    paren True  s = "(" <> s <> ")"
-    paren False s = s
-
-binPrec :: BinOp -> Int
-binPrec Add = 1
-binPrec Sub = 1
-binPrec Mul = 2
-binPrec Div = 2
-binPrec Pow = 5
-
-binSym :: BinOp -> Text
-binSym Add = "+"
-binSym Sub = "-"
-binSym Mul = "*"
-binSym Div = "/"
-binSym Pow = "^"
-
--- | 整数値は小数点無しで (round-trip 安定)。
-prettyNum :: Double -> Text
-prettyNum d
-  | d == fromIntegral n = T.pack (show n)
-  | otherwise           = T.pack (show d)
-  where n = round d :: Integer
diff --git a/src/Hanalyze/Model/Formula/Design.hs b/src/Hanalyze/Model/Formula/Design.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Formula/Design.hs
+++ /dev/null
@@ -1,428 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
--- |
--- Module      : Hanalyze.Model.Formula.Design
--- Description : Formula DSL の設計行列組み立て (designMatrixF) + 線形性/識別性検出
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Formula DSL — designMatrixF + 線形性検出 + 識別性 (A17)。
---   'ModelFrame' から OLS 用の設計行列を組み立て、 線形モデルなら 'fitLMF' で fit する。
---
---   ★中核の考え方:
---     - 右辺を加法項に分解し、 各項を乗法葉 (param / factor 添字 / data 式) に分類。
---     - **線形 OLS では parameter 名自体は fit に効かない** (各設計列に 1 係数が付くだけ)。
---       param 名が効くのは ① 報告 ② 非線形検出。 → param が data 式の内側に現れたら
---       「非線形 (OLS 不可)」 として Left を返す = 線形性検出を兼ねる。
---     - factor は **使われ方 (! 添字)** で展開 ('ModelFrame' が既に判定済)。 識別性は
---       treatment contrast: 切片があれば参照水準 (=第1水準, 昇順先頭) を drop して満ランク化。
---     - 交互作用は専用演算子を持たず、 連続×連続=積・factor×連続=水準別列・factor×factor=
---       添字連鎖の grid 展開、 として加法項ごとに独立に列生成。
---
---   ★検証原理 (parameterization 不変): ŷ と R² は contrast の取り方に依らない。
---     飽和 factor×factor の ŷ = セル平均、 という Python 非依存オラクルで正しさを確認できる。
---
---   spline/poly 基底展開 (@bs ! bspline(x,k)@) は本 sub では未対応 (明示エラー)。 後続で配線。
-module Hanalyze.Model.Formula.Design
-  ( designMatrixF
-  , fitLMF
-  , responseVec
-  , linearityCheck
-    -- * Contrast coding (A2)
-  , ContrastCoding (..)
-  , contrastMatrix
-  , parseContrast
-    -- * weights / offset = WLS (A3)
-  , WLSConfig (..)
-  , defaultWLS
-  , fitWLSF
-  ) where
-
-import           Data.Maybe              (catMaybes, isNothing)
-import           Data.Text               (Text)
-import qualified Data.Text               as T
-import qualified Data.Vector             as V
-import qualified Numeric.LinearAlgebra   as LA
-
-import           Hanalyze.DataIO.Convert    (getDoubleVec)
-import           Hanalyze.DataIO.Preprocess (dropMissingRows)
-import           Hanalyze.Model.Core     (FitResult)
-import           Hanalyze.Model.LM       (fitLM)
-import           Hanalyze.Model.Spline   (bsplineBasis, quantileKnots)
-import           Hanalyze.Model.Formula  (BinOp (..), Formula (..), Term (..),
-                                          prettyTerm)
-import           Hanalyze.Model.Formula.Frame
-import qualified DataFrame.Internal.DataFrame  as DX
-
--- ============================================================================
--- 加法 / 乗法への分解
--- ============================================================================
-
--- | 加法項に分解。 符号 (Sub/Neg) は係数に吸収され ŷ に効かないので Add 扱い。
-flattenAdd :: Term -> [Term]
-flattenAdd (Bin Add a b) = flattenAdd a ++ flattenAdd b
-flattenAdd (Bin Sub a b) = flattenAdd a ++ flattenAdd b
-flattenAdd (Neg a)       = flattenAdd a
-flattenAdd t             = [t]
-
--- | 乗法葉に分解。
-mulLeaves :: Term -> [Term]
-mulLeaves (Bin Mul a b) = mulLeaves a ++ mulLeaves b
-mulLeaves (Neg a)       = mulLeaves a
-mulLeaves t             = [t]
-
--- | Index spine: 入れ子添字を (base項, [添字項]) に。 base が Ref でなければ Nothing。
-indexSpine :: Term -> Maybe (Term, [Term])
-indexSpine (Index a b) = do (base, ixs) <- indexSpine a; pure (base, ixs ++ [b])
-indexSpine t           = Just (t, [])
-
--- ============================================================================
--- 乗法葉の分類
--- ============================================================================
-
-data Leaf
-  = LParam Text                       -- ^ パラメータ単独 (係数。 OLS 列は持たない)
-  | LFactor [(Text, ContrastCoding)]  -- ^ factor 添字 + contrast (1 個=主効果 / 複数=交互作用)
-  | LBasis Text [Term]                -- ^ 基底展開 (bs ! bspline(x,n) / bp ! poly(x,n))
-  | LData Term                        -- ^ データ式 (連続変数・Lit・単項関数・算術)
-
--- | 基底関数名 (! の右に App として現れたら factor でなく基底展開)。
-basisFns :: [Text]
-basisFns = ["poly", "opoly", "bspline"]
-
-classify :: ModelFrame -> Term -> Either String Leaf
-classify mf leaf =
-  case indexSpine leaf of
-    Just (Ref _, [App f args]) | f `elem` basisFns -> Right (LBasis f args)
-    Just (Ref _, ixs@(_:_))                        -> LFactor <$> mapM ixName ixs
-    _ -> case leaf of
-      Ref x
-        | x `elem` mfParams mf -> Right (LParam x)
-        -- 裸の factor = 主効果 (R 意味論 A17b: @y ~ … + g@ の @g@ が factor 列なら treatment
-        --   contrast の主効果列。 @!@ 添字版 @bg!g@ と同一の LFactor に落とす)。
-        | isFactor x           -> Right (LFactor [(x, Treatment)])
-      _ -> Right (LData leaf)
-  where
-    -- 添字 → (factor 名, contrast)。 @Ref g@ = 無注釈 treatment、
-    -- @C(g, coding)@ = contrast 注釈、 @C(g)@ = treatment。
-    ixName (Ref x)
-      | isFactor x = Right (x, Treatment)
-      | otherwise  = Left $ "添字 '" <> T.unpack x <> "' は factor でなければなりません"
-    ixName (App "C" (Ref x : rest))
-      | isFactor x = (\c -> (x, c)) <$> codingOf rest
-      | otherwise  = Left $ "C(...) の '" <> T.unpack x <> "' は factor でなければなりません"
-    ixName (App f _) = Left $ "基底 '" <> T.unpack f
-                              <> "' は factor 添字と混在できません (基底項は単独で)"
-    ixName _         = Left "添字は変数名でなければなりません"
-    codingOf []            = Right Treatment
-    codingOf (Ref c : _)   = parseContrast c
-    codingOf _             = Left "C(g, coding) の coding は名前でなければなりません"
-    isFactor x = case lookup x (mfRoles mf) of
-                   Just (RoleFactor _ _) -> True
-                   _                     -> False
-
--- ============================================================================
--- データ式の評価 (パラメータが内側に出たら非線形)
--- ============================================================================
-
-evalData :: ModelFrame -> Term -> Either String (V.Vector Double)
-evalData mf t = case t of
-  Lit d -> Right (V.replicate n d)
-  Ref x -> case lookup x (mfRoles mf) of
-    Just (RoleContinuous v) -> Right v
-    Just (RoleResponse _)   -> Left $ "応答 '" <> T.unpack x <> "' をデータ式に使えません"
-    Just (RoleFactor _ _)   -> Left $ "factor '" <> T.unpack x
-                                       <> "' は ! で添字してください"
-    Nothing
-      | x `elem` mfParams mf -> Left $ "非線形: パラメータ '" <> T.unpack x
-                                        <> "' がデータ式の内側に現れます (線形モデルでありません)"
-      | otherwise            -> Left $ "未知の変数 '" <> T.unpack x <> "'"
-  Neg a -> V.map negate <$> evalData mf a
-  App f [a]
-    | Just fn <- lookup f unaryFns -> V.map fn <$> evalData mf a
-  App f _ -> Left $ "未対応の関数 '" <> T.unpack f
-                     <> "' (A17 は log/exp/sqrt/sin/cos/tan/abs の単項のみ)"
-  Bin op a b -> V.zipWith (binFn op) <$> evalData mf a <*> evalData mf b
-  Index _ _  -> Left "添字項はデータ式に直接置けません (係数として扱われます)"
-  where n = mfNRows mf
-
-unaryFns :: [(Text, Double -> Double)]
-unaryFns =
-  [ ("log", log), ("exp", exp), ("sqrt", sqrt)
-  , ("sin", sin), ("cos", cos), ("tan", tan), ("abs", abs) ]
-
-binFn :: BinOp -> (Double -> Double -> Double)
-binFn Add = (+)
-binFn Sub = (-)
-binFn Mul = (*)
-binFn Div = (/)
-binFn Pow = (**)
-
--- ============================================================================
--- 加法項 → 設計列
--- ============================================================================
-
--- | 切片項か (data も factor も無く param のみ → 1 の列)。
-isInterceptTerm :: ModelFrame -> Term -> Bool
-isInterceptTerm mf term =
-  case mapM (classify mf) (mulLeaves term) of
-    Right leaves -> not (null leaves)
-                 && all isParam leaves
-    _ -> False
-  where isParam (LParam _) = True
-        isParam _          = False
-
--- | 加法項 1 つの設計列群 (列ラベル, 列ベクトル)。
-termColumns :: Bool -> ModelFrame -> Term -> Either String [(Text, V.Vector Double)]
-termColumns hasInt mf term = do
-  leaves <- mapM (classify mf) (mulLeaves term)
-  let factorNames = concat [ fs | LFactor fs <- leaves ]
-      dataLeaves  = [ d | LData d <- leaves ]
-      basisLeaves = [ (f, a) | LBasis f a <- leaves ]
-  case basisLeaves of
-    [(f, a)]
-      | null factorNames && null dataLeaves -> basisColumns hasInt mf f a
-      | otherwise -> Left "基底項は単独で記述してください (factor/データ式との積は未対応)"
-    (_ : _ : _) -> Left "1 項に複数の基底は未対応"
-    [] -> do
-      dataVec <- case dataLeaves of
-                   [] -> Right (V.replicate (mfNRows mf) 1)
-                   ts -> foldr1 (V.zipWith (*)) <$> mapM (evalData mf) ts
-      let dataLabel | null dataLeaves = Nothing
-                    | otherwise       = Just (T.intercalate "*" (map prettyTerm dataLeaves))
-      case factorNames of
-        [] -> Right [ (prettyTerm term, dataVec) ]
-        fs -> factorColumns hasInt mf fs dataVec dataLabel
-
--- | 基底展開列。
---   - @poly(x,n)@ = x¹..xⁿ (n 列・定数なし。 切片は b0 が担う → polyDesignMatrix と同 span)。
---   - @bspline(x,n)@ = degree-3 clamped B-spline、 knots = quantileKnots n x
---     (= fitSpline (BSpline 3) (quantileKnots n x) と同一基底)。 既定 degree=3、
---     @bspline(x,n,k)@ で degree 指定可。 B-spline 基底は partition of unity ゆえ切片と
---     共線 → 切片併用時 (hasInt) は先頭基底列を drop して満ランク化 (R splines::bs 既定と同様)。
-basisColumns :: Bool -> ModelFrame -> Text -> [Term]
-             -> Either String [(Text, V.Vector Double)]
-basisColumns hasInt mf fname args = case (fname, args) of
-  ("poly", [xe, Lit nd]) -> do
-    xv <- evalData mf xe
-    let deg = round nd :: Int
-    pure [ (lbl xe ("^" <> tshow j), V.map (^ j) xv) | j <- [1 .. deg] ]
-  -- opoly(x,n) = 実測値の直交多項式 (R poly 既定・raw=FALSE と同 span)。
-  --   Vandermonde [1, x, …, xⁿ] を QR 直交化し、 定数列を落とした 1..n 列を返す。
-  --   raw poly と違い列が相互直交 (不等間隔でも linear ⊥ quadratic) ゆえ効果検定が独立。
-  --   ŷ は raw poly と同一 (span 不変・parameterization のみ差)。
-  ("opoly", [xe, Lit nd]) -> do
-    xv <- evalData mf xe
-    let deg    = round nd :: Int
-        xs     = V.toList xv
-        vand   = LA.fromLists [ [ x ^ p | p <- [0 .. deg] ] | x <- xs ]
-        (q, _) = LA.qr vand
-        qcols  = take deg (drop 1 (LA.toColumns q))  -- 定数列を除いた orthogonal 基底 (1..deg)
-    pure [ (lbl xe ("^" <> tshow j), V.fromList (LA.toList c))
-         | (j, c) <- zip [1 :: Int ..] qcols ]
-  ("bspline", [xe, Lit nk])          -> bspl xe (round nk) 3
-  ("bspline", [xe, Lit nk, Lit kk])  -> bspl xe (round nk) (round kk)
-  _ -> Left $ "基底 '" <> T.unpack fname
-              <> "' の引数形が不正 (poly(x,n) / bspline(x,n) / bspline(x,n,k))"
-  where
-    bspl xe nKnots deg = do
-      xv <- evalData mf xe
-      let mat     = bsplineBasis deg (quantileKnots nKnots xv) xv
-          colsAll = map (V.fromList . LA.toList) (LA.toColumns mat)
-          cols    = if hasInt then drop 1 colsAll else colsAll
-      pure [ (lbl xe ("_" <> tshow j), c) | (j, c) <- zip [(1 :: Int) ..] cols ]
-    lbl xe suf = fname <> "(" <> prettyTerm xe <> ")" <> suf
-    tshow      = T.pack . show
-
--- | factor (1 個=主効果 / 複数=交互作用) を contrast 符号化で展開 (A2 一般化)。
---   ★各 factor の **contrast 行列 C** (k×m) で行を符号化する。 交互作用列は factor ごとの
---   contrast 列の **Kronecker 積** (各行で contrast 値の積) を取り、 data ベクトルを掛ける。
---   ★符号化の縮約は **指示列のとき (dataLabel == Nothing) のみ**: 指示列は合計が切片 (1s)
---   と共線ゆえ contrast 行列 (k×(k-1)) で 1 列落として満ランク化する。 一方 factor×連続
---   (dataLabel == Just、 masked データ列) は切片と共線でない → **full coding (k×k 単位行列)**
---   = 全水準保持で per-level の傾きを持つ (Phase 46 の masked 列罠を踏襲。 落とすと参照群の
---   傾きが 0 固定で自由度を失う = statsmodels の C(g):x と不一致)。 full coding では単位行列
---   ゆえ contrast の選択は ŷ に影響しない (= parameterization 不変)。
-factorColumns :: Bool -> ModelFrame -> [(Text, ContrastCoding)] -> V.Vector Double -> Maybe Text
-              -> Either String [(Text, V.Vector Double)]
-factorColumns hasInt mf fcs dataVec dataLabel = do
-  facs <- mapM getFac fcs
-  let reduced = hasInt && isNothing dataLabel
-      facCols = [ factorContrastCols reduced f | f <- facs ]  -- factor ごとの [(列ラベル, 行ベクトル)]
-      combos  = cartesian facCols                              -- 交互作用 = 列の直積
-  pure [ mkCol picks | picks <- combos ]
-  where
-    getFac (name, coding) = case lookup name (mfRoles mf) of
-      Just (RoleFactor lev idx) -> Right (name, lev, idx, coding)
-      _ -> Left $ "factor '" <> T.unpack name <> "' が ModelFrame にありません"
-    mkCol picks =
-      let prodVec = foldr1 (V.zipWith (*)) (map snd picks)   -- 各 factor の contrast 値の積
-          col     = V.zipWith (*) prodVec dataVec
-          lbl     = T.intercalate ":" (map fst picks ++ maybe [] (: []) dataLabel)
-      in (lbl, col)
-
--- | 1 factor の contrast 列群。 reduced=True で contrast 行列 (k×(k-1))、 False で
---   full coding (k×k 単位行列 = 指示変数)。 各列は行ごとの contrast 値ベクトル。
-factorContrastCols :: Bool -> (Text, [Text], V.Vector Int, ContrastCoding)
-                   -> [(Text, V.Vector Double)]
-factorContrastCols reduced (nm, lev, idx, coding) =
-  let k      = length lev
-      cmat   = if reduced then contrastMatrix coding k else LA.ident k
-      m      = LA.cols cmat
-      colVec j = V.map (\l -> cmat `LA.atIndex` (l, j)) idx
-      lbl j
-        | not reduced         = nm <> "=" <> (lev !! j)              -- full = 水準名 (指示)
-        | coding == Treatment = nm <> "=" <> (lev !! (j + 1))        -- 参照 (水準0) を除く
-        | otherwise           = nm <> "[" <> codingTag coding <> "." <> tshow j <> "]"
-  in [ (lbl j, colVec j) | j <- [0 .. m - 1] ]
-  where tshow = T.pack . show
-
-cartesian :: [[a]] -> [[a]]
-cartesian []       = [[]]
-cartesian (xs:rest) = [ x : r | x <- xs, r <- cartesian rest ]
-
--- ============================================================================
--- Contrast coding (A2)
--- ============================================================================
-
--- | factor 符号化方式。 切片併用時に満ランク化する contrast。
-data ContrastCoding
-  = Treatment                    -- ^ 参照水準 (昇順先頭) を 0 に、 他を指示 (既定・R 既定 contr.treatment)
-  | Sum                          -- ^ sum-to-zero (最終水準 = −Σ others、 R contr.sum)
-  | Helmert                      -- ^ 各水準 vs それ以前の平均 (R contr.helmert)
-  | Polynomial                   -- ^ ordered factor 用の直交多項式 (R contr.poly)
-  | CustomContrast (LA.Matrix Double)  -- ^ ユーザ指定の k×(k-1) contrast 行列
-  deriving (Eq, Show)
-
--- | contrast 名 (C(g, name) の name) を解釈。
-parseContrast :: Text -> Either String ContrastCoding
-parseContrast t = case T.toLower t of
-  "treatment" -> Right Treatment
-  "sum"        -> Right Sum
-  "helmert"    -> Right Helmert
-  "poly"       -> Right Polynomial
-  "polynomial" -> Right Polynomial
-  _ -> Left $ "未知の contrast '" <> T.unpack t
-              <> "' (Treatment/Sum/Helmert/Polynomial)"
-
--- | 列ラベル用の短いタグ。
-codingTag :: ContrastCoding -> Text
-codingTag Treatment          = "T"
-codingTag Sum                = "S"
-codingTag Helmert            = "H"
-codingTag Polynomial         = "P"
-codingTag (CustomContrast _) = "C"
-
--- | k 水準の contrast 行列 (k×(k-1))。 切片併用時の満ランク符号化。
---   行 = 水準 (昇順 index)、 列 = contrast。 行 l の値が水準 l の設計行寄与。
-contrastMatrix :: ContrastCoding -> Int -> LA.Matrix Double
-contrastMatrix coding k = case coding of
-  Treatment ->
-    LA.fromLists [ [ if l == j + 1 then 1 else 0 | j <- [0 .. k - 2] ] | l <- [0 .. k - 1] ]
-  Sum ->
-    LA.fromLists [ sumRow l | l <- [0 .. k - 1] ]
-  Helmert ->
-    LA.fromLists [ [ helmert l j | j <- [0 .. k - 2] ] | l <- [0 .. k - 1] ]
-  Polynomial       -> polyContrast k
-  CustomContrast m -> m
-  where
-    sumRow l | l == k - 1 = replicate (k - 1) (-1)
-             | otherwise  = [ if l == j then 1 else 0 | j <- [0 .. k - 2] ]
-    helmert l j | l <= j      = -1
-                | l == j + 1  = fromIntegral (j + 1)
-                | otherwise   = 0
-
--- | 直交多項式 contrast (k×(k-1))。 中心化水準スコアの Vandermonde を QR 分解し
---   定数列を落とした直交基底 (R contr.poly と同 span。 符号差は ŷ 不変ゆえ無害)。
-polyContrast :: Int -> LA.Matrix Double
-polyContrast k =
-  let xs    = map fromIntegral [1 .. k] :: [Double]
-      xbar  = sum xs / fromIntegral k
-      vand  = LA.fromLists [ [ (x - xbar) ^ p | p <- [0 .. k - 1] ] | x <- xs ]
-      (q, _) = LA.qr vand
-  in LA.fromColumns (drop 1 (LA.toColumns q))
-
--- ============================================================================
--- designMatrixF / fitLMF / linearityCheck
--- ============================================================================
-
--- | 'Formula' + 'ModelFrame' → 設計行列 (n×p) と列ラベル。 非線形なら Left。
-designMatrixF :: Formula -> ModelFrame -> Either String (LA.Matrix Double, [Text])
-designMatrixF (Formula _ _ rhs) mf = do
-  let terms  = flattenAdd rhs
-      hasInt = any (isInterceptTerm mf) terms
-  colss <- mapM (termColumns hasInt mf) terms
-  let cols   = concat colss
-      labels = map fst cols
-  if null cols
-    then Left "空のモデル (設計列がありません)"
-    else Right ( LA.fromColumns (map (LA.fromList . V.toList . snd) cols)
-               , labels )
-
--- | 線形モデルを OLS で fit。 設計列ラベルも返す。 非線形なら Left。
-fitLMF :: Formula -> DX.DataFrame -> Either String (FitResult, [Text])
-fitLMF f df = do
-  mf            <- modelFrame f df
-  (x, labels)   <- designMatrixF f mf
-  yv            <- responseVec mf
-  let y = LA.asColumn (LA.fromList (V.toList yv))
-  Right (fitLM x y, labels)
-
--- | 応答ベクトル取り出し。
-responseVec :: ModelFrame -> Either String (V.Vector Double)
-responseVec mf = case mfRoles mf of
-  ((_, RoleResponse v) : _) -> Right v
-  _                         -> Left "ModelFrame に応答列がありません"
-
--- ============================================================================
--- weights / offset = WLS (A3)
--- ============================================================================
-
--- | 重み付き最小二乗 + offset の設定。 statsmodels @smf.wls(formula, data, weights=…)@
---   に倣い、 weights/offset は **列名で渡す** (R でも weights は formula 外)。
-data WLSConfig = WLSConfig
-  { wcWeights :: Maybe Text  -- ^ 重み列名 (WLS。 'Nothing' = 等重み OLS)
-  , wcOffset  :: Maybe Text  -- ^ offset 列名 (η への固定加算。 線形では @y* = y − offset@ を fit)
-  }
-  deriving (Eq, Show)
-
--- | 既定 (重みなし・offset なし = OLS、 'fitLMF' と等価)。
-defaultWLS :: WLSConfig
-defaultWLS = WLSConfig Nothing Nothing
-
--- | weights / offset 付きで線形モデルを fit。
---
---   ★行整列: 'modelFrame' は欠損 policy で行を落とし得るので、 weights/offset 列が frame と
---   ずれないよう **formula 関与列 ∪ weights ∪ offset をまとめて 'dropMissingRows'** してから
---   frame を組み、 weights/offset も同じ DataFrame から取り出す。
---   ★WLS = @√w@ で X/y を行スケール (@X' = diag(√w) X@, @y' = √w ⊙ y@) し OLS に帰着。
---   ★offset = η への固定加算ゆえ線形では @y − offset@ を解けばよい (GLM offset は別経路・未対応)。
-fitWLSF :: WLSConfig -> Formula -> DX.DataFrame -> Either String (FitResult, [Text])
-fitWLSF cfg f@(Formula resp dvars _) df0 = do
-  let extra = catMaybes [wcWeights cfg, wcOffset cfg]
-      df    = dropMissingRows (resp : dvars ++ extra) df0  -- 整列のため一括 drop
-  mf          <- modelFrame f df
-  (x, labels) <- designMatrixF f mf
-  yv0         <- responseVec mf
-  yv <- case wcOffset cfg of
-          Nothing -> Right yv0
-          Just oc -> do ov <- col df oc; Right (V.zipWith (-) yv0 ov)
-  case wcWeights cfg of
-    Nothing -> Right (fitLM x (asCol yv), labels)
-    Just wc -> do
-      wv <- col df wc
-      let swv = LA.fromList (map sqrt (V.toList wv))            -- √w
-          xw  = LA.fromColumns [ swv * c | c <- LA.toColumns x ] -- diag(√w) X
-          yw  = swv * LA.fromList (V.toList yv)                  -- √w ⊙ y
-      Right (fitLM xw (LA.asColumn yw), labels)
-  where
-    col d name = maybe (Left $ "WLS 列 '" <> T.unpack name <> "' が数値列として見つかりません")
-                       Right (getDoubleVec name d)
-    asCol v = LA.asColumn (LA.fromList (V.toList v))
-
--- | 線形性チェック (designMatrixF が通れば線形)。 メッセージ付き Either。
-linearityCheck :: Formula -> DX.DataFrame -> Either String ()
-linearityCheck f df = do
-  mf <- modelFrame f df
-  _  <- designMatrixF f mf
-  Right ()
diff --git a/src/Hanalyze/Model/Formula/Frame.hs b/src/Hanalyze/Model/Formula/Frame.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Formula/Frame.hs
+++ /dev/null
@@ -1,234 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
--- |
--- Module      : Hanalyze.Model.Formula.Frame
--- Description : Formula DSL の ModelFrame (変数役割割り当て + パラメータ分離)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Formula DSL — ModelFrame (A16)。 'Formula' AST + 'DataFrame' を突合し、
---   各名前に役割 (応答 / 連続データ変数 / factor) を割り当て、 推定パラメータを分離する。
---
---   ★設計の要点 (実測で確定): 「factor かどうか」 は **列の型ではなく formula 内の
---   使われ方** で決まる。 すなわち @bg ! group@ のように Index の右オペランドに現れた
---   データ変数を factor とみなす (numeric コードの factor も拾える)。 算術中にのみ現れる
---   データ変数は連続。 左辺で宣言されていない右辺の自由名 = 推定パラメータ。
---
---   基底展開 (@bs ! bspline(x,k)@) の設計行列化や係数ベクトル長の確定は A17
---   ('designMatrixF') に委ねる。 本モジュールは「役割の割り当てとパラメータ抽出」 まで。
---   DataFrame 依存ゆえ Formula.hs (純 AST) とは分離 (portable 区分は維持)。
-module Hanalyze.Model.Formula.Frame
-  ( VarRole (..)
-  , ModelFrame (..)
-  , MissingPolicy (..)
-  , ImputeKind (..)
-  , modelFrame
-  , modelFrameWith
-    -- * 内部 (テスト用に公開)
-  , refNames
-  , indexedVars
-  ) where
-
-import           Control.Applicative    ((<|>))
-import           Data.List              (foldl', nub, sort)
-import qualified Data.Map.Strict        as Map
-import           Data.Text              (Text)
-import qualified Data.Text              as T
-import qualified Data.Vector            as V
-import qualified DataFrame.Internal.DataFrame  as DX
-
-import           Hanalyze.DataIO.Convert    (getDoubleVec, getTextVec)
-import           Hanalyze.DataIO.Preprocess (Value (..), countMissing, deriveText,
-                                             dropMissingRows, imputeMean,
-                                             imputeMedian, isNAString)
-import           Hanalyze.Model.Formula  (Formula (..), Term (..))
-
--- ============================================================================
--- 役割付き列と ModelFrame
--- ============================================================================
-
--- | データ変数 (応答含む) の役割。
-data VarRole
-  = RoleResponse   (V.Vector Double)        -- ^ 応答 y (数値)
-  | RoleContinuous (V.Vector Double)        -- ^ 連続説明変数 (数値)
-  | RoleFactor     [Text] (V.Vector Int)    -- ^ factor: 水準ラベル (昇順) + 行ごとの水準 index
-  deriving (Eq, Show)
-
--- | AST + data を突合した結果。
-data ModelFrame = ModelFrame
-  { mfRoles  :: [(Text, VarRole)]  -- ^ 応答 + データ変数 → 役割 (応答が先頭、 以降は宣言順)
-  , mfParams :: [Text]             -- ^ 推定パラメータ (右辺自由名 − データ変数、 出現順)
-  , mfNRows  :: Int                -- ^ 行数 (応答列の長さ)
-  }
-  deriving (Eq, Show)
-
--- | 欠損値の扱い方。 NA 検出・除去・補完は ModelFrame の **単一責務点** (spec §2.2)。
---   policy で整形した DataFrame を 'buildFrame' に通すことで、 各 fit 関数に
---   NA 検出を散らさず一元化する。
-data MissingPolicy
-  = DropRows           -- ^ NA を含む行を全関与列から除外 (listwise deletion、 既定・後方互換)。
-  | Pairwise           -- ^ 線形 OLS では設計行列が成立しないので DropRows に縮退する
-                       --   (相関等の別用途のために policy 値としては保持。 'fitLMF' 等は警告)。
-  | Impute ImputeKind  -- ^ 連続説明変数を平均/中央値で補完。 応答・factor の NA は
-                       --   別 policy 併用が要る (Impute では埋めない)。
-  | TreatAsCategory    -- ^ factor 列の NA を独立水準 @"<NA>"@ として扱う。
-  | ErrorOnMissing     -- ^ 関与列に NA があれば 'Left' (列名 + 件数つき)。
-  deriving (Eq, Show)
-
--- | 'Impute' の補完方式。
-data ImputeKind = ImputeMean | ImputeMedian
-  deriving (Eq, Show)
-
--- ============================================================================
--- 解析ヘルパ (AST 走査)
--- ============================================================================
-
--- | 右辺に現れる全 Ref 名 (出現順、 重複あり)。
---   contrast 注釈 @C(g, Sum)@ は **factor 名 g のみ** を拾う (coding 名 "Sum" は
---   推定パラメータでもデータ変数でもないので除外)。
-refNames :: Term -> [Text]
-refNames t = case t of
-  Ref x               -> [x]
-  Lit _               -> []
-  App "C" (Ref x : _) -> [x]                   -- contrast 注釈: factor 名のみ
-  App _ as            -> concatMap refNames as -- 関数名 (App の Text) はパラメータでない
-  Index a b           -> refNames a ++ refNames b
-  Neg a               -> refNames a
-  Bin _ a b           -> refNames a ++ refNames b
-
--- | Index の右オペランドに factor として現れた名前 (= factor 候補)。
---   右が @Ref g@ (無注釈 = treatment) または @C(g, coding)@ (contrast 注釈) なら g を拾う。
---   右が基底展開 App (bspline / poly 等) の場合は factor でない (A17 が扱う) ので拾わない。
-indexedVars :: Term -> [Text]
-indexedVars = nub . go
-  where
-    go t = case t of
-      Index a b -> rightRef b ++ go a ++ go b
-      App _ as  -> concatMap go as
-      Neg a     -> go a
-      Bin _ a b -> go a ++ go b
-      _         -> []
-    rightRef (Ref x)               = [x]
-    rightRef (App "C" (Ref x : _)) = [x]       -- C(g, coding) → factor g
-    rightRef _                     = []
-
--- ============================================================================
--- modelFrame
--- ============================================================================
-
--- | 既定 policy ('DropRows') で 'ModelFrame' を構築する (後方互換: NA 無しデータでは不変)。
-modelFrame :: Formula -> DX.DataFrame -> Either String ModelFrame
-modelFrame = modelFrameWith DropRows
-
--- | 欠損 'MissingPolicy' を指定して 'ModelFrame' を構築する。
---   policy で整形した DataFrame を 'buildFrame' に通す (NA 検出・除去・補完を一元化)。
-modelFrameWith :: MissingPolicy -> Formula -> DX.DataFrame -> Either String ModelFrame
-modelFrameWith policy fml@(Formula resp dvars rhs) df = do
-  let involved = resp : dvars
-      factors  = filter (`elem` dvars) (indexedVars rhs)
-      conts    = filter (`notElem` factors) dvars       -- 連続説明変数 (factor 以外)
-      naOf d c = maybe 0 id (lookup c (countMissing d))  -- 列 c の NA 件数
-  df' <- case policy of
-    DropRows -> Right (dropMissingRows involved df)
-    Pairwise -> Right (dropMissingRows involved df)  -- 単一 frame では DropRows と同義
-    ErrorOnMissing ->
-      let bad = [ (T.unpack c, naOf df c) | c <- involved, naOf df c > 0 ]
-      in if null bad then Right df
-         else Left $ "ErrorOnMissing: 欠損のある関与列 " <> show bad
-    Impute kind -> do
-      df1 <- imputeCols kind conts df
-      let stillBad = [ T.unpack c | c <- resp : factors, naOf df1 c > 0 ]
-      if null stillBad then Right df1
-        else Left $ "Impute は連続説明変数のみ補完します。 応答/factor の欠損 "
-                    <> show stillBad <> " は DropRows か TreatAsCategory を併用してください"
-    TreatAsCategory ->
-      let df1      = foldl' (flip naToCategory) df factors
-          stillBad = [ T.unpack c | c <- resp : conts, naOf df1 c > 0 ]
-      in if null stillBad then Right df1
-         else Left $ "TreatAsCategory は factor 列のみ扱います。 応答/連続の欠損 "
-                     <> show stillBad <> " は DropRows か Impute を併用してください"
-  buildFrame fml df'
-
--- | 連続列群を平均/中央値で補完。 数値列でなければ 'Left'。
-imputeCols :: ImputeKind -> [Text] -> DX.DataFrame -> Either String DX.DataFrame
-imputeCols kind = go
-  where
-    impute1 c = case kind of { ImputeMean -> imputeMean c; ImputeMedian -> imputeMedian c }
-    go []     d = Right d
-    go (c:cs) d = case impute1 c d of
-      Just d' -> go cs d'
-      Nothing -> Left $ "連続変数 '" <> T.unpack c <> "' を数値列として補完できません"
-
--- | factor 列の NA を独立水準 @"<NA>"@ に置換した Text 列で上書きする。
---   非 NA 値は 'showNum' で文字列化 ('columnAsText' の数値→文字列と同形)。
-naToCategory :: Text -> DX.DataFrame -> DX.DataFrame
-naToCategory c = deriveText c toLbl
-  where
-    toLbl row = case Map.lookup c row of
-      Just (VText t) | not (isNAString t) -> t
-      Just (VNum d)                       -> T.pack (showNum d)
-      _                                   -> "<NA>"
-
--- | 'Formula' と (policy 適用済) 'DataFrame' を突合して 'ModelFrame' を構築する。
-buildFrame :: Formula -> DX.DataFrame -> Either String ModelFrame
-buildFrame (Formula resp dvars rhs) df = do
-  -- 応答列 (数値必須)
-  yv <- maybe (Left $ "応答変数 '" <> T.unpack resp <> "' が数値列として見つかりません")
-              Right (getDoubleVec resp df)
-  let n        = V.length yv
-      indexed  = filter (`elem` dvars) (indexedVars rhs)
-      -- R 意味論 (A17b): @!@ 添字が無くても **非数値 (Text) 列は factor** として扱う
-      --   (character→factor 自動判定)。 数値列は連続のまま (numeric-coded factor は従来どおり
-      --   @!@ 添字必須) なので、 従来 error だった「Text 列を裸で置いた」場合だけが factor 化する。
-      autoFac  = [ v | v <- dvars, v `notElem` indexed, nonNumericText v ]
-      factors  = indexed ++ autoFac
-      params   = refNames rhs `minus` (resp : dvars)
-      nonNumericText v = case getDoubleVec v df of
-                           Just _  -> False
-                           Nothing -> case getTextVec v df of
-                                        Just _  -> True
-                                        Nothing -> False
-  -- 各データ変数の役割を解決
-  varRoles <- mapM (resolveVar factors df) dvars
-  pure ModelFrame
-    { mfRoles  = (resp, RoleResponse yv) : zip dvars varRoles
-    , mfParams = params
-    , mfNRows  = n
-    }
-
--- | データ変数 1 つを役割に解決する。 factors に含まれれば factor、 さもなくば連続。
-resolveVar :: [Text] -> DX.DataFrame -> Text -> Either String VarRole
-resolveVar factors df name
-  | name `elem` factors = factorRole name df
-  | otherwise           =
-      maybe (Left $ "連続変数 '" <> T.unpack name <> "' が数値列として見つかりません")
-            (Right . RoleContinuous) (getDoubleVec name df)
-
--- | factor 列を水準ラベル (昇順) + 行ごとの水準 index に。
---   text 列を優先、 無ければ数値列を文字列化 (numeric コードの factor)。
-factorRole :: Text -> DX.DataFrame -> Either String VarRole
-factorRole name df =
-  case columnAsText name df of
-    Nothing  -> Left $ "factor 変数 '" <> T.unpack name <> "' が列として見つかりません"
-    Just col ->
-      let levels = sort (nub (V.toList col))           -- 昇順 = treatment contrast の参照=第1水準
-          idxOf v = length (takeWhile (/= v) levels)    -- levels 内の位置
-          idx    = V.map idxOf col
-      in Right (RoleFactor levels idx)
-
--- | 列を [Text] 表現で取得 (factor 水準列挙用)。 text 列優先、 無ければ数値を文字列化。
-columnAsText :: Text -> DX.DataFrame -> Maybe (V.Vector Text)
-columnAsText name df =
-      getTextVec name df
-  <|> (V.map (T.pack . showNum) <$> getDoubleVec name df)
-
--- | 数値を factor 水準ラベル用に文字列化 (整数は小数点なし)。
-showNum :: Double -> String
-showNum d
-  | d == fromIntegral i = show i
-  | otherwise           = show d
-  where i = round d :: Integer
-
--- | リスト差 (左の出現順を保ち、 右に含まれる要素を除く)。
-minus :: Eq a => [a] -> [a] -> [a]
-minus xs ys = foldl' (\acc x -> if x `elem` ys || x `elem` acc then acc else acc ++ [x]) [] xs
diff --git a/src/Hanalyze/Model/Formula/Mixed.hs b/src/Hanalyze/Model/Formula/Mixed.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Formula/Mixed.hs
+++ /dev/null
@@ -1,230 +0,0 @@
--- |
--- Module      : Hanalyze.Model.Formula.Mixed
--- Description : Formula DSL の混合効果モデル (random effect) 接続層
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Formula DSL — 混合効果モデル (random effect) の接続層 (Phase 48)。
---
---   lme4 流の @(1|g)@ / @(x|g)@ / @(1+x|g)@ を Formula DSL に追加し、
---   'Hanalyze.Model.GLMM' の一般ランダム効果フィット ('fitLMEGeneral' /
---   'fitGLMMGeneral') へ route する。
---
---   ★設計判断 (Phase 48): random 項を AST の 'Term' 構成子として持たせず、
---   **字句プリパスで @(…|g)@ ブロックを抽出** する方式を採る。 理由は
---   'Term' に構成子を足すと 'Hanalyze.Model.Formula' 系 5 モジュールの網羅
---   pattern match が全て破壊されるため (計画 phase-48 のリスク注記)。 本方式なら
---   'Term'/'Formula' は不変で、 固定効果は既存の 'parseModel'/'designMatrixF'
---   経路をそのまま使え、 random 項の解釈は本モジュールに閉じる。
---
---   frequentist GLMM ゆえ random 効果に prior 宣言は不要 (分散 G は推定対象)。
-module Hanalyze.Model.Formula.Mixed
-  ( RandomSpec (..)
-  , extractRandom
-  , fitMixedF
-  , fitMixedLME
-  , fitMixedGLMM
-  ) where
-
-import           Control.Monad           (unless, when)
-import           Data.Char               (isSpace)
-import           Data.List               (intercalate)
-import           Data.Text               (Text)
-import qualified Data.Text               as T
-import qualified Data.Vector             as V
-import qualified Numeric.LinearAlgebra   as LA
-
-import qualified DataFrame.Internal.DataFrame as DXD
-import           Hanalyze.DataIO.Convert      (getDoubleVec, getTextVec)
-import           Hanalyze.DataIO.Preprocess   (dropMissingRows)
-import           Hanalyze.Model.Formula       (Formula (..))
-import           Hanalyze.Model.Formula.Design (designMatrixF, responseVec)
-import           Hanalyze.Model.Formula.Frame  (modelFrame)
-import           Hanalyze.Model.Formula.RFormula (parseModel)
-import           Hanalyze.Model.GLM           (Family (..), LinkFn (..))
-import           Hanalyze.Model.GLMM          (GLMMResultRE, buildGroups,
-                                               fitGLMMGeneral, fitLMEGeneral)
-
--- ============================================================================
--- random 項の表現
--- ============================================================================
-
--- | 1 つの @(…|g)@ ブロックの解釈結果。
---   例: @(1+x|g)@ → @RandomSpec True ["x"] "g"@ / @(0+x|g)@ → @RandomSpec False ["x"] "g"@.
-data RandomSpec = RandomSpec
-  { rsIntercept :: Bool    -- ^ random intercept を含むか (@1@ あり or 既定 True、 @0@/@-1@ で抑制)
-  , rsSlopes    :: [Text]  -- ^ random slope の変数名 (左辺の @1@/@0@/@-1@ 以外)
-  , rsGroup     :: Text    -- ^ grouping 変数名 (@|@ の右)
-  } deriving (Eq, Show)
-
--- ============================================================================
--- 字句プリパス: (…|g) ブロックの抽出
--- ============================================================================
-
--- | formula 文字列から random 項 @(…|g)@ を抽出し、 (固定効果 formula, [RandomSpec])
---   を返す。 LHS (@~@ or @=@) は保持し、 RHS から random ブロックを取り除く。
---
---   * R 構文: @"y ~ x + (1+x|g)"@ → (@"y ~ x"@, [RandomSpec True ["x"] "g"])
---   * 独自構文: @"y x = b0 + b1*x + (1|g)"@ → (@"y x = b0 + b1*x"@, [RandomSpec True [] "g"])
---
---   固定効果側に項が残らない場合 (例 @"y ~ (1|g)"@) は intercept @"1"@ を補う。
-extractRandom :: Text -> Either String (Text, [RandomSpec])
-extractRandom t =
-  let s = T.unpack t
-      (lhs, sep, rhs) = splitLHS s
-  in do
-       tokens <- pure (splitTopPlus rhs)
-       (fixedToks, specStrs) <- partitionTokens tokens
-       specs <- mapM parseBlock specStrs
-       let fixedRHS = case map trimStr (filter (not . all isSpace) fixedToks) of
-                        [] -> "1"
-                        ts -> intercalate " + " ts
-           fixedFormula = case sep of
-                            "" -> fixedRHS                       -- LHS 無し (RHS のみ)
-                            _  -> trimStr lhs ++ " " ++ sep ++ " " ++ fixedRHS
-       Right (T.pack fixedFormula, specs)
-
--- | LHS と RHS を @~@ (R) または @=@ (独自) で分割。 区切りが無ければ ("", "", whole)。
-splitLHS :: String -> (String, String, String)
-splitLHS s
-  | Just (l, r) <- breakTop '~' s = (l, "~", r)
-  | Just (l, r) <- breakTop '=' s = (l, "=", r)
-  | otherwise                     = ("", "", s)
-
--- | top-level (括弧外) の最初の区切り文字で 1 回分割。
-breakTop :: Char -> String -> Maybe (String, String)
-breakTop target = go (0 :: Int) []
-  where
-    go _ _   [] = Nothing
-    go d acc (c:cs)
-      | c == '('            = go (d+1) (c:acc) cs
-      | c == ')'            = go (d-1) (c:acc) cs
-      | c == target && d == 0 = Just (reverse acc, cs)
-      | otherwise           = go d (c:acc) cs
-
--- | top-level の @+@ で分割 (括弧内の @+@ は分割しない)。
-splitTopPlus :: String -> [String]
-splitTopPlus = go (0 :: Int) [] []
-  where
-    go _ cur acc [] = reverse (reverse cur : acc)
-    go d cur acc (c:cs)
-      | c == '('            = go (d+1) (c:cur) acc cs
-      | c == ')'            = go (d-1) (c:cur) acc cs
-      | c == '+' && d == 0  = go d [] (reverse cur : acc) cs
-      | otherwise           = go d (c:cur) acc cs
-
--- | 各トークンを固定効果トークンか random ブロック (中身) に振り分ける。
---   random ブロック = trim 後 @(…)@ で囲まれ、 内部 top-level に @|@ を持つもの。
-partitionTokens :: [String] -> Either String ([String], [String])
-partitionTokens = go [] []
-  where
-    go fixed rand [] = Right (reverse fixed, reverse rand)
-    go fixed rand (tok:rest) =
-      case asRandomBlock (trimStr tok) of
-        Just inner -> go fixed (inner : rand) rest
-        Nothing    -> go (tok : fixed) rand rest
-
--- | トークンが @(…|…)@ なら内部文字列を返す。
-asRandomBlock :: String -> Maybe String
-asRandomBlock tok =
-  case tok of
-    ('(':rest) | not (null rest), last rest == ')' ->
-      let inner = init rest
-      in if hasTopPipe inner then Just inner else Nothing
-    _ -> Nothing
-
--- | top-level に @|@ を含むか。
-hasTopPipe :: String -> Bool
-hasTopPipe = go (0 :: Int)
-  where
-    go _ [] = False
-    go d (c:cs)
-      | c == '('          = go (d+1) cs
-      | c == ')'          = go (d-1) cs
-      | c == '|' && d == 0 = True
-      | otherwise         = go d cs
-
--- | @"1 + x | g"@ → 'RandomSpec'。
-parseBlock :: String -> Either String RandomSpec
-parseBlock inner =
-  case breakTop '|' inner of
-    Nothing       -> Left "random ブロックに '|' がありません"
-    Just (lhs, rhs) ->
-      let grp   = trimStr rhs
-          terms = map trimStr (splitTopPlus lhs)
-          isSup t = t == "0" || t == "-1"
-          isOne t = t == "1"
-          hasSup  = any isSup terms
-          slopes  = [ T.pack t | t <- terms, not (isSup t), not (isOne t), not (null t) ]
-      in if null grp
-           then Left "random ブロックの grouping 変数 (| の右) が空です"
-           else Right RandomSpec
-                  { rsIntercept = not hasSup           -- 0/-1 が無ければ intercept あり
-                  , rsSlopes    = slopes
-                  , rsGroup     = T.pack grp
-                  }
-
-trimStr :: String -> String
-trimStr = f . f where f = reverse . dropWhile isSpace
-
--- ============================================================================
--- route 入口: 固定/random を分離し GLMM 一般フィットへ
--- ============================================================================
-
--- | 混合効果モデルを DataFrame からフィットする。 @Nothing@ = Gaussian LME
---   ('fitLMEGeneral')、 @Just (family, link)@ = 非 Gaussian GLMM ('fitGLMMGeneral')。
---   戻り値は (結果, 固定効果係数名)。
---
---   ★現状は **単一 grouping factor** のみ対応 ((1|g) / (x|g) / (1+x|g))。 複数の
---   @(…|g1) + (…|g2)@ は block-diagonal Z が要るため未対応 (明示エラー)。
---
---   TODO (Phase 48 follow-up):
---     * 複数 grouping factor @(…|g1) + (…|g2)@ — 群ごと Z ブロックを block-diagonal に
---       積み、 fitLMEGeneral/fitGLMMGeneral を multi-grouping 一般化する。
---     * GLMM offset (Poisson log-exposure 等) — 現状は線形 offset のみ ('fitWLSF')。
---     * REML 推定 — 現状の EM/Laplace は ML。 REML は固定効果 df 補正付き。
-fitMixedF
-  :: Maybe (Family, LinkFn)
-  -> Text -> DXD.DataFrame
-  -> Either String (GLMMResultRE, [Text])
-fitMixedF mfam formulaText df0 = do
-  (fixedText, specs) <- extractRandom formulaText
-  spec <- case specs of
-            [s] -> Right s
-            []  -> Left "random effect 項 (…|g) がありません (固定効果のみなら fitLMF を使用)"
-            _   -> Left "複数の grouping factor は未対応 (単一の (…|g) のみ)"
-  f@(Formula resp dvars _) <- parseModel fixedText
-  let slopeVars = rsSlopes spec
-      grp       = rsGroup spec
-      -- 行整列: fixedWLF と同じく formula 関与列 ∪ slope ∪ group を一括 drop
-      df        = dropMissingRows (resp : dvars ++ slopeVars ++ [grp]) df0
-  mf          <- modelFrame f df
-  (x, labels) <- designMatrixF f mf
-  yv          <- responseVec mf
-  let n = V.length yv
-  slopeCols <- mapM (\v ->
-                  maybe (Left $ "random slope 列 '" <> T.unpack v <> "' が数値列として見つかりません")
-                        Right (getDoubleVec v df)) slopeVars
-  let interceptCol = [ V.replicate n 1.0 | rsIntercept spec ]
-      zCols        = interceptCol ++ slopeCols
-  when (null zCols) $ Left "random effect の設計列が空です ((0|g) のみは不可)"
-  unless (all ((== n) . V.length) zCols) $
-    Left "random slope 列の長さが応答と一致しません"
-  gv <- maybe (Left $ "grouping 列 '" <> T.unpack grp <> "' が見つかりません")
-              Right (getTextVec grp df)
-  let z = LA.fromColumns (map (LA.fromList . V.toList) zCols)
-      y = LA.fromList (V.toList yv)
-      (glabels, idx, _sizes) = buildGroups gv
-      res = case mfam of
-              Nothing          -> fitLMEGeneral x z y idx glabels
-              Just (fam, link) -> fitGLMMGeneral fam link x z y idx glabels
-  Right (res, labels)
-
--- | Gaussian 線形混合効果モデル (LME)。 @fitMixedLME "y ~ x + (1+x|g)" df@。
-fitMixedLME :: Text -> DXD.DataFrame -> Either String (GLMMResultRE, [Text])
-fitMixedLME = fitMixedF Nothing
-
--- | 非 Gaussian GLMM。 @fitMixedGLMM Binomial Logit "y ~ x + (1|g)" df@。
-fitMixedGLMM :: Family -> LinkFn -> Text -> DXD.DataFrame
-             -> Either String (GLMMResultRE, [Text])
-fitMixedGLMM fam link = fitMixedF (Just (fam, link))
diff --git a/src/Hanalyze/Model/Formula/Nonlinear.hs b/src/Hanalyze/Model/Formula/Nonlinear.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Formula/Nonlinear.hs
+++ /dev/null
@@ -1,111 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
--- |
--- Module      : Hanalyze.Model.Formula.Nonlinear
--- Description : Formula DSL の非線形最小二乗 (NLS) fit
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Formula DSL — 非線形最小二乗 (NLS、 A4)。
---   現状 @a*exp(-b*x)@ のように **パラメータがデータ式の内側に現れる式** ('designMatrixF'
---   は線形でないとして 'Left') を、 parse 済 AST を評価関数化して既存の最適化器
---   ('Hanalyze.Optim.NelderMead') で SSR を最小化し fit する。
---
---   ★考え方: 線形 OLS と違い param 名が ŷ に効く。 @evalNL@ が「params 表 + ModelFrame」 から
---   右辺式を **行ごとの ŷ ベクトル** に評価する (param は定数、 連続データ変数は列ベクトル)。
---   目的関数 @SSR(θ) = Σ(y − ŷ(θ))²@ を Nelder-Mead で最小化。
---   ★初期値はユーザ必須 (NLS は初期値依存)。 factor 添字は非対応 (線形側で扱う)。
---   ★最適化器は IO を返すが決定論的ゆえ 'unsafePerformIO' で pure 化 (Convert.hs 同方針)。
---
---   plot 非依存・portable。
-module Hanalyze.Model.Formula.Nonlinear
-  ( NLSResult (..)
-  , fitNLS
-  , evalNL
-  ) where
-
-import           Data.Text               (Text)
-import qualified Data.Text               as T
-import qualified Data.Vector             as V
-import           System.IO.Unsafe        (unsafePerformIO)
-
-import           Hanalyze.Model.Formula  (BinOp (..), Formula (..), Term (..))
-import           Hanalyze.Model.Formula.Frame
-import           Hanalyze.Optim.Common    (OptimResult (..))
-import           Hanalyze.Optim.NelderMead (runNelderMead)
-import qualified DataFrame.Internal.DataFrame  as DX
-
--- | 非線形 fit の結果。
-data NLSResult = NLSResult
-  { nlsParams    :: [(Text, Double)]   -- ^ 推定パラメータ (名前つき)
-  , nlsFitted    :: V.Vector Double    -- ^ ŷ
-  , nlsResidual  :: V.Vector Double    -- ^ y − ŷ
-  , nlsSSR       :: Double             -- ^ 残差平方和
-  , nlsConverged :: Bool               -- ^ 最適化器が許容誤差で停止したか
-  }
-  deriving (Eq, Show)
-
--- | 右辺式を **行ごとの値ベクトル** に評価する。 params は表から定数、 連続データ変数は
---   ModelFrame の列、 factor / 応答は 'Left'。 (線形の 'evalData' と違い param を許す。)
-evalNL :: [(Text, Double)] -> ModelFrame -> Term -> Either String (V.Vector Double)
-evalNL pm mf = go
-  where
-    n = mfNRows mf
-    go t = case t of
-      Lit d -> Right (V.replicate n d)
-      Ref x -> case lookup x (mfRoles mf) of
-        Just (RoleContinuous v) -> Right v
-        Just (RoleResponse _)   -> Left $ "応答 '" <> T.unpack x <> "' をデータ式に使えません"
-        Just (RoleFactor _ _)   -> Left $ "非線形フィットは factor '" <> T.unpack x
-                                           <> "' を扱えません"
-        Nothing -> case lookup x pm of
-          Just d  -> Right (V.replicate n d)
-          Nothing -> Left $ "未知の変数 '" <> T.unpack x <> "'"
-      Neg a -> V.map negate <$> go a
-      App f [a] | Just fn <- lookup f unaryFns -> V.map fn <$> go a
-      App f _   -> Left $ "未対応の関数 '" <> T.unpack f
-                           <> "' (log/exp/sqrt/sin/cos/tan/abs の単項のみ)"
-      Bin op a b -> V.zipWith (binFn op) <$> go a <*> go b
-      Index _ _  -> Left "非線形フィットは factor 添字を扱えません"
-
-unaryFns :: [(Text, Double -> Double)]
-unaryFns =
-  [ ("log", log), ("exp", exp), ("sqrt", sqrt)
-  , ("sin", sin), ("cos", cos), ("tan", tan), ("abs", abs) ]
-
-binFn :: BinOp -> (Double -> Double -> Double)
-binFn Add = (+)
-binFn Sub = (-)
-binFn Mul = (*)
-binFn Div = (/)
-binFn Pow = (**)
-
--- | 非線形最小二乗。 @inits@ = 各パラメータの初期値 (mfParams を網羅する必要がある)。
---   SSR を Nelder-Mead で最小化する。 不正値 (NaN) を出すパラメータ域は +∞ で罰する。
-fitNLS :: Formula -> DX.DataFrame -> [(Text, Double)] -> Either String NLSResult
-fitNLS f@(Formula _ _ rhs) df inits = do
-  mf <- modelFrame f df
-  yv <- case mfRoles mf of
-          ((_, RoleResponse v) : _) -> Right v
-          _                         -> Left "ModelFrame に応答列がありません"
-  let pnames  = map fst inits
-      missing = filter (`notElem` pnames) (mfParams mf)
-  if not (null missing)
-    then Left $ "初期値が無いパラメータ: " <> show (map T.unpack missing)
-    else do
-      _ <- evalNL inits mf rhs                       -- 評価可能性を先に検証
-      let sse yhat = V.sum (V.map (\e -> e * e) (V.zipWith (-) yv yhat))
-          ssrAt vals = case evalNL (zip pnames vals) mf rhs of
-                         Right yhat -> let s = sse yhat in if isNaN s then 1 / 0 else s
-                         Left _     -> 1 / 0
-          res  = unsafePerformIO (runNelderMead ssrAt (map snd inits))
-          pm   = zip pnames (orBest res)
-      yhat <- evalNL pm mf rhs
-      let resid = V.zipWith (-) yv yhat
-      Right NLSResult
-        { nlsParams    = pm
-        , nlsFitted    = yhat
-        , nlsResidual  = resid
-        , nlsSSR       = sse yhat
-        , nlsConverged = orConverged res
-        }
diff --git a/src/Hanalyze/Model/Formula/RFormula.hs b/src/Hanalyze/Model/Formula/RFormula.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Formula/RFormula.hs
+++ /dev/null
@@ -1,268 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
--- |
--- Module      : Hanalyze.Model.Formula.RFormula
--- Description : Formula DSL の R/patsy 互換 front-end (@y ~ x + C(g)@ 構文)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Formula DSL — R/patsy front-end (A18)。 @y ~ x + C(g)@ 形式を **同じ 'Formula' AST**
---   に落とす (サブ front-end)。 正本は独自構文 (A15)、 本モジュールは互換・オラクル用途。
---
---   ★dispatch: 文字列に @~@ が含まれれば R、 無ければ独自 ('parseModel')。 @~@ と @=@ は
---   字句的に分離ゆえ曖昧性ゼロ。
---
---   ★R formula 意味論 → 我々の AST:
---     - @~@ で 応答 / 予測子 を分離。 予測子は @+@ 区切り (これは「項追加」、 算術でない)。
---     - 暗黙の切片あり。 @-1@ / @0@ で切片除去。
---     - 連続変数 @x@ → @b*x@ (本物の積)。 ★categorical は **@C(g)@** で明示
---       (patsy 同様。 data 無しで parse するため列型推論はしない)。
---     - @a:b@ = 交互作用のみ、 @a*b@ = @a + b + a:b@ (crossing)。
---     - @I(expr)@ = 算術 (@x**2@/@x^2@ 等)、 @log(x)@ = 関数変換、 @poly(x,n)@/@bs(x,n)@ = 基底。
---   ★パラメータ名は合成 (@_p0,_p1,…@)。 線形 OLS では係数名は fit に無関係ゆえ問題なし。
---   ★data 変数は RHS に現れた変数名 (合成パラメータ以外) を収集。
---
---   plot 非依存・portable (AST のみ依存)。
-module Hanalyze.Model.Formula.RFormula
-  ( parseRFormula
-  , parseModel
-  ) where
-
-import           Control.Monad.Combinators.Expr (Operator (..), makeExprParser)
-import           Data.List                      (isPrefixOf, nub, subsequences)
-import           Data.Text                      (Text)
-import qualified Data.Text                      as T
-import           Data.Void                      (Void)
-import           Text.Megaparsec
-import           Text.Megaparsec.Char           (alphaNumChar, char, letterChar,
-                                                 space1)
-import qualified Text.Megaparsec.Char.Lexer     as L
-
-import           Hanalyze.Model.Formula          (BinOp (..), Formula (..),
-                                                  Term (..), parseFormula)
-
--- ============================================================================
--- dispatch
--- ============================================================================
-
--- | front-end 自動判別: @~@ を含めば R、 さもなくば独自構文。
-parseModel :: Text -> Either String Formula
-parseModel t
-  | T.any (== '~') t = parseRFormula t
-  | otherwise        = parseFormula t
-
--- ============================================================================
--- 字句
--- ============================================================================
-
-type P = Parsec Void Text
-
-sc :: P ()
-sc = L.space space1 empty empty
-
-lexeme :: P a -> P a
-lexeme = L.lexeme sc
-
-symbol :: Text -> P Text
-symbol = L.symbol sc
-
-ident :: P Text
-ident = lexeme $ do
-  c  <- letterChar <|> char '_'
-  cs <- many (alphaNumChar <|> char '_' <|> char '.')
-  pure (T.pack (c : cs))
-
-intLit :: P Int
-intLit = lexeme (L.signed (pure ()) L.decimal)
-
-numLit :: P Double
-numLit = lexeme (try (L.signed (pure ()) L.float)
-                 <|> (fromIntegral <$> L.signed (pure ()) (L.decimal :: P Integer)))
-
-parens :: P a -> P a
-parens = between (symbol "(") (symbol ")")
-
--- ============================================================================
--- 中間表現 (R 項)
--- ============================================================================
-
--- | R 項の因子。
-data RFactor
-  = RVar  Text             -- ^ 連続変数 x
-  | RCat  Text (Maybe Text) -- ^ C(g) / C(g, Sum) categorical (+ contrast 名)
-  | RFun  Text Term        -- ^ log(x) 等の関数変換 (1 引数)
-  | RI    Term        -- ^ I(expr) 算術
-  | RPoly Text Int    -- ^ poly(x, n)   生べき (x¹..xⁿ)
-  | ROPoly Text Int   -- ^ opoly(x, n)  実測値の直交多項式 (R poly 既定と同じ)
-  | RBs   Text Int    -- ^ bs(x, n)
-
--- | R 項: 数値 (0/1) か、 因子の積 (hasStar=True なら crossing 展開)。
-data RComp = RNum Int | RProd Bool [RFactor]
-
--- ============================================================================
--- パーサ
--- ============================================================================
-
--- | @lhs ~ rhs@。
-pRFormula :: P Formula
-pRFormula = do
-  sc
-  lhs   <- ident
-  _     <- symbol "~"
-  comps <- pRHS
-  eof
-  buildFormula lhs comps
-
--- | RHS = 符号付き項の並び。 戻り値 = (符号, 項)。
-pRHS :: P [(Int, RComp)]
-pRHS = do
-  s0 <- option 1 sign
-  c0 <- pComp
-  rest <- many ((,) <$> sign <*> pComp)
-  pure ((s0, c0) : rest)
-  where sign = (1 <$ symbol "+") <|> ((-1) <$ symbol "-")
-
--- | 1 項 (数値 or 因子の積)。
-pComp :: P RComp
-pComp =
-      try (RNum <$> lexeme L.decimal)
-  <|> pProduct
-
--- | 因子を @*@ / @:@ で結んだ積。 @*@ が 1 つでもあれば crossing。
-pProduct :: P RComp
-pProduct = do
-  f0 <- pFactor
-  rest <- many ((,) <$> ((True <$ symbol "*") <|> (False <$ symbol ":")) <*> pFactor)
-  let hasStar = any fst rest
-      facs    = f0 : map snd rest
-  pure (RProd hasStar facs)
-
-pFactor :: P RFactor
-pFactor =
-      try (symbol "C" *> parens pCatArgs)
-  <|> try (RI    <$> (symbol "I"  *> parens pArith))
-  <|> try (ROPoly <$> (symbol "opoly" *> symbol "(" *> ident) <*> (symbol "," *> intLit <* symbol ")"))
-  <|> try (RPoly <$> (symbol "poly" *> symbol "(" *> ident) <*> (symbol "," *> intLit <* symbol ")"))
-  <|> try (RBs   <$> (symbol "bs"   *> symbol "(" *> ident) <*> (symbol "," *> intLit <* symbol ")"))
-  <|> try pFunOrVar
-
--- | @C(g)@ / @C(g, Sum)@ の中身: factor 名 + 省略可能な contrast 名。
-pCatArgs :: P RFactor
-pCatArgs = do
-  g     <- ident
-  mcode <- optional (symbol "," *> ident)
-  pure (RCat g mcode)
-
--- | @log(x)@ のような関数変換、 または裸の変数。
-pFunOrVar :: P RFactor
-pFunOrVar = do
-  nm <- ident
-  margs <- optional (parens pArith)
-  pure $ case margs of
-    Just a  -> RFun nm a
-    Nothing -> RVar nm
-
--- | I(...) 内の算術式 (@+ - * / ^ **@・関数適用・括弧)。
-pArith :: P Term
-pArith = makeExprParser pArithApp
-  [ [ InfixR (Bin Pow <$ (symbol "**" <|> symbol "^")) ]
-  , [ Prefix (Neg     <$ symbol "-") ]
-  , [ InfixL (Bin Mul <$ symbol "*"), InfixL (Bin Div <$ symbol "/") ]
-  , [ InfixL (Bin Add <$ symbol "+"), InfixL (Bin Sub <$ symbol "-") ]
-  ]
-
-pArithApp :: P Term
-pArithApp = do
-  h <- pArithAtom
-  case h of
-    Ref f -> do
-      margs <- optional (parens (pArith `sepBy1` symbol ","))
-      pure $ maybe h (App f) margs
-    _ -> pure h
-
-pArithAtom :: P Term
-pArithAtom =
-      (Lit <$> numLit)
-  <|> parens pArith
-  <|> (Ref <$> ident)
-
--- ============================================================================
--- 構築 (中間表現 → Formula AST)
--- ============================================================================
-
-buildFormula :: Text -> [(Int, RComp)] -> P Formula
-buildFormula lhs comps = do
-  let removeInt = any (\(s, c) -> case c of
-                         RNum 0 -> s == 1            -- + 0
-                         RNum 1 -> s == (-1)         -- - 1
-                         _      -> False) comps
-      prods = [ p | (_, RProd star fs) <- comps, p <- expand star fs ]
-      terms = (if removeInt then [] else [const1]) ++ map prodToTerm prods
-  if null terms
-    then fail "R formula: 項がありません"
-    else do
-      let named   = zipWith (\i mk -> mk (synth i)) [0 :: Int ..] terms
-          rhs     = foldr1 (Bin Add) named
-          dvars   = nub (filter (not . isSynth) (refNamesT rhs))
-      pure (Formula lhs dvars rhs)
-  where
-    synth i  = T.pack ("_p" ++ show i)
-    const1 p = Ref p                                  -- 切片 (定数項)
-
--- | crossing 展開: @*@ なら全非空部分集合 (R の a*b = a + b + a:b)、 @:@ なら単一交互作用。
---   列の順序は fit (ŷ) に無関係ゆえ 'subsequences' の順序で可。
-expand :: Bool -> [RFactor] -> [[RFactor]]
-expand False fs = [fs]
-expand True  fs = filter (not . null) (subsequences fs)
-
--- | 1 つの積 (因子リスト) → パラメータ名を取って Term を作る関数。
-prodToTerm :: [RFactor] -> (Text -> Term)
-prodToTerm facs p =
-  let cats   = [ (nm, mc) | RCat nm mc <- facs ]
-      polys  = [ (nm, n) | RPoly nm n <- facs ]
-      opolys = [ (nm, n) | ROPoly nm n <- facs ]
-      bss    = [ (nm, n) | RBs   nm n <- facs ]
-      datums = concatMap factorData facs
-  in case (polys, opolys, bss) of
-       ((nm, n) : _, _, _) -> Index (Ref p) (App "poly"    [Ref nm, Lit (fromIntegral n)])
-       (_, (nm, n) : _, _) -> Index (Ref p) (App "opoly"   [Ref nm, Lit (fromIntegral n)])
-       (_, _, (nm, n) : _) -> Index (Ref p) (App "bspline" [Ref nm, Lit (fromIntegral n)])
-       _ ->
-         let base = foldl (\acc (nm, mc) -> Index acc (catTerm nm mc)) (Ref p) cats
-         in case datums of
-              []     -> base                          -- 切片 or 純 factor
-              (d:ds) -> Bin Mul base (foldl (Bin Mul) d ds)
-
--- | categorical 添字項を AST に: @C(g)@ → @Ref g@ (無注釈 treatment)、
---   @C(g, Sum)@ → @App "C" [Ref g, Ref Sum]@ (contrast 注釈・正本 AST と同形)。
-catTerm :: Text -> Maybe Text -> Term
-catTerm nm Nothing  = Ref nm
-catTerm nm (Just c) = App "C" [Ref nm, Ref c]
-
--- | 因子のデータ式部分 (連続/関数/I)。 factor/basis はここに出さない。
-factorData :: RFactor -> [Term]
-factorData (RVar x)   = [Ref x]
-factorData (RFun f a) = [App f [a]]
-factorData (RI t)     = [t]
-factorData _          = []
-
--- | 合成パラメータ名か。
-isSynth :: Text -> Bool
-isSynth n = "_p" `isPrefixOf` T.unpack n
-
--- | Term 中の Ref 名 (data 変数収集用)。
-refNamesT :: Term -> [Text]
-refNamesT t = case t of
-  Ref x               -> [x]
-  Lit _               -> []
-  App "C" (Ref x : _) -> [x]                     -- contrast 注釈: factor 名のみ (coding 名は除外)
-  App _ as            -> concatMap refNamesT as
-  Index a b           -> refNamesT a ++ refNamesT b
-  Neg a               -> refNamesT a
-  Bin _ a b           -> refNamesT a ++ refNamesT b
-
--- | 文字列 → 'Formula' (R front-end)。
-parseRFormula :: Text -> Either String Formula
-parseRFormula txt = case parse pRFormula "<r-formula>" txt of
-  Left e  -> Left (errorBundlePretty e)
-  Right f -> Right f
diff --git a/src/Hanalyze/Model/GAM.hs b/src/Hanalyze/Model/GAM.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/GAM.hs
+++ /dev/null
@@ -1,329 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.GAM
--- Description : 一般化加法モデル (Generalized Additive Model, GAM)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Generalized Additive Model (GAM).
---
--- @y = β₀ + Σ_j s_j(x_j) + ε@ where each smooth term @s_j(x_j) = B_j(x_j) γ_j@
--- is **linear in its coefficients** for *any* basis @B_j@ (B-spline / natural
--- cubic / polynomial / Fourier / RBF). The basis is therefore abstracted as
--- 'GAMBasis' (Phase 70.6 F1); the fit, predict, and per-component paths all
--- dispatch on the realized basis ('BasisRealized') learned from the training
--- @x@, so prediction at new points rebuilds the *same* basis matrix.
---
--- Design:
---
---   * For each predictor @x_j@, build a basis matrix @B_j@ (@n × m_j@) per
---     'GAMBasis'.
---   * Stack into a single design matrix
---     @X = [1 | B_1 | B_2 | ... | B_p]@ (@1 + Σ m_j@ columns).
---   * Ridge-regularized OLS:
---     @β = (XᵀX + λ P)⁻¹ Xᵀ y@ with @P = diag(0,1,…,1)@ (intercept免除).
---     The same @λ@ stabilizes every basis (smoothness regularization).
---   * @λ@ may be fixed ('FixedL') or chosen by GCV ('GCV') — Phase 70.6 F2.
---   * Prediction: the per-feature contribution @s_j(x_j)@ can be extracted
---     individually for visualization of each factor's effect.
---
--- 注: 識別性のため、各基底は中央化 (列平均を引く) する。
--- これで β₀ は y の平均、s_j は変動成分のみを表す。
-module Hanalyze.Model.GAM
-  ( -- * 基底の抽象化 (Phase 70.6 F1)
-    GAMBasis (..)
-  , BasisRealized (..)
-  , GAMLambda (..)
-    -- * フィット結果
-  , GAMFit (..)
-    -- * フィット
-  , fitGAM
-  , fitGAMWith
-  , fitGAMAuto
-    -- * 予測
-  , predictGAM
-  , predictGAMSE
-  , predictGAMComponent
-  ) where
-
-import qualified Data.Vector as V
-import qualified Numeric.LinearAlgebra as LA
-import Hanalyze.Model.Spline (bsplineBasis, naturalSplineBasis, equalSpacedKnots)
-
--- ---------------------------------------------------------------------------
--- 基底の抽象化
--- ---------------------------------------------------------------------------
-
--- | 平滑項 @s_j(x_j)@ の基底の種類 (係数について線形なものを列挙)。
---   各々 @x → 基底行列 (n × m)@ を与える。
-data GAMBasis
-  = BSplineB Int Int   -- ^ @BSplineB degree nKnots@: degree 次 B-spline (内部ノット @nKnots@)。
-  | NaturalCubicB Int  -- ^ @NaturalCubicB nKnots@: 自然3次回帰スプライン (内部ノット @nKnots@)。
-  | PolyB Int          -- ^ @PolyB degree@: 直交化なしの多項式 (@[t,t²,…,t^degree]@・@t∈[-1,1]@ にスケール)。
-  | FourierB Int       -- ^ @FourierB nHarmonics@: Fourier 基底 (@sin/cos@ を @nHarmonics@ 次まで)。
-  | RBFB Int Double    -- ^ @RBFB nCenters bandwidthRel@: ガウス RBF (等間隔中心・帯域 = 中心間隔×bandwidthRel)。
-  deriving (Show, Eq)
-
--- | 学習済み基底。 訓練 @x@ から決まる具体パラメタ (ノット/中心/レンジ) を保持し、
---   任意の新 @x@ に対し同一の基底行列を再構築できる ('evalBasis')。
-data BasisRealized
-  = RBSpline Int [Double]      -- ^ degree, 内部ノット列。
-  | RNaturalCubic [Double]     -- ^ ノット列。
-  | RPoly Int Double Double    -- ^ degree, xmin, xmax (@t = 2(x−lo)/(hi−lo)−1@ にスケール)。
-  | RFourier Int Double Double -- ^ nHarmonics, xmin, period (@t = (x−lo)/period@)。
-  | RRBF [Double] Double       -- ^ 中心列, 帯域 (絶対値)。
-  deriving (Show)
-
--- | @λ@ の決め方。 'FixedL' は固定値、 'GCV' は一般化交差検証で 1 次元探索 (Phase 70.6 F2)。
-data GAMLambda
-  = FixedL Double  -- ^ 固定 @λ@ (@0@ で罰則なし)。
-  | GCV            -- ^ GCV @λ* = argmin_λ n·RSS(λ)/(n−edf(λ))²@ を log グリッド探索。
-  deriving (Show, Eq)
-
--- | 'GAMBasis' を訓練 @x@ で実体化する。
-realizeBasis :: GAMBasis -> V.Vector Double -> BasisRealized
-realizeBasis b xs =
-  let lo = if V.null xs then 0 else V.minimum xs
-      hi = if V.null xs then 1 else V.maximum xs
-  in case b of
-       BSplineB deg nK     -> RBSpline deg (equalSpacedKnots (nK + 2) lo hi)
-       -- 自然3次は基底に ≥3 ノット必要 (端2 + 内部)。 等間隔で nK+2 点 (両端含む)。
-       NaturalCubicB nK    -> RNaturalCubic (equalSpacedKnots (max 3 (nK + 2)) lo hi)
-       PolyB deg           -> RPoly (max 1 deg) lo hi
-       FourierB h          -> RFourier (max 1 h) lo (let p = hi - lo in if p <= 0 then 1 else p)
-       RBFB c bwRel        ->
-         let nc      = max 2 c
-             centers = equalSpacedKnots nc lo hi
-             spacing = if nc < 2 then 1 else (hi - lo) / fromIntegral (nc - 1)
-             bw      = (if spacing <= 0 then 1 else spacing) * (if bwRel <= 0 then 1 else bwRel)
-         in RRBF centers bw
-
--- | 学習済み基底で新 @x@ の基底行列 (@n × m@・**未中央化**) を作る。
-evalBasis :: BasisRealized -> V.Vector Double -> LA.Matrix Double
-evalBasis br xs = case br of
-  RBSpline deg knots -> bsplineBasis deg knots xs
-  -- naturalSplineBasis は先頭に定数列を含む → GAM は別途切片を持つので落とす。
-  RNaturalCubic knots ->
-    let m = naturalSplineBasis knots xs
-    in if LA.cols m <= 1 then m else m LA.?? (LA.All, LA.Drop 1)
-  RPoly deg lo hi ->
-    let denom = hi - lo
-        t x   = if denom <= 0 then 0 else 2 * (x - lo) / denom - 1
-        row x = [ t x ^^ k | k <- [1 .. deg] ]
-    in LA.fromLists [ row x | x <- V.toList xs ]
-  RFourier h lo period ->
-    let t x   = (x - lo) / period
-        row x = concat [ [ sin (2 * pi * fromIntegral k * t x)
-                         , cos (2 * pi * fromIntegral k * t x) ]
-                       | k <- [1 .. h] ]
-    in LA.fromLists [ row x | x <- V.toList xs ]
-  RRBF centers bw ->
-    let row x = [ exp (negate 0.5 * ((x - c) / bw) ^ (2 :: Int)) | c <- centers ]
-    in LA.fromLists [ row x | x <- V.toList xs ]
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
--- | GAM fit result.
-data GAMFit = GAMFit
-  { gamDegree    :: Int                  -- ^ (後方互換) 先頭 B-spline 項の degree。非 B-spline は 0。
-  , gamKnots     :: [[Double]]           -- ^ (後方互換) 項ごとのノット列。ノットを持たない基底は @[]@。
-  , gamBases     :: [BasisRealized]      -- ^ ★評価の正典: 項ごとの学習済み基底。
-  , gamBetas     :: [LA.Vector Double]   -- ^ Per-feature spline coefficients @γ_j@.
-  , gamColMeans  :: [LA.Vector Double]   -- ^ Per-feature column means of @B_j@ (for centering).
-  , gamIntercept :: Double               -- ^ Intercept @β₀@.
-  , gamYHat      :: LA.Vector Double     -- ^ Fitted values.
-  , gamResid     :: LA.Vector Double     -- ^ Residuals.
-  , gamR2        :: Double               -- ^ R².
-  , gamLambda    :: Double               -- ^ Ridge penalty @λ@ used (GCV のときは選ばれた値)。
-  , gamEdf       :: Double               -- ^ 有効自由度 @tr(S_λ)@ (GCV 用)。
-  , gamCov       :: LA.Matrix Double     -- ^ 係数共分散 @Vβ = (XᵀX+λP)⁻¹·φ̂@
-                                         --   (mgcv 流 Bayesian CI 用・@φ̂ = RSS/(n−edf)@)。
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- フィット
--- ---------------------------------------------------------------------------
-
--- | Fit a GAM (B-spline 基底固定の薄ラッパ・後方互換)。
-fitGAM :: Int                    -- ^ B-spline degree (3 = cubic recommended).
-       -> Int                    -- ^ Number of interior knots (e.g. 5).
-       -> Double                 -- ^ Ridge penalty @λ@ (0 disables regularization).
-       -> [V.Vector Double]      -- ^ Predictors @[x₁, x₂, …]@.
-       -> V.Vector Double        -- ^ Response @y@.
-       -> GAMFit
-fitGAM degree nKnots lambda xss =
-  fitGAMWith [ BSplineB degree nKnots | _ <- xss ] lambda xss
-
--- | Fit a GAM with per-term基底を明示 + 固定 @λ@ (Phase 70.6 F1)。
-fitGAMWith :: [GAMBasis]          -- ^ 項ごとの基底 (長さ = 予測子数)。
-           -> Double              -- ^ Ridge penalty @λ@.
-           -> [V.Vector Double]   -- ^ Predictors.
-           -> V.Vector Double     -- ^ Response @y@.
-           -> GAMFit
-fitGAMWith bases lambda xss y =
-  let realized = zipWith realizeBasis bases xss
-  in fitCore realized lambda xss y
-
--- | Fit a GAM choosing @λ@ via 'GAMLambda' (FixedL / GCV) (Phase 70.6 F2)。
-fitGAMAuto :: [GAMBasis] -> GAMLambda -> [V.Vector Double] -> V.Vector Double -> GAMFit
-fitGAMAuto bases lam xss y =
-  let realized = zipWith realizeBasis bases xss
-  in case lam of
-       FixedL l -> fitCore realized l xss y
-       GCV      ->
-         let grid = [ 10 ** e | e <- [(-4.0), (-3.5) .. 4.0 :: Double] ]
-             score l = gamGCV (fitCore realized l xss y)
-             best = snd (minimum [ (score l, l) | l <- grid ])
-         in fitCore realized best xss y
-
--- | GCV 値 @n·RSS/(n−edf)²@ (小さいほど良い)。
-gamGCV :: GAMFit -> Double
-gamGCV fit =
-  let n   = fromIntegral (LA.size (gamResid fit)) :: Double
-      rss = LA.sumElements (LA.cmap (^ (2 :: Int)) (gamResid fit))
-      den = n - gamEdf fit
-  in if den <= 1e-9 then 1/0 else n * rss / (den * den)
-
--- | 学習済み基底列 + 固定 @λ@ で最小二乗を解く中核。
-fitCore :: [BasisRealized] -> Double -> [V.Vector Double] -> V.Vector Double -> GAMFit
-fitCore realized lambda xss y =
-  let n         = V.length y
-      -- 各 B_j (n × m_j) を構築 + 列平均で中央化
-      basisRaw  = zipWith evalBasis realized xss
-      colMeans  = [ LA.fromList
-                      [ LA.sumElements (LA.flatten (b LA.¿ [j])) / fromIntegral n
-                      | j <- [0 .. LA.cols b - 1] ]
-                  | b <- basisRaw ]
-      basisCent = zipWith centerCols basisRaw colMeans
-
-      -- 統合計画行列 X = [1 | B_1 | B_2 | ...]
-      ones = LA.asColumn (LA.konst 1 n)
-      x    = foldl1 (LA.|||) (ones : basisCent)
-      yLA  = LA.fromList (V.toList y)
-      p    = LA.cols x
-
-      -- Ridge: β = (XᵀX + λ I')⁻¹ Xᵀ y  (intercept 列はペナルティ免除)
-      pen  = LA.diag (LA.fromList (0 : replicate (p - 1) lambda))
-      xtx  = LA.tr x LA.<> x
-      lhs  = xtx + pen
-      lhsInv = LA.inv lhs                -- (XᵀX+λP)⁻¹ (edf と Vβ で共用)
-      xty  = LA.tr x LA.#> yLA
-      beta = lhsInv LA.#> xty
-
-      -- 有効自由度 edf = tr(S_λ) = tr((XᵀX+λP)⁻¹ XᵀX)
-      edf  = sumDiag (lhsInv LA.<> xtx)
-
-      -- intercept = β[0]、各特徴の γ_j を切り出す
-      mSizes = [ LA.cols b | b <- basisRaw ]
-      starts = scanl (+) 1 mSizes        -- intercept は 0
-      betas  = [ LA.subVector (starts !! j) (mSizes !! j) beta
-               | j <- [0 .. length xss - 1] ]
-      intercept = beta LA.! 0
-
-      yhat  = x LA.#> beta
-      resid = yLA - yhat
-      yMean = LA.sumElements yLA / fromIntegral n
-      tss   = LA.sumElements (LA.cmap (\v -> (v - yMean) ^ (2 :: Int)) yLA)
-      rss   = LA.sumElements (LA.cmap (^ (2 :: Int)) resid)
-      r2    = if tss < 1e-12 then 0 else 1 - rss / tss
-      -- CI 用係数共分散 Vβ = (XᵀX+λP)⁻¹·φ̂ (mgcv 流 Bayesian・φ̂ = RSS/(n−edf))。
-      dfRes = fromIntegral n - edf
-      phi   = if dfRes > 1e-9 then rss / dfRes else rss
-      cov   = LA.scale phi lhsInv
-  in GAMFit
-       { gamDegree    = case realized of { (RBSpline d _ : _) -> d; _ -> 0 }
-       , gamKnots     = map knotsOf realized
-       , gamBases     = realized
-       , gamBetas     = betas
-       , gamColMeans  = colMeans
-       , gamIntercept = intercept
-       , gamYHat      = yhat
-       , gamResid     = resid
-       , gamR2        = r2
-       , gamLambda    = lambda
-       , gamEdf       = edf
-       , gamCov       = cov
-       }
-  where
-    -- 列平均を引いて中央化
-    centerCols :: LA.Matrix Double -> LA.Vector Double -> LA.Matrix Double
-    centerCols m mu =
-      let cols = LA.toColumns m
-          centered = zipWith (\c muVal -> LA.cmap (\v -> v - muVal) c)
-                       cols (LA.toList mu)
-      in LA.fromColumns centered
-    sumDiag :: LA.Matrix Double -> Double
-    sumDiag = LA.sumElements . LA.takeDiag
-    knotsOf :: BasisRealized -> [Double]
-    knotsOf (RBSpline _ k)     = k
-    knotsOf (RNaturalCubic k)  = k
-    knotsOf _                  = []
-
--- ---------------------------------------------------------------------------
--- 予測
--- ---------------------------------------------------------------------------
-
--- | Predict at new predictors.
-predictGAM :: GAMFit -> [V.Vector Double] -> V.Vector Double
-predictGAM fit xss =
-  let n = if null xss then 0 else V.length (head xss)
-      contributions = zipWith4 componentVec
-                        (gamBases fit) (gamBetas fit) (gamColMeans fit) xss
-      total = foldl' (V.zipWith (+)) (V.replicate n (gamIntercept fit))
-                contributions
-  in total
-  where
-    foldl' f z [] = z
-    foldl' f z (a:as) = let !z' = f z a in foldl' f z' as
-    componentVec :: BasisRealized -> LA.Vector Double -> LA.Vector Double
-                 -> V.Vector Double -> V.Vector Double
-    componentVec br gamma mu xs =
-      let b      = evalBasis br xs
-          n'     = LA.rows b
-          ys     = b LA.#> gamma
-          shiftV = LA.dot mu gamma
-      in V.fromList [ ys LA.! i - shiftV | i <- [0 .. n' - 1] ]
-
--- | Predict + 各評価点の **pointwise standard error** を返す (CI 帯用)。
---
---   評価点設計行列 @Xeval = [1 | (B_j − colMean_j) | …]@ を fit と同じ中央化で組み、
---   @se_i = √(b_i Vβ b_iᵀ)@ ('gamCov' = @Vβ@)。 中心 @μ̂@ は 'predictGAM' と一致する。
---   信頼水準 → 臨界値 (t) の掛け算は呼び出し側 (描画層) が行う。
-predictGAMSE :: GAMFit -> [V.Vector Double] -> (V.Vector Double, V.Vector Double)
-predictGAMSE fit xss =
-  let nEval     = if null xss then 0 else V.length (head xss)
-      mu        = predictGAM fit xss
-      basisRaw  = zipWith evalBasis (gamBases fit) xss
-      basisCent = zipWith subtractColMeans basisRaw (gamColMeans fit)
-      ones      = LA.asColumn (LA.konst 1 nEval)
-      xEval     = foldl1 (LA.|||) (ones : basisCent)      -- nEval × p
-      m1        = xEval LA.<> gamCov fit                  -- nEval × p
-      varVec    = [ LA.dot rM rX | (rM, rX) <- zip (LA.toRows m1) (LA.toRows xEval) ]
-      se        = map (sqrt . max 0) varVec
-  in (mu, V.fromList se)
-
--- | 各列から学習時の列平均を引く (評価点を fit と同じ中央化にする)。
-subtractColMeans :: LA.Matrix Double -> LA.Vector Double -> LA.Matrix Double
-subtractColMeans m mu =
-  LA.fromColumns (zipWith (\c muVal -> LA.cmap (subtract muVal) c)
-                          (LA.toColumns m) (LA.toList mu))
-
--- | The contribution @s_j(x)@ from feature @j@ only (without the intercept).
-predictGAMComponent :: GAMFit -> Int -> V.Vector Double -> V.Vector Double
-predictGAMComponent fit j xs
-  | j < 0 || j >= length (gamBetas fit) = V.empty
-  | otherwise =
-      let b      = evalBasis (gamBases fit !! j) xs
-          gamma  = gamBetas fit !! j
-          mu     = gamColMeans fit !! j
-          ys     = b LA.#> gamma
-          shiftV = LA.dot mu gamma
-          n      = LA.rows b
-      in V.fromList [ ys LA.! i - shiftV | i <- [0 .. n - 1] ]
-
--- 4-引数 zipWith (base に無いので局所定義)。
-zipWith4 :: (a -> b -> c -> d -> e) -> [a] -> [b] -> [c] -> [d] -> [e]
-zipWith4 f (a:as) (b:bs) (c:cs) (d:ds) = f a b c d : zipWith4 f as bs cs ds
-zipWith4 _ _ _ _ _ = []
diff --git a/src/Hanalyze/Model/GARCH.hs b/src/Hanalyze/Model/GARCH.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/GARCH.hs
+++ /dev/null
@@ -1,222 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Model.GARCH
--- Description : GARCH(1,1) 条件付き分散モデル (Generalized AutoRegressive Conditional Heteroskedasticity)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- GARCH(1,1) — Generalized AutoRegressive Conditional Heteroskedasticity.
---
--- Bollerslev (1986). Models time-varying conditional variance for a
--- (de-meaned) return series:
---
--- @
---   y_t       = μ + ε_t,   ε_t = σ_t · z_t,   z_t ~ N(0, 1)
---   σ²_t      = ω + α · ε²_{t-1} + β · σ²_{t-1}
--- @
---
--- Constraints: @ω > 0, α ≥ 0, β ≥ 0, α + β < 1@ (stationarity).
---
--- Estimation by quasi-MLE under Gaussian innovations, optimized with
--- L-BFGS using numeric gradients. The constraints are enforced via a
--- reparametrization (softplus for ω, a stick-breaking sigmoid pair for
--- α and β capped at @0.999@).
---
--- @
--- import Hanalyze.Model.GARCH
---
--- let fit = fitGARCH ys                    -- GARCH(1,1) on the series
---     vh  = forecastGARCH fit 10           -- 10-step ahead σ² forecast
--- @
---
--- == Implemented
---
---   * 'fitGARCH' (GARCH(1,1) Gaussian QMLE)
---   * 'forecastGARCH' (h-step-ahead conditional variance)
-module Hanalyze.Model.GARCH
-  ( GARCHFit (..)
-  , fitGARCH
-  , forecastGARCH
-  ) where
-
-import qualified Numeric.LinearAlgebra      as LA
-import qualified Hanalyze.Optim.LBFGS       as LBFGS
-import qualified Hanalyze.Optim.Common      as OC
-import           System.IO.Unsafe           (unsafePerformIO)
-
--- ---------------------------------------------------------------------------
--- Types
--- ---------------------------------------------------------------------------
-
--- | Fitted GARCH(1,1) model.
-data GARCHFit = GARCHFit
-  { gOmega      :: !Double            -- ^ Unconditional variance offset @ω@.
-  , gAlpha      :: !Double            -- ^ ARCH coefficient @α@.
-  , gBeta       :: !Double            -- ^ GARCH coefficient @β@.
-  , gMu         :: !Double            -- ^ Mean of @y_t@.
-  , gSigma2     :: !(LA.Vector Double) -- ^ In-sample conditional variance @σ²_t@.
-  , gResiduals  :: !(LA.Vector Double) -- ^ In-sample residuals @ε_t = y_t - μ@.
-  , gLogLik     :: !Double            -- ^ Maximized Gaussian log-likelihood.
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- Reparametrization helpers
--- ---------------------------------------------------------------------------
-
-softplus :: Double -> Double
-softplus x
-  | x >  50 = x
-  | x < -50 = exp x
-  | otherwise = log1p (exp x)
-  where log1p z = log (1 + z)
-
-sigmoid :: Double -> Double
-sigmoid x
-  | x >  500 = 1
-  | x < -500 = 0
-  | otherwise = 1 / (1 + exp (-x))
-
--- | Map unconstrained @(θω, θα, θβ)@ to @(ω, α, β)@.
-unpackParams :: Double -> Double -> Double -> (Double, Double, Double)
-unpackParams t0 t1 t2 =
-  let w   = softplus t0
-      s   = sigmoid t1                -- total persistence ∈ (0, 1)
-      sab = s * 0.999                 -- α + β strictly < 1
-      r   = sigmoid t2                -- α-share of total ∈ (0, 1)
-      a   = sab * r
-      b   = sab * (1 - r)
-  in (w, a, b)
-
--- Inverse of 'unpackParams' for warm-starting from a feasible point.
-packParams :: Double -> Double -> Double -> (Double, Double, Double)
-packParams w a b =
-  let !sab = a + b
-      !t0  = invSoftplus w
-      !t1  = invSigmoid (sab / 0.999)
-      !r   = if sab > 0 then a / sab else 0.5
-      !t2  = invSigmoid r
-  in (t0, t1, t2)
-  where
-    invSoftplus y
-      | y > 50    = y
-      | otherwise = log (exp y - 1)
-    invSigmoid p =
-      let pc = min 0.99999 (max 1e-5 p)
-      in log (pc / (1 - pc))
-
--- ---------------------------------------------------------------------------
--- Recursion
--- ---------------------------------------------------------------------------
-
--- | Run the GARCH(1,1) σ² recursion. @σ²_0@ is initialized to the sample
--- variance of @ε@ (a standard QMLE starting choice; alternatives such as
--- the unconditional variance @ω/(1-α-β)@ are equivalent in the limit).
-recurseSigma2
-  :: Double            -- ^ ω.
-  -> Double            -- ^ α.
-  -> Double            -- ^ β.
-  -> LA.Vector Double  -- ^ ε.
-  -> LA.Vector Double  -- ^ σ² of same length as ε.
-recurseSigma2 !w !a !b eps =
-  let n      = LA.size eps
-      var0   = LA.dot eps eps / fromIntegral n
-      sig0   = max 1e-12 var0
-      step !s2Prev !ePrev = w + a * ePrev * ePrev + b * s2Prev
-      go !i !s2Prev acc
-        | i >= n   = reverse acc
-        | otherwise =
-            let !s2 = if i == 0
-                       then sig0
-                       else step s2Prev (LA.atIndex eps (i - 1))
-            in go (i + 1) s2 (s2 : acc)
-  in LA.fromList (go 0 0 [])
-
--- | Negative Gaussian log-likelihood (to be minimized).
-negLL
-  :: LA.Vector Double  -- ^ ε.
-  -> Double            -- ^ ω.
-  -> Double            -- ^ α.
-  -> Double            -- ^ β.
-  -> Double
-negLL eps w a b =
-  let s2 = recurseSigma2 w a b eps
-      n  = LA.size eps
-      ll = sum [ let s = max 1e-12 (LA.atIndex s2 i)
-                     e = LA.atIndex eps i
-                 in log (2 * pi * s) + e * e / s
-               | i <- [0 .. n - 1] ]
-  in 0.5 * ll
-
--- ---------------------------------------------------------------------------
--- Fitting
--- ---------------------------------------------------------------------------
-
--- | Fit a GARCH(1,1) model to @y@ by Gaussian QMLE. The mean @μ@ is
--- estimated as the sample mean; ω/α/β are jointly optimized by L-BFGS
--- with numeric gradients in an unconstrained reparametrization.
---
--- Starting values: @α = 0.05@, @β = 0.90@, @ω = (1 - α - β) · Var(ε)@
--- (so that the unconditional variance matches the sample variance).
-fitGARCH :: LA.Vector Double -> GARCHFit
-fitGARCH y =
-  let n     = LA.size y
-      mu    = LA.sumElements y / fromIntegral n
-      eps   = y - LA.scalar mu
-      var0  = LA.dot eps eps / fromIntegral n
-      a0    = 0.05
-      b0    = 0.90
-      w0    = max 1e-8 ((1 - a0 - b0) * var0)
-      (t00, t10, t20) = packParams w0 a0 b0
-      objL [t0, t1, t2] =
-        let (w, a, b) = unpackParams t0 t1 t2
-        in negLL eps w a b
-      objL _ = error "fitGARCH: expected 3 parameters"
-      cfg   = LBFGS.defaultLBFGSConfig
-      res   = unsafePerformIO (LBFGS.runLBFGSNumeric cfg objL [t00, t10, t20])
-      [t0, t1, t2] = OC.orBest res
-      (w, a, b) = unpackParams t0 t1 t2
-      s2    = recurseSigma2 w a b eps
-  in GARCHFit
-       { gOmega     = w
-       , gAlpha     = a
-       , gBeta      = b
-       , gMu        = mu
-       , gSigma2    = s2
-       , gResiduals = eps
-       , gLogLik    = negate (OC.orValue res)
-       }
-
--- ---------------------------------------------------------------------------
--- Forecasting
--- ---------------------------------------------------------------------------
-
--- | @h@-step-ahead conditional variance forecast. The recursion is
---
--- @
---   σ²_{T+1} = ω + α · ε²_T + β · σ²_T
---   σ²_{T+k} = ω + (α + β) · σ²_{T+k-1}    (k ≥ 2)
--- @
---
--- so that the forecast converges to the unconditional variance
--- @ω / (1 - α - β)@.
-forecastGARCH :: GARCHFit -> Int -> LA.Vector Double
-forecastGARCH fit h
-  | h <= 0    = LA.fromList []
-  | otherwise =
-      let w   = gOmega fit
-          a   = gAlpha fit
-          b   = gBeta fit
-          s2  = gSigma2 fit
-          eps = gResiduals fit
-          n   = LA.size s2
-          sT  = LA.atIndex s2 (n - 1)
-          eT  = LA.atIndex eps (n - 1)
-          s1  = w + a * eT * eT + b * sT
-          go !k !prev
-            | k > h     = []
-            | k == 1    = s1 : go 2 s1
-            | otherwise =
-                let !nxt = w + (a + b) * prev
-                in nxt : go (k + 1) nxt
-      in LA.fromList (go 1 0)
diff --git a/src/Hanalyze/Model/GLM.hs b/src/Hanalyze/Model/GLM.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/GLM.hs
+++ /dev/null
@@ -1,718 +0,0 @@
-{-# LANGUAGE StrictData #-}
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.GLM
--- Description : IRLS による一般化線形モデル (Generalized Linear Models)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Generalized Linear Models fit by Iteratively Reweighted Least Squares.
---
--- Provides Gaussian, Binomial and Poisson families with identity, log,
--- logit and sqrt link functions. 'runIRLS' returns both a 'FitResult' and
--- the inverse Fisher information @(XᵀWX)⁻¹@ used for standard errors and
--- predictive intervals. The multi-output variant 'fitGLMMulti' shares the
--- family / link across response columns and runs IRLS column-wise.
-module Hanalyze.Model.GLM
-  ( Family (..)
-  , parseFamily
-  , LinkFn (..)
-  , parseLink
-  , canonicalLink
-  , GLMSolver (..)
-  , fitGLM
-  , fitGLMFull
-  , fitGLMWith
-  , fitGLMWithSmooth
-  , runIRLS
-  , runLBFGS_GLM
-    -- * Multi-output (per-column IRLS; Family/Link shared)
-  , GLMFitMulti (..)
-  , fitGLMMulti
-    -- * Diagnostic primitives (新規 export, request/090-CD)
-  , Link
-  , linkFnOf
-  , glmDeviance
-  , glmLogLik
-  , glmVariance
-    -- * Residuals + predict SE (request/090-AB)
-  , glmPearsonResiduals
-  , glmDevianceResiduals
-  , GlmPredictCI (..)
-  , predictGlmEtaWithSE
-  , predictGlmMuWithCI
-  ) where
-
-import qualified DataFrame.Internal.DataFrame as DXD
-import Hanalyze.DataIO.Convert (getDoubleVec)
-import Hanalyze.Model.Core
-import Hanalyze.Model.LM (multiPolyDesignMatrix, linspace, SmoothFit (..))
-
-import Data.Text (Text)
-import qualified Data.Vector as V
-import qualified Data.Vector.Storable as VS
-import qualified Numeric.LinearAlgebra as LA
-import qualified Hanalyze.Stat.Cholesky        as Chol
-import qualified Hanalyze.Optim.LBFGS          as LBFGS
-import qualified Hanalyze.Optim.Common         as OC
-import           System.IO.Unsafe     (unsafePerformIO)
-import Statistics.Distribution (quantile)
-import Statistics.Distribution.Normal (normalDistr)
-import Statistics.Distribution.StudentT (studentT)
-
--- ---------------------------------------------------------------------------
--- Family (response distribution)
--- ---------------------------------------------------------------------------
-
--- | GLM exponential-family distribution.
-data Family = Gaussian | Binomial | Poisson
-  deriving (Show, Eq)
-
--- | Parse a 'Family' name (case-sensitive).
-parseFamily :: String -> Either String Family
-parseFamily "gaussian" = Right Gaussian
-parseFamily "binomial" = Right Binomial
-parseFamily "poisson"  = Right Poisson
-parseFamily s          = Left ("Unknown distribution '" ++ s ++ "'. Use: gaussian | binomial | poisson")
-
--- ---------------------------------------------------------------------------
--- Link function
--- ---------------------------------------------------------------------------
-
--- | GLM link function.
-data LinkFn = Identity | Log | Logit | Sqrt
-  deriving (Show, Eq)
-
--- | Parse a 'LinkFn' name.
-parseLink :: String -> Either String LinkFn
-parseLink "identity" = Right Identity
-parseLink "log"      = Right Log
-parseLink "logit"    = Right Logit
-parseLink "sqrt"     = Right Sqrt
-parseLink s          = Left ("Unknown link '" ++ s ++ "'. Use: identity | log | logit | sqrt")
-
--- | The canonical link function for a given family.
-canonicalLink :: Family -> LinkFn
-canonicalLink Gaussian = Identity
-canonicalLink Binomial = Logit
-canonicalLink Poisson  = Log
-
--- Internal triple: (g, g⁻¹, g')
-type Link = (Double -> Double, Double -> Double, Double -> Double)
-
--- | Resolve a 'LinkFn' to its triple @(g, g⁻¹, g')@.
-linkFnOf :: LinkFn -> Link
-linkFnOf Identity = (id,   id,                const 1.0)
-linkFnOf Log      = (log,  exp,               recip)
-linkFnOf Logit    = ( \x  -> log (x / (1 - x))
-                    , \eta -> 1 / (1 + exp (-eta))
-                    , \mu  -> 1.0 / (mu * (1 - mu))
-                    )
-linkFnOf Sqrt     = (sqrt, \eta -> eta * eta, \mu -> 0.5 / sqrt (max 1e-10 mu))
-
--- | Variance function @V(μ)@ for the given family.
-varOf :: Family -> Double -> Double
-varOf Gaussian _  = 1.0
-varOf Binomial mu = mu * (1 - mu)
-varOf Poisson  mu = mu
-
--- | Public alias for the family variance @V(μ)@; see @varOf@. Exposed
--- so HPotfire diagnostics can compute Pearson-style standardisations
--- without re-implementing the family table.
-glmVariance :: Family -> Double -> Double
-glmVariance = varOf
-
--- | Clamp @μ@ to its valid range, avoiding boundary singularities.
-safeMu :: Family -> LA.Vector Double -> LA.Vector Double
-safeMu Binomial = LA.cmap (max 1e-8 . min (1 - 1e-8))
-safeMu Poisson  = LA.cmap (max 1e-8)
-safeMu Gaussian = id
-
--- | Fused @safeMu (gInv eta)@ for canonical-link GLMs — single
--- @VS.map@ pass instead of @gInv@ followed by @safeMu@.
---
--- P36 (2026-05-07): the Poisson IRLS loop did
--- @safeMu (VS.map (exp . min 500) eta)@ each iteration, which is two
--- passes over an @n@-vector and two allocations. Most iterations
--- spend the bulk of time in 'irlsStep' BLAS calls anyway, but on the
--- @n=10000@ Poisson bench this fused form trims ~10% off per-iter μ
--- compute. For non-canonical links callers fall back to the generic
--- @safeMu . VS.map gInv@ path.
---
--- Currently only used for the Poisson canonical link — Binomial
--- empirically regresses under fusion (GHC inlines the two-pass split
--- form better on the logit bench) so it stays on the
--- @safeMu . VS.map gInv@ path.
-muCanonical :: Family -> LA.Vector Double -> LA.Vector Double
-muCanonical Poisson =
-  VS.map (\e -> max 1e-8 (exp (min 500 e)))
-muCanonical f =
-  -- Generic fallback: callers should normally not hit this for
-  -- Binomial / Gaussian; defined for totality.
-  safeMu f
-{-# INLINE muCanonical #-}
-
--- ---------------------------------------------------------------------------
--- IRLS
--- ---------------------------------------------------------------------------
-
-maxIter :: Int
-maxIter = 100
-
-tol :: Double
-tol = 1e-8
-
--- | Per-observation log-likelihood for the canonical-link GLMs we
--- support. Used for the IRLS log-likelihood-based early termination
--- (see 'runIRLS').
---
---   * Gaussian: @-½ (y − μ)²@ (constant terms dropped, harmless for the
---     ratio-based stopping rule).
---   * Binomial: @y log μ + (1 − y) log (1 − μ)@.
---   * Poisson : @y log μ − μ@ (Stirling term dropped).
--- Phase 11c: list-based zipWith/sum was 11.3% time + 8.3% alloc on
--- the n=10k logit profile. Replaced with vector-native zipVectorWith
--- + sumElements (no list materialization, single BLAS-friendly pass).
--- The family is dispatched once at the top-level let-binding so the
--- inner zipVectorWith sees a fully monomorphic Double -> Double ->
--- Double closure that GHC can specialize.
-glmLogLik :: Family -> LA.Vector Double -> LA.Vector Double -> Double
-glmLogLik family y mu = VS.sum (VS.zipWith f y mu)
-  where
-    f = case family of
-      Gaussian -> \yi mi -> -0.5 * (yi - mi) ** 2
-      Binomial -> \yi mi ->
-        let m' = max 1e-12 (min (1 - 1e-12) mi)
-        in yi * log m' + (1 - yi) * log (1 - m')
-      Poisson  -> \yi mi ->
-        let m' = max 1e-12 mi
-        in yi * log m' - m'
-
-initBeta :: Family -> LinkFn -> LA.Vector Double -> Int -> LA.Vector Double
-initBeta family linkFn y p =
-  let (g, _, _) = linkFnOf linkFn
-      yMean = LA.sumElements y / fromIntegral (LA.size y)
-      yC = case family of
-             Binomial -> max 1e-6 (min (1 - 1e-6) yMean)
-             Poisson  -> max 1e-6 yMean
-             Gaussian -> yMean
-  in LA.fromList (g yC : replicate (p - 1) 0.0)
-
--- | One IRLS step. Returns the updated @β@ together with the
--- corresponding @μ@ and the log-likelihood at the /input/ @β@.
---
--- Returning @μ_old@ and @ll_old@ here lets the convergence loop in
--- 'runIRLS' avoid an extra @x #> beta@ + @gInv μ@ + 'glmLogLik' pass
--- per iteration that the old API forced (see glmbench §1).
-irlsStep :: Link -> (Double -> Double)
-          -> (Family -> LA.Vector Double -> LA.Vector Double)
-          -> Family -> LA.Matrix Double -> LA.Vector Double -> LA.Vector Double
-          -> (LA.Vector Double, LA.Vector Double, Double)
-irlsStep (_, gInv, gDeriv) varFn clamp family x y beta =
-  -- Phase 12a (2026-05-06): replaced massiv-based map/zipWith3 with
-  -- pure VS.{map,zipWith3}. Profile (Phase 11) showed
-  -- @trivialScheduler_@ (massiv) consumed 9.8% of GLM IRLS time —
-  -- pure overhead since 'compFor' was always 'Seq'. The replacement
-  -- is single-pass, allocation-equivalent, and avoids the
-  -- hmatrix↔massiv round trip.
-  --
-  -- P36 (2026-05-07): for Poisson canonical link, fuse @gInv@ and
-  -- @safeMu@ into a single VS.map. Empirically Binomial regresses
-  -- under the same fusion (GHC inlines the two-pass split better),
-  -- so it stays on the generic path. The family pattern-match is
-  -- hot-loop constant and gets specialized away by GHC.
-  let eta    = x LA.#> beta
-      mu     = case family of
-                 Poisson -> muCanonical Poisson eta
-                 _       -> clamp family (VS.map gInv eta)
-      llHere = glmLogLik family y mu
-      ws     = VS.map (\m -> max 1e-10
-                               (1.0 / (gDeriv m ^ (2 :: Int) * varFn m)))
-                      mu
-      zs     = VS.zipWith3 (\ei yi mi -> ei + (yi - mi) * gDeriv mi)
-                           eta y mu
-      -- Normal-equations form: solve (Xᵀ W X) β = Xᵀ W z via SPD Cholesky.
-      -- Faster than solving (√W X) β = (√W z) with the general LSQ
-      -- (dgels) when n ≫ p, which is the common GLM regime.
-      wxT     = LA.tr x * LA.asRow ws            -- p × n with column scaling
-      gMat    = wxT LA.<> x                       -- p × p (SPD)
-      bRhs    = LA.asColumn (wxT LA.#> zs)        -- p × 1
-      betaNew = LA.flatten (Chol.cholSolveJitter gMat bRhs)
-  in (betaNew, mu, llHere)
-
--- ---------------------------------------------------------------------------
--- Solver selection
--- ---------------------------------------------------------------------------
-
--- | GLM solver back-end.
---
---   * 'IRLS' — Iteratively Re-weighted Least Squares. Each iteration
---     builds and solves the SPD normal equations @XᵀWX β = XᵀWz@ via
---     'Hanalyze.Stat.Cholesky.cholSolveJitter'. Quadratic convergence (= a full
---     Newton step every iteration); each iteration is @O(np²)@.
---   * 'LBFGS' — direct L-BFGS minimization of the negative
---     log-likelihood with the analytic gradient @Xᵀ(μ − y)@ (canonical
---     link). Per-iteration cost is @O(np)@. This is what @sklearn@
---     uses, and is the better choice in @n ≫ p²@ regimes once the
---     'Hanalyze.Optim.LBFGS' inner loop is moved off Haskell-list operations.
---
--- Default solver: 'IRLS'. In the current bench regime (@n ≤ 10000@,
--- @p ≤ 20@), IRLS-with-Cholesky beats the pure-Haskell-list L-BFGS
--- because @O(np²)@ on small @p@ is dominated by hmatrix's BLAS calls
--- whereas the L-BFGS path pays per-step Haskell overhead. Switch to
--- 'LBFGS' for problems with @p > 50@ or when 'Hanalyze.Optim.LBFGS' itself is
--- vectorized.
-data GLMSolver
-  = IRLS
-  | LBFGS
-  deriving (Eq, Show)
-
-defaultGLMSolver :: GLMSolver
-defaultGLMSolver = IRLS
-
--- ---------------------------------------------------------------------------
--- L-BFGS direct GLM
--- ---------------------------------------------------------------------------
-
--- | Negative log-likelihood @-ℓ(β)@ for a canonical-link GLM.
-glmNegLogLik :: Family -> LA.Matrix Double -> LA.Vector Double
-             -> LA.Vector Double -> Double
-glmNegLogLik family x y beta = negate (glmLogLik family y mu)
-  where
-    eta = x LA.#> beta
-    mu  = case family of
-            Gaussian -> eta
-            Binomial -> LA.cmap (\e -> 1 / (1 + exp (-e))) eta
-            Poisson  -> LA.cmap (\e -> exp (min 500 e))   eta
-
--- | Gradient of @-ℓ(β)@ for a canonical-link GLM:
---
--- @∇(-ℓ) = Xᵀ (μ - y)@
---
--- This identity holds for /every/ exponential-family GLM with the
--- canonical link, which is why L-BFGS is so attractive here — no
--- per-family branching is needed inside the gradient.
-glmGrad :: Family -> LA.Matrix Double -> LA.Vector Double
-        -> LA.Vector Double -> LA.Vector Double
-glmGrad family x y beta =
-  let eta = x LA.#> beta
-      mu  = case family of
-              Gaussian -> eta
-              Binomial -> LA.cmap (\e -> 1 / (1 + exp (-e))) eta
-              Poisson  -> LA.cmap (\e -> exp (min 500 e))   eta
-  in LA.tr x LA.#> (mu - y)
-
--- | Fit a canonical-link GLM by minimizing the negative log-likelihood
--- with L-BFGS. This is the path that 'sklearn.linear_model.\*' uses
--- internally for logistic and Poisson regression and is markedly
--- faster than IRLS when @n ≫ p@ because each L-BFGS iteration costs
--- only @O(np)@ versus IRLS's @O(np²)@ for the @XᵀWX@ build.
---
--- Returns the same @(FitResult, fisherInv)@ pair as 'runIRLS'; the
--- Fisher information is computed once at the converged β via the same
--- Cholesky path used by IRLS, so downstream uses (CIs, WAIC, …) are
--- identical.
-runLBFGS_GLM :: Family -> LA.Matrix Double -> LA.Vector Double
-             -> (FitResult, LA.Matrix Double)
-runLBFGS_GLM family x y =
-  -- Only canonical-link GLMs are supported here (the simple gradient
-  -- formula above relies on the canonical link). For non-canonical
-  -- links (e.g. probit, sqrt link) the caller should use 'runIRLS'.
-  let p     = LA.cols x
-      beta0 = initBeta family (canonicalLink family) y p
-      -- Vector-native objective and gradient (no list conversion per
-      -- L-BFGS step, which used to dominate runtime when @p ≈ 20@).
-      fV b = glmNegLogLik family x y b
-      gV b = glmGrad      family x y b
-      cfg  = LBFGS.defaultLBFGSConfig
-               { LBFGS.lbStop = OC.defaultStopCriteria
-                                  { OC.stMaxIter = 200
-                                  , OC.stTolFun  = 1e-10
-                                  , OC.stTolX    = 1e-10 } }
-      result = unsafePerformIO $
-                 LBFGS.runLBFGSWithV cfg fV gV beta0
-      betaF  = LA.fromList (OC.orBest result)
-      mu     = safeMu family $ case family of
-                 Gaussian -> x LA.#> betaF
-                 Binomial -> LA.cmap (\e -> 1 / (1 + exp (-e))) (x LA.#> betaF)
-                 Poisson  -> LA.cmap (\e -> exp (min 500 e))   (x LA.#> betaF)
-      resid  = y - mu
-      r2     = pseudoR2 family y mu
-      fitR   = FitResult (LA.asColumn betaF)
-                         (LA.asColumn mu)
-                         (LA.asColumn resid)
-                         (LA.fromList [r2])
-      -- Fisher information at convergence (same path as IRLS).
-      ws     = VS.map (\m -> max 1e-10 (1.0 / (gDeriv m ^ (2::Int)
-                                                * varOf family m)))
-                      mu
-      wxT    = LA.tr x * LA.asRow ws
-      gMat   = wxT LA.<> x
-      fisher = Chol.cholSolveJitter gMat (LA.ident p)
-  in (fitR, fisher)
-  where
-    (_, _, gDeriv) = linkFnOf (canonicalLink family)
-
--- ---------------------------------------------------------------------------
-
--- | Run IRLS to fit a single-output GLM. Returns both the fit result
--- and the inverse Fisher information @(XᵀWX)⁻¹@ used for standard
--- errors and credible/predictive intervals.
-runIRLS :: Family -> LinkFn -> LA.Matrix Double -> LA.Vector Double
-        -> (FitResult, LA.Matrix Double)
-runIRLS family linkFn x y = (mkResult betaFinal muFinal, fisherInvFromMu muFinal)
-  where
-    link@(_, gInv, _) = linkFnOf linkFn
-    step  = irlsStep link (varOf family) safeMu family x y
-    beta0 = initBeta family linkFn y (LA.cols x)
-    isCanonicalLink = linkFn == canonicalLink family
-
-    -- Mu at convergence boundary: mirror 'irlsStep' Poisson fusion
-    -- when on the canonical link.
-    muOf beta
-      | isCanonicalLink && family == Poisson
-                  = muCanonical Poisson (x LA.#> beta)
-      | otherwise = safeMu family (VS.map gInv (x LA.#> beta))
-
-    -- 'converge' tracks β and the /previous/ iteration's log-likelihood.
-    -- 'irlsStep' returns @(β_{k+1}, μ_at_β_k, ll_at_β_k)@: the updated β
-    -- plus the current iter's μ + ll, all free side-products of the
-    -- IRLS step itself. We pass @ll_at_β_k@ forward as the next iter's
-    -- @llP@, eliminating the dedicated O(np) @llOf β@ pass per iter
-    -- that the previous code performed (glmbench §1).
-    --
-    -- Convergence is checked on β-norm or relative ll change. The ll
-    -- comparison is between ll(β_k) and ll(β_{k-1}) — one iteration
-    -- lagged from the standard ll(β_{k+1}) vs ll(β_k) form, which is
-    -- equivalent in steady state and avoids any extra μ pass in the
-    -- inner loop.
-    (betaFinal, muFinal) = converge maxIter True beta0 (glmLogLik family y (muOf beta0))
-
-    -- ★初回反復だけ dLL 判定を無効化する: 'irlsStep' が返す @llHere@ は入力 β での
-    -- @ll(β_k)@ なので、 初回は seed @llP = ll(β0)@ と一致し @dLL = 0 < tol@ で
-    -- IRLS が 1 ステップで早期停止してしまう (= 28d1feb7 の per-iter ll 再利用
-    -- リライトで混入した回帰)。 dB (β-norm) 判定は初回も正しいので残し、 dLL は
-    -- 2 反復目以降 @ll(β_k) vs ll(β_{k-1})@ が揃ってから使う。
-    converge 0 _     beta _  = (beta, muOf beta)
-    converge n first beta llP =
-      let (betaNew, _muHere, llHere) = step beta
-      in if any notFinite (LA.toList betaNew)
-         then (beta, muOf beta)          -- divergence; keep last good β
-         else
-           let dB  = LA.norm_2 (betaNew - beta)
-               dLL = abs (llHere - llP) / max (abs llP) 1
-           in if dB < tol || (not first && dLL < tol)
-                then (betaNew, muOf betaNew)   -- final μ pass once
-                else converge (n - 1) False betaNew llHere
-
-    notFinite b = isNaN b || isInfinite b
-
-    mkResult beta mu =
-      let resid = y - mu
-          r2    = pseudoR2 family y mu
-      in FitResult (LA.asColumn beta)
-                   (LA.asColumn mu)
-                   (LA.asColumn resid)
-                   (LA.fromList [r2])
-
-    fisherInvFromMu mu =
-      let (_, _, gDeriv) = link
-          ws   = VS.map (\m -> max 1e-10
-                                 (1.0 / (gDeriv m ^ (2::Int) * varOf family m)))
-                        mu
-          wxT  = LA.tr x * LA.asRow ws    -- p × n
-          gMat = wxT LA.<> x               -- p × p (SPD)
-          p    = LA.cols x
-      in Chol.cholSolveJitter gMat (LA.ident p)
-
--- ---------------------------------------------------------------------------
--- Public API
--- ---------------------------------------------------------------------------
-
--- | Fit a GLM with the canonical link, returning just the 'FitResult'.
--- Uses @defaultGLMSolver@ (currently 'IRLS').
-fitGLM :: Family -> LA.Matrix Double -> LA.Vector Double -> FitResult
-fitGLM family x y =
-  fst (fitGLMWith defaultGLMSolver family (canonicalLink family) x y)
-
--- | Like 'fitGLM' but also returns the inverse Fisher information
--- (Laplace-approximate posterior covariance). Used by the WAIC / LOO-CV
--- posterior-sampling helpers.
---
--- Routes through 'fitGLMWith' with @defaultGLMSolver@. When the
--- supplied 'LinkFn' is /not/ the canonical link of the family, the
--- 'LBFGS' solver is unsupported and the function silently falls back
--- to 'IRLS' so existing call sites that pass non-canonical links keep
--- working.
-fitGLMFull :: Family -> LinkFn -> LA.Matrix Double -> LA.Vector Double
-           -> (FitResult, LA.Matrix Double)
-fitGLMFull family linkFn x y
-  | linkFn == canonicalLink family = fitGLMWith defaultGLMSolver family linkFn x y
-  | otherwise                      = runIRLS family linkFn x y
-
--- | Pick the solver explicitly. The 'LBFGS' path is only valid for the
--- canonical link of the family; non-canonical links transparently fall
--- back to 'IRLS'.
-fitGLMWith
-  :: GLMSolver -> Family -> LinkFn
-  -> LA.Matrix Double -> LA.Vector Double
-  -> (FitResult, LA.Matrix Double)
-fitGLMWith IRLS  family linkFn x y = runIRLS family linkFn x y
-fitGLMWith LBFGS family linkFn x y
-  | linkFn == canonicalLink family = runLBFGS_GLM family x y
-  | otherwise                      = runIRLS family linkFn x y
-
--- | Fit GLM with specified distribution and link function.
--- Accepts multiple x columns with per-column polynomial degrees.
--- Returns SmoothFit only when there is exactly one x column (for scatter plot).
--- For PI with non-Gaussian families, falls back to CI (warn at call site).
-fitGLMWithSmooth
-  :: Family
-  -> LinkFn
-  -> [(Text, Int)]   -- ^ [(x column name, polynomial degree)]
-  -> Band            -- ^ uncertainty band specification
-  -> Int             -- ^ grid resolution for smooth curve
-  -> DXD.DataFrame
-  -> Text            -- ^ y column
-  -> Maybe (FitResult, Maybe SmoothFit)
-fitGLMWithSmooth family linkFn colDegs band nGrid df yCol = do
-  xVecs <- mapM (flip getDoubleVec df . fst) colDegs
-  yVec  <- getDoubleVec yCol df
-
-  let degrees       = map snd colDegs
-      dm            = multiPolyDesignMatrix (zip xVecs degrees)
-      y             = LA.fromList (V.toList yVec)
-      (res, fisher) = runIRLS family linkFn dm y
-      (_, gInv, _)  = linkFnOf linkFn
-      beta          = coefficientsV res
-      n             = LA.rows dm
-      p             = LA.cols dm
-
-      -- PI falls back to CI for non-Gaussian (caller should warn)
-      effectiveBand = case (band, family) of
-        (PI lvl, Gaussian) -> PI lvl
-        (PI lvl, _)        -> CI lvl
-        (b,      _)        -> b
-
-      mSmooth = case (xVecs, degrees) of
-        ([xVec], [deg]) -> Just (makeSmoothFit xVec deg)
-        _               -> Nothing
-
-      makeSmoothFit xVec deg =
-        let xLa    = LA.fromList (V.toList xVec)
-            xMin   = LA.minElement xLa
-            xMax   = LA.maxElement xLa
-            span'  = max 1e-8 (xMax - xMin)
-            xGrid  = V.fromList (linspace (xMin - 0.5*span') (xMax + 0.5*span') nGrid)
-            dmG    = multiPolyDesignMatrix [(xGrid, deg)]
-            etaG   = dmG LA.#> beta
-            yGrid  = map gInv (LA.toList etaG)
-            gRows  = LA.toRows dmG
-        in case effectiveBand of
-          NoBand ->
-            SmoothFit (V.toList xGrid) yGrid yGrid yGrid False
-          CI level ->
-            let qVal  = ciQuantile level
-                halfW xi = qVal * sqrt (max 0 (xi `LA.dot` (fisher LA.#> xi)))
-                etaL  = LA.toList etaG
-                lowers = zipWith (\eta xi -> gInv (eta - halfW xi)) etaL gRows
-                uppers = zipWith (\eta xi -> gInv (eta + halfW xi)) etaL gRows
-            in SmoothFit (V.toList xGrid) yGrid lowers uppers True
-          PI level ->
-            -- Gaussian only: add s²·1 term to CI variance
-            let dfStat = fromIntegral (n - p) :: Double
-                etaL  = LA.toList etaG
-            -- df<=0 (飽和) は s²=0/0・studentT が例外 → 帯を線に潰す (lo=hi=ĝ⁻¹(η))。
-            in if dfStat <= 0
-                 then SmoothFit (V.toList xGrid) yGrid (map gInv etaL) (map gInv etaL) True
-                 else
-                   let s2     = let resV = residualsV res
-                                in (resV `LA.dot` resV) / dfStat
-                       tVal   = quantile (studentT dfStat) ((1 + level) / 2)
-                       xtxi   = LA.inv (LA.tr dm LA.<> dm)
-                       halfW xi = tVal * sqrt (s2 * (1 + xi `LA.dot` (xtxi LA.#> xi)))
-                       lowers = zipWith (\eta xi -> gInv (eta - halfW xi)) etaL gRows
-                       uppers = zipWith (\eta xi -> gInv (eta + halfW xi)) etaL gRows
-                   in SmoothFit (V.toList xGrid) yGrid lowers uppers True
-
-      ciQuantile level = case family of
-        -- 飽和 (df=n-p<=0) は studentT が例外 → 分位点 0 = CI 幅ゼロ (帯を線に潰す)。
-        Gaussian | n - p <= 0 -> 0
-                 | otherwise  -> quantile (studentT (fromIntegral (n - p))) ((1 + level) / 2)
-        _        -> quantile (normalDistr 0 1) ((1 + level) / 2)
-
-  return (res, mSmooth)
-
--- ---------------------------------------------------------------------------
--- Goodness of fit
--- ---------------------------------------------------------------------------
-
--- | McFadden-style pseudo-R² for GLMs.
-pseudoR2 :: Family -> LA.Vector Double -> LA.Vector Double -> Double
-pseudoR2 Gaussian y mu =
-  let resid = y - mu
-      yMean = LA.sumElements y / fromIntegral (LA.size y)
-      dev   = LA.cmap (subtract yMean) y
-  in 1 - (resid `LA.dot` resid) / (dev `LA.dot` dev)
-pseudoR2 family y mu =
-  let yMean  = LA.sumElements y / fromIntegral (LA.size y)
-      muNull = LA.konst yMean (LA.size y)
-      dFit   = glmDeviance family y mu
-      dNull  = glmDeviance family y muNull
-  in if dNull == 0 then 1 else 1 - dFit / dNull
-
--- | GLM deviance: @D(y, μ̂) = 2 (ℓ_sat − ℓ_model)@.
-glmDeviance :: Family -> LA.Vector Double -> LA.Vector Double -> Double
-glmDeviance Gaussian y mu =
-  let r = y - mu in r `LA.dot` r
-glmDeviance Binomial y mu =
-  let muC  = LA.cmap (max 1e-15 . min (1 - 1e-15)) mu
-      term = VS.zipWith
-               (\yi mui -> xlogy yi (yi / mui)
-                         + xlogy (1 - yi) ((1 - yi) / (1 - mui)))
-               y muC
-  in 2 * VS.sum term
-glmDeviance Poisson y mu =
-  let muC  = LA.cmap (max 1e-15) mu
-      term = VS.zipWith
-               (\yi mui -> xlogy yi (yi / mui) - (yi - mui))
-               y muC
-  in 2 * VS.sum term
-
-xlogy :: Double -> Double -> Double
-xlogy 0 _ = 0
-xlogy x y = x * log y
-
--- ---------------------------------------------------------------------------
--- 090-A: Residuals (request/090-AB)
--- ---------------------------------------------------------------------------
-
--- | Pearson residuals @(y - μ) / sqrt(V(μ))@.
-glmPearsonResiduals
-  :: Family
-  -> LA.Vector Double   -- ^ Observations @y@.
-  -> LA.Vector Double   -- ^ Fitted means @μ@.
-  -> LA.Vector Double
-glmPearsonResiduals family y mu =
-  VS.zipWith (\yi mui ->
-                let v = varOf family mui
-                in if v <= 0 then 0 else (yi - mui) / sqrt v)
-             y mu
-
--- | Deviance residuals @sign(y - μ) · sqrt(d_i)@ where @d_i@ is the
--- per-observation contribution to the deviance @D = Σ d_i@.
-glmDevianceResiduals
-  :: Family
-  -> LA.Vector Double
-  -> LA.Vector Double
-  -> LA.Vector Double
-glmDevianceResiduals family y mu =
-  let perObs = pointwiseDeviance family y mu
-  in VS.zipWith3 (\yi mui di -> signum (yi - mui) * sqrt (max 0 di))
-                 y mu perObs
-  where
-    pointwiseDeviance Gaussian ys ms =
-      VS.zipWith (\yi mui -> let r = yi - mui in r * r) ys ms
-    pointwiseDeviance Binomial ys ms =
-      VS.zipWith
-        (\yi mui ->
-            let muC = max 1e-15 (min (1 - 1e-15) mui)
-            in 2 * ( xlogy yi (yi / muC)
-                   + xlogy (1 - yi) ((1 - yi) / (1 - muC)) ))
-        ys ms
-    pointwiseDeviance Poisson ys ms =
-      VS.zipWith
-        (\yi mui ->
-            let muC = max 1e-15 mui
-            in 2 * (xlogy yi (yi / muC) - (yi - muC)))
-        ys ms
-
--- ---------------------------------------------------------------------------
--- 090-B: Predict + SE (request/090-AB)
--- ---------------------------------------------------------------------------
-
--- | Prediction with Wald confidence interval on the response (μ) scale.
-data GlmPredictCI = GlmPredictCI
-  { gpMu :: !Double
-  , gpLo :: !Double
-  , gpHi :: !Double
-  } deriving (Show)
-
--- | Linear-predictor prediction @η = xᵀβ@ with @SE = sqrt(xᵀ Σ x)@,
--- where @Σ@ is @(XᵀWX)⁻¹@ from 'fitGLMFull'. The intercept must be
--- present in @x@.
-predictGlmEtaWithSE
-  :: LA.Vector Double
-  -> LA.Matrix Double
-  -> LA.Vector Double
-  -> (Double, Double)
-predictGlmEtaWithSE beta sigma x =
-  let eta   = x `LA.dot` beta
-      sigX  = sigma LA.#> x
-      seEta = sqrt (max 0 (x `LA.dot` sigX))
-  in (eta, seEta)
-
--- | Wald CI on the response scale: build CI in @η@ space then transform
--- both endpoints through the inverse link.
-predictGlmMuWithCI
-  :: LinkFn
-  -> Double
-  -> LA.Vector Double
-  -> LA.Matrix Double
-  -> LA.Vector Double
-  -> GlmPredictCI
-predictGlmMuWithCI link level beta sigma x =
-  let (eta, se)  = predictGlmEtaWithSE beta sigma x
-      z          = waldZ level
-      (_, gInv, _) = linkFnOf link
-      mu  = gInv eta
-      lo  = gInv (eta - z * se)
-      hi  = gInv (eta + z * se)
-  in GlmPredictCI { gpMu = mu, gpLo = min lo hi, gpHi = max lo hi }
-
--- | Two-sided Wald z: @z = √2 · erf⁻¹(level)@ (so @level=0.95@ →
--- @1.95996…@). Uses Winitzki's rational approximation of @erf⁻¹@
--- (~1e-3 accuracy) to keep @statistics@ out of this module.
-waldZ :: Double -> Double
-waldZ lvl
-  | lvl <= 0 || lvl >= 1 =
-      error "predictGlmMuWithCI: confidence level must lie in (0, 1)"
-  | otherwise            = sqrt 2 * inverfApprox lvl
-
-inverfApprox :: Double -> Double
-inverfApprox x =
-  let a   = 0.147
-      ln1 = log (1 - x * x)
-      term1 = 2 / (pi * a) + ln1 / 2
-  in signum x * sqrt (sqrt (term1 * term1 - ln1 / a) - term1)
-
--- ---------------------------------------------------------------------------
--- 多出力 GLM (列ごと IRLS)
--- ---------------------------------------------------------------------------
-
--- | Multi-output GLM result. The same family and link function are
--- used for all @q@ output columns; IRLS is run column-wise.
-data GLMFitMulti = GLMFitMulti
-  { gfmFamily   :: Family
-  , gfmLinkFn   :: LinkFn
-  , gfmFits     :: [FitResult]            -- ^ 列ごと FitResult
-  , gfmFisher   :: [LA.Matrix Double]     -- ^ 列ごと (XᵀWX)⁻¹
-  , gfmBeta     :: LA.Matrix Double       -- ^ 係数行列 p × q
-  , gfmFitted   :: LA.Matrix Double       -- ^ 予測 n × q
-  , gfmResid    :: LA.Matrix Double       -- ^ 残差 n × q
-  } deriving (Show)
-
--- | Fit a multi-output GLM. @Y@ has shape @n × q@; family and link
--- function are shared across all columns.
-fitGLMMulti :: Family -> LinkFn -> LA.Matrix Double -> LA.Matrix Double
-            -> GLMFitMulti
-fitGLMMulti family linkFn x y =
-  let q     = LA.cols y
-      perCol j = runIRLS family linkFn x (LA.flatten (y LA.¿ [j]))
-      pairs = [perCol j | j <- [0 .. q - 1]]
-      fits  = map fst pairs
-      fishs = map snd pairs
-      betaM = LA.fromColumns [LA.flatten (coefficients f) | f <- fits]
-      fitM  = LA.fromColumns [LA.flatten (fitted     f) | f <- fits]
-      resM  = LA.fromColumns [LA.flatten (residuals  f) | f <- fits]
-  in GLMFitMulti family linkFn fits fishs betaM fitM resM
diff --git a/src/Hanalyze/Model/GLMM.hs b/src/Hanalyze/Model/GLMM.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/GLMM.hs
+++ /dev/null
@@ -1,823 +0,0 @@
--- |
--- Module      : Hanalyze.Model.GLMM
--- Description : 線形/一般化線形混合効果モデル (random intercept/slope)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Linear and generalized linear mixed-effects models.
---
--- 'fitLME' / 'fitGLMM' fit a __random-intercept__ mixed model: a single
--- scalar random effect per group (variance @σ²_u@, scalar BLUP @û_j@).
--- 'fitLME' is Gaussian via exact EM; 'fitGLMM' is non-Gaussian via Laplace.
---
--- 'fitLMEGeneral' / 'fitGLMMGeneral' (Phase 48) generalise to __vector
--- random effects__ (random intercept + slopes): a per-group design block
--- @Z_j@ with an @r×r@ covariance matrix @G@ and a vector BLUP @b̂_j@. With
--- @r = 1@ (intercept only) they reduce exactly to 'fitLME' / 'fitGLMM'.
---
--- The multi-output variants ('fitLMEMulti', 'fitGLMMMulti') run the
--- random-intercept algorithm independently per response column.
-module Hanalyze.Model.GLMM
-  ( GLMMResult (..)
-  , fitLME
-  , fitGLMM
-  , fitLMEDataFrame
-  , fitGLMMDataFrame
-    -- * General random effects (intercept + slope; Phase 48)
-  , GLMMResultRE (..)
-  , fitLMEGeneral
-  , fitGLMMGeneral
-    -- * Multi-output (per-column EM/Laplace; Family/Link shared)
-  , GLMMResultMulti (..)
-  , fitLMEMulti
-  , fitGLMMMulti
-    -- * Standard errors (request/100)
-  , glmmFixedSE
-  , glmmBLUPSE
-    -- * Group helper (shared with Formula.Mixed)
-  , buildGroups
-  ) where
-
-import qualified DataFrame.Internal.DataFrame as DXD
-import Hanalyze.DataIO.Convert (getDoubleVec, getTextVec)
-import Hanalyze.Model.Core     (FitResult (..))
-import Hanalyze.Model.GLM      (Family (..), LinkFn (..))
-import Hanalyze.Model.LM       (multiPolyDesignMatrix)
-
-import qualified Data.Map.Strict as Map
-import qualified Data.Set        as Set
-import Data.Text           (Text)
-import qualified Data.Vector    as V
-import qualified Numeric.LinearAlgebra as LA
-
--- ---------------------------------------------------------------------------
--- Result type
--- ---------------------------------------------------------------------------
-
--- | Fit result for a random-intercept mixed model.
---
---   * LME (Gaussian):     @y = Xβ + Zu + ε@, @u_j ~ N(0, σ²_u)@,
---     @ε_i ~ N(0, σ²)@.
---   * GLMM (non-Gaussian): @g(E[y|u]) = Xβ + Zu@, @u_j ~ N(0, σ²_u)@.
-data GLMMResult = GLMMResult
-  { glmmFixed    :: FitResult        -- ^ Fixed-effect fit (β, conditional
-                                     --   fitted values, residuals, R²).
-  , glmmRandVar  :: Double           -- ^ Random-intercept variance @σ²_u@.
-  , glmmResidVar :: Double           -- ^ Residual variance @σ²@ (1.0 for non-Gaussian families).
-  , glmmBLUPs    :: V.Vector Double  -- ^ Best linear unbiased predictions
-                                     --   @û_j@, aligned with 'glmmGroups'.
-  , glmmGroups   :: V.Vector Text    -- ^ Sorted unique group labels.
-  , glmmICC      :: Double           -- ^ Intraclass correlation (exact
-                                     --   for Gaussian; link-scale
-                                     --   approximation otherwise).
-  } deriving (Show)
-
--- | Fit result for a __general__ mixed model with vector random effects
---   (random intercept + slopes), Phase 48.
---
---   * LME (Gaussian):     @y_j = X_j β + Z_j b_j + ε_j@, @b_j ~ N(0, G)@,
---     @ε_i ~ N(0, σ²)@, where @Z_j@ is the per-group random-effect design
---     block (@n_j × r@) and @G@ is the @r×r@ random-effect covariance.
---   * GLMM (non-Gaussian): @g(E[y|b]) = X_j β + Z_j b_j@, @b_j ~ N(0, G)@.
---
---   With @r = 1@ and an intercept-only @Z@ this reduces exactly to the
---   scalar 'GLMMResult' (@reRandCov = [[σ²_u]]@, @reBLUPs@ a single column).
-data GLMMResultRE = GLMMResultRE
-  { reFixed    :: FitResult        -- ^ Fixed-effect fit (β, conditional
-                                   --   fitted values, residuals, R²).
-  , reRandCov  :: LA.Matrix Double -- ^ Random-effect covariance @G@ (@r×r@).
-  , reResidVar :: Double           -- ^ Residual variance @σ²@ (1.0 for
-                                   --   non-Gaussian families).
-  , reBLUPs    :: LA.Matrix Double -- ^ BLUPs @b̂@ as a @q×r@ matrix (row j =
-                                   --   group j, aligned with 'reGroups').
-  , reGroups   :: V.Vector Text    -- ^ Sorted unique group labels (length q).
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- Group helpers (shared by LME and GLMM)
--- ---------------------------------------------------------------------------
-
--- | Parse grouping vector into (sorted unique labels, per-obs index, per-group sizes).
-buildGroups :: V.Vector Text -> (V.Vector Text, V.Vector Int, V.Vector Int)
-buildGroups gvec =
-  -- Phase 11b (2026-05-14): Set-based dedup + sort, O(n log n) instead of
-  -- the O(n²) 'nub'. Important for grouping vectors with thousands of IDs.
-  let labels   = V.fromList . Set.toAscList . Set.fromList . V.toList $ gvec
-      q        = V.length labels
-      labelMap = Map.fromList (zip (V.toList labels) ([0..] :: [Int]))
-      idx      = V.map (\g -> Map.findWithDefault 0 g labelMap) gvec
-      szMap    = Map.fromListWith (+) (V.toList (V.map (\j -> (j, 1 :: Int)) idx))
-      sizes    = V.fromList [ Map.findWithDefault 0 j szMap | j <- [0..q-1] ]
-  in (labels, idx, sizes)
-
--- | Group sums: (Zᵀv)_j = Σ_{i in group j} v_i
-zGroupSums :: V.Vector Int -> V.Vector Double -> Int -> V.Vector Double
-zGroupSums idx v q =
-  let smap = Map.fromListWith (+) (V.toList (V.zipWith (,) idx v))
-  in V.fromList [ Map.findWithDefault 0.0 j smap | j <- [0..q-1] ]
-
--- | Scatter random effects to observations: (Zu)_i = u_{g(i)}
-zuScatter :: V.Vector Int -> V.Vector Double -> V.Vector Double
-zuScatter idx u = V.map (u V.!) idx
-
--- ---------------------------------------------------------------------------
--- EM algorithm for LME (Gaussian, exact)
--- ---------------------------------------------------------------------------
-
-maxEmIter :: Int
-maxEmIter = 500
-
-emTol :: Double
-emTol = 1e-8
-
--- | Fit a random-intercept LME via EM (ML).
--- The E-step exploits the diagonal structure of the precision matrix for random intercepts:
---   P_jj = 1 / (1/σ²_u + n_j/σ²)
--- The M-step updates β by OLS on partial residuals; σ²_u and σ² analytically.
-fitLME
-  :: LA.Matrix Double  -- X (design matrix, must include intercept column)
-  -> LA.Vector Double  -- y
-  -> V.Vector Int      -- per-observation group index (0-based)
-  -> V.Vector Text     -- sorted group labels (length q)
-  -> V.Vector Int      -- per-group observation counts (length q)
-  -> GLMMResult
-fitLME x y idx labels sizes =
-  let n = LA.rows x
-      q = V.length labels
-
-      beta0 = LA.flatten (x LA.<\> LA.asColumn y)
-      yMean = LA.sumElements y / fromIntegral n
-      yDev  = y - LA.konst yMean n
-      ssTot = yDev `LA.dot` yDev
-      varY  = ssTot / fromIntegral n
-      su2_0 = varY / 2
-      s2_0  = varY / 2
-
-      emStep (beta, su2, s2) =
-        let pDiag  = V.fromList [ 1.0 / (1.0/su2 + fromIntegral (sizes V.! j) / s2)
-                                | j <- [0..q-1] ]
-            r0     = V.fromList . LA.toList $ y - x LA.#> beta
-            ztR    = zGroupSums idx r0 q
-            utilde = V.zipWith (\pj sj -> pj * sj / s2) pDiag ztR
-            zuU    = LA.fromList . V.toList $ zuScatter idx utilde
-            betaNew = LA.flatten (x LA.<\> LA.asColumn (y - zuU))
-            trP    = V.sum pDiag
-            su2New = max 1e-8 $ (trP + V.sum (V.map (\u -> u*u) utilde)) / fromIntegral q
-            r1     = y - x LA.#> betaNew - zuU
-            trZPZt = V.sum (V.zipWith (\nj pj -> fromIntegral nj * pj) sizes pDiag)
-            s2New  = max 1e-8 $ (r1 `LA.dot` r1 + trZPZt) / fromIntegral n
-        in (betaNew, su2New, s2New)
-
-      converge 0 st            = st
-      converge k st@(b, su, s) =
-        let st'@(b', su', s') = emStep st
-        in if    LA.norm_2 (b' - b) < emTol
-              && abs (su' - su)      < emTol
-              && abs (s'  - s)       < emTol
-           then st'
-           else converge (k-1) st'
-
-      (betaF, su2F, s2F) = converge maxEmIter (beta0, su2_0, s2_0)
-
-      pDiagF = V.fromList [ 1.0 / (1.0/su2F + fromIntegral (sizes V.! j) / s2F)
-                          | j <- [0..q-1] ]
-      r0F    = V.fromList . LA.toList $ y - x LA.#> betaF
-      ztRF   = zGroupSums idx r0F q
-      uF     = V.zipWith (\pj sj -> pj * sj / s2F) pDiagF ztRF
-      zuF    = LA.fromList . V.toList $ zuScatter idx uF
-      fittedV = x LA.#> betaF + zuF
-      residV  = y - fittedV
-      ssResF  = residV `LA.dot` residV
-      r2      = if ssTot == 0 then 1.0 else 1.0 - ssResF / ssTot
-      icc     = su2F / (su2F + s2F)
-      fitRes  = FitResult (LA.asColumn betaF)
-                          (LA.asColumn fittedV)
-                          (LA.asColumn residV)
-                          (LA.fromList [r2])
-
-  in GLMMResult fitRes su2F s2F uF labels icc
-
--- ---------------------------------------------------------------------------
--- General random effects (intercept + slope): vector EM for Gaussian LME
--- ---------------------------------------------------------------------------
-
--- | Fit a Gaussian LME with __vector__ random effects via EM (ML), Phase 48.
---
--- Per group @j@ the model is @y_j = X_j β + Z_j b_j + ε_j@ with
--- @b_j ~ N(0, G)@ (@G@ is @r×r@) and @ε ~ N(0, σ²I)@. The @Z@ argument holds
--- the raw random-effect design columns (usually a sub-block of @X@, e.g. the
--- intercept column plus the slope column for @(1+x|g)@); rows align with @X@.
---
--- EM (Laird-Ware), each step given @(β, G, σ²)@:
---
---   * E-step (per group, @r×r@): @P_j = (G⁻¹ + Z_jᵀZ_j/σ²)⁻¹@,
---     @b̂_j = P_j Z_jᵀ r_j / σ²@ with @r_j = y_j − X_j β@.
---   * M-step: @β = (XᵀX)⁻¹Xᵀ(y − Zb̂)@,
---     @G = (1/q) Σ_j (P_j + b̂_j b̂_jᵀ)@,
---     @σ² = (1/n)[Σ‖y_j − X_j β − Z_j b̂_j‖² + Σ tr(Z_jᵀZ_j P_j)]@.
---
--- With @r = 1@ and an intercept-only @Z@ this reproduces 'fitLME' exactly.
--- All linear algebra is hmatrix-native (no list-based fallbacks).
---
--- TODO (Phase 48 follow-up): this is ML; a REML variant would correct the
--- variance estimates for the fixed-effect degrees of freedom.
-fitLMEGeneral
-  :: LA.Matrix Double  -- ^ X (fixed-effect design, must include intercept)
-  -> LA.Matrix Double  -- ^ Z (random-effect design, @n × r@; rows align with X)
-  -> LA.Vector Double  -- ^ y
-  -> V.Vector Int      -- ^ per-observation group index (0-based)
-  -> V.Vector Text     -- ^ sorted group labels (length q)
-  -> GLMMResultRE
-fitLMEGeneral x z y idx labels =
-  let n       = LA.rows x
-      q       = V.length labels
-      r       = LA.cols z
-      members = precompMembers idx q n
-      zRows   = V.fromList (LA.toRows z)         -- O(1) per-row access for scatter
-
-      -- per-group X_j, Z_j, y_j (and Z_jᵀZ_j) precomputed once
-      groupBlk j =
-        let mem = members V.! j
-            xj  = x LA.? mem
-            zj  = z LA.? mem
-            yj  = LA.fromList [ y `LA.atIndex` i | i <- mem ]
-            ztz = LA.tr zj LA.<> zj
-        in (xj, zj, yj, ztz)
-      blocks = V.fromList [ groupBlk j | j <- [0..q-1] ]
-
-      -- initial values: OLS fixed fit, residual variance split intercept/resid
-      beta0 = LA.flatten (x LA.<\> LA.asColumn y)
-      yMean = LA.sumElements y / fromIntegral n
-      yDev  = y - LA.konst yMean n
-      ssTot = yDev `LA.dot` yDev
-      varY  = ssTot / fromIntegral n
-      g0    = LA.scale (varY / 2) (LA.ident r)
-      s20   = varY / 2
-
-      -- scatter (Zb̂)_i = Z_i · b̂_{g(i)}
-      scatterZb bhats =
-        LA.fromList [ (zRows V.! i) `LA.dot` (bhats V.! (idx V.! i)) | i <- [0..n-1] ]
-
-      emStep (beta, gMat, s2) =
-        let gInv  = LA.inv gMat
-            -- E-step: posterior cov P_j and mean b̂_j per group
-            pbs   = V.map (\(xj, zj, yj, ztz) ->
-                      let rj  = yj - xj LA.#> beta
-                          pj  = LA.inv (gInv + LA.scale (1/s2) ztz)
-                          bj  = LA.scale (1/s2) (pj LA.#> (LA.tr zj LA.#> rj))
-                      in (pj, bj, ztz)) blocks
-            bhats = V.map (\(_, bj, _) -> bj) pbs
-            zb    = scatterZb bhats
-            -- M-step β
-            betaN = LA.flatten (x LA.<\> LA.asColumn (y - zb))
-            -- M-step G = (1/q) Σ (P_j + b̂_j b̂_jᵀ)
-            gAcc  = V.foldl' (\acc (pj, bj, _) -> acc + pj + LA.outer bj bj)
-                             (LA.konst 0 (r, r)) pbs
-            gN    = LA.scale (1 / fromIntegral q) gAcc
-            -- M-step σ²: conditional residuals (using updated β) + trace term
-            zbN   = scatterZb bhats
-            r1    = y - x LA.#> betaN - zbN
-            trc   = V.sum (V.map (\(pj, _, ztz) -> LA.sumElements (ztz * pj)) pbs)
-            s2N   = max 1e-10 $ (r1 `LA.dot` r1 + trc) / fromIntegral n
-        in (betaN, gN, s2N)
-
-      converge 0 st            = st
-      converge k st@(b, gM, s) =
-        let st'@(b', gM', s') = emStep st
-        in if    LA.norm_2 (b' - b)            < emTol
-              && LA.norm_2 (LA.flatten (gM' - gM)) < emTol
-              && abs (s' - s)                   < emTol
-           then st'
-           else converge (k-1) st'
-
-      (betaF, gF, s2F) = converge maxEmIter (beta0, g0, s20)
-
-      -- final BLUPs and conditional fit
-      gInvF  = LA.inv gF
-      bhatsF = V.map (\(xj, zj, yj, ztz) ->
-                 let rj = yj - xj LA.#> betaF
-                     pj = LA.inv (gInvF + LA.scale (1/s2F) ztz)
-                 in LA.scale (1/s2F) (pj LA.#> (LA.tr zj LA.#> rj))) blocks
-      zbF     = scatterZb bhatsF
-      fittedV = x LA.#> betaF + zbF
-      residV  = y - fittedV
-      ssResF  = residV `LA.dot` residV
-      r2      = if ssTot == 0 then 1.0 else 1.0 - ssResF / ssTot
-      fitRes  = FitResult (LA.asColumn betaF)
-                          (LA.asColumn fittedV)
-                          (LA.asColumn residV)
-                          (LA.fromList [r2])
-      blupMat = LA.fromRows (V.toList bhatsF)   -- q×r
-
-  in GLMMResultRE fitRes gF s2F blupMat labels
-
--- ---------------------------------------------------------------------------
--- Laplace approximation for non-Gaussian GLMM
--- ---------------------------------------------------------------------------
-
--- | Inverse link: μ = g⁻¹(η)
-glmmInvLink :: LinkFn -> Double -> Double
-glmmInvLink Identity η = η
-glmmInvLink Log      η = exp (min 500 η)
-glmmInvLink Logit    η = 1.0 / (1.0 + exp (-η))
-glmmInvLink Sqrt     η = η * η
-
--- | Forward link: η = g(μ)
-glmmFwdLink :: LinkFn -> Double -> Double
-glmmFwdLink Identity μ = μ
-glmmFwdLink Log      μ = log (max 1e-10 μ)
-glmmFwdLink Logit    μ = let c = max 1e-8 (min (1-1e-8) μ) in log (c / (1 - c))
-glmmFwdLink Sqrt     μ = sqrt (max 0 μ)
-
--- | Link derivative: g'(μ)
-glmmLinkDeriv :: LinkFn -> Double -> Double
-glmmLinkDeriv Identity _ = 1.0
-glmmLinkDeriv Log      μ = 1.0 / max 1e-10 μ
-glmmLinkDeriv Logit    μ = let c = max 1e-8 (min (1-1e-8) μ) in 1.0 / (c * (1 - c))
-glmmLinkDeriv Sqrt     μ = 0.5 / sqrt (max 1e-10 μ)
-
--- | GLM variance function: V(μ)
-glmmVarFn :: Family -> Double -> Double
-glmmVarFn Gaussian _ = 1.0
-glmmVarFn Binomial μ = let c = max 1e-8 (min (1-1e-8) μ) in c * (1 - c)
-glmmVarFn Poisson  μ = max 1e-8 μ
-
--- | Clamp μ to numerically safe range.
-glmmClampMu :: Family -> Double -> Double
-glmmClampMu Binomial = max 1e-8 . min (1 - 1e-8)
-glmmClampMu Poisson  = max 1e-8
-glmmClampMu Gaussian = id
-
--- | IRLS weight: w_i = 1 / (g'(μ)² V(μ))
-glmmWeight :: Family -> LinkFn -> Double -> Double
-glmmWeight family link μ =
-  let d = glmmLinkDeriv link μ
-  in max 1e-10 (1.0 / (d * d * glmmVarFn family μ))
-
--- | Score contribution: s_i = (y_i − μ_i) / (g'(μ_i) V(μ_i))
-glmmScore :: Family -> LinkFn -> Double -> Double -> Double
-glmmScore family link y μ =
-  (y - μ) / (glmmLinkDeriv link μ * glmmVarFn family μ)
-
--- | ICC approximation for non-Gaussian models (on the link scale).
--- Binomial/logit: π²/3 is the variance of the standard logistic distribution.
--- Poisson/log:    1 is the log-scale residual variance (approximation).
-iccApprox :: Family -> Double -> Double
-iccApprox Gaussian su2 = su2 / (su2 + 1.0)        -- placeholder; LME gives exact ICC
-iccApprox Binomial su2 = su2 / (su2 + pi*pi/3.0)
-iccApprox Poisson  su2 = su2 / (su2 + 1.0)
-
--- | Precompute group member index lists (O(n) preprocessing).
-precompMembers :: V.Vector Int -> Int -> Int -> V.Vector [Int]
-precompMembers idx q n =
-  let mmap = Map.fromListWith (++) [ (idx V.! i, [i]) | i <- [0..n-1] ]
-  in V.fromList [ Map.findWithDefault [] j mmap | j <- [0..q-1] ]
-
-maxNRIter :: Int
-maxNRIter = 50
-
-nrTol :: Double
-nrTol = 1e-10
-
--- | Inner Newton-Raphson: find conditional mode û_j for one group.
--- Maximises Q_j(u) = Σ log p(y_i | g⁻¹(ηᵢ + u)) − u²/(2σ²_u)
--- NR step: u ← u + grad/hess  where
---   grad = Σ s_i − u/σ²_u,   hess = Σ w_i + 1/σ²_u
-nrOneGroup :: Family -> LinkFn -> Double -> [Double] -> [Double] -> Double -> Double
-nrOneGroup family link su2 etaFixed ys = go maxNRIter
-  where
-    clamp = glmmClampMu family
-    gInv  = glmmInvLink link
-
-    go 0 u = u
-    go k u =
-      let mus   = map (clamp . gInv . (+ u)) etaFixed
-          grad  = sum (zipWith (glmmScore family link) ys mus) - u / su2
-          hess  = sum (map (glmmWeight family link) mus) + 1.0 / su2
-          delta = grad / hess
-          u'    = u + delta
-      in if abs delta < nrTol then u' else go (k-1) u'
-
-maxGLMMIter :: Int
-maxGLMMIter = 200
-
-glmmTol :: Double
-glmmTol = 1e-7
-
--- | One outer GLMM iteration:
---   1. NR(û)    — find conditional modes given current β and σ²_u
---   2. IRLS(β)  — one IRLS step with random effects as offset
---   3. EM(σ²_u) — Laplace-approximated posterior variance update
-glmmStep
-  :: Family -> LinkFn
-  -> LA.Matrix Double    -- X
-  -> LA.Vector Double    -- y
-  -> V.Vector Int        -- per-obs group index
-  -> V.Vector [Int]      -- per-group member index lists (precomputed)
-  -> (LA.Vector Double, Double, V.Vector Double)
-  -> (LA.Vector Double, Double, V.Vector Double)
-glmmStep family link x y idx members (beta, su2, u) =
-  let q     = V.length u
-      clamp = glmmClampMu family
-      gInv  = glmmInvLink link
-      gD    = glmmLinkDeriv link
-
-      xBeta     = x LA.#> beta
-      etaFixedV = V.fromList (LA.toList xBeta)
-      yV        = V.fromList (LA.toList y)
-
-      -- 1. Inner NR: update û_j for each group j
-      uNew = V.fromList
-               [ nrOneGroup family link su2
-                   [ etaFixedV V.! i | i <- members V.! j ]
-                   [ yV        V.! i | i <- members V.! j ]
-                   (u V.! j)
-               | j <- [0..q-1] ]
-
-      -- 2. IRLS step for β (offset = Zû)
-      -- z_adj_i = (y_i − μ_i) g'(μ_i) + (Xβ)_i   (WLS target without offset)
-      uScatter  = LA.fromList . V.toList $ zuScatter idx uNew
-      etaFull   = xBeta + uScatter
-      musV      = V.map (clamp . gInv) (V.fromList (LA.toList etaFull))
-      wsV       = V.map (glmmWeight family link) musV
-      xBetaV    = V.fromList (LA.toList xBeta)
-      zAdjV     = V.zipWith3 (\yi mui xbi -> (yi - mui) * gD mui + xbi) yV musV xBetaV
-      sqrtW     = LA.diag (LA.fromList . V.toList $ V.map sqrt wsV)
-      zAdj      = LA.fromList (V.toList zAdjV)
-      betaNew   = LA.flatten $
-                    (sqrtW LA.<> x) LA.<\> LA.asColumn (sqrtW LA.#> zAdj)
-
-      -- 3. EM-like σ²_u update using Laplace-approximated posterior variance
-      -- ṽ_j = 1 / (Σ_{i∈j} w_i + 1/σ²_u)  ≈ Var(u_j | y)
-      -- σ²_u_new = Σ_j (ṽ_j + û_j²) / q
-      etaNew    = x LA.#> betaNew + uScatter
-      musNewV   = V.map (clamp . gInv) (V.fromList (LA.toList etaNew))
-      wsNewV    = V.map (glmmWeight family link) musNewV
-      wSumsV    = zGroupSums idx wsNewV q
-      su2New    = max 1e-8 $
-                    V.sum (V.zipWith (\ws uj -> 1.0/(ws + 1.0/su2) + uj*uj) wSumsV uNew)
-                    / fromIntegral q
-
-  in (betaNew, su2New, uNew)
-
--- | Fit a non-Gaussian GLMM (random intercept) via Laplace approximation.
--- For Gaussian/Identity, prefer fitLMEDataFrame which uses exact EM.
-fitGLMM
-  :: Family -> LinkFn
-  -> LA.Matrix Double
-  -> LA.Vector Double
-  -> V.Vector Int      -- per-obs group index
-  -> V.Vector Text     -- sorted group labels
-  -> V.Vector Int      -- per-group sizes (unused; kept for API symmetry with fitLME)
-  -> GLMMResult
-fitGLMM family link x y idx labels _sizes =
-  let n = LA.rows x
-      p = LA.cols x
-      q = V.length labels
-
-      members = precompMembers idx q n
-
-      -- Initialise: β₀ = g(ȳ_safe), rest 0; û = 0; σ²_u = half total variance
-      yMean = LA.sumElements y / fromIntegral n
-      ySafe = case family of
-                Binomial -> max 1e-6 (min (1-1e-6) yMean)
-                Poisson  -> max 1e-6 yMean
-                Gaussian -> yMean
-      beta0 = LA.fromList (glmmFwdLink link ySafe : replicate (p - 1) 0.0)
-      u0    = V.replicate q 0.0
-      yDev  = y - LA.konst yMean n
-      su2_0 = max 1e-4 ((yDev `LA.dot` yDev) / fromIntegral n / 2)
-
-      norm2V v = sqrt $ V.foldl' (\acc d -> acc + d*d) 0.0 v
-
-      converge 0 st              = st
-      converge k st@(b, su, u') =
-        let st'@(b', su', u'') = glmmStep family link x y idx members st
-        in if    LA.norm_2 (b' - b)             < glmmTol
-              && abs (su' - su)                  < glmmTol
-              && norm2V (V.zipWith (-) u'' u')   < glmmTol
-           then st'
-           else converge (k-1) st'
-
-      (betaF, su2F, uF) = converge maxGLMMIter (beta0, su2_0, u0)
-
-      -- Final conditional fitted values and statistics
-      uScatterF = LA.fromList . V.toList $ zuScatter idx uF
-      fittedLA  = LA.cmap (glmmClampMu family . glmmInvLink link) (x LA.#> betaF + uScatterF)
-      residLA   = y - fittedLA
-      ssTot     = yDev `LA.dot` yDev
-      ssRes     = residLA `LA.dot` residLA
-      r2        = if ssTot == 0 then 1.0 else 1.0 - ssRes / ssTot
-      icc       = iccApprox family su2F
-      fitRes    = FitResult (LA.asColumn betaF)
-                            (LA.asColumn fittedLA)
-                            (LA.asColumn residLA)
-                            (LA.fromList [r2])
-
-  in GLMMResult fitRes su2F 1.0 uF labels icc
-
--- ---------------------------------------------------------------------------
--- General random effects (intercept + slope): vector Laplace for GLMM
--- ---------------------------------------------------------------------------
-
--- | Multivariate inner Newton-Raphson: find the conditional mode @b̂_j@ of one
--- group and return @(b̂_j, P_j)@ where @P_j = (Σ_i w_i z_i z_iᵀ + G⁻¹)⁻¹@ is
--- the Laplace posterior covariance at the mode.
---
--- Maximises @Q_j(b) = Σ_i log p(y_i | g⁻¹(η_i + z_iᵀ b)) − ½ bᵀ G⁻¹ b@.
--- Newton step solves @H δ = grad@ with
--- @grad = Σ_i s_i z_i − G⁻¹ b@, @H = Σ_i w_i z_i z_iᵀ + G⁻¹@.
-nrOneGroupVec
-  :: Family -> LinkFn
-  -> LA.Matrix Double    -- ^ G⁻¹ (r×r)
-  -> [LA.Vector Double]  -- ^ z_i rows for this group (each length r)
-  -> [Double]            -- ^ etaFixed_i = (X_i β)
-  -> [Double]            -- ^ y_i
-  -> LA.Vector Double    -- ^ initial b (length r)
-  -> (LA.Vector Double, LA.Matrix Double)
-nrOneGroupVec family link gInv zs etaFixed ys = go maxNRIter
-  where
-    clamp = glmmClampMu family
-    gInvL = glmmInvLink link
-    r     = LA.rows gInv
-
-    -- negative Hessian (= posterior precision) at b: Σ_i w_i z_i z_iᵀ + G⁻¹
-    hessAt b =
-      let etas = zipWith (\z ef -> ef + z `LA.dot` b) zs etaFixed
-          mus  = map (clamp . gInvL) etas
-          ws   = map (glmmWeight family link) mus
-      in foldr (\(w, z) acc -> acc + LA.scale w (LA.outer z z)) gInv (zip ws zs)
-
-    go 0 b = (b, LA.inv (hessAt b))
-    go k b =
-      let etas  = zipWith (\z ef -> ef + z `LA.dot` b) zs etaFixed
-          mus   = map (clamp . gInvL) etas
-          ss    = zipWith (glmmScore family link) ys mus
-          ws    = map (glmmWeight family link) mus
-          grad  = foldr (\(s, z) acc -> acc + LA.scale s z) (LA.konst 0 r) (zip ss zs)
-                    - (gInv LA.#> b)
-          hess  = foldr (\(w, z) acc -> acc + LA.scale w (LA.outer z z)) gInv (zip ws zs)
-          delta = LA.flatten (hess LA.<\> LA.asColumn grad)
-          b'    = b + delta
-      in if LA.norm_2 delta < nrTol then (b', LA.inv hess) else go (k-1) b'
-
--- | Fit a non-Gaussian GLMM with __vector__ random effects via Laplace
--- approximation (Phase 48). Per group @j@: @g(E[y|b]) = X_j β + Z_j b_j@,
--- @b_j ~ N(0, G)@ (@G@ is @r×r@). Outer loop: multivariate NR for the modes
--- @b̂_j@ (with Laplace posterior cov @P_j@), one IRLS step for @β@ (random
--- effects as offset), and an EM update @G = (1/q) Σ_j (P_j + b̂_j b̂_jᵀ)@.
---
--- With @r = 1@ and an intercept-only @Z@ this matches 'fitGLMM'. Supports the
--- same families/links as 'fitGLMM' (Binomial/Logit, Poisson/Log).
-fitGLMMGeneral
-  :: Family -> LinkFn
-  -> LA.Matrix Double  -- ^ X (fixed-effect design, must include intercept)
-  -> LA.Matrix Double  -- ^ Z (random-effect design, @n × r@; rows align with X)
-  -> LA.Vector Double  -- ^ y
-  -> V.Vector Int      -- ^ per-observation group index (0-based)
-  -> V.Vector Text     -- ^ sorted group labels (length q)
-  -> GLMMResultRE
-fitGLMMGeneral family link x z y idx labels =
-  let n       = LA.rows x
-      p       = LA.cols x
-      q       = V.length labels
-      r       = LA.cols z
-      members = precompMembers idx q n
-      zRows   = V.fromList (LA.toRows z)
-      yV      = V.fromList (LA.toList y)
-
-      groupZs = V.fromList [ [ zRows V.! i | i <- members V.! j ] | j <- [0..q-1] ]
-      groupYs = V.fromList [ [ yV    V.! i | i <- members V.! j ] | j <- [0..q-1] ]
-
-      clamp = glmmClampMu family
-      gInvL = glmmInvLink link
-      gD    = glmmLinkDeriv link
-
-      yMean = LA.sumElements y / fromIntegral n
-      ySafe = case family of
-                Binomial -> max 1e-6 (min (1-1e-6) yMean)
-                Poisson  -> max 1e-6 yMean
-                Gaussian -> yMean
-      beta0 = LA.fromList (glmmFwdLink link ySafe : replicate (p - 1) 0.0)
-      b0    = V.replicate q (LA.konst 0 r)
-      yDev  = y - LA.konst yMean n
-      su2_0 = max 1e-4 ((yDev `LA.dot` yDev) / fromIntegral n / 2)
-      g0    = LA.scale su2_0 (LA.ident r)
-
-      scatterZb bs =
-        LA.fromList [ (zRows V.! i) `LA.dot` (bs V.! (idx V.! i)) | i <- [0..n-1] ]
-
-      step (beta, gMat, bs) =
-        let gInv      = LA.inv gMat
-            xBeta     = x LA.#> beta
-            etaFixedV = V.fromList (LA.toList xBeta)
-            results   = V.fromList
-                          [ nrOneGroupVec family link gInv (groupZs V.! j)
-                              [ etaFixedV V.! i | i <- members V.! j ]
-                              (groupYs V.! j)
-                              (bs V.! j)
-                          | j <- [0..q-1] ]
-            bsNew = V.map fst results
-            pjs   = V.map snd results
-            -- IRLS β with random offset Zb̂ held fixed
-            zb     = scatterZb bsNew
-            etaF   = xBeta + zb
-            musV   = V.map (clamp . gInvL) (V.fromList (LA.toList etaF))
-            wsV    = V.map (glmmWeight family link) musV
-            xBetaV = V.fromList (LA.toList xBeta)
-            zAdjV  = V.zipWith3 (\yi mui xbi -> (yi - mui) * gD mui + xbi) yV musV xBetaV
-            sqrtW  = LA.diag (LA.fromList . V.toList $ V.map sqrt wsV)
-            zAdj   = LA.fromList (V.toList zAdjV)
-            betaN  = LA.flatten $ (sqrtW LA.<> x) LA.<\> LA.asColumn (sqrtW LA.#> zAdj)
-            -- EM update G = (1/q) Σ (P_j + b̂_j b̂_jᵀ)
-            gAcc   = V.foldl' (\acc (pj, bj) -> acc + pj + LA.outer bj bj)
-                              (LA.konst 0 (r, r)) (V.zip pjs bsNew)
-            gN     = LA.scale (1 / fromIntegral q) gAcc
-        in (betaN, gN, bsNew)
-
-      bsDiff a b = V.sum (V.zipWith (\u v -> LA.norm_2 (u - v)) a b)
-      converge 0 st                = st
-      converge k st@(beta, gM, bs) =
-        let st'@(beta', gM', bs') = step st
-        in if    LA.norm_2 (beta' - beta)                  < glmmTol
-              && LA.norm_2 (LA.flatten (gM' - gM))          < glmmTol
-              && bsDiff bs' bs                              < glmmTol
-           then st'
-           else converge (k-1) st'
-
-      (betaF, gF, bsF) = converge maxGLMMIter (beta0, g0, b0)
-
-      zbF     = scatterZb bsF
-      fittedV = LA.cmap (clamp . gInvL) (x LA.#> betaF + zbF)
-      residV  = y - fittedV
-      ssTot   = yDev `LA.dot` yDev
-      ssRes   = residV `LA.dot` residV
-      r2      = if ssTot == 0 then 1.0 else 1.0 - ssRes / ssTot
-      fitRes  = FitResult (LA.asColumn betaF)
-                          (LA.asColumn fittedV)
-                          (LA.asColumn residV)
-                          (LA.fromList [r2])
-      blupMat = LA.fromRows (V.toList bsF)
-
-  in GLMMResultRE fitRes gF 1.0 blupMat labels
-
--- ---------------------------------------------------------------------------
--- DataFrame-level API
--- ---------------------------------------------------------------------------
-
--- | Fit a random-intercept LME from a DataFrame (Gaussian, exact EM).
-fitLMEDataFrame
-  :: [(Text, Int)]   -- ^ x column specs
-  -> Text            -- ^ grouping column (text/categorical)
-  -> Text            -- ^ response column
-  -> DXD.DataFrame
-  -> Maybe GLMMResult
-fitLMEDataFrame colDegs groupCol yCol df = do
-  xVecs <- mapM (\(col, _) -> getDoubleVec col df) colDegs
-  yVec  <- getDoubleVec yCol df
-  gVec  <- getTextVec   groupCol df
-  let degrees              = map snd colDegs
-      dm                   = multiPolyDesignMatrix (zip xVecs degrees)
-      y                    = LA.fromList (V.toList yVec)
-      (labels, idx, sizes) = buildGroups gVec
-  return (fitLME dm y idx labels sizes)
-
--- | Fit a non-Gaussian GLMM from a DataFrame (Laplace approximation).
--- Supports Binomial/Logit and Poisson/Log; for Gaussian/Identity prefer fitLMEDataFrame.
-fitGLMMDataFrame
-  :: Family -> LinkFn
-  -> [(Text, Int)]   -- ^ x column specs
-  -> Text            -- ^ grouping column (text/categorical)
-  -> Text            -- ^ response column
-  -> DXD.DataFrame
-  -> Maybe GLMMResult
-fitGLMMDataFrame family link colDegs groupCol yCol df = do
-  xVecs <- mapM (\(col, _) -> getDoubleVec col df) colDegs
-  yVec  <- getDoubleVec yCol df
-  gVec  <- getTextVec   groupCol df
-  let degrees              = map snd colDegs
-      dm                   = multiPolyDesignMatrix (zip xVecs degrees)
-      y                    = LA.fromList (V.toList yVec)
-      (labels, idx, sizes) = buildGroups gVec
-  return (fitGLMM family link dm y idx labels sizes)
-
--- ---------------------------------------------------------------------------
--- Multi-output GLMM (per-column EM/Laplace; grouping shared across columns)
--- ---------------------------------------------------------------------------
-
--- | Multi-output GLMM/LME fit result.
-data GLMMResultMulti = GLMMResultMulti
-  { glmmFits  :: [GLMMResult]    -- ^ Per-column fit results.
-  , glmmGrpsM :: V.Vector Text   -- ^ Sorted group labels (shared across columns).
-  } deriving (Show)
-
--- | Multi-output Gaussian LME. @Y@ has shape @n × q@; 'fitLME' is run
--- independently on each column.
-fitLMEMulti :: LA.Matrix Double -> LA.Matrix Double
-            -> V.Vector Int -> V.Vector Text -> V.Vector Int
-            -> GLMMResultMulti
-fitLMEMulti x y idx labels sizes =
-  let q     = LA.cols y
-      yCol j = LA.flatten (y LA.¿ [j])
-      fits  = [fitLME x (yCol j) idx labels sizes | j <- [0 .. q - 1]]
-  in GLMMResultMulti fits labels
-
--- | Multi-output non-Gaussian GLMM. @Y@ has shape @n × q@; 'fitGLMM' is
--- run independently on each column.
-fitGLMMMulti :: Family -> LinkFn
-             -> LA.Matrix Double -> LA.Matrix Double
-             -> V.Vector Int -> V.Vector Text -> V.Vector Int
-             -> GLMMResultMulti
-fitGLMMMulti family link x y idx labels sizes =
-  let q     = LA.cols y
-      yCol j = LA.flatten (y LA.¿ [j])
-      fits  = [fitGLMM family link x (yCol j) idx labels sizes
-              | j <- [0 .. q - 1]]
-  in GLMMResultMulti fits labels
-
--- ---------------------------------------------------------------------------
--- Standard errors (request/100)
--- ---------------------------------------------------------------------------
-
--- | Standard errors of the fixed-effect coefficients @β@.
---
--- For LME (Gaussian, Identity link) this is /exact/: it inverts
--- @Xᵀ V⁻¹ X@ where @V = σ² I + σ²_u Z Zᵀ@ is the marginal covariance
--- under the random-intercept model. The block structure of @V@ is
--- exploited so this stays @O(n p² + q p²)@ instead of forming a
--- dense @n × n@ matrix:
---
--- > Xᵀ V⁻¹ X = (1/σ²) Xᵀ X − Σ_j (α_j / σ²) s_j s_jᵀ
--- > α_j     = σ²_u / (σ² + n_j σ²_u)
--- > s_j     = Σ_{i ∈ group j} x_i           (column sums of X within group j)
---
--- For non-Gaussian families this returns a Gaussian-approximation
--- (treats @σ² = 1@) — adequate for /relative/ ordering of coefficients
--- but absolute values are off; matching lme4-style non-Gaussian SE
--- requires the converged IRLS weights which are not currently exposed
--- by 'fitGLMM'.
-glmmFixedSE
-  :: LA.Matrix Double      -- ^ Design matrix @X@ (n × p, intercept inclusive).
-  -> V.Vector Int          -- ^ Group index per observation (length n; same as
-                           --   the @idx@ produced by @buildGroups@).
-  -> GLMMResult
-  -> LA.Vector Double      -- ^ Length @p@; coefficient SEs in column order.
-glmmFixedSE x groupIdx res =
-  let n       = LA.rows x
-      p       = LA.cols x
-      sig2u   = glmmRandVar  res
-      sig2RAW = glmmResidVar res
-      sig2    = if sig2RAW > 0 then sig2RAW else 1.0   -- non-Gaussian fallback
-      q       = V.length (glmmGroups res)
-
-      -- per-group n_j
-      nj :: Map.Map Int Int
-      nj = V.foldl' (\acc j -> Map.insertWith (+) j 1 acc) Map.empty groupIdx
-
-      -- per-group column sum s_j = Σ_{i ∈ group j} x_i  (length p)
-      groupSum :: Map.Map Int (LA.Vector Double)
-      groupSum =
-        V.foldl' (\acc i ->
-                    let j = groupIdx V.! i
-                        xi = LA.flatten (x LA.? [i])
-                    in Map.insertWith (+) j xi acc)
-                 Map.empty
-                 (V.enumFromN 0 n)
-
-      xtxFull = LA.tr x LA.<> x
-
-      correction :: LA.Matrix Double
-      correction =
-        Map.foldlWithKey'
-          (\acc j s ->
-              let nj_j = Map.findWithDefault 0 j nj
-                  alpha = sig2u / (sig2 + fromIntegral nj_j * sig2u)
-              in acc + LA.scale alpha (LA.outer s s))
-          (LA.konst 0 (p, p))
-          groupSum
-
-      xvtinvX = LA.scale (1 / sig2) (xtxFull - correction)
-      cov     = LA.inv xvtinvX
-      _ = q  -- kept to make q's role explicit in the docstring
-  in LA.fromList [ sqrt (max 0 (LA.atIndex cov (i, i))) | i <- [0 .. p - 1] ]
-
--- | Posterior standard errors of the BLUPs @û_j@ under the
--- random-intercept model:
---
--- > Var(u_j | data) = (1 / σ²_u + n_j / σ²)⁻¹
---
--- (For non-Gaussian families this uses @σ² = 1@; same caveat as
--- 'glmmFixedSE'.) Length matches 'glmmGroups'.
-glmmBLUPSE :: V.Vector Int -> GLMMResult -> V.Vector Double
-glmmBLUPSE groupIdx res =
-  let q       = V.length (glmmGroups res)
-      sig2u   = glmmRandVar  res
-      sig2RAW = glmmResidVar res
-      sig2    = if sig2RAW > 0 then sig2RAW else 1.0
-      njMap   = V.foldl' (\acc j -> Map.insertWith (+) j 1 acc)
-                         Map.empty groupIdx
-      ng j    = Map.findWithDefault 0 j njMap
-  in V.generate q (\j ->
-       let nDouble = fromIntegral (ng j) :: Double
-           varInv  = 1.0 / sig2u + nDouble / sig2
-       in sqrt (1.0 / varInv))
diff --git a/src/Hanalyze/Model/GP.hs b/src/Hanalyze/Model/GP.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/GP.hs
+++ /dev/null
@@ -1,887 +0,0 @@
-{-# LANGUAGE StrictData #-}
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.GP
--- Description : ガウス過程回帰 (Gaussian-process regression)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Gaussian-process regression.
---
--- Pick a kernel, fit it to training data and obtain the posterior
--- predictive at arbitrary test points. Hyperparameters can be tuned
--- automatically by maximizing the log marginal likelihood.
---
--- @
--- import Hanalyze.Model.GP
---
--- -- 訓練データ
--- let xs = [0, 0.5 .. 5]
---     ys = map (\x -> sin x + 0.1 * noise) xs
---
--- -- ハイパーパラメータをデータから初期化し最適化
--- let p0  = initParamsFromData xs ys
---     opt = optimizeGP RBF xs ys p0
---     res = fitGP (GPModel RBF opt) xs ys testXs
---
--- -- gpMean res, gpLower res, gpUpper res で結果を取得
--- @
-module Hanalyze.Model.GP
-  ( -- * カーネル型 (re-export from "Hanalyze.Model.Kernel")
-    Kernel (..)
-  , kernelName
-  , KernelParams (..)
-  , defaultKernelParams
-    -- * Hyperparameters
-  , GPParams (..)
-  , defaultGPParams
-  , gpKernelParams
-  , initParamsFromData
-  , initParamsFromDataMV
-    -- * Model and result
-  , GPModel (..)
-  , GPResult (..)
-    -- * Kernel computation
-  , kernelFn
-  , kEvalMV
-  , buildKernelMatrix
-    -- * Inference
-  , logMarginalLikelihood
-  , fitGP
-  , fitGPMulti
-  , optimizeGP
-  , gramLOOCV
-  , autoCVHyperGP
-  , autoCVHyperGPMV
-    -- * Data for interactive prediction
-  , GPPredData (..)
-  , gpPredData
-    -- * Multi-input (primary API; X is @n × p@, Y is @n × q@)
-  , GPResultMV (..)
-  , buildKernelMatrixMV
-  , noiseKernelMV
-  , logMarginalLikelihoodMV
-  , fitGPMV
-  , fitGPMVMulti
-  , optimizeGPMV
-  , optimizeGPMVCached
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import qualified Hanalyze.Optim.LBFGS as LBFGS
-import qualified Hanalyze.Optim.Common as OC
-import qualified Hanalyze.Stat.KernelDist as KD
-import qualified Hanalyze.Stat.Cholesky   as Chol
-import qualified Data.Vector.Storable         as VS
-import qualified Data.Vector.Storable.Mutable as VSM
-import           Control.Monad.ST             (runST)
-import           System.IO.Unsafe             (unsafePerformIO)
--- 共有カーネル語彙は 'Model.Kernel' (Phase 75.18 で分離)。 GP は後方互換のため
--- 'Kernel'/'KernelParams'/評価関数を re-export する。
-import           Hanalyze.Model.Kernel
-                   ( Kernel (..), kernelName, KernelParams (..), defaultKernelParams
-                   , kernelFn, buildKernelMatrix, applyKernel, kernelOfParams
-                   , ardScaleXY, buildKernelMatrixMV, kEvalMV )
-
--- ---------------------------------------------------------------------------
--- Types
--- ---------------------------------------------------------------------------
---
--- NB: 'Kernel' / 'kernelName' / 'KernelParams' と評価関数群は Phase 75.18 で
--- 'Hanalyze.Model.Kernel' へ分離。 GP は後方互換のため re-export する
--- (上の import 参照)。
-
--- | GP hyperparameters (= 'KernelParams' + 観測ノイズ σ_n²)。
---
--- カーネル系フィールド (ℓ / σ_f² / period / ARD) は 'gpKernelParams' で
--- 'KernelParams' へ射影でき、 カーネル評価関数 ('kernelFn' / 'kEvalMV' /
--- 'buildKernelMatrix' 等) はその 'KernelParams' を取る。
-data GPParams = GPParams
-  { gpLengthScale  :: Double
-    -- ^ Isotropic length scale @ℓ@; larger means smoother. Used unless
-    --   'gpLengthScales' is 'Just' (= ARD), in which case the per-dim
-    --   vector overrides this for multi-input kernel evaluation.
-  , gpSignalVar    :: Double
-    -- ^ Signal variance @σ_f²@; the variability of the function values.
-  , gpNoiseVar     :: Double
-    -- ^ Observation noise variance @σ_n²@; near 0 interpolates, larger
-    --   smooths.
-  , gpPeriod       :: Double
-    -- ^ Period @p@ (only used by the @Periodic@ kernel).
-  , gpLengthScales :: Maybe (LA.Vector Double)
-    -- ^ Per-dim length scales for ARD (Automatic Relevance
-    --   Determination). When 'Just' v, the multi-input kernel uses
-    --   @D_ARD[i,j] = Σ_d (X[i,d] − X'[j,d])² / ℓ_d²@ instead of the
-    --   isotropic distance / ℓ². Has no effect on the 1D 'kernelFn' /
-    --   'fitGP' path. 'Nothing' = isotropic (default).
-  } deriving (Show)
-
--- | Default hyperparameters: @ℓ = σ_f² = p = 1@, @σ_n² = 0.1@.
-defaultGPParams :: GPParams
-defaultGPParams = GPParams 1.0 1.0 0.1 1.0 Nothing
-
--- | Project the kernel hyperparameters of a 'GPParams' onto a
--- 'KernelParams' (drops the observation noise σ_n²). カーネル評価関数へ
--- 渡す際に使う。
-gpKernelParams :: GPParams -> KernelParams
-gpKernelParams p = KernelParams
-  { kpLengthScale  = gpLengthScale p
-  , kpSignalVar    = gpSignalVar p
-  , kpPeriod       = gpPeriod p
-  , kpLengthScales = gpLengthScales p
-  }
-
--- | Build a sensible initial 'GPParams' from data statistics, suitable
--- as a starting point for optimization.
-initParamsFromData :: [Double] -> [Double] -> GPParams
-initParamsFromData xs ys = GPParams
-  { gpLengthScale  = max 0.01 ((xMax - xMin) / 4)
-  , gpSignalVar    = max 0.01 yVar
-  , gpNoiseVar     = max 1e-4 (yVar * 0.05)
-  , gpPeriod       = max 0.01 (xMax - xMin)
-  , gpLengthScales = Nothing
-  }
-  where
-    xMin  = minimum xs
-    xMax  = maximum xs
-    yMean = sum ys / fromIntegral (length ys)
-    yVar  = sum (map (\y -> (y - yMean) ^ (2 :: Int)) ys) / fromIntegral (length ys)
-
--- | Multi-input variant of 'initParamsFromData'. Computes the length
--- scale from the /average/ per-dimension range of @X@ rather than
--- collapsing the @n × p@ matrix into a flat list (which the previous
--- @MultiGP@ call site did via @concat (toLists trainX)@ — yielding
--- nonsensical @xMin/xMax@ statistics, a poor length-scale init, and
--- in turn slow LBFGS convergence).
-initParamsFromDataMV :: LA.Matrix Double -> LA.Vector Double -> GPParams
-initParamsFromDataMV trainX y =
-  let p     = LA.cols trainX
-      cols  = LA.toColumns trainX            -- p column vectors
-      ranges = [ LA.maxElement c - LA.minElement c | c <- cols ]
-      avgRng = if null ranges then 1.0
-                              else sum ranges / fromIntegral (length ranges)
-      ys    = LA.toList y
-      yMean = LA.sumElements y / fromIntegral (LA.size y)
-      yVar  = sum (map (\v -> (v - yMean) ^ (2 :: Int)) ys)
-              / fromIntegral (LA.size y)
-      _     = p
-  in GPParams
-       { gpLengthScale  = max 0.01 (avgRng / 4)
-       , gpSignalVar    = max 0.01 yVar
-       , gpNoiseVar     = max 1e-4 (yVar * 0.05)
-       , gpPeriod       = max 0.01 avgRng
-       , gpLengthScales = Nothing
-       }
-
--- | A GP model: a kernel paired with its hyperparameters.
-data GPModel = GPModel
-  { gpKernel :: Kernel
-  , gpParams :: GPParams
-  } deriving (Show)
-
--- | GP posterior-predictive result.
-data GPResult = GPResult
-  { gpTestX :: [Double]   -- ^ Test points @x_*@.
-  , gpMean  :: [Double]   -- ^ Posterior mean @μ(x_*)@.
-  , gpVar   :: [Double]   -- ^ Posterior variance @σ²(x_*)@.
-  , gpLower :: [Double]   -- ^ @mean − 2σ@ (≈ 95 % credible-interval lower).
-  , gpUpper :: [Double]   -- ^ @mean + 2σ@ (≈ 95 % credible-interval upper).
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- Inference
--- ---------------------------------------------------------------------------
-
--- ノイズ付きカーネル行列 K_y = K(X,X) + σ_n² I を構築する（最小ジッター付き）。
-noiseKernel :: Kernel -> GPParams -> [Double] -> LA.Matrix Double
-noiseKernel ker p xs =
-  let n      = length xs
-      k      = buildKernelMatrix ker (gpKernelParams p) xs xs
-      jitter = max (gpNoiseVar p) 1e-6
-  in k `LA.add` LA.scale jitter (LA.ident n)
-
--- | Log marginal likelihood @log p(y | X, θ)@. Used as the objective
--- when optimizing GP hyperparameters.
---
--- @log p = −½ yᵀ Ky⁻¹ y − ½ log|Ky| − n/2 log(2π)@.
---
--- When the parameters are pathological (e.g. very small length scales)
--- and Cholesky fails, returns the penalty value @-10³⁰@ so the
--- optimizer steers away from that region.
-logMarginalLikelihood :: [Double] -> [Double] -> Kernel -> GPParams -> Double
-logMarginalLikelihood trainX trainY ker params =
-  let n      = length trainX
-      ky     = noiseKernel ker params trainX
-      y      = LA.fromList trainY
-      mR = case Chol.cholFactor ky of
-             Just r  -> Just (r, ky)
-             Nothing ->
-               -- jitter を追加して再試行
-               let kyJ = ky `LA.add` LA.scale 1e-4 (LA.ident n)
-               in case Chol.cholFactor kyJ of
-                    Just r  -> Just (r, kyJ)
-                    Nothing -> Nothing
-  in case mR of
-       Nothing -> -1e30
-       Just (r, _kyUsed)  ->
-         let logDet  = 2 * sum (map log (LA.toList (LA.takeDiag r)))
-             -- Reuse the already-computed Cholesky factor (avoids a
-             -- second factorization in the inner GP HP loop).
-             alpha   = LA.flatten
-                       (Chol.cholSolveWithFactor r (LA.asColumn y))
-             dataFit = LA.dot y alpha
-         in -0.5 * dataFit - 0.5 * logDet - fromIntegral n / 2 * log (2 * pi)
-
--- | Single-output GP posterior prediction at @testX@.
--- 多出力 'fitGPMulti' に y を 1 列行列化して委譲、列 0 を取り出す。
---
--- 事後平均: μ_* = K_*ᵀ Ky⁻¹ y
--- 事後分散: σ²_i = k(x*_i, x*_i) − K_*[i] Ky⁻¹ K_*[i]ᵀ
-fitGP :: GPModel -> [Double] -> [Double] -> [Double] -> GPResult
-fitGP model trainX trainY testX =
-  let yMat = LA.asColumn (LA.fromList trainY)
-      (meanMat, varList) = fitGPMulti model trainX yMat testX
-      mu = LA.toList (LA.flatten (meanMat LA.¿ [0]))
-      stdList = map sqrt varList
-  in GPResult
-       { gpTestX  = testX
-       , gpMean   = mu
-       , gpVar    = varList
-       , gpLower  = zipWith (\m s -> m - 2 * s) mu stdList
-       , gpUpper  = zipWith (\m s -> m + 2 * s) mu stdList
-       }
-
--- | Multi-output GP posterior prediction. @Y@ has shape @n × q@ (one
--- column per output task) and shares a single kernel and
--- ハイパーパラメータを共有する (Cholesky / Ky⁻¹ も共有)。
---
--- 戻り値: (事後平均行列 m × q, 事後分散ベクトル 長さ m)。
--- 分散は y に依らないため q 出力で共通。
-fitGPMulti :: GPModel -> [Double] -> LA.Matrix Double -> [Double]
-           -> (LA.Matrix Double, [Double])
-fitGPMulti model trainX trainY testX =
-  let ker    = gpKernel model
-      params = gpParams model
-      ky     = noiseKernel ker params trainX
-      kStar  = buildKernelMatrix ker (gpKernelParams params) testX trainX  -- (m × n)
-      -- α = Ky⁻¹ Y via SPD Cholesky (n × q)
-      alpha  = Chol.cholSolveJitter ky trainY
-      meanMt = kStar LA.<> alpha                          -- (m × q)
-      -- v = Ky⁻¹ K_*ᵀ via the same Cholesky factor (n × m).
-      -- Then var_i = k(x*_i, x*_i) − K_*[i,:] · v[:,i].
-      v       = Chol.cholSolveJitter ky (LA.tr kStar)
-      diagKss = [kernelFn ker (gpKernelParams params) x x | x <- testX]
-      -- F1: vectorise diag(kStar · v).
-      kStarDotV = LA.toList (KD.diagAB kStar v)
-      varList   = zipWith (\d kv -> max 0 (d - kv)) diagKss kStarDotV
-  in (meanMt, varList)
-
--- ---------------------------------------------------------------------------
--- Hyperparameter optimisation
--- ---------------------------------------------------------------------------
-
--- | Optimize GP hyperparameters by maximizing the log marginal likelihood.
---
--- Operates in log-space on @(ℓ, σ_f², σ_n²)@ using L-BFGS (numerical
--- central-difference gradients, no user-provided gradient required).
---
--- Typically 5-10× faster than the older @Hanalyze.Optim.GradAscent@ + numeric
--- gradient path, and less sensitive to the initial point.
--- Internally uses 'System.IO.Unsafe.unsafePerformIO', but L-BFGS is
--- deterministic so the result is referentially transparent.
-optimizeGP :: Kernel -> [Double] -> [Double] -> GPParams -> GPParams
-optimizeGP ker trainX trainY p0 =
-  let u0   = [log (gpLengthScale p0), log (gpSignalVar p0), log (gpNoiseVar p0)]
-      -- L-BFGS は最小化なので、log-mlik を最大化したいときは Maximize 指定
-      cfg  = LBFGS.defaultLBFGSConfig
-               { LBFGS.lbDir   = OC.Maximize
-               , LBFGS.lbStop  = OC.defaultStopCriteria
-                                   { OC.stMaxIter = 200, OC.stTolFun = 1e-8 }
-               }
-      result = unsafePerformIO $ LBFGS.runLBFGSNumeric cfg obj u0
-      uOpt   = OC.orBest result
-  in p0
-       { gpLengthScale = exp (uOpt !! 0)
-       , gpSignalVar   = exp (uOpt !! 1)
-       , gpNoiseVar    = exp (uOpt !! 2)
-       }
-  where
-    toParams u = p0
-      { gpLengthScale = exp (u !! 0)
-      , gpSignalVar   = exp (u !! 1)
-      , gpNoiseVar    = exp (u !! 2)
-      }
-    obj u = logMarginalLikelihood trainX trainY ker (toParams u)
-
--- ---------------------------------------------------------------------------
--- LOOCV hyperparameter selection (exact / Gram path) — Phase 70.5 項目 E
--- ---------------------------------------------------------------------------
-
--- | Leave-one-out CV (PRESS) for exact kernel-ridge / GP-mean prediction
--- from a /noiseless/ Gram matrix @K@. Closed form
--- @PRESS = (1/n) Σ ((yᵢ − ŷᵢ)/(1 − Hᵢᵢ))²@ with @H = K (K + λI)⁻¹@ and
--- @ŷ = H y@ (no @n@-fold refit). This is the Gram-space analogue of
--- 'Hanalyze.Model.RFF.loocvFromPhi' (identical PRESS algebra, but
--- in the @n@-dim Gram space instead of the @D@-dim RFF feature space).
--- KRR ≡ GP posterior mean with @λ = σ_n²@, so the same routine selects
--- @λ@ for both the @Ridge@ and @Gp@ quadrants of the unified @gp@ spec.
-gramLOOCV :: LA.Matrix Double   -- ^ Noiseless Gram matrix @K@ (@n × n@).
-          -> LA.Vector Double   -- ^ Targets @y@ (length @n@).
-          -> Double             -- ^ Ridge penalty @λ@ (= @σ_n²@).
-          -> Double
-gramLOOCV k y lam =
-  let n         = LA.rows k
-      regK      = addToDiag lam k                 -- K + λI (SPD)
-      -- H = K (K+λI)⁻¹ = (regK⁻¹ K)ᵀ (K, regK symmetric). Solve once.
-      h         = LA.tr (regK LA.<\> k)
-      yhat      = h LA.#> y
-      hDiag     = LA.takeDiag h
-      oneMinusH = LA.cmap (\hh -> max 1e-12 (1 - hh)) hDiag
-      resid     = y - yhat
-      ratios    = zipWith (/) (LA.toList resid) (LA.toList oneMinusH)
-  in sum [ r * r | r <- ratios ] / fromIntegral (max 1 n)
-
--- | Pick GP/KRR hyperparameters by minimizing leave-one-out CV (PRESS)
--- over a log-spaced @(ℓ, λ)@ grid. @σ_f@ is fixed at @std(y)@ (mirroring
--- 'Hanalyze.Model.RFF.gridSearchLOOCVRBFMV', where @σ_f@ and @λ@
--- are degenerate and @λ@ absorbs the scale). Returns 'GPParams' with the
--- selected @ℓ*@, @σ_f² = std(y)²@ and @σ_n² = λ*@ (KRR ≡ GP mean with
--- @λ = σ_n²@). Used by the @AutoCV@ 'HyperStrategy' for the exact
--- ('Gp'/'Ridge') quadrants.
-autoCVHyperGP :: Kernel -> [Double] -> [Double] -> GPParams
-autoCVHyperGP ker xs ys =
-  let p0      = initParamsFromData xs ys
-      yStd    = max 1e-9 (sqrt (varOfList ys))
-      ell0    = gpLengthScale p0
-      ellGrid = logSpaceList (ell0 * 0.1)   (ell0 * 10) 10
-      lamGrid = logSpaceList (yStd * 1e-6)  (yStd * 10) 20
-      yV      = LA.fromList ys
-      score ell lam =
-        let pk = p0 { gpLengthScale = ell, gpSignalVar = yStd * yStd }
-            k  = buildKernelMatrix ker (gpKernelParams pk) xs xs
-        in gramLOOCV k yV lam
-      cands = [ (ell, lam, score ell lam) | ell <- ellGrid, lam <- lamGrid ]
-      (bEll, bLam, _) =
-        foldr1 (\a@(_,_,sa) b@(_,_,sb) -> if sa <= sb then a else b) cands
-  in p0 { gpLengthScale = bEll, gpSignalVar = yStd * yStd, gpNoiseVar = bLam }
-
--- | Multi-input analogue of 'autoCVHyperGP'. Same log-spaced @(ℓ, λ)@
--- Gram-LOOCV search but builds the kernel from an @n × p@ training
--- matrix via 'buildKernelMatrixMV' (isotropic; ℓ shared across inputs).
-autoCVHyperGPMV :: Kernel -> LA.Matrix Double -> LA.Vector Double -> GPParams
-autoCVHyperGPMV ker trainX y =
-  let p0      = initParamsFromDataMV trainX y
-      yStd    = max 1e-9 (sqrt (varOfList (LA.toList y)))
-      ell0    = gpLengthScale p0
-      ellGrid = logSpaceList (ell0 * 0.1)  (ell0 * 10) 8
-      lamGrid = logSpaceList (yStd * 1e-6) (yStd * 10) 16
-      score ell lam =
-        let pk = p0 { gpLengthScale = ell, gpSignalVar = yStd * yStd }
-            k  = buildKernelMatrixMV ker (gpKernelParams pk) trainX trainX
-        in gramLOOCV k y lam
-      cands = [ (ell, lam, score ell lam) | ell <- ellGrid, lam <- lamGrid ]
-      (bEll, bLam, _) =
-        foldr1 (\a@(_,_,sa) b@(_,_,sb) -> if sa <= sb then a else b) cands
-  in p0 { gpLengthScale = bEll, gpSignalVar = yStd * yStd, gpNoiseVar = bLam }
-
--- | Population variance of a list (LOOCV σ_f init).
-varOfList :: [Double] -> Double
-varOfList zs =
-  let n = fromIntegral (length zs)
-      m = sum zs / n
-  in if n <= 0 then 0 else sum [ (z - m) ^ (2 :: Int) | z <- zs ] / n
-
--- | @n@ points log-spaced in @[lo, hi]@ (inclusive). @lo,hi > 0@.
-logSpaceList :: Double -> Double -> Int -> [Double]
-logSpaceList lo hi n
-  | n <= 1    = [lo]
-  | otherwise = [ exp (logLo + (logHi - logLo) * fromIntegral i / fromIntegral (n - 1))
-                | i <- [0 .. n - 1] ]
-  where logLo = log lo
-        logHi = log hi
-
--- ---------------------------------------------------------------------------
--- Interactive prediction data (for Hanalyze.Viz.GPReport)
--- ---------------------------------------------------------------------------
-
--- | JavaScript 対話予測に必要な内部データ。
--- Ky⁻¹ と α = Ky⁻¹ y を事前に計算して保持する。
-data GPPredData = GPPredData
-  { pdTrainX :: [Double]     -- ^ 訓練点 X
-  , pdAlpha  :: [Double]     -- ^ α = Ky⁻¹ y (長さ n)
-  , pdKyInv  :: [[Double]]   -- ^ Ky⁻¹ を行リストで表現 (n × n)
-  } deriving (Show)
-
--- | 訓練データから GPPredData を計算する。
-gpPredData :: GPModel -> [Double] -> [Double] -> GPPredData
-gpPredData model trainX trainY =
-  let ker    = gpKernel model
-      params = gpParams model
-      n      = length trainX
-      k      = buildKernelMatrix ker (gpKernelParams params) trainX trainX
-      jitter = max (gpNoiseVar params) 1e-6
-      ky     = addToDiag jitter k
-      -- SPD: solve via Cholesky rather than 'LA.inv'. Equivalent to
-      -- 'kyInv = Ky⁻¹' (used to project the JS-side prediction
-      -- formula); the explicit inverse is fine here because @n@ is
-      -- typically small for the interactive viewer and the inverse is
-      -- consumed downstream. Cholesky is more accurate than LU.
-      kyInv  = Chol.cholSolveJitter ky (LA.ident n)
-      alpha  = LA.toList (kyInv LA.#> LA.fromList trainY)
-  in GPPredData trainX alpha (map LA.toList (LA.toRows kyInv))
-
--- ---------------------------------------------------------------------------
--- Multi-input (multivariate X) API
---
--- The kernel of every supported family ('RBF', 'Matern52', 'Periodic') is a
--- function of the Euclidean distance @r = ‖x − x'‖@, so the multi-input
--- version reduces to building the @n × n@ pairwise distance matrix once
--- (via 'Hanalyze.Stat.KernelDist.pairwiseSqDist') and applying the kernel function
--- element-wise via 'LA.cmap'.
---
--- A single shared length scale @ℓ@ is used across every input dimension.
--- For axis-specific length scales, scale columns of @X@ by @1 / ℓ_d@
--- before calling these functions.
--- ---------------------------------------------------------------------------
-
--- | Multi-input GP posterior result. Mirrors 'GPResult' but stores the
--- @m × p@ test-point matrix instead of a 1D list.
-data GPResultMV = GPResultMV
-  { gpmvTestX :: LA.Matrix Double  -- ^ Test points (@m × p@).
-  , gpmvMean  :: LA.Vector Double  -- ^ Posterior mean (length @m@).
-  , gpmvVar   :: LA.Vector Double  -- ^ Posterior variance (length @m@).
-  , gpmvLower :: LA.Vector Double  -- ^ @mean − 2σ@.
-  , gpmvUpper :: LA.Vector Double  -- ^ @mean + 2σ@.
-  } deriving (Show)
-
--- | Add a scalar @c@ to the diagonal of a square matrix in one pass.
---
--- Replaces the @M + c·I@ pattern (which allocates a fresh @n × n@
--- identity scaled by @c@). With @runST@ + flat-index update, this
--- is one allocation of the result and an in-place fill — significant
--- in 'noiseKernelMV', which is on every log-marginal-likelihood
--- evaluation.
-addToDiag :: Double -> LA.Matrix Double -> LA.Matrix Double
-addToDiag c m =
-  let n    = LA.rows m
-      flat = LA.flatten m
-      out = runST $ do
-        v <- VSM.new (n * n)
-        let go i
-              | i >= n * n = pure ()
-              | otherwise  = do
-                  VSM.unsafeWrite v i (flat `VS.unsafeIndex` i)
-                  go (i + 1)
-        go 0
-        let goDiag i
-              | i >= n    = pure ()
-              | otherwise = do
-                  let !idx = i * n + i
-                  d <- VSM.unsafeRead v idx
-                  VSM.unsafeWrite v idx (d + c)
-                  goDiag (i + 1)
-        goDiag 0
-        VS.unsafeFreeze v
-  in LA.reshape n out
-
--- | Build the noise-augmented kernel matrix @K + jitter·I@ in a single
--- pass over the squared-distance matrix.
---
--- Replaces the previous @applyKernel d2 |> addToDiag jitter@ pipeline,
--- which allocated /two/ @n × n@ Storable vectors per evaluation: one
--- for the kernel-applied output, one for the diagonal-augmented copy.
--- This fused version emits a single @n²@ allocation and writes each
--- cell exactly once, branching on @i == j@ to fold the jitter into the
--- diagonal write. A @noiseKernelMVCached@ call profile fraction was
--- 35.3% of @optimizeGPMV@; halving its allocation footprint translates
--- to a measurable wall-time reduction in the LBFGS hot loop.
-mkNoiseKernelFromD2
-  :: Kernel -> KernelParams -> Double -> LA.Matrix Double -> LA.Matrix Double
-mkNoiseKernelFromD2 ker p jitter d2 =
-  let n     = LA.rows d2
-      flatD = LA.flatten d2
-      kFn   = kernelOfParams ker p
-      out   = runST $ do
-        v <- VSM.new (n * n)
-        let go i j
-              | i >= n    = pure ()
-              | j >= n    = go (i + 1) 0
-              | otherwise = do
-                  let !idx = i * n + j
-                      !s   = flatD `VS.unsafeIndex` idx
-                      !kij = kFn s
-                      !val = if i == j then kij + jitter else kij
-                  VSM.unsafeWrite v idx val
-                  go i (j + 1)
-        go 0 0
-        VS.unsafeFreeze v
-  in LA.reshape n out
-
--- | Multi-input @K + σ_n² I@. Uses the fused @mkNoiseKernelFromD2@ so
--- that the kernel evaluation and jitter-on-diagonal write happen in a
--- single @n²@ pass rather than two.
-noiseKernelMV :: Kernel -> GPParams -> LA.Matrix Double -> LA.Matrix Double
-noiseKernelMV ker p x =
-  let (xs, _, p') = ardScaleXY ker (gpKernelParams p) x x
-      d2          = KD.pairwiseSqDist xs
-      jitter      = max (gpNoiseVar p) 1e-6
-  in mkNoiseKernelFromD2 ker p' jitter d2
-
--- | Like 'noiseKernelMV' but reuses a pre-computed pairwise squared
--- distance matrix @D = pairwiseSqDist trainX@. Valid only when no ARD
--- scaling is applied (isotropic kernel) — the kernel is then a
--- function of @D@ alone, independent of length scale. Single-pass
--- (kernel + jitter fused).
-noiseKernelMVCached
-  :: Kernel -> GPParams -> LA.Matrix Double -> LA.Matrix Double
-noiseKernelMVCached ker p d2 =
-  let jitter = max (gpNoiseVar p) 1e-6
-  in mkNoiseKernelFromD2 ker (gpKernelParams p) jitter d2
-
--- | D-cached version of 'logMarginalLikelihoodMV' — accepts a
--- pre-computed @D = pairwiseSqDist trainX@ instead of recomputing it
--- each call. Used by 'optimizeGPMV' in the isotropic case where @D@
--- is independent of the optimization variables.
-logMarginalLikelihoodMVCached
-  :: LA.Matrix Double  -- ^ Pre-computed @D@ (@n × n@).
-  -> LA.Vector Double  -- ^ Training @y@ (length @n@).
-  -> Kernel -> GPParams -> Double
-logMarginalLikelihoodMVCached d2 y ker params =
-  let n   = LA.rows d2
-      ky  = noiseKernelMVCached ker params d2
-      mR = case Chol.cholFactor ky of
-             Just r  -> Just (r, ky)
-             Nothing ->
-               let kyJ = addToDiag 1e-4 ky
-               in case Chol.cholFactor kyJ of
-                    Just r  -> Just (r, kyJ)
-                    Nothing -> Nothing
-  in case mR of
-       Nothing -> -1e30
-       Just (r, _kyUsed) ->
-         let logDet  = 2 * VS.sum (VS.map log (LA.takeDiag r))
-             alpha   = LA.flatten
-                       (Chol.cholSolveWithFactor r (LA.asColumn y))
-             dataFit = LA.dot y alpha
-         in -0.5 * dataFit - 0.5 * logDet
-            - fromIntegral n / 2 * log (2 * pi)
-
--- | Multi-input log marginal likelihood.
-logMarginalLikelihoodMV
-  :: LA.Matrix Double  -- ^ Training @X@ (@n × p@).
-  -> LA.Vector Double  -- ^ Training @y@ (length @n@).
-  -> Kernel -> GPParams -> Double
-logMarginalLikelihoodMV trainX y ker params =
-  let n   = LA.rows trainX
-      ky  = noiseKernelMV ker params trainX
-      mR = case Chol.cholFactor ky of
-             Just r  -> Just (r, ky)
-             Nothing ->
-               let kyJ = addToDiag 1e-4 ky
-               in case Chol.cholFactor kyJ of
-                    Just r  -> Just (r, kyJ)
-                    Nothing -> Nothing
-  in case mR of
-       Nothing -> -1e30
-       Just (r, _kyUsed) ->
-         let logDet  = 2 * VS.sum (VS.map log (LA.takeDiag r))
-             alpha   = LA.flatten
-                       (Chol.cholSolveWithFactor r (LA.asColumn y))
-             dataFit = LA.dot y alpha
-         in -0.5 * dataFit - 0.5 * logDet
-            - fromIntegral n / 2 * log (2 * pi)
-
--- | Multi-input single-output GP posterior prediction.
-fitGPMV
-  :: GPModel
-  -> LA.Matrix Double    -- ^ Training @X@ (@n × p@).
-  -> LA.Vector Double    -- ^ Training @y@ (length @n@).
-  -> LA.Matrix Double    -- ^ Test @X_*@ (@m × p@).
-  -> GPResultMV
-fitGPMV model trainX y testX =
-  let yMat               = LA.asColumn y
-      (meanMat, varVec)  = fitGPMVMulti model trainX yMat testX
-      mu                 = LA.flatten (meanMat LA.¿ [0])
-      stdVec             = LA.cmap sqrt varVec
-  in GPResultMV
-       { gpmvTestX = testX
-       , gpmvMean  = mu
-       , gpmvVar   = varVec
-       , gpmvLower = mu - LA.scale 2 stdVec
-       , gpmvUpper = mu + LA.scale 2 stdVec
-       }
-
--- | Multi-input multi-output GP posterior prediction. @Y@ has shape
--- @n × q@ (one column per output task). The variance does not depend on
--- @y@, so a single length-@m@ vector is shared by every output.
-fitGPMVMulti
-  :: GPModel
-  -> LA.Matrix Double    -- ^ Training @X@ (@n × p@).
-  -> LA.Matrix Double    -- ^ Training @Y@ (@n × q@).
-  -> LA.Matrix Double    -- ^ Test @X_*@ (@m × p@).
-  -> (LA.Matrix Double, LA.Vector Double)
-fitGPMVMulti model trainX trainY testX =
-  let ker    = gpKernel model
-      params = gpParams model
-      ky     = noiseKernelMV ker params trainX
-      kStar  = buildKernelMatrixMV ker (gpKernelParams params) testX trainX -- m × n
-      -- α = Ky⁻¹ Y via SPD Cholesky (reused for v below by passing both
-      -- right-hand sides through the same factorization).
-      rhs    = trainY LA.||| LA.tr kStar           -- n × (q + m)
-      sol    = Chol.cholSolveJitter ky rhs         -- n × (q + m)
-      q      = LA.cols trainY
-      alpha  = sol LA.?? (LA.All, LA.Take q)       -- n × q
-      v      = sol LA.?? (LA.All, LA.Drop q)       -- n × m
-      meanMt = kStar LA.<> alpha                   -- m × q
-      sf     = gpSignalVar params
-      diagKss = LA.konst sf (LA.rows testX)         -- k(x*, x*) = σ_f²
-      -- F1: diagonal of (kStar · v) without forming the m×m product.
-      -- 'KD.diagAB' = element-wise (kStar ⊙ vᵀ) · ones.
-      varVec  = LA.cmap (max 0) (diagKss - KD.diagAB kStar v)
-      -- Tested split-solve (alpha and v separately via cholFactor +
-      -- cholSolveWithFactor, avoiding the concat allocation) but the
-      -- saving is dwarfed by the @O(n² · (q+m))@ triangular-solve
-      -- work itself. Keep the simpler concatenated form.
-  in (meanMt, varVec)
-
--- | Multi-input GP hyperparameter optimization. Mirrors 'optimizeGP' but
--- accepts a multi-input training matrix.
---
--- When @gpLengthScales p0 = Just v@, optimizes per-dim length scales
--- (ARD): the parameter vector becomes
--- @[log ℓ_1, …, log ℓ_p, log σ_f², log σ_n²]@. Otherwise optimises the
--- isotropic @[log ℓ, log σ_f², log σ_n²]@.
-optimizeGPMV
-  :: Kernel -> LA.Matrix Double -> LA.Vector Double -> GPParams -> GPParams
-optimizeGPMV ker trainX y p0 =
-  optimizeGPMVCached ker Nothing trainX y p0
-
--- | Like 'optimizeGPMV' but accepts a /pre-computed/ pairwise squared
--- distance matrix. Used by 'Hanalyze.Model.MultiGP' to share @D = pairwiseSqDist
--- trainX@ across all @q@ outputs (the same @trainX@ is used for every
--- output, so re-computing @D@ inside each per-output optimisation is
--- pure waste). For ARD the cache is ignored (the kernel depends on
--- per-feature length scales and @D@ varies with the optimisation
--- variables).
-optimizeGPMVCached
-  :: Kernel
-  -> Maybe (LA.Matrix Double)   -- ^ Pre-computed @D = pairwiseSqDist trainX@.
-  -> LA.Matrix Double
-  -> LA.Vector Double
-  -> GPParams
-  -> GPParams
-optimizeGPMVCached ker mPreD trainX y p0
-  -- Analytic-gradient fast path for the isotropic non-ARD case under
-  -- the RBF kernel. Replaces the central-difference numeric gradient
-  -- (which costs 6 × the Cholesky-based log-marginal-likelihood
-  -- evaluation per LBFGS step) with a closed-form formula that re-uses
-  -- a single explicit @Ky⁻¹@ for all three parameters. See
-  -- 'optimizeRBFAnalytic'.
-  | ker == RBF && not (isARDOf p0 (LA.cols trainX)) =
-      optimizeRBFAnalytic mPreD trainX y p0
-  | otherwise =
-  let cfg  = LBFGS.defaultLBFGSConfig
-               { LBFGS.lbDir   = OC.Maximize
-               , LBFGS.lbStop  = OC.defaultStopCriteria
-                                   { OC.stMaxIter = 200, OC.stTolFun = 1e-8 }
-               }
-      u0v    = LA.fromList initU
-      -- Vector-native objective: takes the LBFGS state Vector directly.
-      -- Saves the list conversion that 'runLBFGSNumeric' / 'runLBFGSWith'
-      -- do on every objective and gradient call.
-      objV uv = obj (LA.toList uv)
-      -- Central-difference gradient on the Vector representation. We
-      -- experimented with forward differences (half the evaluations
-      -- per gradient) but L-BFGS needed more iterations to converge
-      -- under the looser O(h) error, giving a net wall-time regression.
-      h    = 1e-5 :: Double
-      gradV uv =
-        let n = LA.size uv
-        in LA.fromList
-             [ let plus  = uv VS.// [(i, uv VS.! i + h)]
-                   minus = uv VS.// [(i, uv VS.! i - h)]
-               in (objV plus - objV minus) / (2 * h)
-             | i <- [0 .. n - 1] ]
-      result = unsafePerformIO $ LBFGS.runLBFGSWithV cfg objV gradV u0v
-      uOpt   = OC.orBest result
-  in toParams uOpt
-  where
-    p      = LA.cols trainX
-    isARD  = case gpLengthScales p0 of
-               Just v | LA.size v == p && p > 0 -> True
-               _                                -> False
-    -- Pre-compute the pairwise squared distance matrix for the
-    -- isotropic case. The kernel of every supported family is a
-    -- function of @D@ alone (length scale enters via @applyKernel@),
-    -- so the LBFGS log-marginal-likelihood loop reuses @D@ instead of
-    -- recomputing 'pairwiseSqDist' on every evaluation. Profile
-    -- (see bench/results/) showed 'pairwiseSqDist' was 26.8% of
-    -- 'optimizeGPMV' wall time before this cache.
-    -- For ARD, the per-dim length scales rescale columns of @X@, so
-    -- @D@ depends on the optimization variables and cannot be cached.
-    cachedD :: Maybe (LA.Matrix Double)
-    cachedD
-      | isARD     = Nothing
-      | otherwise = case mPreD of
-                      Just d  -> Just d                         -- caller-supplied
-                      Nothing -> Just (KD.pairwiseSqDist trainX) -- compute now
-    initU
-      | isARD     = case gpLengthScales p0 of
-                      Just v ->
-                        let ls = LA.toList v
-                        in map log ls
-                           ++ [log (gpSignalVar p0), log (gpNoiseVar p0)]
-                      Nothing ->
-                        -- Cannot happen: isARD already requires Just.
-                        [ log (gpLengthScale p0)
-                        , log (gpSignalVar  p0)
-                        , log (gpNoiseVar   p0) ]
-      | otherwise = [ log (gpLengthScale p0)
-                    , log (gpSignalVar  p0)
-                    , log (gpNoiseVar   p0) ]
-    toParams u
-      | isARD     =
-          let lsV = LA.fromList (map exp (take p u))
-          in p0
-               { gpLengthScales = Just lsV
-               , gpSignalVar    = exp (u !! p)
-               , gpNoiseVar     = exp (u !! (p + 1))
-               }
-      | otherwise = p0
-          { gpLengthScale = exp (u !! 0)
-          , gpSignalVar   = exp (u !! 1)
-          , gpNoiseVar    = exp (u !! 2)
-          }
-    -- For ARD, add a weak log-normal prior on each ℓ_d centred at the
-    -- initial value (Gaussian in log-space, σ_prior = 1.5 ≈ ratio 4.5).
-    -- Without it, log marginal likelihood with only 30 BO points and
-    -- many ℓ_d's tends to drive ℓ_d to extreme values (over-fit). The
-    -- prior is informative enough to keep ℓ_d within ~one order of
-    -- magnitude of the init while still letting individual dims relax.
-    obj u
-      | isARD     =
-          case gpLengthScales p0 of
-            Just v0 ->
-              let lml   = logMarginalLikelihoodMV trainX y ker (toParams u)
-                  logL0 = map log (LA.toList v0)
-                  sig2  = 1.5 * 1.5
-                  prior = sum [ -0.5 * (l - l0) ^ (2 :: Int) / sig2
-                              | (l, l0) <- zip (take p u) logL0 ]
-              in lml + prior
-            Nothing ->
-              -- Cannot happen by isARD construction; fall back to
-              -- the un-prior-ed ARD likelihood.
-              logMarginalLikelihoodMV trainX y ker (toParams u)
-      | otherwise =
-          case cachedD of
-            Just d2 -> logMarginalLikelihoodMVCached d2 y ker (toParams u)
-            Nothing -> logMarginalLikelihoodMV trainX y ker (toParams u)
-
--- | Whether the given 'GPParams' / input dimension imply ARD.
-isARDOf :: GPParams -> Int -> Bool
-isARDOf p0 p = case gpLengthScales p0 of
-  Just v | LA.size v == p && p > 0 -> True
-  _                                -> False
-
--- | Analytic-gradient L-BFGS for the isotropic RBF GP marginal
--- likelihood. Replaces the central-difference numeric gradient (6 extra
--- evaluations per LBFGS step) with a closed-form formula that re-uses
--- a single explicit @Ky⁻¹@ across all three parameters
--- @[log ℓ, log σ_f², log σ_n²]@.
---
--- For RBF, @∂Ky/∂(log θ_k)@ is:
---
--- *   @log ℓ@:    @K ⊙ (D / ℓ²)@
--- *   @log σ_f²@: @K@         (linear in @σ_f²@)
--- *   @log σ_n²@: @σ_n² · I@
---
--- and the gradient contribution is
--- @½ tr((α αᵀ − Ky⁻¹) ∂Ky/∂(log θ_k))@. We form @Ky⁻¹@ once per LBFGS
--- step (@O(n³)@ via @cholSolveJitter ky I@) and assemble each
--- coordinate of the gradient via element-wise sums (@O(n²)@). Total
--- work per step: roughly @n³/2 + O(n²)@ vs the numeric path's
--- @≈ n³ + O(n²)@, plus L-BFGS converges in fewer iterations when fed
--- exact gradients.
-optimizeRBFAnalytic
-  :: Maybe (LA.Matrix Double) -> LA.Matrix Double -> LA.Vector Double
-  -> GPParams -> GPParams
-optimizeRBFAnalytic mPreD trainX y p0 =
-  let n     = LA.rows trainX
-      d2    = case mPreD of
-                Just d  -> d
-                Nothing -> KD.pairwiseSqDist trainX
-      cfg   = LBFGS.defaultLBFGSConfig
-                { LBFGS.lbDir   = OC.Maximize
-                , LBFGS.lbStop  = OC.defaultStopCriteria
-                                    { OC.stMaxIter = 200
-                                    , OC.stTolFun  = 1e-8 }
-                }
-      u0v   = LA.fromList
-                [ log (gpLengthScale p0)
-                , log (gpSignalVar  p0)
-                , log (gpNoiseVar   p0) ]
-
-      -- Build the kernel matrix and noise-augmented matrix from
-      -- params (re-using the precomputed @D@).
-      buildK uv =
-        let !ll  = exp (uv VS.! 0)        -- length scale ℓ
-            !sf2 = exp (uv VS.! 1)        -- σ_f²
-            !sn2 = exp (uv VS.! 2)        -- σ_n²
-            !inv2L2 = 1 / (2 * ll * ll)
-            !kMat = LA.cmap (\s -> sf2 * exp (- s * inv2L2)) d2
-            !ky   = addToDiag sn2 kMat
-        in (ll, sf2, sn2, kMat, ky)
-
-      -- Objective only (used by L-BFGS line search).
-      objV uv =
-        let (_, _, _, _, ky) = buildK uv
-        in case Chol.cholFactor ky of
-             Nothing -> -1e30
-             Just r  ->
-               let logDet = 2 * VS.sum (VS.map log (LA.takeDiag r))
-                   alpha  = LA.flatten
-                              (Chol.cholSolveWithFactor r (LA.asColumn y))
-                   dataFit = LA.dot y alpha
-               in -0.5 * dataFit - 0.5 * logDet
-                  - fromIntegral n / 2 * log (2 * pi)
-
-      -- Analytic gradient.
-      gradV uv =
-        let (ll, _sf2, sn2, kMat, ky) = buildK uv
-        in case Chol.cholFactor ky of
-             Nothing -> LA.fromList [0, 0, 0]   -- bail out at singular Ky
-             Just r  ->
-               let alpha  = LA.flatten
-                              (Chol.cholSolveWithFactor r (LA.asColumn y))
-                   -- Explicit @Ky⁻¹@ (n × n). 'cholSolveWithFactor'
-                   -- against the n×n identity is an @O(n³)@ pair of
-                   -- triangular solves but only happens once per LBFGS
-                   -- gradient call.
-                   kyInv  = Chol.cholSolveWithFactor r (LA.ident n)
-                   -- Q = α αᵀ − Ky⁻¹. We don't materialise this
-                   -- separately; instead each gradient component is
-                   -- computed as @α^T V α − tr(Ky⁻¹ V)@ inline.
-                   --
-                   -- ∂Ky/∂(log ℓ) = K ⊙ (D / ℓ²)
-                   !invL2 = 1 / (ll * ll)
-                   !vL    = LA.scale invL2 (kMat * d2)
-                   !aT_vL = LA.dot alpha (vL LA.#> alpha)
-                   !tr_KyInv_vL = LA.sumElements (kyInv * vL)
-                   !gLogL = 0.5 * (aT_vL - tr_KyInv_vL)
-                   -- ∂Ky/∂(log σ_f²) = K
-                   !aT_K   = LA.dot alpha (kMat LA.#> alpha)
-                   !tr_KyInv_K = LA.sumElements (kyInv * kMat)
-                   !gLogSf = 0.5 * (aT_K - tr_KyInv_K)
-                   -- ∂Ky/∂(log σ_n²) = σ_n² I
-                   !aT_a   = LA.dot alpha alpha
-                   !tr_KyInv = LA.sumElements (LA.takeDiag kyInv)
-                   !gLogSn = 0.5 * sn2 * (aT_a - tr_KyInv)
-               in LA.fromList [gLogL, gLogSf, gLogSn]
-
-      result = unsafePerformIO $ LBFGS.runLBFGSWithV cfg objV gradV u0v
-      uOpt   = OC.orBest result
-  in p0
-       { gpLengthScale = exp (uOpt !! 0)
-       , gpSignalVar   = exp (uOpt !! 1)
-       , gpNoiseVar    = exp (uOpt !! 2)
-       }
diff --git a/src/Hanalyze/Model/GPRobust.hs b/src/Hanalyze/Model/GPRobust.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/GPRobust.hs
+++ /dev/null
@@ -1,365 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.GPRobust
--- Description : ロバストガウス過程 (重尾観測尤度: Student-t / Cauchy)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Robust GP (heavy-tailed observation likelihoods).
---
--- A closed-form Gaussian-likelihood GP is sensitive to outliers. This
--- module replaces the observation likelihood with Student-t or Cauchy and
--- iterates an IRLS-style scheme (a stable variant of variational EM /
--- Laplace) to obtain a MAP estimate.
---
--- Algorithm:
---
---   1. @f ← 0@ (GP prior mean).
---   2. Iterate until convergence:
---      a. Residual @r = y − f@.
---      b. Compute the per-observation weight:
---         * Student-t @(ν, σ)@:  @w_i = (ν + 1) / (ν + (r_i/σ)²)@.
---         * Cauchy @(γ)@:       @w_i = 2 / (1 + (r_i/γ)²)@.
---    c. 各点の有効ノイズ分散 σ²/w_i (heteroscedastic)
---    d. f ← K (K + σ² W⁻¹)⁻¹ y
--- 3. 予測点 x* で:
---    mean = k_*ᵀ (K + σ² W⁻¹)⁻¹ y
---    var  = k(x*,x*) − k_*ᵀ (K + σ² W⁻¹)⁻¹ k_*
---
--- カーネル関連 ('Kernel', 'GPParams', 'kernelFn') は 'Hanalyze.Model.GP' を再利用。
-module Hanalyze.Model.GPRobust
-  ( -- * 観測尤度
-    RobustLikelihood (..)
-  , -- * フィット結果と推論
-    RobustGPFit (..)
-  , fitGPRobust
-  , predictGPRobust
-    -- * Multi-output (primary API)
-  , RobustGPFitMulti (..)
-  , fitGPRobustMulti
-  , predictGPRobustMulti
-    -- * Multi-input (primary API; X is @n × p@, Y is @n × q@)
-  , RobustGPFitMV (..)
-  , fitGPRobustMV
-  , predictGPRobustMV
-  , RobustGPFitMVMulti (..)
-  , fitGPRobustMVMulti
-  , predictGPRobustMVMulti
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import qualified Hanalyze.Stat.Cholesky        as Chol
-import qualified Hanalyze.Stat.KernelDist      as KD
-import Hanalyze.Model.GP
-  ( Kernel
-  , GPParams (..)
-  , gpKernelParams
-  , kernelFn
-  , buildKernelMatrix
-  , buildKernelMatrixMV
-  )
-
--- ---------------------------------------------------------------------------
--- 観測尤度
--- ---------------------------------------------------------------------------
-
--- | Heavy-tailed observation likelihood.
-data RobustLikelihood
-  = RGaussian Double            -- ^ Gaussian @(σ_n)@ — equivalent to a
-                                --   standard GP (sanity-check baseline).
-  | RStudentT Double Double     -- ^ Student-t @(df=ν, scale=σ)@; smaller
-                                --   @ν@ means heavier tails.
-  | RCauchy   Double            -- ^ Cauchy @(scale=γ)@, equivalent to
-                                --   @StudentT(1, γ)@.
-  deriving (Show, Eq)
-
--- | IRLS weight @w(r)@ for residual @r@. The effective noise variance is
--- @σ_eff² / w_i@ at each step.
-likelihoodWeight :: RobustLikelihood -> Double -> Double
-likelihoodWeight (RGaussian _)        _ = 1.0
-likelihoodWeight (RStudentT nu sigma) r =
-  let z = r / sigma
-  in (nu + 1) / (nu + z * z)
-likelihoodWeight (RCauchy gamma) r =
-  let z = r / gamma
-  in 2 / (1 + z * z)
-
--- | Reference variance @σ_eff²@ used to scale the IRLS weights.
-likelihoodScale2 :: RobustLikelihood -> Double
-likelihoodScale2 (RGaussian s)      = s * s
-likelihoodScale2 (RStudentT _ s)    = s * s
-likelihoodScale2 (RCauchy g)        = g * g
-
--- ---------------------------------------------------------------------------
--- フィット結果
--- ---------------------------------------------------------------------------
-
--- | Robust GP fit result.
-data RobustGPFit = RobustGPFit
-  { rgpKernel  :: Kernel
-  , rgpParams  :: GPParams
-  , rgpLik     :: RobustLikelihood
-  , rgpTrainX  :: [Double]              -- ^ Training inputs.
-  , rgpTrainY  :: [Double]              -- ^ Training targets.
-  , rgpAlpha   :: LA.Vector Double      -- ^ @α = (K + σ² W⁻¹)⁻¹ y@.
-  , rgpKyInv   :: LA.Matrix Double      -- ^ @(K + σ² W⁻¹)⁻¹@ at convergence.
-  , rgpWeights :: LA.Vector Double      -- ^ IRLS weights at convergence.
-  , rgpIters   :: Int                   -- ^ Number of IRLS iterations executed.
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- フィット
--- ---------------------------------------------------------------------------
-
--- | Compute the MAP of a robust GP via IRLS iteration. At most 50
--- iterations; convergence when @‖f_new − f‖∞ < 10⁻⁶@.
-fitGPRobust
-  :: Kernel
-  -> GPParams                    -- ^ Kernel hyperparameters (held fixed —
-                                 --   optimize them separately).
-  -> RobustLikelihood
-  -> [Double]                    -- ^ Training @X@.
-  -> [Double]                    -- ^ Training @Y@.
-  -> RobustGPFit
-fitGPRobust ker params lik trainX trainY =
-  let n         = length trainX
-      kMatrix   = buildKernelMatrix ker (gpKernelParams params) trainX trainX  -- K (n×n)
-      yV        = LA.fromList trainY
-      sigEff2   = likelihoodScale2 lik
-      -- 1 反復: f, w を更新
-      step (f, w, _iter) =
-        let r          = LA.toList (yV - f)
-            wNew'      = [ max 1e-8 (likelihoodWeight lik ri)
-                         | ri <- r ]
-            wNewVec    = LA.fromList wNew'
-            wInvDiag   = LA.diag (LA.fromList [ sigEff2 / wi | wi <- wNew' ])
-            ky         = kMatrix `LA.add` wInvDiag
-            -- α = (K + σ²W⁻¹)⁻¹ y via SPD Cholesky (replaces inv + matvec).
-            alpha      = LA.flatten
-                          (Chol.cholSolveJitter ky (LA.asColumn yV))
-            fNew       = kMatrix LA.#> alpha
-            delta      = LA.maxElement (LA.cmap abs (fNew - f))
-        in (fNew, wNewVec, delta)
-
-      maxIters     = 50
-      tol          = 1e-6 :: Double
-
-      loop f w iter
-        | iter >= maxIters = (f, w, iter)
-        | otherwise =
-            let (fNew, wNew, delta) = step (f, w, iter)
-            in if delta < tol
-                 then (fNew, wNew, iter + 1)
-                 else loop fNew wNew (iter + 1)
-
-      f0     = LA.fromList (replicate n 0.0)
-      w0     = LA.fromList (replicate n 1.0)
-      (_fOpt, wOpt, iters) = loop f0 w0 0
-
-      -- 最終 K_y, α, K_y⁻¹ を再計算 (収束後の重みで)。
-      -- kyInv は予測時の分散計算で必要なため陽に保持する。
-      wInvDiag' = LA.diag (LA.cmap (\wi -> sigEff2 / max 1e-8 wi) wOpt)
-      ky'       = kMatrix `LA.add` wInvDiag'
-      kyInv'    = Chol.cholSolveJitter ky' (LA.ident n)
-      alpha'    = LA.flatten
-                  (Chol.cholSolveJitter ky' (LA.asColumn yV))
-  in RobustGPFit
-       { rgpKernel  = ker
-       , rgpParams  = params
-       , rgpLik     = lik
-       , rgpTrainX  = trainX
-       , rgpTrainY  = trainY
-       , rgpAlpha   = alpha'
-       , rgpKyInv   = kyInv'
-       , rgpWeights = wOpt
-       , rgpIters   = iters
-       }
-
--- ---------------------------------------------------------------------------
--- 予測
--- ---------------------------------------------------------------------------
-
--- | Predictive mean and variance of @f@ at the given test points.
--- mean = k_*ᵀ α, var = k(x*,x*) − k_*ᵀ K_y⁻¹ k_*
-predictGPRobust :: RobustGPFit -> [Double] -> [(Double, Double)]
-predictGPRobust fit testX =
-  let ker     = rgpKernel fit
-      params  = rgpParams fit
-      trainX  = rgpTrainX fit
-      kStar   = buildKernelMatrix ker (gpKernelParams params) testX trainX     -- (m, n)
-      means   = LA.toList (kStar LA.#> rgpAlpha fit)
-      kyInv   = rgpKyInv fit
-      diagKss = [ kernelFn ker (gpKernelParams params) x x | x <- testX ]
-      ws      = kStar LA.<> kyInv                              -- (m, n)
-      -- F1: vectorise per-row dots.
-      rowDots = LA.toList (KD.rowDotsAB kStar ws)
-      varList = zipWith (\d kw -> max 0 (d - kw)) diagKss rowDots
-  in zip means varList
-
--- ---------------------------------------------------------------------------
--- 多出力 (列ごと IRLS、カーネル行列を共有)
--- ---------------------------------------------------------------------------
-
--- | 多出力ロバスト GP の結果。q 出力ぶんの 'RobustGPFit' を保持し、
--- カーネル / ハイパラ / 尤度は共通。
-data RobustGPFitMulti = RobustGPFitMulti
-  { rgmKernel :: Kernel
-  , rgmParams :: GPParams
-  , rgmLik    :: RobustLikelihood
-  , rgmTrainX :: [Double]
-  , rgmFits   :: [RobustGPFit]   -- ^ 列ごとの単出力 fit
-  } deriving (Show)
-
--- | 多出力ロバスト GP fit。Y は n × q、各列ごとに IRLS (重みは出力依存)。
-fitGPRobustMulti
-  :: Kernel
-  -> GPParams
-  -> RobustLikelihood
-  -> [Double]            -- ^ 訓練 X
-  -> LA.Matrix Double    -- ^ Y (n × q)
-  -> RobustGPFitMulti
-fitGPRobustMulti ker params lik trainX yMat =
-  let q     = LA.cols yMat
-      yCols = [ LA.toList (LA.flatten (yMat LA.¿ [j])) | j <- [0 .. q - 1] ]
-      fits  = [ fitGPRobust ker params lik trainX y | y <- yCols ]
-  in RobustGPFitMulti ker params lik trainX fits
-
--- | 多出力ロバスト GP 予測。戻り値: (mean 行列 m × q, 列ごとの分散リスト)。
-predictGPRobustMulti :: RobustGPFitMulti -> [Double]
-                     -> (LA.Matrix Double, [[Double]])
-predictGPRobustMulti mf testX =
-  let preds = [ predictGPRobust f testX | f <- rgmFits mf ]
-      meansCols = map (map fst) preds
-      varsCols  = map (map snd) preds
-      meansMat  = LA.fromColumns [ LA.fromList col | col <- meansCols ]
-  in (meansMat, varsCols)
-
--- ---------------------------------------------------------------------------
--- Multi-input (multivariate X) API
--- ---------------------------------------------------------------------------
-
--- | Robust GP fit with multivariate input. Mirrors 'RobustGPFit' but
--- stores @X@ as an @n × p@ matrix and @y@ as a 'LA.Vector'.
-data RobustGPFitMV = RobustGPFitMV
-  { rgpmvKernel  :: Kernel
-  , rgpmvParams  :: GPParams
-  , rgpmvLik     :: RobustLikelihood
-  , rgpmvTrainX  :: LA.Matrix Double      -- ^ @n × p@.
-  , rgpmvTrainY  :: LA.Vector Double      -- ^ length @n@.
-  , rgpmvAlpha   :: LA.Vector Double
-  , rgpmvKyInv   :: LA.Matrix Double
-  , rgpmvWeights :: LA.Vector Double
-  , rgpmvIters   :: Int
-  } deriving (Show)
-
--- | Compute the MAP of a multi-input robust GP via the same IRLS scheme
--- as 'fitGPRobust'. @X@ is @n × p@; @y@ has length @n@.
-fitGPRobustMV
-  :: Kernel
-  -> GPParams
-  -> RobustLikelihood
-  -> LA.Matrix Double          -- ^ Training @X@ (@n × p@).
-  -> LA.Vector Double          -- ^ Training @y@ (length @n@).
-  -> RobustGPFitMV
-fitGPRobustMV ker params lik trainX yV =
-  let n         = LA.rows trainX
-      kMatrix   = buildKernelMatrixMV ker (gpKernelParams params) trainX trainX
-      sigEff2   = likelihoodScale2 lik
-      step (f, w, _iter) =
-        let r          = LA.toList (yV - f)
-            wNew'      = [ max 1e-8 (likelihoodWeight lik ri) | ri <- r ]
-            wNewVec    = LA.fromList wNew'
-            wInvDiag   = LA.diag (LA.fromList [ sigEff2 / wi | wi <- wNew' ])
-            ky         = kMatrix `LA.add` wInvDiag
-            -- α = (K + σ²W⁻¹)⁻¹ y via SPD Cholesky.
-            alpha      = LA.flatten
-                          (Chol.cholSolveJitter ky (LA.asColumn yV))
-            fNew       = kMatrix LA.#> alpha
-            delta      = LA.maxElement (LA.cmap abs (fNew - f))
-        in (fNew, wNewVec, delta)
-
-      maxIters = 50
-      tol      = 1e-6 :: Double
-
-      loop f w iter
-        | iter >= maxIters = (f, w, iter)
-        | otherwise =
-            let (fNew, wNew, delta) = step (f, w, iter)
-            in if delta < tol
-                 then (fNew, wNew, iter + 1)
-                 else loop fNew wNew (iter + 1)
-
-      f0 = LA.fromList (replicate n 0.0)
-      w0 = LA.fromList (replicate n 1.0)
-      (_fOpt, wOpt, iters) = loop f0 w0 0
-
-      wInvDiag' = LA.diag (LA.cmap (\wi -> sigEff2 / max 1e-8 wi) wOpt)
-      ky'       = kMatrix `LA.add` wInvDiag'
-      kyInv'    = Chol.cholSolveJitter ky' (LA.ident n)
-      alpha'    = LA.flatten
-                  (Chol.cholSolveJitter ky' (LA.asColumn yV))
-  in RobustGPFitMV
-       { rgpmvKernel  = ker
-       , rgpmvParams  = params
-       , rgpmvLik     = lik
-       , rgpmvTrainX  = trainX
-       , rgpmvTrainY  = yV
-       , rgpmvAlpha   = alpha'
-       , rgpmvKyInv   = kyInv'
-       , rgpmvWeights = wOpt
-       , rgpmvIters   = iters
-       }
-
--- | Predictive mean and variance at multi-input test points (@m × p@).
-predictGPRobustMV
-  :: RobustGPFitMV -> LA.Matrix Double
-  -> (LA.Vector Double, LA.Vector Double)
-predictGPRobustMV fit testX =
-  let ker     = rgpmvKernel fit
-      params  = rgpmvParams fit
-      trainX  = rgpmvTrainX fit
-      kStar   = buildKernelMatrixMV ker (gpKernelParams params) testX trainX  -- m × n
-      means   = kStar LA.#> rgpmvAlpha fit
-      kyInv   = rgpmvKyInv fit
-      sf      = gpSignalVar params
-      diagKss = LA.konst sf (LA.rows testX)
-      ws      = kStar LA.<> kyInv                            -- m × n
-      -- F1: vectorise per-row dots.
-      vars    = LA.cmap (max 0) (diagKss - KD.rowDotsAB kStar ws)
-  in (means, vars)
-
--- | Multi-input multi-output robust GP. Per-column IRLS (weights are
--- output-specific), but the kernel matrix @K@ is shared.
-data RobustGPFitMVMulti = RobustGPFitMVMulti
-  { rgmvKernel :: Kernel
-  , rgmvParams :: GPParams
-  , rgmvLik    :: RobustLikelihood
-  , rgmvTrainX :: LA.Matrix Double
-  , rgmvFits   :: [RobustGPFitMV]
-  } deriving (Show)
-
--- | Fit a multi-input multi-output robust GP. @Y@ has shape @n × q@.
-fitGPRobustMVMulti
-  :: Kernel
-  -> GPParams
-  -> RobustLikelihood
-  -> LA.Matrix Double          -- ^ Training @X@ (@n × p@).
-  -> LA.Matrix Double          -- ^ Training @Y@ (@n × q@).
-  -> RobustGPFitMVMulti
-fitGPRobustMVMulti ker params lik trainX yMat =
-  let q     = LA.cols yMat
-      cols  = [ LA.flatten (yMat LA.¿ [j]) | j <- [0 .. q - 1] ]
-      fits  = [ fitGPRobustMV ker params lik trainX y | y <- cols ]
-  in RobustGPFitMVMulti ker params lik trainX fits
-
--- | Multi-input multi-output robust GP prediction. Returns the @m × q@
--- mean matrix and a per-column variance vector.
-predictGPRobustMVMulti
-  :: RobustGPFitMVMulti -> LA.Matrix Double
-  -> (LA.Matrix Double, [LA.Vector Double])
-predictGPRobustMVMulti mf testX =
-  let preds   = [ predictGPRobustMV f testX | f <- rgmvFits mf ]
-      meanCs  = map fst preds
-      varCs   = map snd preds
-      meanMat = LA.fromColumns meanCs
-  in (meanMat, varCs)
diff --git a/src/Hanalyze/Model/GradientBoosting.hs b/src/Hanalyze/Model/GradientBoosting.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/GradientBoosting.hs
+++ /dev/null
@@ -1,176 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Model.GradientBoosting
--- Description : 勾配ブースティング (Gradient Boosting Machine、 回帰 + 二値分類)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Gradient Boosting Machine (回帰 + 二値分類).
---
--- 弱学習器は 'Hanalyze.Model.RandomForest' の回帰木 ('RF.Tree' /
--- 'RF.buildTreeV') を流用 (bootstrap 無 + mtry = d で full-data /
--- 全特徴を使う通常の GBM 木に縮約)。
---
--- @
--- import qualified Hanalyze.Model.GradientBoosting as GB
--- gb <- GB.fitGBRegressor GB.defaultGBM x y
--- let yhat = GB.predictGBR gb x
--- @
---
--- 損失:
---
---   * 回帰: 二乗誤差 (negative gradient = 残差)
---   * 分類 (binary): log-loss (negative gradient = y - sigmoid(F))
-module Hanalyze.Model.GradientBoosting
-  ( GBConfig (..)
-  , defaultGBM
-  , GBRegressor (..)
-  , GBClassifier (..)
-  , fitGBRegressor
-  , fitGBClassifier
-  , predictGBR
-  , predictGBRRow
-  , predictGBC
-  , predictGBCProbs
-  ) where
-
-import qualified Data.Vector.Unboxed   as VU
-import qualified Numeric.LinearAlgebra as LA
-import qualified Hanalyze.Model.RandomForest as RF
-
--- ---------------------------------------------------------------------------
--- Config
--- ---------------------------------------------------------------------------
-
--- | GBM 設定。
-data GBConfig = GBConfig
-  { gbNRounds    :: !Int     -- ^ ブースティング回数 M。
-  , gbMaxDepth   :: !Int     -- ^ 各弱学習器の最大深さ (典型 3-5)。
-  , gbMinSamples :: !Int     -- ^ 葉最小サンプル数。
-  , gbLearnRate  :: !Double  -- ^ 学習率 η (typ 0.1)。
-  } deriving (Show)
-
-defaultGBM :: GBConfig
-defaultGBM = GBConfig
-  { gbNRounds    = 100
-  , gbMaxDepth   = 3
-  , gbMinSamples = 2
-  , gbLearnRate  = 0.1
-  }
-
--- | 弱学習器設定 (full-data / 全特徴利用、 木の深さは gbMaxDepth)。
-weakRFCfg :: Int -> GBConfig -> RF.RFConfig
-weakRFCfg d cfg = RF.RFConfig
-  { RF.rfTrees      = 1
-  , RF.rfMaxDepth   = gbMaxDepth cfg
-  , RF.rfMinSamples = gbMinSamples cfg
-  , RF.rfMtry       = Just d
-  , RF.rfBootstrap  = False
-  }
-
--- ---------------------------------------------------------------------------
--- Regressor
--- ---------------------------------------------------------------------------
-
--- | 回帰 GBM。 予測 = init + η · Σ tree_m(x).
-data GBRegressor = GBRegressor
-  { gbrInit  :: !Double
-  , gbrTrees :: ![RF.Tree]
-  , gbrLR    :: !Double
-  } deriving (Show)
-
-fitGBRegressor :: GBConfig
-               -> LA.Matrix Double   -- ^ X (n × d)
-               -> VU.Vector Double   -- ^ y (n)
-               -> GBRegressor
-fitGBRegressor cfg x y =
-  let !n     = VU.length y
-      !d     = LA.cols x
-      !cfgW  = weakRFCfg d cfg
-      !lr    = gbLearnRate cfg
-      !f0    = VU.sum y / fromIntegral n
-      !preds0 = VU.replicate n f0
-      idx    = VU.enumFromN 0 n
-
-      step (!preds, !trees) _ =
-        let !res = VU.zipWith (-) y preds
-            !t   = RF.buildTreeV cfgW x res idx 0
-            !upd = VU.map (\i -> lr * RF.predictTree t (rowList x i))
-                          (VU.enumFromN 0 n)
-            !preds' = VU.zipWith (+) preds upd
-        in (preds', t : trees)
-
-      (_, treesRev) = foldl step (preds0, []) [1 .. gbNRounds cfg]
-  in GBRegressor f0 (reverse treesRev) lr
-
--- | 1 行を [Double] 化 (predictTree のための一時変換)。
-rowList :: LA.Matrix Double -> Int -> [Double]
-rowList x i = LA.toList (LA.flatten (x LA.? [i]))
-
--- | 1 サンプルの予測。
-predictGBRRow :: GBRegressor -> [Double] -> Double
-predictGBRRow gb xs =
-  gbrInit gb
-    + gbrLR gb * sum [ RF.predictTree t xs | t <- gbrTrees gb ]
-
--- | 行列入力に対する予測 (n).
-predictGBR :: GBRegressor -> LA.Matrix Double -> VU.Vector Double
-predictGBR gb x =
-  let !n = LA.rows x
-  in VU.generate n (\i -> predictGBRRow gb (rowList x i))
-
--- ---------------------------------------------------------------------------
--- Classifier (binary)
--- ---------------------------------------------------------------------------
-
--- | 二値分類 GBM (logit + log-loss)。 ラベルは 0/1。
-data GBClassifier = GBClassifier
-  { gbcInit  :: !Double          -- ^ logit(p̂_0)
-  , gbcTrees :: ![RF.Tree]
-  , gbcLR    :: !Double
-  } deriving (Show)
-
-sigmoid :: Double -> Double
-sigmoid z = 1 / (1 + exp (negate z))
-
-clamp :: Double -> Double -> Double -> Double
-clamp lo hi v = max lo (min hi v)
-
-fitGBClassifier :: GBConfig
-                -> LA.Matrix Double   -- ^ X (n × d)
-                -> VU.Vector Int      -- ^ y ∈ {0,1} (n)
-                -> GBClassifier
-fitGBClassifier cfg x y =
-  let !n    = VU.length y
-      !d    = LA.cols x
-      !cfgW = weakRFCfg d cfg
-      !lr   = gbLearnRate cfg
-      !yD   = VU.map fromIntegral y :: VU.Vector Double
-      !p0   = clamp 1e-6 (1 - 1e-6) (VU.sum yD / fromIntegral n)
-      !f0   = log (p0 / (1 - p0))
-      !logits0 = VU.replicate n f0
-      idx   = VU.enumFromN 0 n
-
-      step (!logits, !trees) _ =
-        let !grad = VU.zipWith (\yi z -> yi - sigmoid z) yD logits
-            !t    = RF.buildTreeV cfgW x grad idx 0
-            !upd  = VU.map (\i -> lr * RF.predictTree t (rowList x i))
-                           (VU.enumFromN 0 n)
-            !logits' = VU.zipWith (+) logits upd
-        in (logits', t : trees)
-
-      (_, treesRev) = foldl step (logits0, []) [1 .. gbNRounds cfg]
-  in GBClassifier f0 (reverse treesRev) lr
-
--- | クラス確率 p(y=1 | x) を返す。
-predictGBCProbs :: GBClassifier -> LA.Matrix Double -> VU.Vector Double
-predictGBCProbs gb x =
-  let !n = LA.rows x
-      logit xs = gbcInit gb
-                   + gbcLR gb * sum [ RF.predictTree t xs | t <- gbcTrees gb ]
-  in VU.generate n (\i -> sigmoid (logit (rowList x i)))
-
--- | クラス予測 (閾値 0.5)。
-predictGBC :: GBClassifier -> LA.Matrix Double -> VU.Vector Int
-predictGBC gb x =
-  VU.map (\p -> if p >= 0.5 then 1 else 0) (predictGBCProbs gb x)
diff --git a/src/Hanalyze/Model/HBM.hs b/src/Hanalyze/Model/HBM.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/HBM.hs
+++ /dev/null
@@ -1,214 +0,0 @@
--- |
--- Module      : Hanalyze.Model.HBM
--- Description : 多相階層ベイズモデル (Hierarchical Bayesian Model, HBM) DSL の facade
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Polymorphic Hierarchical Bayesian Model (HBM) DSL.
---
--- Phase 58 で責務別 submodule に分割済み。 本モジュールは **facade**:
--- 下位 8 module (Util/Distribution/Sampling/Model/Track/Eval/IR/Gradient) を
--- import し、 従来の公開 API を export list 経由でそのまま再公開する。
--- 既存 importer (18 src module + test) は無改修で従来通り使える。
---
--- A free-monad embedded language for probabilistic programs. The
--- continuation type is left polymorphic so that a single model term can
--- be reinterpreted as:
---
---   * a structural inspector (parameter / observation graph),
---   * a log-joint density,
---   * an automatically-differentiated log-joint
---     (via @Numeric.AD.Mode.Reverse.Double@ — Double 特化の reverse モードゆえ
---      勾配は latent 数 p に依らず ~1 sweep。 Phase 53 で forward から切替:
---      forward は勾配 1 本に p 回評価が要り階層モデルで線形悪化していた。
---      generic Reverse は tape boxing で低次元が遅く、 Reverse.Double が全 p で
---      forward/generic を上回ると実測),
---   * a dependency tracker (the 'Track' interpretation, used by
---     @Hanalyze.Viz.ModelGraph@ to build a Mermaid DAG).
---
--- See @docs/bayesian/02-probabilistic-model.md@ for an extended
--- introduction.
---
--- @
--- data ModelF a next
---   = Sample  Text (Distribution a) (a -> next)
---   | Observe Text (Distribution a) [Double] next
---   deriving Functor
--- @
---
--- ユーザーは @forall a. (Floating a, Ord a) => Model a r@ という
--- 「型に多相なモデル」を一度だけ書き、解釈時に @a@ を選ぶことで
--- 同じモデルから複数の解釈 (サンプリング・log joint・AD 勾配・依存抽出)
--- を取り出せる。
---
--- == 使い方
---
--- @
--- import Hanalyze.Model.HBM
---
--- myModel :: ModelP ()
--- myModel = do
---   mu    <- sample "mu"    (Normal 0 10)
---   sigma <- sample "sigma" (Exponential 1)
---   observe "y" (Normal mu sigma) [1.5, 2.0, 1.8]
---
--- -- 異なる解釈:
--- logVal = logJoint myModel (Map.fromList [("mu",1),("sigma",2)])  -- 数値評価
--- gVec   = gradAD myModel ["mu","sigma"] [1, 2]                    -- AD 勾配
--- deps   = extractDeps myModel                                      -- 依存関係
--- @
-module Hanalyze.Model.HBM
-  ( -- * Polymorphic distributions
-    Distribution (..)
-  , distName
-  , logDensity
-  , logDensityObs
-  , sampleDist
-  , sampleMvDist
-  , distCDF
-  , logCDF
-  , logSF
-    -- * Polymorphic model DSL
-  , Free (..)
-  , liftF
-  , ModelF (..)
-  , Model
-  , ModelP
-  , sample
-  , observe
-  , observeMV
-  , observeColumns
-  , observeLM
-  , observeLMR
-  , observeNormalLM
-  , LMFamily (..)
-  , REff (..)
-  , REffect (..)
-  , reffNames
-  , reNormal
-  , at
-  , indexed
-  , (.#)
-  , potential
-  , deterministic
-  , runDeterministics
-  , deterministicNames
-  , augmentChainWithDeterministic
-  , nonCenteredNormal
-  , dirichlet
-  , orderedCuts
-  , dpStickBreaking
-  , hmmLatent
-  -- ** Phase 40 plate notation
-  , plate
-  , plateI
-  , plateI_
-  , plateForM
-  , plateForM_
-  , withPlate
-  , hmmForwardLogLik
-  , GlmmFamily (..)
-  , glmmRandomIntercept
-  , dataNamed
-  , dataNamedX
-  , dataNamedIx
-  , dataNamedObs
-  , Ix (..)
-  , TrackTag (..)
-  , (!!!)
-  , atIx
-  , withData
-  , withDataIx
-  , mvNormalLatent
-  , mvNormalLogDensity
-  , mvNormalCholLogDensity
-  , multinomialLogDensity
-  , mvStudentTLogDensity
-  , dirichletMultinomialLogDensity
-  , wishartLogDensity
-  , obsLogSum
-  , lkjCorrCholesky
-  , gpExpQuadCov
-  , gpLatent
-  , ar1Latent
-    -- * Structural inspection
-  , Node (..)
-  , NodeKind (..)
-  , collectNodes
-  , sampleNames
-  , dataSlots
-  , dataIxSlots
-  , extractDeps
-    -- * Type aliases
-  , Params
-    -- * Interpreters
-  , logJoint
-  , logPrior
-  , logLikelihood
-  , perObsLogLiks
-  , runObserveDists
-  , priorList
-  , describeModel
-    -- * Model graph (visualization)
-  , ModelGraph (..)
-  , buildModelGraph
-  , collapseIndexedPlateNodes
-    -- * AD gradient
-  , gradAD
-  , gradADU
-  , compileGradU
-  , compileGradUV
-  , compileGradValUV
-  , compileGradValUVM
-  , compileLogPU
-  , compileLogPUV
-  , synthGaussLMBlocks
-  , synthVecIR
-  , gradPathLabel
-    -- * Numeric utilities (test 用・Phase 56.1)
-  , lgammaApprox
-  , digamma
-    -- * Constraint transforms (for HMC)
-  , getTransforms
-  , logJointUnconstrained
-  , invTransformF
-  , logJacF
-    -- * Dependency-tracking interpretation
-  , Track (..)
-  , trackVar
-  , trackConst
-  ) where
-
--- Phase 58.2: 純粋な数値・線形代数 leaf util を分離。 internal 利用に加え
--- 'lgammaApprox' / 'digamma' は export list 経由でそのまま再エクスポートされる。
-import Hanalyze.Model.HBM.Util
--- Phase 58.3/58.6a: 多相分布 ADT + 密度 + CDF を分離 (Util の上層)。 公開 API
--- (Distribution(..)/distName/logDensity/logDensityObs/obsLogSum/distCDF/logCDF/
--- logSF/MV密度群) は export list 経由でそのまま再エクスポート。 ★58.6a で事前
--- logDensity と観測 logDensityObs/obsLogSum を本体から Distribution へ集約
--- (Eval の logJoint/logPrior が logDensity を参照する back-edge を解消・密度は
--- 本来 Distribution の責務。 INLINABLE は AD cross-module inlining 維持で保持)。
-import Hanalyze.Model.HBM.Distribution
--- Phase 58.4: 分布からのサンプリング (sampleDist/sampleMvDist) を分離。
--- export list 経由でそのまま再エクスポート。 PrimMonad/mwc-random 依存・非ホット。
-import Hanalyze.Model.HBM.Sampling
--- Phase 58.5: 多相モデル DSL (Free monad + ModelF + plate + 構造検査) を分離。
--- 公開 API (Free/liftF/ModelF/Model/ModelP/sample/observe/plate/collectNodes 等)
--- は export list 経由でそのまま再エクスポート。
-import Hanalyze.Model.HBM.Model
--- Phase 58.6b: 依存追跡型 Track (Track/trackVar/trackConst/extractDeps) を分離。
--- Model/Distribution の上層・非ホット (DAG 抽出のみ・NUTS per-draw 非経路)。
--- export list 経由でそのまま再エクスポート。
-import Hanalyze.Model.HBM.Track
--- Phase 58.6c: 評価層 (ObserveLM 評価 + logJoint/logPrior/logLikelihood interp +
--- 互換 API runDeterministics/buildModelGraph 等 + runTrack) を分離。 Track の上層。
--- ★ホット (logJoint は AD 勾配経路)。 AD 勾配・IR (本体残置) は本モジュールを
--- forward import する。 公開 API は export list 経由でそのまま再エクスポート。
-import Hanalyze.Model.HBM.Eval
--- Phase 58.7: IR (中間表現) 層 (affine/非線形/密度 IR) を分離。 最ホット (gradVecIR)。
-import Hanalyze.Model.HBM.IR
--- Phase 58.8: AD 勾配コンパイラ層 (compileGradUV/hybridGradClosure/gaussLMBlocks/
--- 定数 prior 解析勾配/制約変換) を分離。 IR の上層・最ホット (NUTS per-draw 本経路)。
--- 公開 API (gradAD/gradADU/compileGradU/compileGradUV/compileLogPU/compileLogPUV/
--- getTransforms/logJointUnconstrained/invTransformF/logJacF) は export list 経由で再公開。
-import Hanalyze.Model.HBM.Gradient
diff --git a/src/Hanalyze/Model/HBM/Ast.hs b/src/Hanalyze/Model/HBM/Ast.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/HBM/Ast.hs
+++ /dev/null
@@ -1,243 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
--- |
--- Module      : Hanalyze.Model.HBM.Ast
--- Description : HBM dialog DSL の AST 型と JSON decoder
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- HBM dialog DSL の AST 型と JSON decoder。
---
--- Phase 27.5 (2026-05-31): canvas-backend @フロントエンド app.Analysis.HBM@ から
--- 移設。 frontend が backend 統一 parser (@/api/v1/dsl/parse@) から得た
--- @program_ast@ (JSON) を、 streaming sidecar が直接 decode して実モデルを
--- 構築できるよう、 AST 型 + 'parseAst' をライブラリ層 (hanalyze) に置く。
---
--- 本 module は **canvas wire 型にも text parser (DSL frontend) にも依存しない**
--- (= aeson のみ)。 text → AST 変換 ('parseHbmTextToExpr' 等) は HT (DSL frontend)
--- に依存するため canvas-backend 側に残す。
-module Hanalyze.Model.HBM.Ast
-  ( -- * AST
-    Expr (..)
-  , Lit (..)
-  , Bind (..)
-  , DoStmt (..)
-    -- * JSON decode (= frontend program_ast → Expr)
-  , parseAst
-  , parseLit
-  , parseBind
-  , parseDoStmt
-    -- * JSON encode (= 'parseAst' の正確な逆。 backend が sidecar に
-    --   program_ast / top_binds を送る際に使う、 Phase 27.5 step 3)
-  , exprToJSON
-  , litToJSON
-  , bindToJSON
-  , doStmtToJSON
-    -- * helpers
-  , Err
-  , collectApp
-  , getField
-  , getStr
-  , getNum
-  , getBool
-  , getArray
-  ) where
-
-import Data.Text (Text)
-import qualified Data.Aeson as A
-import Data.Aeson.Types (Pair)
-import qualified Data.Aeson.Key as Key
-import qualified Data.Aeson.KeyMap as KM
-import qualified Data.Vector as V
-
--- ---------------------------------------------------------------------------
--- AST (= frontend App.Hbm.Ast / DSL frontend hanalyze.HBM.Text.HbmExpr と同形、
---   11 ctor: ELit / ECol / EVar / EApp / ELam / EIf / ELet / ENeg / EOp /
---   EList / EDo)
--- ---------------------------------------------------------------------------
-
-data Expr
-  = ELit Lit
-  | ECol Text
-  | EVar Text
-  | EApp Expr Expr
-  | ELam Text Expr
-  | EIf Expr Expr Expr
-  | ELet [Bind] Expr
-  | ENeg Expr
-  | EOp Text Expr Expr
-  | EList [Expr]
-  | EDo [DoStmt] Expr
-  deriving (Show)
-
-data Lit = LNumber Double | LText Text | LBool Bool deriving (Show)
-
-data Bind = Bind { bindName :: Text, bindValue :: Expr } deriving (Show)
-
-data DoStmt
-  = DoBind Text Expr
-  | DoLet [Bind]
-  | DoExpr Expr
-  deriving (Show)
-
--- | 評価系で多用する Either alias。
-type Err a = Either Text a
-
--- ---------------------------------------------------------------------------
--- JSON parser (= frontend が送る program_ast を Expr に decode)
--- ---------------------------------------------------------------------------
-
-parseAst :: A.Value -> Either Text Expr
-parseAst v = case v of
-  A.Object o -> do
-    tag <- getStr o "tag"
-    case tag of
-      "ELit" -> ELit <$> (parseLit =<< getField o "lit")
-      "ECol" -> ECol <$> getStr o "name"
-      "EVar" -> EVar <$> getStr o "name"
-      "EApp" -> EApp <$> (parseAst =<< getField o "f") <*> (parseAst =<< getField o "x")
-      "ELam" -> ELam <$> getStr o "arg" <*> (parseAst =<< getField o "body")
-      "EIf"  -> EIf <$> (parseAst =<< getField o "c")
-                    <*> (parseAst =<< getField o "a")
-                    <*> (parseAst =<< getField o "b")
-      "ELet" -> do
-        bs <- getArray o "binds" >>= mapM parseBind
-        body <- parseAst =<< getField o "body"
-        Right (ELet bs body)
-      "ENeg" -> ENeg <$> (parseAst =<< getField o "e")
-      "EOp"  -> EOp <$> getStr o "op"
-                    <*> (parseAst =<< getField o "a")
-                    <*> (parseAst =<< getField o "b")
-      "EList" -> EList <$> (getArray o "items" >>= mapM parseAst)
-      "EDo"  -> do
-        stmts <- getArray o "stmts" >>= mapM parseDoStmt
-        ret <- parseAst =<< getField o "ret"
-        Right (EDo stmts ret)
-      _ -> Left ("Unknown AST tag: " <> tag)
-  _ -> Left "AST root must be a JSON object"
-
-parseLit :: A.Value -> Either Text Lit
-parseLit v = case v of
-  A.Object o -> do
-    tag <- getStr o "tag"
-    case tag of
-      "LNumber" -> do
-        n <- getNum o "value"
-        Right (LNumber n)
-      "LText"   -> LText <$> getStr o "value"
-      "LBool"   -> LBool <$> getBool o "value"
-      _ -> Left ("Unknown literal tag: " <> tag)
-  _ -> Left "Literal must be an object"
-
-parseBind :: A.Value -> Either Text Bind
-parseBind v = case v of
-  A.Object o -> do
-    n <- getStr o "name"
-    e <- parseAst =<< getField o "value"
-    Right (Bind n e)
-  _ -> Left "Bind must be an object"
-
-parseDoStmt :: A.Value -> Either Text DoStmt
-parseDoStmt v = case v of
-  A.Object o -> do
-    tag <- getStr o "tag"
-    case tag of
-      "DoBind" -> do
-        name <- getStr o "name"
-        rawValue <- parseAst =<< getField o "value"
-        -- Phase 9.1d-4 fix: frontend が `x <- sample "obsName" dist` を
-        -- DoBind の value に raw expression として渡してくる。 validateStmts
-        -- 以降は value が「純粋な distribution」 であることを期待するので、
-        -- ここで sample wrapper を剥がす。 sample 形でなければそのまま通す
-        -- (互換: 直接 dist を入れた古い経路があった場合のため)。
-        let distOnly = case collectApp rawValue of
-              Right ("sample", [ELit (LText _samplerName), d]) -> d
-              _ -> rawValue
-        pure (DoBind name distOnly)
-      "DoLet"  -> DoLet <$> (getArray o "binds" >>= mapM parseBind)
-      "DoExpr" -> DoExpr <$> (parseAst =<< getField o "value")
-      _ -> Left ("Unknown DoStmt tag: " <> tag)
-  _ -> Left "DoStmt must be an object"
-
--- ---------------------------------------------------------------------------
--- JSON encoder (= parseAst の正確な逆。 round-trip: parseAst . exprToJSON ≡ Right)
---
--- Phase 27.5 step 3 (2026-06-01): topology B で backend が stream sidecar に
--- start.params を組む際、 resolveHbmModel が返す Expr / TopBind を worker
--- (= parseAst で decode) が読める JSON 文字列に直す必要がある。 decoder と
--- 同じ module に逆変換を置き、 tag / field 名のズレを構造的に防ぐ。
---
--- 注: DoBind の value は sample wrapper を剥がした dist-only を前提とする
--- (parseDoStmt は sample wrapper を剥がすが、 既に剥がれた式には作用しない =
--- idempotent。 resolveHbmModel 経由の Expr は剥がし済)。
--- ---------------------------------------------------------------------------
-
-exprToJSON :: Expr -> A.Value
-exprToJSON e = case e of
-  ELit l       -> obj "ELit"  ["lit"   A..= litToJSON l]
-  ECol n       -> obj "ECol"  ["name"  A..= n]
-  EVar n       -> obj "EVar"  ["name"  A..= n]
-  EApp f x     -> obj "EApp"  ["f"     A..= exprToJSON f, "x" A..= exprToJSON x]
-  ELam a b     -> obj "ELam"  ["arg"   A..= a, "body" A..= exprToJSON b]
-  EIf c a b    -> obj "EIf"   ["c"     A..= exprToJSON c, "a" A..= exprToJSON a, "b" A..= exprToJSON b]
-  ELet bs body -> obj "ELet"  ["binds" A..= map bindToJSON bs, "body" A..= exprToJSON body]
-  ENeg x       -> obj "ENeg"  ["e"     A..= exprToJSON x]
-  EOp op a b   -> obj "EOp"   ["op"    A..= op, "a" A..= exprToJSON a, "b" A..= exprToJSON b]
-  EList xs     -> obj "EList" ["items" A..= map exprToJSON xs]
-  EDo stmts r  -> obj "EDo"   ["stmts" A..= map doStmtToJSON stmts, "ret" A..= exprToJSON r]
-  where
-    obj :: Text -> [Pair] -> A.Value
-    obj tag fields = A.object (("tag" A..= tag) : fields)
-
-litToJSON :: Lit -> A.Value
-litToJSON l = case l of
-  LNumber n -> A.object ["tag" A..= ("LNumber" :: Text), "value" A..= n]
-  LText t   -> A.object ["tag" A..= ("LText" :: Text),   "value" A..= t]
-  LBool b   -> A.object ["tag" A..= ("LBool" :: Text),   "value" A..= b]
-
-bindToJSON :: Bind -> A.Value
-bindToJSON (Bind n v) = A.object ["name" A..= n, "value" A..= exprToJSON v]
-
-doStmtToJSON :: DoStmt -> A.Value
-doStmtToJSON s = case s of
-  DoBind n v -> A.object ["tag" A..= ("DoBind" :: Text), "name" A..= n, "value" A..= exprToJSON v]
-  DoLet bs   -> A.object ["tag" A..= ("DoLet"  :: Text), "binds" A..= map bindToJSON bs]
-  DoExpr v   -> A.object ["tag" A..= ("DoExpr" :: Text), "value" A..= exprToJSON v]
-
--- | @EApp (EApp (EVar f) a) b@ → @(f, [a, b])@。 distribution / 関数適用の
--- head + 引数列を取り出す。 head が変数でなければ Left。
-collectApp :: Expr -> Err (Text, [Expr])
-collectApp e0 = go e0 []
-  where
-    go (EVar n) acc = Right (n, acc)
-    go (EApp f x) acc = go f (x : acc)
-    go _ _ = Left "Distribution must be a function applied to scalar args"
-
--- ---------------------------------------------------------------------------
--- helpers
--- ---------------------------------------------------------------------------
-
-getField :: A.Object -> Text -> Either Text A.Value
-getField o k = case KM.lookup (Key.fromText k) o of
-  Just v -> Right v
-  Nothing -> Left ("Missing field: " <> k)
-
-getStr :: A.Object -> Text -> Either Text Text
-getStr o k = case KM.lookup (Key.fromText k) o of
-  Just (A.String s) -> Right s
-  _ -> Left ("Field not string: " <> k)
-
-getNum :: A.Object -> Text -> Either Text Double
-getNum o k = case KM.lookup (Key.fromText k) o of
-  Just (A.Number n) -> Right (realToFrac n)
-  _ -> Left ("Field not number: " <> k)
-
-getBool :: A.Object -> Text -> Either Text Bool
-getBool o k = case KM.lookup (Key.fromText k) o of
-  Just (A.Bool b) -> Right b
-  _ -> Left ("Field not bool: " <> k)
-
-getArray :: A.Object -> Text -> Either Text [A.Value]
-getArray o k = case KM.lookup (Key.fromText k) o of
-  Just (A.Array xs) -> Right (V.toList xs)
-  _ -> Left ("Field not array: " <> k)
diff --git a/src/Hanalyze/Model/HBM/Distribution.hs b/src/Hanalyze/Model/HBM/Distribution.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/HBM/Distribution.hs
+++ /dev/null
@@ -1,1381 +0,0 @@
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE ScopedTypeVariables #-}
--- |
--- Module      : Hanalyze.Model.HBM.Distribution
--- Description : HBM の多相確率分布 ADT と密度・CDF
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- HBM の多相確率分布 ADT と密度・CDF。
---
--- 'Distribution' は値型 @a@ に多相な確率分布。 @a@ は @Double@ (サンプリング・
--- 密度)、 @Reverse s Double@ (AD 勾配)、 @Track@ (依存追跡) を渡せる。 本モジュール
--- は型・名前・**事前密度** 'logDensity'・多変量密度・閉形式 CDF を提供し、 純粋
--- leaf 'Hanalyze.Model.HBM.Util' のみに依存する。
---
--- ★観測尤度 'logDensityObs' / 'obsLogSum' は **含めない** (Eval 層へ残置。
--- Distribution→Eval の cycle を避けるため・request/254)。
---
--- Phase 58.3 で 'Hanalyze.Model.HBM' から責務分離して抽出。 数値は 1 bit 不変。
-module Hanalyze.Model.HBM.Distribution
-  ( Distribution (..)
-  , distName
-  , nameToTransform
-  , distToTransform
-  , logDensity
-  , logDensityRD
-  , logDensityObs
-  , obsLogSum
-  , multinomialLogDensity
-  , mvNormalLogDensity
-  , mvNormalCholLogDensity
-  , mvStudentTLogDensity
-  , dirichletMultinomialLogDensity
-  , wishartLogDensity
-  , erfA
-  , phiCdfA
-  , distCDF
-  , logCDF
-  , logSF
-  , logCDFInterval
-  ) where
-
-import Data.List (mapAccumL, zip4)
-import Data.Text (Text)
--- Phase 92 B3: 'logDensityRD' (AD 定数正規化項の畳み込み) 用。 多相 'logDensity'
--- 本体は AD 非依存のまま。
-import Data.Reflection (Reifies)
-import qualified Numeric.AD.Internal.Reverse.Double as ADRD
-import Hanalyze.Model.HBM.Util
-import Hanalyze.Stat.Distribution (Transform (..))
-
--- ---------------------------------------------------------------------------
--- 多相分布
--- ---------------------------------------------------------------------------
-
--- | A probability distribution polymorphic in its value type @a@.
---
--- @a@ ranges over @Double@ (sampling and density), @Reverse s Double@
--- (AD-based gradient), @Track@ (dependency tracking) and so on.
-data Distribution a
-  = Normal      a a       -- ^ Normal(μ, σ)
-  | Exponential a         -- ^ Exp(rate)
-  | Gamma       a a       -- ^ Gamma(shape, rate)
-  | Beta        a a       -- ^ Beta(α, β)
-  | Poisson     a         -- ^ Poisson(λ)
-  | Binomial    Int a     -- ^ Binomial(n, p)
-  | Uniform     a a       -- ^ Uniform(low, high)
-  | StudentT    a a a     -- ^ StudentT(ν degrees of freedom, μ location, σ scale)
-  | Cauchy      a a       -- ^ Cauchy(x₀ location, γ scale)
-  | HalfNormal  a         -- ^ HalfNormal(σ) — support: x ≥ 0
-  | HalfCauchy  a         -- ^ HalfCauchy(γ scale) — support: x ≥ 0
-  | LogNormal   a a       -- ^ LogNormal(μ log-mean, σ log-sd) — support: x > 0
-  | Bernoulli   a         -- ^ Bernoulli(p) — observed: 0 or 1
-  | Categorical [a]       -- ^ Categorical(probs) — observed: 0..K-1
-  | Mixture [a] [Distribution a]
-    -- ^ @Mixture(weights, components)@ —
-    --   @log p(x) = logSumExp(log w_k + log p_k(x))@.
-    --   Weights need only be positive; they are auto-normalized.
-  | Truncated (Distribution a) (Maybe a) (Maybe a)
-    -- ^ @Truncated(d, lo, hi)@: restrict the support of @d@ to
-    --   @[lo, hi]@. Out-of-range observations get @-∞@.
-    --   'Nothing' bounds mean @-∞ / +∞@. Only base distributions with a
-    --   CDF (Normal / Exponential / LogNormal / Uniform) are supported.
-  | Censored  (Distribution a) (Maybe a) (Maybe a)
-    -- ^ @Censored(d, lo, hi)@: censor @y ≤ lo@ on the left and
-    --   @y ≥ hi@ on the right. When @y_i@ equals a threshold the CDF/SF
-    --   is used. Useful for Tobit-style models. Only CDF-supporting
-    --   base distributions.
-  | MvNormal [a] [[a]]
-    -- ^ @MvNormal(μ, Σ)@: multivariate normal (observation-only).
-    --   @μ@ is a length-@k@ mean vector, @Σ@ is the @k×k@
-    --   symmetric-positive-definite covariance. Pass @k@-vector
-    --   observations through 'observeMV'. Density is computed via
-    --   Cholesky. /Not supported/ as a latent ('sample' returns 0
-    --   density).
-  | MvNormalChol [a] [a] [[a]]
-    -- ^ @MvNormalChol(μ, σ, L)@: multivariate normal parameterized by a
-    --   scale vector @σ@ (length @k@) and a /correlation/ Cholesky factor
-    --   @L@ (lower-triangular @k×k@, typically from 'lkjCorrCholesky').
-    --   The covariance is @Σ = (diag σ · L)(diag σ · L)ᵀ@. The density
-    --   uses the scaled Cholesky @M = diag σ · L@ directly (forward
-    --   substitution, no re-decomposition) — numerically the most stable
-    --   parameterization (Stan's @multi_normal_cholesky@ idiom).
-    --   Observation-only; pass @k@-vectors via 'observeMV'.
-  | MvNormalGpRBF [a] a a a
-    -- ^ Phase 95 B-dsl: @MvNormalGpRBF(x, α, ρ, σ)@ — zero-mean GP 回帰尤度
-    --   専用の多変量正規 (observation-only)。 共分散は RBF (exp-quad) カーネル
-    --   @Σ_ij = α² exp(-0.5 (x_i-x_j)²/ρ²) + [i=j](1e-10 + σ)@ で内部構築する。
-    --   汎用 'MvNormal' と密度は同値だが、 カーネルの役割 (x/α/ρ/σ) を型で明示
-    --   保持することで、 勾配コンパイラ ('gpRBFAnalyticVG') が **Cholesky を AD
-    --   tape に載せない閉形式随伴** (@∂Σ/∂α=2K'/α@・@∂Σ/∂ρ=K'∘d²/ρ³@・
-    --   @∂Σ/∂σ=I@) を使える。 @x@ は共変量 data (定数)、 α/ρ/σ は latent。
-    --   観測は length-@k@ ベクトルを 'observeMV' で渡す (μ=0 固定)。
-  | HmmForwardNormal [a] [[a]] [a] a
-    -- ^ Phase 92 A2: @HmmForwardNormal(π_0, trans, μs, σ)@ — Normal emission の
-    --   隠れマルコフモデル周辺尤度 (observation-only)。 観測列 y_{1..T} 全体を
-    --   1 つの多変量観測として 'observeMV' で渡す (@observeMV nm d [ys]@)。
-    --   密度は @'hmmForwardLogLik' π_0 trans emit@ (emit[t][k] =
-    --   Normal(μs[k], σ) の logpdf(y_t)) と同値。 状態役割 (π_0/遷移行/emission
-    --   平均/σ) を型で明示保持することで、 勾配コンパイラ ('hmmAnalyticVG') が
-    --   **forward-backward の閉形式随伴** (∂logL/∂emit = γ_t・∂logL/∂T_ij = ξ
-    --   集計・AD tape ゼロ) を使える。 π_0 は非正規化可 (log 空間で加算されるのみ)。
-  | ArmaNormal a a a a
-    -- ^ Phase 101 A2: @ArmaNormal(μ, φ, θ, σ)@ — ARMA(1,1) の条件付き尤度
-    --   (observation-only)。 観測列 y_{1..T} 全体を 1 つの多変量観測として
-    --   'observeMV' で渡す (@observeMV nm d [ys]@)。 密度は Stan 原典 arma11 の
-    --   err 逐次再帰 (@err_1 = y_1 − (μ+φμ)@・@err_t = y_t − μ − φ·y_{t−1} −
-    --   θ·err_{t−1}@・@err_t ~ Normal(0, σ)@) と同値。 役割 (μ/φ/θ/σ) を型で
-    --   明示保持することで、 勾配コンパイラ ('armaAnalyticVG') が **逆向き
-    --   1 パスの閉形式随伴** (@ē_t = −e_t/σ² − θ·ē_{t+1}@ の線形随伴再帰・
-    --   AD tape ゼロ) を使える。
-  | GradedResponseIrt [a] [Int] [Double] [[Double]]
-    -- ^ Phase 101 A3: @GradedResponseIrt(θs, ncats, δs, γs)@ — graded response
-    --   IRT (順序ロジット・BUGS bones) の尤度 (observation-only)。 @θs@ =
-    --   受験者能力 (latent・唯一の param 側)、 @ncats[j]@/@δs[j]@/@γs[j][k]@ =
-    --   項目 j のカテゴリ数/識別力/カットポイント (**定数データ**)。 観測は
-    --   grade 行列 (nChild×nItem 行優先・1-based カテゴリ・欠測 = −1) を
-    --   'observeMV' で 1 観測として渡す (@observeMV nm d [grades]@)。
-    --   密度は @Q_k = invlogit(δ(θ−γ_k))@ の隣接差 p のカテゴリ対数確率と
-    --   同値。 θ_i (スカラ) 毎に独立なため、 勾配コンパイラ
-    --   ('gradedIrtAnalyticVG') が **解析勾配** (@dQ/dθ = δ·Q(1−Q)@ の差分・
-    --   AD tape ゼロ) を使える。
-  | NegativeBinomial a a
-    -- ^ @NegativeBinomial(μ, α)@ (PyMC parameterization).
-    --   @mean = μ@, @var = μ + μ²/α@ (Poisson in the limit
-    --   @α → ∞@). Likelihood for over-dispersed count data;
-    --   observations are non-negative integers.
-  | Multinomial Int [a]
-    -- ^ @Multinomial(n, [p_0, …, p_{K-1}])@ (observation-only).
-    --   @n@ is the trial count and @p@ the probability vector.
-    --   Observations are @K@-dimensional count vectors summing to @n@,
-    --   passed via 'observeMV'.
-  | ZeroInflatedPoisson a a
-    -- ^ @ZeroInflatedPoisson(ψ, λ)@: zero-inflated Poisson.
-    --   @ψ ∈ [0, 1]@ is the structural-zero probability.
-    --   @P(0) = ψ + (1-ψ) e^{-λ}@,
-    --   @P(k>0) = (1-ψ) λ^k e^{-λ} / k!@.
-  | ZeroInflatedBinomial Int a a
-    -- ^ @ZeroInflatedBinomial(n, ψ, p)@: zero-inflated binomial.
-    --   @P(0) = ψ + (1-ψ) (1-p)^n@,
-    --   @P(k>0) = (1-ψ) C(n,k) p^k (1-p)^{n-k}@.
-  | InverseGamma a a
-    -- ^ @InverseGamma(α, β)@. Support @x > 0@. If
-    --   @X ~ InverseGamma(α, β)@ then @1/X ~ Gamma(α, β)@ (rate
-    --   parameterization). Common conjugate prior on variance
-    --   (@mean = β/(α−1)@, finite when @α > 1@).
-  | Weibull a a
-    -- ^ @Weibull(k shape, λ scale)@: a standard survival distribution.
-    --   Support @x > 0@. @pdf = (k/λ) (x/λ)^{k-1} exp(-(x/λ)^k)@.
-    --   With @k = 1@ this is @Exponential(rate = 1/λ)@.
-  | Pareto a a
-    -- ^ @Pareto(α shape, x_m scale)@: heavy-tailed power law.
-    --   Support @x ≥ x_m > 0@. @pdf = α x_m^α / x^{α+1}@.
-    --   Mean @= α x_m / (α-1)@ when @α > 1@.
-  | BetaBinomial Int a a
-    -- ^ @BetaBinomial(n, α, β)@ overdispersed binomial
-    --   (observation-only).
-    --   @P(k) = C(n, k) B(k+α, n-k+β) / B(α, β)@. With @α = β = 1@
-    --   this is uniform on @{0, …, n}@; large @α/β@ tends to a
-    --   binomial.
-  | VonMises a a
-    -- ^ @VonMises(μ location, κ concentration)@: distribution on the
-    --   circle @(-π, π]@.
-    --   @pdf = exp(κ cos(x − μ)) / (2π I_0(κ))@.
-    --   @κ → 0@ approaches uniform; @κ → ∞@ approaches
-    --   @Normal(μ, 1/√κ)@.
-  | SkewNormal a a a
-    -- ^ @SkewNormal(μ location, σ scale, α shape)@ (Phase 37-A2).
-    --   @pdf = (2/σ) φ((x−μ)/σ) Φ(α(x−μ)/σ)@.
-    --   @α = 0@ で標準正規。 @α > 0@ で右側に歪み、 @α < 0@ で左側。
-    --   Sample は Henze 1986: @δ = α/√(1+α²)@,
-    --   @X = μ + σ(δ |U₀| + √(1−δ²) U₁)@ with i.i.d. @U_i ~ N(0,1)@.
-  | Logistic a a
-    -- ^ @Logistic(μ location, s scale)@ (Phase 37-A2).
-    --   @pdf = e^{−z} / (s(1+e^{−z})²)@ with @z = (x−μ)/s@.
-    --   平均 @μ@、 分散 @s²π²/3@。 closed-form CDF あり。
-  | Gumbel a a
-    -- ^ @Gumbel(μ location, β scale)@ (Phase 37-A2、 最大値型極値分布)。
-    --   @pdf = (1/β) exp(−z − e^{−z})@ with @z = (x−μ)/β@.
-    --   平均 @μ + βγ@ (γ ≈ 0.5772 オイラー定数)、 分散 @β²π²/6@。
-    --   closed-form CDF: @F(x) = exp(−exp(−z))@.
-  | AsymmetricLaplace a a a
-    -- ^ @AsymmetricLaplace(b scale > 0, κ asymmetry > 0, μ location)@
-    --   (Phase 37-A2、 PyMC parameterization、 分位点回帰の尤度)。
-    --   @pdf = b/(κ+1/κ) · exp(−b·κ·(x−μ))@ for @x ≥ μ@、
-    --   @pdf = b/(κ+1/κ) · exp(b/κ·(x−μ))@ for @x < μ@。
-    --   @κ = 1@ で対称ラプラス、 @κ > 1@ で右側裾長。
-  | OrderedLogistic a [a]
-    -- ^ @OrderedLogistic(η linear predictor, cuts = [c₁, …, c_{K-1}])@
-    --   (Phase 37-A3、 順序ロジット回帰)。
-    --   観測 @y ∈ {0, …, K-1}@、
-    --   @P(y=k) = σ(c_{k+1} − η) − σ(c_k − η)@ with
-    --   @σ(x) = 1/(1+e^{-x})@, @c_0 = −∞, c_K = +∞@.
-    --   cuts は **increasing** 列、 入力側で確保すること。
-    --   observation-only。
-  | DiscreteUniform Int Int
-    -- ^ @DiscreteUniform(lo, hi)@ (Phase 37-A3、 包含両端)。
-    --   @pmf = 1/(hi-lo+1)@ for @lo ≤ y ≤ hi@。 observation-only。
-  | Geometric a
-    -- ^ @Geometric(p)@ (Phase 37-A3、 PyMC 慣例 = 初回成功までの試行回数)。
-    --   support @y = 1, 2, 3, …@、 @pmf = (1−p)^{y-1} p@。
-    --   observation-only。
-  | HyperGeometric Int Int Int
-    -- ^ @HyperGeometric(N total, K successes, n draws)@ (Phase 37-A3、
-    --   非復元抽出の成功数)。
-    --   @pmf = C(K, y) C(N-K, n-y) / C(N, n)@、
-    --   support @max(0, n+K-N) ≤ y ≤ min(n, K)@。 observation-only。
-  | ZeroInflatedNegativeBinomial a a a
-    -- ^ @ZeroInflatedNegativeBinomial(ψ, μ, α)@ (Phase 37-A3、 過分散ゼロ過剰)。
-    --   @P(0) = ψ + (1-ψ) (α/(α+μ))^α@、
-    --   @P(k>0) = (1-ψ) · NegBin(k | μ, α)@。
-  | MvStudentT a [a] [[a]]
-    -- ^ @MvStudentT(ν, μ, Σ)@ (Phase 37-A4、 ロバスト多変量)。
-    --   @ν > 0@ 自由度、 @μ@ は @k@ 次元平均、 @Σ@ は @k×k@ SPD scale matrix。
-    --   観測 (observation-only)、 @y :: [Double]@ は flatten された
-    --   @k@ ベクトル列 (@observeMV@ で渡す)。
-    --   @ν → ∞@ で MvNormal に収束。
-  | DirichletMultinomial Int [a]
-    -- ^ @DirichletMultinomial(n trials, α concentration K-vector)@
-    --   (Phase 37-A4、 過分散 multinomial)。
-    --   観測 y は @K@ 次元 counts、 @Σ yᵢ = n@。
-    --   @logpmf = log Γ(α₀) − log Γ(α₀+n)
-    --           + Σ [log Γ(yᵢ+αᵢ) − log Γ(αᵢ)]
-    --           + log n! − Σ log yᵢ!@、 @α₀ = Σαᵢ@.
-    --   observation-only。
-  | Triangular a a a
-    -- ^ @Triangular(lower, c mode, upper)@ (Phase 39-A1、 弱情報事前)。
-    --   Support @[lower, upper]@、 @lower ≤ c ≤ upper@。
-    --   @pdf = 2(x-lower)/((upper-lower)(c-lower))@ for @lower ≤ x ≤ c@、
-    --   @pdf = 2(upper-x)/((upper-lower)(upper-c))@ for @c < x ≤ upper@。
-    --   closed-form CDF / 逆 CDF sample。
-  | Kumaraswamy a a
-    -- ^ @Kumaraswamy(a, b)@ (Phase 39-A1、 Beta 代替、 closed-form CDF)。
-    --   Support @(0, 1)@、 @pdf = a·b·x^{a-1}(1-x^a)^{b-1}@。
-    --   CDF @= 1 - (1-x^a)^b@、 sample @x = (1-(1-u)^{1/b})^{1/a}@。
-  | Rice a a
-    -- ^ @Rice(ν, σ)@ (Phase 39-A1、 MRI / Rayleigh 拡張)。
-    --   Support @x ≥ 0@、 @ν ≥ 0@、 @σ > 0@。
-    --   @pdf = (x/σ²) exp(-(x²+ν²)/(2σ²)) I_0(xν/σ²)@、
-    --   @ν = 0@ で Rayleigh(σ)。 @logBesselI0@ で評価。
-    --   sample: @X = √(Y₁² + Y₂²)@ with @Y₁ ~ N(ν, σ²), Y₂ ~ N(0, σ²)@。
-  | DiscreteWeibull a a
-    -- ^ @DiscreteWeibull(q, β)@ (Phase 39-A1、 整数 Weibull)。
-    --   Support @{0, 1, 2, …}@、 @0 < q < 1, β > 0@。
-    --   @P(X ≤ k) = 1 - q^{(k+1)^β}@、
-    --   @pmf(k) = q^{k^β} - q^{(k+1)^β}@。 observation-only。
-    --   sample: @k = ⌈(log(1-u)/log q)^{1/β}⌉ - 1@。
-  | Wishart a [[a]]
-    -- ^ @Wishart(ν degrees, V scale matrix)@ (Phase 39-A2、 共分散プライアの直接表現)。
-    --   @ν > k-1@、 @V@ は @k×k@ SPD scale matrix。
-    --   観測 (observation-only)、 @k×k@ 観測行列 W を flatten で渡す
-    --   (長さ @k²@、 row-major)。 @observeMV@ で渡す想定。
-    --   @logpdf(W) = -(νk/2) log 2 - (ν/2) log|V| - log Γ_k(ν/2)
-    --              + ((ν-k-1)/2) log|W| - (1/2) tr(V⁻¹ W)@、
-    --   @log Γ_k(z) = (k(k-1)/4) log π + Σ_{i=1}^k log Γ((z+1-i)/2)@。
-  | Bound (Distribution a) (Maybe a) (Maybe a)
-    -- ^ @Bound(d, lo, hi)@ (Phase 39-A3、 PyMC 互換)。
-    --   @d@ の支持を @[lo, hi]@ に制限する。 'Truncated' とほぼ同義
-    --   (実装も委譲)。 'Nothing' は @-∞ / +∞@。
-    --   違いは語用論のみ: PyMC では prior 寄りで Bound、 観測寄りで
-    --   Truncated を使う慣例があるため API として並べた。
-  | OrderedProbit a [a]
-    -- ^ @OrderedProbit(η linear predictor, cuts = [c₁, …, c_{K-1}])@
-    --   (Phase 39-A3、 順序プロビット回帰)。
-    --   @P(y=k) = Φ(c_{k+1} − η) − Φ(c_k − η)@ with
-    --   @c_0 = −∞, c_K = +∞@、 Φ は標準正規 CDF (@phiCdfA@)。
-    --   cuts は increasing 列、 入力側で確保。 observation-only。
-  deriving (Show, Functor)
-
--- | Display name of a distribution constructor (e.g. @\"Normal\"@).
-distName :: Distribution a -> Text
-distName Normal{}      = "Normal"
-distName Exponential{} = "Exponential"
-distName Gamma{}       = "Gamma"
-distName Beta{}        = "Beta"
-distName Poisson{}     = "Poisson"
-distName Binomial{}    = "Binomial"
-distName Uniform{}     = "Uniform"
-distName StudentT{}    = "StudentT"
-distName Cauchy{}      = "Cauchy"
-distName HalfNormal{}  = "HalfNormal"
-distName HalfCauchy{}  = "HalfCauchy"
-distName LogNormal{}   = "LogNormal"
-distName Bernoulli{}   = "Bernoulli"
-distName Categorical{} = "Categorical"
-distName Mixture{}     = "Mixture"
-distName Truncated{}   = "Truncated"
-distName Censored{}    = "Censored"
-distName MvNormal{}    = "MvNormal"
-distName MvNormalChol{} = "MvNormalChol"
-distName MvNormalGpRBF{} = "MvNormalGpRBF"
-distName HmmForwardNormal{} = "HmmForwardNormal"
-distName ArmaNormal{} = "ArmaNormal"
-distName GradedResponseIrt{} = "GradedResponseIrt"
-distName NegativeBinomial{} = "NegativeBinomial"
-distName Multinomial{}          = "Multinomial"
-distName ZeroInflatedPoisson{}  = "ZeroInflatedPoisson"
-distName ZeroInflatedBinomial{} = "ZeroInflatedBinomial"
-distName InverseGamma{}         = "InverseGamma"
-distName Weibull{}              = "Weibull"
-distName Pareto{}               = "Pareto"
-distName BetaBinomial{}         = "BetaBinomial"
-distName VonMises{}             = "VonMises"
-distName SkewNormal{}           = "SkewNormal"
-distName Logistic{}             = "Logistic"
-distName Gumbel{}               = "Gumbel"
-distName AsymmetricLaplace{}    = "AsymmetricLaplace"
-distName OrderedLogistic{}      = "OrderedLogistic"
-distName DiscreteUniform{}      = "DiscreteUniform"
-distName Geometric{}            = "Geometric"
-distName HyperGeometric{}       = "HyperGeometric"
-distName ZeroInflatedNegativeBinomial{} = "ZeroInflatedNegativeBinomial"
-distName MvStudentT{}           = "MvStudentT"
-distName DirichletMultinomial{} = "DirichletMultinomial"
-distName Triangular{}           = "Triangular"
-distName Kumaraswamy{}          = "Kumaraswamy"
-distName Rice{}                 = "Rice"
-distName DiscreteWeibull{}      = "DiscreteWeibull"
-distName Wishart{}              = "Wishart"
-distName Bound{}                = "Bound"
-distName OrderedProbit{}        = "OrderedProbit"
-
--- | 分布名 → NUTS が探索する **unconstrained 変換種別**。 latent の制約付き台
--- (正値・単位区間) を実数空間へ写す種別を返す。
---
--- ★これが分布→変換の **唯一の表**。 'getTransforms'
--- (@Gradient@・node walk 版) も本関数へ委譲する。 分布を latent 化して台が
--- 変わる場合はここを更新する (1 箇所)。 未列挙は保守的に 'UnconstrainedT'。
-nameToTransform :: Text -> Transform
-nameToTransform "Exponential"  = PositiveT
-nameToTransform "Gamma"        = PositiveT
-nameToTransform "HalfNormal"   = PositiveT
-nameToTransform "HalfCauchy"   = PositiveT
-nameToTransform "LogNormal"    = PositiveT     -- support: x>0 (log は AD 安全)
-nameToTransform "InverseGamma" = PositiveT
-nameToTransform "Weibull"      = PositiveT
-nameToTransform "Pareto"       = PositiveT
-nameToTransform "Beta"         = UnitIntervalT
-nameToTransform "Bernoulli"    = UnitIntervalT -- p ∈ (0,1)
-nameToTransform "BetaBinomial" = UnitIntervalT
-nameToTransform _              = UnconstrainedT -- Normal/StudentT/Cauchy/Uniform 等
--- 注: Uniform の真の制約変換は logit-on-(lo,hi) だが現状未実装 (unconstrained 扱い)。
-
--- | 分布 (ADT) → unconstrained 変換種別。 'nameToTransform' の値レベル版。
-distToTransform :: Distribution a -> Transform
-distToTransform = nameToTransform . distName
-
--- | Log probability of a single multinomial observation (a @K@-vector
--- of counts).
---   log P(k_1, …, k_K) = log n!/Π k_i! + Σ k_i log p_i
-{-# INLINABLE multinomialLogDensity #-}
-multinomialLogDensity :: forall a. (Floating a, Ord a)
-                      => Int -> [a] -> [Double] -> a
-multinomialLogDensity n probs counts
-  | length probs /= length counts = negInf
-  | sum (map round counts :: [Int]) /= n = negInf
-  | any (< 0) counts                = negInf
-  | any (\p -> p <= 0) probs        = negInf
-  | otherwise =
-      let logFactN = realToFrac (logFactorial n) :: a
-          logFactSum = sum [ realToFrac (logFactorial (round c :: Int)) :: a
-                           | c <- counts ]
-          dotPart = sum (zipWith (\c p -> realToFrac c * log p) counts probs)
-      in logFactN - logFactSum + dotPart
-
--- | Log density of an 'MvNormal' at a single @k@-vector observation.
---   log p(y) = -k/2 log(2π) - 0.5 log|Σ| - 0.5 (y-μ)ᵀ Σ⁻¹ (y-μ)
---   Σ⁻¹ と log|Σ| は Cholesky 分解 Σ = L Lᵀ から計算。
-{-# INLINABLE mvNormalLogDensity #-}
-mvNormalLogDensity :: forall a. (Floating a, Ord a) => [a] -> [[a]] -> [a] -> a
-mvNormalLogDensity mu cov yObs
-  | length mu == 0           = 0
-  | length yObs /= length mu = negInf
-  | otherwise =
-      case choleskyL cov of
-        Nothing -> negInf
-        Just l  ->
-          let k      = length mu
-              kA     = fromIntegral k :: a
-              d      = zipWith (-) yObs mu
-              z      = forwardSub l d           -- L z = d
-              quad   = sum (map (\zi -> zi * zi) z)
-              logDet = 2 * sum [ log ((l !! i) !! i) | i <- [0 .. k - 1] ]
-          in -0.5 * kA * log (2 * pi) - 0.5 * logDet - 0.5 * quad
-
--- | 'MvNormalChol' の 1 観測 (k-vector) の log density (Phase 44)。
---   scale vector @σ@ と /相関/ Cholesky 因子 @L@ から scaled Cholesky
---   @M = diag σ · L@ (= @M_ij = σ_i · L_ij@) を直接構成し、 共分散
---   @Σ = M Mᵀ@ を /再分解せず/ 評価する:
---     @log p(y) = -k/2 log(2π) - Σ log M_ii - 0.5 |z|²@、 @M z = (y-μ)@ を
---   前進代入で解く。 @log|Σ| = 2 Σ log M_ii@ なので密度の @-0.5 log|Σ|@ は
---   @-Σ log M_ii@。 'mvNormalLogDensity' (full Σ → choleskyL) と @Σ = M Mᵀ@ で
---   数値一致する。 Stan の @multi_normal_cholesky@ と同じ idiom。
-{-# INLINABLE mvNormalCholLogDensity #-}
-mvNormalCholLogDensity :: forall a. (Floating a, Ord a) => [a] -> [a] -> [[a]] -> [a] -> a
-mvNormalCholLogDensity mu sigma l yObs
-  | k == 0                                       = 0
-  | length yObs /= k || length sigma /= k        = negInf
-  | length l /= k || any ((/= k) . length) l     = negInf
-  | otherwise =
-      let m      = [ [ (sigma !! i) * ((l !! i) !! j) | j <- [0 .. k - 1] ]
-                   | i <- [0 .. k - 1] ]
-          kA     = fromIntegral k :: a
-          d      = zipWith (-) yObs mu
-          z      = forwardSub m d           -- M z = d (M 下三角)
-          quad   = sum (map (\zi -> zi * zi) z)
-          logDet = sum [ log ((m !! i) !! i) | i <- [0 .. k - 1] ]  -- = 0.5 log|Σ|
-      in -0.5 * kA * log (2 * pi) - logDet - 0.5 * quad
-  where k = length mu
-
--- | MvStudentT(ν, μ, Σ) の 1 観測 (k-vector) の log density (Phase 37-A4)。
---   @logpdf(y) = log Γ((ν+k)/2) − log Γ(ν/2) − (k/2) log(νπ) − (1/2) log|Σ|
---              − ((ν+k)/2) log(1 + m²/ν)@、
---   @m² = (y−μ)ᵀ Σ⁻¹ (y−μ)@ を Cholesky で評価。
-{-# INLINABLE mvStudentTLogDensity #-}
-mvStudentTLogDensity :: forall a. (Floating a, Ord a)
-                     => a -> [a] -> [[a]] -> [a] -> a
-mvStudentTLogDensity nu mu cov yObs
-  | nu <= 0                   = negInf
-  | length mu == 0            = 0
-  | length yObs /= length mu  = negInf
-  | otherwise =
-      case choleskyL cov of
-        Nothing -> negInf
-        Just l  ->
-          let k      = length mu
-              kA     = fromIntegral k :: a
-              d      = zipWith (-) yObs mu
-              z      = forwardSub l d
-              quad   = sum (map (\zi -> zi * zi) z)
-              logDet = 2 * sum [ log ((l !! i) !! i) | i <- [0 .. k - 1] ]
-          in lgammaApprox ((nu + kA) / 2)
-           - lgammaApprox (nu / 2)
-           - 0.5 * kA * log (nu * pi)
-           - 0.5 * logDet
-           - 0.5 * (nu + kA) * log (1 + quad / nu)
-
--- | DirichletMultinomial(n, α) の 1 観測 (K-vector counts) の log pmf
---   (Phase 37-A4)。
---   @logpmf = log Γ(α₀) − log Γ(α₀+n) + Σ [log Γ(yᵢ+αᵢ) − log Γ(αᵢ)]
---           + log n! − Σ log yᵢ!@、 @α₀ = Σ αᵢ@.
-{-# INLINABLE dirichletMultinomialLogDensity #-}
-dirichletMultinomialLogDensity :: forall a. (Floating a, Ord a)
-                               => Int -> [a] -> [Double] -> a
-dirichletMultinomialLogDensity n alpha counts
-  | length alpha /= length counts = negInf
-  | sum (map round counts :: [Int]) /= n = negInf
-  | any (< 0) counts = negInf
-  | any (\al -> al <= 0) alpha = negInf
-  | otherwise =
-      let nA       = realToFrac (fromIntegral n :: Double) :: a
-          a0       = sum alpha
-          logFactN = realToFrac (logFactorial n) :: a
-          logFactSum = sum
-            [ realToFrac (logFactorial (round c :: Int)) :: a | c <- counts ]
-          term = sum
-            [ lgammaApprox (realToFrac c + ai)  -- yᵢ + αᵢ
-              - lgammaApprox ai
-            | (c, ai) <- zip counts alpha
-            ]
-      in lgammaApprox a0
-       - lgammaApprox (a0 + nA)
-       + term
-       + logFactN
-       - logFactSum
-
--- | Wishart(ν, V) の 1 観測 (k×k 行列を flatten した長さ k² の列) の log density
---   (Phase 39-A2)。
---   @logpdf(W) = -(νk/2) log 2 - (ν/2) log|V| - log Γ_k(ν/2)
---              + ((ν-k-1)/2) log|W| - (1/2) tr(V⁻¹ W)@、
---   @log Γ_k(z) = (k(k-1)/4) log π + Σ_{i=1}^k log Γ((z+1-i)/2)@。
---   V / W の Cholesky で log determinant と tr(V⁻¹ W) を評価。
-{-# INLINABLE wishartLogDensity #-}
-wishartLogDensity :: forall a. (Floating a, Ord a)
-                  => a -> [[a]] -> [a] -> a
-wishartLogDensity nu vRows wFlat
-  | nu <= fromIntegral (k - 1) = negInf
-  | length wFlat /= k * k      = negInf
-  | otherwise =
-      case (choleskyL vRows, choleskyL wRows) of
-        (Just lV, Just lW) ->
-          let logDetV = 2 * sum [ log ((lV !! i) !! i) | i <- [0 .. k - 1] ]
-              logDetW = 2 * sum [ log ((lW !! i) !! i) | i <- [0 .. k - 1] ]
-              -- tr(V⁻¹ W) を列ごとに solve V z_j = w_j で計算
-              wCols   = [ [ (wRows !! i) !! j | i <- [0 .. k - 1] ]
-                        | j <- [0 .. k - 1] ]
-              solveV b =
-                let y = forwardSub lV b
-                    x = backSubLT lV y     -- Lᵀ x = y
-                in x
-              traceVW = sum [ solveV (wCols !! j) !! j
-                            | j <- [0 .. k - 1] ]
-              kA      = fromIntegral k :: a
-              -- log Γ_k(ν/2)
-              logMvGam =
-                (kA * (kA - 1) / 4) * log pi
-                + sum [ lgammaApprox ((nu + 1 - fromIntegral i) / 2)
-                      | i <- [1 .. k] ]
-          in -(nu * kA / 2) * log 2
-           - (nu / 2) * logDetV
-           - logMvGam
-           + ((nu - kA - 1) / 2) * logDetW
-           - 0.5 * traceVW
-        _ -> negInf
-  where
-    k     = length vRows
-    wRows = chunksOf k wFlat
-
--- ---------------------------------------------------------------------------
--- 多相 CDF / log-CDF (Truncated / Censored 用)
--- ---------------------------------------------------------------------------
-
--- | 多相 erf 近似 (Abramowitz & Stegun 7.1.26)。誤差 < 1.5e-7。
--- AD でも Track でも動く。
-{-# INLINABLE erfA #-}
-erfA :: (Floating a, Ord a) => a -> a
-erfA x =
-  let p   = 0.3275911
-      a1  = 0.254829592
-      a2  = -0.284496736
-      a3  = 1.421413741
-      a4  = -1.453152027
-      a5  = 1.061405429
-      sgn = if x < 0 then -1 else 1
-      ax  = abs x
-      t   = 1 / (1 + p * ax)
-      poly = a1*t + a2*t*t + a3*t*t*t + a4*t*t*t*t + a5*t*t*t*t*t
-  in sgn * (1 - poly * exp (- ax * ax))
-
--- | 標準正規 CDF Φ(x)。
-{-# INLINABLE phiCdfA #-}
-phiCdfA :: (Floating a, Ord a) => a -> a
-phiCdfA x = 0.5 * (1 + erfA (x / sqrt 2))
-
--- | CDF @F(x) = P(Y ≤ x)@ of a 'Distribution'. Returns 'Nothing' for
--- distributions that do not have a closed-form CDF in this library.
-{-# INLINABLE distCDF #-}
-distCDF :: (Floating a, Ord a) => Distribution a -> a -> Maybe a
-distCDF (Normal mu sig) x
-  | sig <= 0  = Nothing
-  | otherwise = Just (phiCdfA ((x - mu) / sig))
-distCDF (Exponential rate) x
-  | rate <= 0 = Nothing
-  | x <= 0    = Just 0
-  | otherwise = Just (1 - exp (-rate * x))
-distCDF (LogNormal mu sig) x
-  | sig <= 0 || x <= 0 = Nothing
-  | otherwise = Just (phiCdfA ((log x - mu) / sig))
-distCDF (Uniform lo hi) x
-  | hi <= lo  = Nothing
-  | x <= lo   = Just 0
-  | x >= hi   = Just 1
-  | otherwise = Just ((x - lo) / (hi - lo))
-distCDF (HalfNormal sig) x
-  | sig <= 0 = Nothing
-  | x <= 0   = Just 0
-  | otherwise = Just (erfA (x / (sig * sqrt 2)))
-distCDF (HalfCauchy sc) x
-  | sc <= 0 = Nothing
-  | x <= 0  = Just 0
-  | otherwise = Just (2 * atan (x / sc) / pi)
-distCDF (Cauchy loc sc) x
-  | sc <= 0   = Nothing
-  | otherwise = Just (0.5 + atan ((x - loc) / sc) / pi)
-distCDF (Gamma shape rate) x
-  | shape <= 0 || rate <= 0 = Nothing
-  | x <= 0                  = Just 0
-  | otherwise               = Just (incGammaPA shape (rate * x))
-distCDF (Beta a b) x
-  | a <= 0 || b <= 0 = Nothing
-  | x <= 0           = Just 0
-  | x >= 1           = Just 1
-  | otherwise        = Just (incBetaA x a b)
-distCDF (StudentT df mu sig) x
-  | df <= 0 || sig <= 0 = Nothing
-  | otherwise =
-      let z     = (x - mu) / sig
-          -- F_t(z; df) = 1 - 0.5 * I(df/(df+z²); df/2, 1/2)   (z >= 0)
-          --            =     0.5 * I(df/(df+z²); df/2, 1/2)   (z <  0)
-          ratio = df / (df + z * z)
-          ix    = incBetaA ratio (df / 2) 0.5
-      in Just (if z >= 0 then 1 - 0.5 * ix else 0.5 * ix)
-distCDF (Logistic mu s) x
-  | s <= 0    = Nothing
-  | otherwise = Just (1 / (1 + exp (-((x - mu) / s))))
-distCDF (Gumbel mu beta) x
-  | beta <= 0 = Nothing
-  | otherwise = Just (exp (- exp (-((x - mu) / beta))))
-distCDF (AsymmetricLaplace b kappa mu) x
-  | b <= 0 || kappa <= 0 = Nothing
-  | otherwise =
-      let k2  = kappa * kappa
-          pc  = k2 / (1 + k2)  -- F(μ)
-          d   = x - mu
-      in if d < 0
-           then Just (pc * exp ((b / kappa) * d))
-           else Just (1 - (1 - pc) * exp (- b * kappa * d))
-distCDF _ _ = Nothing  -- SkewNormal / 離散・Mixture・Truncated 内 Truncated 等は未対応
-
--- | @log F(x)@. Computed as @log(F)@ directly to avoid loss of
--- precision near the tails where @F@ approaches 0 or 1.
-{-# INLINABLE logCDF #-}
-logCDF :: (Floating a, Ord a) => Distribution a -> a -> a
-logCDF d x = case distCDF d x of
-  Nothing -> negInf
-  Just c | c <= 0    -> negInf
-         | c >= 1    -> 0
-         | otherwise -> log c
-
--- | Log of the right-tail survival function @log(1 − F(x))@.
-{-# INLINABLE logSF #-}
-logSF :: (Floating a, Ord a) => Distribution a -> a -> a
-logSF d x = case distCDF d x of
-  Nothing -> negInf
-  Just c | c <= 0    -> 0
-         | c >= 1    -> negInf
-         | otherwise -> log (1 - c)
-
--- | log(F(hi) − F(lo)) — Truncated の正規化定数。
-{-# INLINABLE logCDFInterval #-}
-logCDFInterval :: (Floating a, Ord a) => Distribution a -> Maybe a -> Maybe a -> a
-logCDFInterval d mLo mHi = case (mLo, mHi) of
-  (Nothing, Nothing) -> 0  -- log(1)
-  (Just lo, Nothing) -> logSF d lo
-  (Nothing, Just hi) -> logCDF d hi
-  (Just lo, Just hi) ->
-    case (distCDF d lo, distCDF d hi) of
-      (Just cl, Just ch)
-        | ch <= cl  -> negInf
-        | otherwise -> log (ch - cl)
-      _ -> negInf
-
--- ---------------------------------------------------------------------------
--- 多相 log 密度 (事前 logDensity + 観測 logDensityObs/obsLogSum)
--- ---------------------------------------------------------------------------
--- Phase 58.6: 元 HBM.hs の「事前 log 密度」節 (logDensity は 58.3 で AD 勾配と
--- 同居のため残置していたが、 logJoint/logPrior が参照するため Eval 抽出 (58.6c) で
--- back-edge になる。 密度は本来 Distribution の責務 (Phase 58 計画の module sketch)
--- ゆえここへ集約する。 INLINABLE は AD 経路の cross-module inlining 維持のため保持。
-
--- | Log prior density at a sample value of type @a@.
-{-# INLINABLE logDensity #-}
-logDensity :: (Floating a, Ord a) => Distribution a -> a -> a
-logDensity (Normal mu sig) x
-  | sig <= 0  = negInf
-  | otherwise = -0.5 * log (2 * pi) - log sig
-              - 0.5 * ((x - mu) / sig) ^ (2::Int)
-logDensity (Exponential rate) x
-  | x < 0 || rate <= 0 = negInf
-  | otherwise          = log rate - rate * x
-logDensity (Gamma shape rate) x
-  | x <= 0 || shape <= 0 || rate <= 0 = negInf
-  | otherwise =
-      (shape - 1) * log x - rate * x
-      + shape * log rate - lgammaApprox shape
-logDensity (Beta alpha beta) x
-  | x <= 0 || x >= 1 || alpha <= 0 || beta <= 0 = negInf
-  | otherwise =
-      (alpha - 1) * log x + (beta - 1) * log (1 - x)
-      - (lgammaApprox alpha + lgammaApprox beta - lgammaApprox (alpha + beta))
-logDensity (Poisson lam) x
-  | lam <= 0 = negInf
-  | x  < 0   = negInf
-  | otherwise =
-      -- x はサンプル値なので連続として扱う (整数化はしない)
-      x * log lam - lam
-logDensity (Binomial _ p) _
-  | p <= 0 || p >= 1 = negInf
-  | otherwise        = 0  -- サンプル時は使わない (構造のみ)
-logDensity (Uniform lo hi) x
-  | hi <= lo            = negInf
-  | x  < lo || x  > hi  = negInf
-  | otherwise           = -log (hi - lo)
-logDensity (StudentT df mu sig) x
-  | df <= 0 || sig <= 0 = negInf
-  | otherwise =
-      let z = (x - mu) / sig
-      in lgammaApprox ((df + 1) / 2)
-       - lgammaApprox (df / 2)
-       - 0.5 * log (df * pi)
-       - log sig
-       - ((df + 1) / 2) * log (1 + z * z / df)
-logDensity (Cauchy loc sc) x
-  | sc <= 0   = negInf
-  | otherwise =
-      let z = (x - loc) / sc
-      in -log pi - log sc - log (1 + z * z)
-logDensity (HalfNormal sig) x
-  | sig <= 0 = negInf
-  | x < 0    = negInf
-  | otherwise =
-      0.5 * log 2 - 0.5 * log pi - log sig
-      - 0.5 * (x / sig) ^ (2::Int)
-logDensity (HalfCauchy sc) x
-  | sc <= 0 = negInf
-  | x < 0   = negInf
-  | otherwise =
-      log 2 - log pi - log sc - log (1 + (x / sc) ^ (2::Int))
-logDensity (LogNormal mu sig) x
-  | sig <= 0 = negInf
-  | x  <= 0  = negInf
-  | otherwise =
-      let lx = log x
-      in -0.5 * log (2 * pi) - log sig - lx
-         - 0.5 * ((lx - mu) / sig) ^ (2::Int)
-logDensity (Bernoulli p) _
-  | p <= 0 || p >= 1 = negInf
-  | otherwise        = 0  -- 構造のみ (離散なので連続 prior 評価には使わない)
-logDensity (Categorical _) _ = 0  -- 同上
-logDensity (Mixture ws comps) x
-  | null ws || length ws /= length comps = negInf
-  | otherwise =
-      let total      = sum ws
-          logTotal   = log total
-          -- log(w_k / Σw) + log p_k(x)
-          logTerms   = zipWith (\w d -> log w - logTotal + logDensity d x) ws comps
-      in logSumExpA logTerms
-logDensity (Truncated d mLo mHi) x =
-  -- 範囲外なら 0 (=> log で −∞)
-  let outOfRange = case (mLo, mHi) of
-        (Just lo, _      ) | x < lo  -> True
-        (_,       Just hi) | x > hi  -> True
-        _                            -> False
-  in if outOfRange
-       then negInf
-       else logDensity d x - logCDFInterval d mLo mHi
-logDensity (Censored d _ _) x =
-  -- prior 評価では通常の密度を使う (打ち切りは観測時のみ意味を持つ)
-  logDensity d x
-logDensity MvNormal{} _ = 0  -- observation-only: latent としては使わない
-logDensity MvNormalChol{} _ = 0  -- observation-only
-logDensity MvNormalGpRBF{} _ = 0  -- observation-only (Phase 95 B-dsl)
-logDensity HmmForwardNormal{} _ = 0  -- observation-only (Phase 92 A2)
-logDensity ArmaNormal{} _ = 0  -- observation-only (Phase 101 A2)
-logDensity GradedResponseIrt{} _ = 0  -- observation-only (Phase 101 A3)
-logDensity Multinomial{} _ = 0  -- observation-only
-logDensity (InverseGamma alpha beta) x
-  | alpha <= 0 || beta <= 0 || x <= 0 = negInf
-  | otherwise =
-      alpha * log beta - lgammaApprox alpha
-      - (alpha + 1) * log x - beta / x
-logDensity (Weibull kShape lam) x
-  | kShape <= 0 || lam <= 0 || x <= 0 = negInf
-  | otherwise =
-      log kShape - log lam
-      + (kShape - 1) * (log x - log lam)
-      - (x / lam) ** kShape
-logDensity (Pareto alpha xm) x
-  | alpha <= 0 || xm <= 0 || x < xm = negInf
-  | otherwise =
-      log alpha + alpha * log xm - (alpha + 1) * log x
-logDensity BetaBinomial{} _ = 0  -- 観測専用 (離散)
-logDensity (VonMises mu kappa) x
-  | kappa <= 0 = negInf
-  | otherwise =
-      kappa * cos (x - mu)
-      - log (2 * pi)
-      - logBesselI0 kappa
-logDensity (ZeroInflatedPoisson psi lam) x
-  | psi < 0 || psi > 1 || lam <= 0 || x < 0 = negInf
-  | x == 0 =
-      -- log(ψ + (1-ψ) e^{-λ})
-      logSumExpA [log psi, log (1 - psi) - lam]
-  | otherwise =
-      -- log(1-ψ) + Poisson logpmf
-      log (1 - psi) + x * log lam - lam - lgammaApprox (x + 1)
-logDensity (ZeroInflatedBinomial n psi p) x
-  | psi < 0 || psi > 1 || p <= 0 || p >= 1 || x < 0 = negInf
-  | otherwise =
-      let nA   = realToFrac (fromIntegral n :: Double)
-          -- log(C(n,k)) = lgamma(n+1) - lgamma(k+1) - lgamma(n-k+1) (多相)
-          logC = lgammaApprox (nA + 1)
-               - lgammaApprox (x + 1)
-               - lgammaApprox (nA - x + 1)
-      in if x == 0
-           then logSumExpA [log psi
-                           , log (1 - psi) + nA * log (1 - p)]
-           else log (1 - psi)
-                + logC + x * log p + (nA - x) * log (1 - p)
-logDensity (NegativeBinomial mu alpha) x
-  | mu <= 0 || alpha <= 0 || x < 0 = negInf
-  | otherwise =
-      let p = alpha / (alpha + mu)        -- success prob
-      in lgammaApprox (x + alpha)
-       - lgammaApprox alpha
-       - lgammaApprox (x + 1)
-       + alpha * log p
-       + x * log (1 - p)
-logDensity (SkewNormal mu sig alpha) x
-  | sig <= 0  = negInf
-  | otherwise =
-      let z      = (x - mu) / sig
-          logPhi = -0.5 * log (2 * pi) - 0.5 * z * z
-          -- log Φ(αz) を phiCdfA 経由で。 引数が大きく負だと数値的に困るが、
-          -- phiCdfA は erfA ベースなので clip して log を取る
-          cdfArg = phiCdfA (alpha * z)
-          -- 数値下限 1e-300 程度に防御
-          logCdf = log (max cdfArg 1e-300)
-      in log 2 - log sig + logPhi + logCdf
-logDensity (Logistic mu s) x
-  | s <= 0    = negInf
-  | otherwise =
-      let z = (x - mu) / s
-      in -z - log s - 2 * log (1 + exp (-z))
-logDensity (Gumbel mu beta) x
-  | beta <= 0 = negInf
-  | otherwise =
-      let z = (x - mu) / beta
-      in -log beta - z - exp (-z)
-logDensity (AsymmetricLaplace b kappa mu) x
-  | b <= 0 || kappa <= 0 = negInf
-  | otherwise =
-      let logNorm = log b - log (kappa + 1 / kappa)
-          d       = x - mu
-      in if d >= 0
-           then logNorm - b * kappa * d
-           else logNorm + (b / kappa) * d
--- 離散分布は構造のみ (observation-only の意味で logDensity は使われない)
-logDensity OrderedLogistic{} _      = 0
-logDensity DiscreteUniform{} _      = 0
-logDensity (Geometric p) _
-  | p <= 0 || p >= 1 = negInf
-  | otherwise        = 0
-logDensity HyperGeometric{} _       = 0
-logDensity (ZeroInflatedNegativeBinomial psi mu alpha) _
-  | psi < 0 || psi > 1 || mu <= 0 || alpha <= 0 = negInf
-  | otherwise = 0
-logDensity MvStudentT{} _ = 0          -- observation-only
-logDensity DirichletMultinomial{} _ = 0  -- observation-only
-logDensity (Triangular lo c hi) x
-  | hi <= lo || c < lo || c > hi = negInf
-  | x < lo || x > hi             = negInf
-  | x <= c =
-      log 2 + log (x - lo)
-      - log (hi - lo) - log (c - lo)
-  | otherwise =
-      log 2 + log (hi - x)
-      - log (hi - lo) - log (hi - c)
-logDensity (Kumaraswamy a b) x
-  | a <= 0 || b <= 0 || x <= 0 || x >= 1 = negInf
-  | otherwise =
-      let xa = x ** a
-      in log a + log b + (a - 1) * log x + (b - 1) * log (1 - xa)
-logDensity (Rice nu sig) x
-  | sig <= 0 || nu < 0 || x < 0 = negInf
-  | otherwise =
-      let s2 = sig * sig
-          z  = x * nu / s2
-      in log x - 2 * log sig - (x * x + nu * nu) / (2 * s2)
-         + logBesselI0 z
-logDensity DiscreteWeibull{} _ = 0   -- 離散: structure only
-logDensity Wishart{} _ = 0           -- observation-only (k×k 行列観測)
-logDensity (Bound d mLo mHi) x = logDensity (Truncated d mLo mHi) x
-logDensity OrderedProbit{} _ = 0     -- observation-only (離散)
-
--- | 'logDensity' の AD ('ADRD.ReverseDouble') 特化版 (Phase 92 B3)。
--- hyperparameter が**定数** ('ADRD.Zero' / 'ADRD.Lift' = tape 由来でない) の
--- lgamma 正規化項を Double で 1 発計算して 'ADRD.Lift' で戻す。 'ADRD.Lift'
--- 同士の AD 演算は @Lift (f b c)@ (同一の Double 演算列・tape 追記なし) なので
--- 結果は generic 'logDensity' と **bit-identical**、 定数に勾配は流れないので
--- 微分も不変。 hyperparameter が tape 変数 (階層 prior) なら generic へ
--- fallback し勾配は AD がそのまま構成する。
---
--- 動機 (hmm reduced prof): Dirichlet(1,…,1) = 棒折り Beta(1,1) の定数濃度
--- lgamma が AD walk 上で毎 eval Stirling recurrence (z<12 の梯子 ~11 段 ×
--- lgamma 3 呼び出し) を boxed 'ADRD.Lift' で歩いていた (550,480 entries =
--- 70 call/eval・time 6.6%/alloc 14.4%)。 対象は lgamma を持つ定数 prior 3 種
--- (Beta / Gamma / StudentT の ν) のみ・折り畳み式の結合順は generic 実装と
--- 完全一致させてある (bit 一致の根拠)。
--- ※ 'lgammaApprox' への RULES 書き換えは過負荷関数 + 辞書引数で発火せず断念
---    (2026-07-17 実測)、 呼び出し点注入 ('logPriorWith') 方式にした。
-logDensityRD
-  :: forall s. Reifies s ADRD.Tape
-  => Distribution (ADRD.ReverseDouble s) -> ADRD.ReverseDouble s
-  -> ADRD.ReverseDouble s
-logDensityRD d x = case d of
-  Beta a b
-    | Just a' <- constRD a, Just b' <- constRD b
-    , not (x <= 0 || x >= 1 || a' <= 0 || b' <= 0) ->
-        (a - 1) * log x + (b - 1) * log (1 - x)
-          - ADRD.Lift (lgammaApprox a' + lgammaApprox b' - lgammaApprox (a' + b'))
-  Gamma sh ra
-    | Just sh' <- constRD sh, Just ra' <- constRD ra
-    , not (x <= 0 || sh' <= 0 || ra' <= 0) ->
-        (sh - 1) * log x - ra * x
-          + ADRD.Lift (sh' * log ra') - ADRD.Lift (lgammaApprox sh')
-  StudentT df mu sig
-    | Just df' <- constRD df
-    , not (df' <= 0 || sig <= 0) ->
-        let z = (x - mu) / sig
-        in ADRD.Lift (lgammaApprox ((df' + 1) / 2) - lgammaApprox (df' / 2)
-                        - 0.5 * log (df' * pi))
-           - log sig
-           - ((df + 1) / 2) * log (1 + z * z / df)
-  _ -> logDensity d x
-  where
-    -- Zero/Lift = tape に乗らない定数 (Lift 同士の演算は Lift に閉じる)
-    constRD :: ADRD.ReverseDouble s -> Maybe Double
-    constRD ADRD.Zero             = Just 0
-    constRD (ADRD.Lift v)         = Just v
-    constRD ADRD.ReverseDouble{}  = Nothing
-
--- | Log likelihood density at an observation (a fixed @Double@).
--- Observations are passed as @[Double]@, so this uses only the
--- @Floating a@ constraint.
--- Phase 58.6c: ObserveLM 評価 (lmObsLogLiks) と logJoint が AD で微分しながら呼ぶ
--- ホット経路。 58.6a で本体から移したため cross-module になった。 INLINABLE で
--- 境界跨ぎ inline を維持 (M1/M2 の +25% 劣化を解消・58.6 bench 実測)。
-{-# INLINABLE logDensityObs #-}
-logDensityObs :: forall a. (Floating a, Ord a) => Distribution a -> Double -> a
-logDensityObs (Normal mu sig) y
-  | sig <= 0  = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-      in -0.5 * log (2 * pi) - log sig - 0.5 * ((yA - mu) / sig) ^ (2::Int)
-logDensityObs (Exponential rate) y
-  | y < 0      = negInf
-  | rate <= 0  = negInf
-  | otherwise  = log rate - rate * (realToFrac y :: a)
-logDensityObs (Gamma shape rate) y
-  | y <= 0     = negInf
-  | shape <= 0 || rate <= 0 = negInf
-  | otherwise  =
-      let yA = realToFrac y :: a
-      in (shape - 1) * log yA - rate * yA
-         + shape * log rate - lgammaApprox shape
-logDensityObs (Beta alpha beta) y
-  | y <= 0 || y >= 1 || alpha <= 0 || beta <= 0 = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-      in (alpha - 1) * log yA + (beta - 1) * log (1 - yA)
-         - (lgammaApprox alpha + lgammaApprox beta - lgammaApprox (alpha + beta))
-logDensityObs (Poisson lam) y
-  | lam <= 0 = negInf
-  | y < 0    = negInf
-  | otherwise =
-      let kA   = realToFrac y :: a
-          kInt = round y :: Int
-          logFactK = realToFrac (logFactorial kInt) :: a
-      in kA * log lam - lam - logFactK
-logDensityObs (Binomial n p) y
-  | p <= 0 || p >= 1 = negInf
-  | otherwise =
-      let k    = round y :: Int
-          kA   = realToFrac y :: a
-          nA   = realToFrac (fromIntegral n :: Double) :: a
-          logC = realToFrac (logBinomCoeff n k) :: a
-      in logC + kA * log p + (nA - kA) * log (1 - p)
-logDensityObs (Uniform lo hi) y
-  | hi <= lo  = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-      in if yA < lo || yA > hi then negInf else -log (hi - lo)
-logDensityObs (StudentT df mu sig) y
-  | df <= 0 || sig <= 0 = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-          z  = (yA - mu) / sig
-      in lgammaApprox ((df + 1) / 2)
-       - lgammaApprox (df / 2)
-       - 0.5 * log (df * pi)
-       - log sig
-       - ((df + 1) / 2) * log (1 + z * z / df)
-logDensityObs (Cauchy loc sc) y
-  | sc <= 0   = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-          z  = (yA - loc) / sc
-      in -log pi - log sc - log (1 + z * z)
-logDensityObs (HalfNormal sig) y
-  | sig <= 0 = negInf
-  | y  < 0   = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-      in 0.5 * log 2 - 0.5 * log pi - log sig
-       - 0.5 * (yA / sig) ^ (2::Int)
-logDensityObs (HalfCauchy sc) y
-  | sc <= 0 = negInf
-  | y  < 0  = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-      in log 2 - log pi - log sc - log (1 + (yA / sc) ^ (2::Int))
-logDensityObs (LogNormal mu sig) y
-  | sig <= 0 = negInf
-  | y  <= 0  = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-          lx = log yA
-      in -0.5 * log (2 * pi) - log sig - lx
-       - 0.5 * ((lx - mu) / sig) ^ (2::Int)
-logDensityObs (Bernoulli p) y
-  | p <= 0 || p >= 1 = negInf
-  | otherwise =
-      let k = round y :: Int
-      in case k of
-           1 -> log p
-           0 -> log (1 - p)
-           _ -> negInf
-logDensityObs (Categorical probs) y =
-  let k    = round y :: Int
-      n    = length probs
-  in if k < 0 || k >= n
-       then negInf
-       else
-         -- log p_k - log(Σ p_i)  (probs を正規化)
-         let pk     = probs !! k
-             total  = sum probs
-         in if pk <= 0 || total <= 0
-              then negInf
-              else log pk - log total
-logDensityObs (Mixture ws comps) y
-  | null ws || length ws /= length comps = negInf
-  | otherwise =
-      let total    = sum ws
-          logTotal = log total
-          logTerms = zipWith (\w d -> log w - logTotal + logDensityObs d y) ws comps
-      in logSumExpA logTerms
-logDensityObs (Truncated d mLo mHi) y =
-  let yA = realToFrac y :: a
-      outOfRange = case (mLo, mHi) of
-        (Just lo, _      ) | yA < lo  -> True
-        (_,       Just hi) | yA > hi  -> True
-        _                             -> False
-  in if outOfRange
-       then negInf
-       else logDensityObs d y - logCDFInterval d mLo mHi
-logDensityObs (Censored d mLo mHi) y =
-  -- 観測値 y が境界 lo / hi に等しい場合は左/右打ち切り尤度
-  let yA = realToFrac y :: a
-      eps = 1e-9 :: a
-      isAt v target = abs (v - target) < eps
-  in case (mLo, mHi) of
-       (Just lo, _) | yA <= lo || isAt yA lo -> logCDF d lo                -- 左打ち切り
-       (_, Just hi) | yA >= hi || isAt yA hi -> logSF  d hi                -- 右打ち切り
-       _                                     -> logDensityObs d y          -- 通常観測
-logDensityObs MvNormal{} _ = 0
-logDensityObs MvNormalChol{} _ = 0
-logDensityObs MvNormalGpRBF{} _ = 0  -- Phase 95 B-dsl: obsLogSum 経由 (下と同じ)
-logDensityObs HmmForwardNormal{} _ = 0  -- Phase 92 A2: obsLogSum 経由 (下と同じ)
-logDensityObs ArmaNormal{} _ = 0  -- Phase 101 A2: obsLogSum 経由 (下と同じ)
-logDensityObs GradedResponseIrt{} _ = 0  -- Phase 101 A3: obsLogSum 経由 (下と同じ)
-  -- スカラー観測経路では使わない (chunk して 'mvNormalLogDensity' を呼ぶ obsLogSum 経由)
-logDensityObs Multinomial{} _ = 0
-  -- スカラー観測経路では使わない (k 次元 chunk で multinomialLogDensity を呼ぶ)
-logDensityObs (InverseGamma alpha beta) y
-  | alpha <= 0 || beta <= 0 || y <= 0 = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-      in alpha * log beta - lgammaApprox alpha
-       - (alpha + 1) * log yA - beta / yA
-logDensityObs (Weibull kShape lam) y
-  | kShape <= 0 || lam <= 0 || y <= 0 = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-      in log kShape - log lam
-       + (kShape - 1) * (log yA - log lam)
-       - (yA / lam) ** kShape
-logDensityObs (Pareto alpha xm) y
-  | alpha <= 0 || xm <= 0 = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-      in if yA < xm
-           then negInf
-           else log alpha + alpha * log xm - (alpha + 1) * log yA
-logDensityObs (BetaBinomial n alpha beta) y
-  | alpha <= 0 || beta <= 0 || y < 0 = negInf
-  | otherwise =
-      let yA   = realToFrac y :: a
-          nA   = realToFrac (fromIntegral n :: Double) :: a
-          k    = round y :: Int
-          logC = realToFrac (logBinomCoeff n k) :: a
-      in logC
-       + lgammaApprox (yA + alpha)
-       + lgammaApprox (nA - yA + beta)
-       - lgammaApprox (nA + alpha + beta)
-       - (lgammaApprox alpha + lgammaApprox beta - lgammaApprox (alpha + beta))
-logDensityObs (VonMises mu kappa) y
-  | kappa <= 0 = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-      in kappa * cos (yA - mu) - log (2 * pi) - logBesselI0 kappa
-logDensityObs (ZeroInflatedPoisson psi lam) y
-  | psi < 0 || psi > 1 || lam <= 0 || y < 0 = negInf
-  | y == 0 =
-      logSumExpA [log psi, log (1 - psi) - lam]
-  | otherwise =
-      let kA       = realToFrac y :: a
-          kInt     = round y :: Int
-          logFactK = realToFrac (logFactorial kInt) :: a
-      in log (1 - psi) + kA * log lam - lam - logFactK
-logDensityObs (ZeroInflatedBinomial n psi p) y
-  | psi < 0 || psi > 1 || p <= 0 || p >= 1 || y < 0 = negInf
-  | otherwise =
-      let kA   = realToFrac y :: a
-          k    = round y :: Int
-          nA   = realToFrac (fromIntegral n :: Double) :: a
-          logC = realToFrac (logBinomCoeff n k) :: a
-      in if y == 0
-           then logSumExpA [log psi
-                           , log (1 - psi) + nA * log (1 - p)]
-           else log (1 - psi)
-                + logC + kA * log p + (nA - kA) * log (1 - p)
-logDensityObs (NegativeBinomial mu alpha) y
-  | mu <= 0 || alpha <= 0 || y < 0 = negInf
-  | otherwise =
-      let kA = realToFrac y :: a
-          p  = alpha / (alpha + mu)
-      in lgammaApprox (kA + alpha)
-       - lgammaApprox alpha
-       - lgammaApprox (kA + 1)
-       + alpha * log p
-       + kA * log (1 - p)
-logDensityObs (SkewNormal mu sig alpha) y
-  | sig <= 0 = negInf
-  | otherwise =
-      let yA     = realToFrac y :: a
-          z      = (yA - mu) / sig
-          logPhi = -0.5 * log (2 * pi) - 0.5 * z * z
-          cdfArg = phiCdfA (alpha * z)
-          logCdf = log (max cdfArg 1e-300)
-      in log 2 - log sig + logPhi + logCdf
-logDensityObs (Logistic mu s) y
-  | s <= 0 = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-          z  = (yA - mu) / s
-      in -z - log s - 2 * log (1 + exp (-z))
-logDensityObs (Gumbel mu beta) y
-  | beta <= 0 = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-          z  = (yA - mu) / beta
-      in -log beta - z - exp (-z)
-logDensityObs (AsymmetricLaplace b kappa mu) y
-  | b <= 0 || kappa <= 0 = negInf
-  | otherwise =
-      let yA      = realToFrac y :: a
-          logNorm = log b - log (kappa + 1 / kappa)
-          d       = yA - mu
-      in if d >= 0
-           then logNorm - b * kappa * d
-           else logNorm + (b / kappa) * d
-logDensityObs (OrderedLogistic eta cuts) y
-  | null cuts                 = negInf
-  | k < 0 || k > kMax         = negInf
-  | otherwise =
-      -- σ(c_{k+1} − η) − σ(c_k − η)、 c_0 = −∞、 c_K = +∞
-      let sigm x  = 1 / (1 + exp (-x))
-          kMax_a  = kMax  -- 上限カテゴリ index
-          probHi
-            | k == kMax_a = 1
-            | otherwise   = sigm (cuts !! k - eta)
-          probLo
-            | k == 0    = 0
-            | otherwise = sigm (cuts !! (k - 1) - eta)
-          pK = probHi - probLo
-      in if pK <= 0 then negInf else log pK
-  where
-    k    = round y :: Int
-    kMax = length cuts
-logDensityObs (DiscreteUniform lo hi) y
-  | hi < lo                = negInf
-  | yI < lo || yI > hi     = negInf
-  | otherwise              = -log (realToFrac (hi - lo + 1) :: a)
-  where
-    yI = round y :: Int
-logDensityObs (Geometric p) y
-  | p <= 0 || p >= 1 = negInf
-  | yI < 1           = negInf
-  | otherwise =
-      let kA = realToFrac y :: a
-      in (kA - 1) * log (1 - p) + log p
-  where
-    yI = round y :: Int
-logDensityObs (HyperGeometric nN kK nDraw) y
-  | nN <= 0 || kK < 0 || kK > nN || nDraw < 0 || nDraw > nN = negInf
-  | yI < max 0 (nDraw + kK - nN) || yI > min nDraw kK       = negInf
-  | otherwise =
-      let lc = realToFrac (logBinomCoeff kK yI
-                         + logBinomCoeff (nN - kK) (nDraw - yI)
-                         - logBinomCoeff nN nDraw) :: a
-      in lc
-  where
-    yI = round y :: Int
-logDensityObs (ZeroInflatedNegativeBinomial psi mu alpha) y
-  | psi < 0 || psi > 1 || mu <= 0 || alpha <= 0 || y < 0 = negInf
-  | y == 0 =
-      -- log(ψ + (1-ψ) (α/(α+μ))^α)
-      let p0NB = alpha * (log alpha - log (alpha + mu))
-      in logSumExpA [log psi, log (1 - psi) + p0NB]
-  | otherwise =
-      let kA = realToFrac y :: a
-          p  = alpha / (alpha + mu)
-          logNB = lgammaApprox (kA + alpha)
-                - lgammaApprox alpha
-                - lgammaApprox (kA + 1)
-                + alpha * log p
-                + kA * log (1 - p)
-      in log (1 - psi) + logNB
-logDensityObs MvStudentT{} _ = 0
-  -- スカラー観測経路では使わない (k chunk で mvStudentTLogDensity 経由)
-logDensityObs DirichletMultinomial{} _ = 0
-  -- スカラー観測経路では使わない (K chunk で dirichletMultinomialLogDensity 経由)
-logDensityObs (Triangular lo c hi) y
-  | hi <= lo || c < lo || c > hi = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-      in if yA < lo || yA > hi
-           then negInf
-           else if yA <= c
-             then log 2 + log (yA - lo)
-                  - log (hi - lo) - log (c - lo)
-             else log 2 + log (hi - yA)
-                  - log (hi - lo) - log (hi - c)
-logDensityObs (Kumaraswamy a b) y
-  | a <= 0 || b <= 0 = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-      in if yA <= 0 || yA >= 1
-           then negInf
-           else let xa = yA ** a
-                in log a + log b + (a - 1) * log yA + (b - 1) * log (1 - xa)
-logDensityObs (Rice nu sig) y
-  | sig <= 0 || nu < 0 || y < 0 = negInf
-  | otherwise =
-      let yA = realToFrac y :: a
-          s2 = sig * sig
-          z  = yA * nu / s2
-      in log yA - 2 * log sig - (yA * yA + nu * nu) / (2 * s2)
-         + logBesselI0 z
-logDensityObs Wishart{} _ = 0
-  -- スカラー観測経路では使わない (k² chunk で wishartLogDensity 経由)
-logDensityObs (Bound d mLo mHi) y = logDensityObs (Truncated d mLo mHi) y
-logDensityObs (OrderedProbit eta cuts) y
-  | null cuts                 = negInf
-  | k < 0 || k > kMax         = negInf
-  | otherwise =
-      let probHi
-            | k == kMax = 1
-            | otherwise = phiCdfA (cuts !! k - eta)
-          probLo
-            | k == 0    = 0
-            | otherwise = phiCdfA (cuts !! (k - 1) - eta)
-          pK = probHi - probLo
-      in if pK <= 0 then negInf else log pK
-  where
-    k    = round y :: Int
-    kMax = length cuts
-logDensityObs (DiscreteWeibull q beta) y
-  | y < 0 = negInf
-  | otherwise =
-      -- q は (0,1)、 β > 0
-      -- pmf(k) = q^(k^β) - q^((k+1)^β)
-      let qVal :: a
-          qVal = q
-          bVal :: a
-          bVal = beta
-      in if qVal <= 0 || qVal >= 1 || bVal <= 0
-           then negInf
-           else
-             let kI    = round y :: Int
-                 kA    = realToFrac (fromIntegral kI :: Double) :: a
-                 logQ  = log qVal
-                 -- log(q^(k^β) - q^((k+1)^β))
-                 --   = log q^(k^β) + log(1 - q^((k+1)^β - k^β))
-                 -- 安定化: a1 = (k+1)^β - k^β > 0 (β>0)
-                 pk    = kA ** bVal
-                 pk1   = (kA + 1) ** bVal
-                 diffP = pk1 - pk
-                 -- log(1 - q^diffP) = log(1 - exp(diffP * logQ))
-                 -- diffP * logQ <= 0
-                 expArg = diffP * logQ
-                 log1mE = log (1 - exp expArg)
-             in pk * logQ + log1mE
-
--- | Sum of log likelihoods over a list of observations. For ordinary
--- distributions one observation contributes one scalar log-density.
--- For 'MvNormal' (which expects @k@-vectors), the flattened @[Double]@
--- is chunked into length-@k@ groups before evaluation.
--- Phase 58.6c: logJoint/logLikelihood の Observe 分岐が AD で呼ぶ。 cross-module
--- inline 維持のため INLINABLE。
-{-# INLINABLE obsLogSum #-}
-obsLogSum :: forall a. (Floating a, Ord a) => Distribution a -> [Double] -> a
-obsLogSum (MvNormal mu cov) ys =
-  let k       = length mu
-      chunks  = chunksOf k ys
-  in sum [ mvNormalLogDensity mu cov (map realToFrac yv :: [a])
-         | yv <- chunks ]
-obsLogSum (MvNormalGpRBF xs alpha rho sigma) ys =
-  -- Phase 95 B-dsl: zero-mean・cov = RBF カーネル + (1e-10 + σ)·I。 値は汎用
-  -- 'MvNormal' 経路と同値 (ホット勾配のみ 'gpRBFAnalyticVG' で閉形式化)。
-  let k       = length xs
-      cov     = gpRBFCovList xs alpha rho sigma
-      mu      = replicate k 0
-      chunks  = chunksOf k ys
-  in sum [ mvNormalLogDensity mu cov (map realToFrac yv :: [a])
-         | yv <- chunks ]
-obsLogSum (GradedResponseIrt thetas ncats deltas gammas) ys =
-  -- Phase 101 A3: grade 行列 (nChild×nItem 行優先・欠測 −1) 全体を 1 観測として
-  -- 評価。 値は従来の @logCatProb + potential@ 書きと同値
-  -- (ホット勾配のみ 'gradedIrtAnalyticVG' で閉形式化)。
-  let nItem = length ncats
-      rows  = chunksOf nItem ys
-      logCatP th nc dl gm gr =
-        let kMax = nc - 1
-            qs = [ 1 / (1 + exp (negate (realToFrac dl * (th - realToFrac (gm !! (kk - 1))))))
-                 | kk <- [1 .. kMax] ]
-            ps = [ if k == 1 then 1 - head qs
-                   else if k == nc then qs !! (kMax - 1)
-                   else (qs !! (k - 2)) - (qs !! (k - 1))
-                 | k <- [1 .. nc] ]
-        in log (ps !! (gr - 1))
-  in sum [ logCatP th nc dl gm (round gr)
-         | (th, row) <- zip thetas rows
-         , (nc, dl, gm, gr) <- zip4 ncats deltas gammas row
-         , gr /= -1 ]
-obsLogSum (ArmaNormal mu phi theta sg) ys =
-  -- Phase 101 A2: 観測列全体 (長さ T) を 1 観測として err 逐次再帰で評価。
-  -- 値は従来の @mapAccumL + potential@ 書きと同値
-  -- (ホット勾配のみ 'armaAnalyticVG' で閉形式化)。
-  case ys of
-    [] -> 0
-    (y1 : rest) ->
-      let e1 = realToFrac y1 - (mu + phi * mu)
-          step (prevY, prevErr) yt =
-            let err = realToFrac yt - (mu + phi * realToFrac prevY + theta * prevErr)
-            in ((yt, err), err)
-          errs = e1 : snd (mapAccumL step (y1, e1) rest)
-      in sum [ logDensity (Normal 0 sg) e | e <- errs ]
-obsLogSum (HmmForwardNormal pi0 trans mus sg) ys =
-  -- Phase 92 A2: 観測列全体 (長さ T) を 1 観測として forward algorithm で周辺化。
-  -- 値は従来の @potential nm (hmmForwardLogLik pi0 trans emit)@ 書きと同値
-  -- (ホット勾配のみ 'hmmAnalyticVG' で閉形式化)。
-  let emit = [ [ logDensity (Normal mu sg) (realToFrac y) | mu <- mus ] | y <- ys ]
-  in hmmForwardLogLik pi0 trans emit
-obsLogSum (Multinomial n probs) ys =
-  let k      = length probs
-      chunks = chunksOf k ys
-  in sum [ multinomialLogDensity n probs yv | yv <- chunks ]
-obsLogSum (MvNormalChol mu sigma l) ys =
-  let k      = length mu
-      chunks = chunksOf k ys
-  in sum [ mvNormalCholLogDensity mu sigma l (map realToFrac yv :: [a])
-         | yv <- chunks ]
-obsLogSum (MvStudentT nu mu cov) ys =
-  let k      = length mu
-      chunks = chunksOf k ys
-  in sum [ mvStudentTLogDensity nu mu cov (map realToFrac yv :: [a])
-         | yv <- chunks ]
-obsLogSum (DirichletMultinomial n alpha) ys =
-  let k      = length alpha
-      chunks = chunksOf k ys
-  in sum [ dirichletMultinomialLogDensity n alpha yv | yv <- chunks ]
-obsLogSum (Wishart nu vRows) ys =
-  let k       = length vRows
-      chunks  = chunksOf (k * k) ys
-  in sum [ wishartLogDensity nu vRows (map realToFrac yv :: [a])
-         | yv <- chunks ]
-obsLogSum d ys = sum [ logDensityObs d y | y <- ys ]
diff --git a/src/Hanalyze/Model/HBM/Eval.hs b/src/Hanalyze/Model/HBM/Eval.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/HBM/Eval.hs
+++ /dev/null
@@ -1,561 +0,0 @@
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- |
--- Module      : Hanalyze.Model.HBM.Eval
--- Description : HBM のモデル評価層 (log-joint/尤度インタープリタ + DAG 構築)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Phase 58.6c: モデル評価層を 'Hanalyze.Model.HBM' から分離。
---
--- PPL の **評価層** (記述層 'Hanalyze.Model.HBM.Model' の上):
---
---   * 構造化線形予測子 observe ('ObserveLM') の評価 (lmObsLogSum 等)
---   * log-joint / log-prior / log-likelihood の多相インタープリタ
---   * Gibbs 共役検出向けの runObserveDists / priorList
---   * 派生量評価 (runDeterministics / augmentChainWithDeterministic) と
---     DAG 構築 (buildModelGraph / collapseIndexedPlateNodes)
---
--- 依存は下層 Model / Distribution (密度) / Track (extractDeps) / Util / MCMC.Core
--- のみ。 AD 勾配・IR は **上層** に置かれ本モジュールへ依存する (一方向)。
-module Hanalyze.Model.HBM.Eval
-  ( -- * ObserveLM 評価
-    lmObsLogSum
-    -- * Interpreters
-  , logJoint
-  , logPrior
-  , logPriorWith
-  , logLikelihood
-  , perObsLogLiks
-  , runObserveDists
-  , mvNormalObserveOf
-  , priorList
-  , describeModel
-    -- * Type aliases
-  , Params
-    -- * 派生量
-  , runDeterministics
-  , deterministicNames
-  , augmentChainWithDeterministic
-    -- * Model graph (visualization)
-  , ModelGraph (..)
-  , buildModelGraph
-  , collapseIndexedPlateNodes
-  ) where
-
-import Data.List (nub)
-import Data.Map.Strict (Map)
-import qualified Data.Map.Strict as Map
-import qualified Data.Set as Set
-import Data.Text (Text)
-import qualified Data.Text as T
-
-import Hanalyze.MCMC.Core (Chain (..))
-import Hanalyze.Model.HBM.Util (negInf, chunksOf)
-import Hanalyze.Model.HBM.Distribution
-import Hanalyze.Model.HBM.Model
-import Hanalyze.Model.HBM.Track (Track, extractDeps)
-
--- ---------------------------------------------------------------------------
--- ObserveLM (構造化線形予測子 observe) の評価 (Phase 54.1)
--- ---------------------------------------------------------------------------
-
--- | 線形予測子 η_i = Σ_j β_j·X_ij。
--- Phase 58.6c: synthGaussLMBlocks (本体) / IR が AD で微分しながら呼ぶホット経路。
--- monolith では同一モジュール inline されていた。 境界跨ぎで失われると M1/M2 が
--- 約 +25% 劣化する (58.6 bench で実測) ため INLINABLE で cross-module inline を維持。
-{-# INLINABLE lmEta #-}
-lmEta :: Fractional a => [a] -> [Double] -> a
-lmEta betas xrow = sum (zipWith (\b x -> b * realToFrac x) betas xrow)
-
--- | ランダム効果項の per-obs 寄与 @Σ_re w_i·u^{re}[gid_i]@ (長さ n)。
--- 重み @Nothing@ = 全 1 (Phase 54.10)。
-{-# INLINABLE lmReffEta #-}
-lmReffEta :: forall a. Fractional a => [REff] -> Int -> Map Text a -> [a]
-lmReffEta reffs n params =
-  foldr (zipWith (+)) (replicate n 0)
-    [ let uvals = [ Map.findWithDefault 0 nm params | nm <- uNames ]
-          base  = [ uvals !! g | g <- gids ]
-      in case mw of
-           Nothing -> base
-           Just ws -> zipWith (\v w -> v * realToFrac w) base ws
-    | REff uNames gids _ mw _ <- reffs ]
-
--- | 'ObserveLM' ブロックの各観測の log-density (per-obs)。 param Map から
--- β / u / (Gaussian の) σ を名前で引く。 η_i = Σ_j β_j X_ij + Σ_re u^{re}[gid_i]
--- を scalar 経路と同じ式で評価する。
-{-# INLINABLE lmObsLogLiks #-}
-lmObsLogLiks :: forall a. (Floating a, Ord a)
-             => [Text] -> [[Double]] -> [REff] -> LMFamily -> [Double] -> Map Text a -> [a]
-lmObsLogLiks betaNames designX reffs fam ys params =
-  let betas = [ Map.findWithDefault 0 n params | n <- betaNames ]
-      reEta = lmReffEta reffs (length ys) params
-      etas  = zipWith (\xr re -> lmEta betas xr + re) designX reEta
-      rows  = zip etas ys
-  in case fam of
-       LMGaussian sName ->
-         let sigma = Map.findWithDefault 0 sName params
-         in [ logDensityObs (Normal eta sigma) y | (eta, y) <- rows ]
-       LMPoisson ->
-         [ logDensityObs (Poisson (exp eta)) y | (eta, y) <- rows ]
-       LMBernoulli ->
-         [ logDensityObs (Bernoulli (1 / (1 + exp (negate eta)))) y
-         | (eta, y) <- rows ]
-
--- | 'ObserveLM' ブロックの log-likelihood 和。
-{-# INLINABLE lmObsLogSum #-}
-lmObsLogSum :: (Floating a, Ord a)
-            => [Text] -> [[Double]] -> [REff] -> LMFamily -> [Double] -> Map Text a -> a
-lmObsLogSum betaNames designX reffs fam ys params =
-  sum (lmObsLogLiks betaNames designX reffs fam ys params)
-
--- ---------------------------------------------------------------------------
--- 評価インタープリタ
--- ---------------------------------------------------------------------------
-
--- | Polymorphic interpreter that computes the log-joint
--- @log p(θ, y)@.
--- 引数 @a@ を @Double@ にすると数値評価、@Reverse s Double@ にすると AD 評価が可能。
-logJoint :: (Floating a, Ord a) => Model a r -> Map Text a -> a
-logJoint model params = go model 0
-  where
-    go (Pure _) acc = acc
-    go (Free (Sample n d k)) acc =
-      case Map.lookup n params of
-        Nothing  -> negInf
-        Just v   ->
-          let lp = logDensity d v
-          in go (k v) (acc + lp)
-    go (Free (Observe _ d ys next)) acc =
-      let ll = obsLogSum d ys
-      in go next (acc + ll)
-    go (Free (ObserveLM _ bs xs re fam ys next)) acc =
-      go next (acc + lmObsLogSum bs xs re fam ys params)
-    go (Free (Potential _ v next)) acc = go next (acc + v)
-    go (Free (Deterministic _ v k)) acc = go (k v) acc
-    -- Phase 60.2: Data 継続は [a] を受ける (lazy list・消費 1 回で O(n)/eval)
-    go (Free (Data _ ys k)) acc = go (k (map realToFrac ys, ys)) acc
-    go (Free (DataIx _ is k)) acc = go (k is) acc
-    go (Free (PlateBegin _ _ next)) acc = go next acc
-    go (Free (PlateEnd next))       acc = go next acc
-
--- | log p(θ) のみ (prior 部分)。
-logPrior :: (Floating a, Ord a) => Model a r -> Map Text a -> a
-logPrior = logPriorWith logDensity
-
--- | 'logPrior' の密度関数注入版 (Phase 92 B3)。 AD 経路が定数 hyperparameter の
--- lgamma 正規化項を Double へ畳み込む 'logDensityRD' を差し込むために使う
--- ('Gradient' の fRest 参照)。 @logPriorWith logDensity@ = 従来の 'logPrior'。
-logPriorWith :: (Floating a, Ord a)
-             => (Distribution a -> a -> a) -> Model a r -> Map Text a -> a
-logPriorWith density model params = go model 0
-  where
-    go (Pure _) acc = acc
-    go (Free (Sample n d k)) acc =
-      case Map.lookup n params of
-        Nothing -> negInf
-        Just v  -> go (k v) (acc + density d v)
-    go (Free (Observe _ _ _ next)) acc = go next acc
-    go (Free (ObserveLM _ _ _ _ _ _ next)) acc = go next acc  -- prior 部分には寄与しない
-    go (Free (Potential _ v next)) acc = go next (acc + v)
-    go (Free (Deterministic _ v k)) acc = go (k v) acc
-    go (Free (Data _ ys k)) acc = go (k (map realToFrac ys, ys)) acc
-    go (Free (DataIx _ is k)) acc = go (k is) acc
-    go (Free (PlateBegin _ _ next)) acc = go next acc
-    go (Free (PlateEnd next))       acc = go next acc
-
--- | log p(y | θ) のみ (likelihood 部分)。
-logLikelihood :: (Floating a, Ord a) => Model a r -> Map Text a -> a
-logLikelihood model params = go model 0
-  where
-    go (Pure _) acc = acc
-    go (Free (Sample n _ k)) acc =
-      case Map.lookup n params of
-        Nothing -> go (k 0) acc
-        Just v  -> go (k v) acc
-    go (Free (Observe _ d ys next)) acc =
-      let ll = obsLogSum d ys
-      in go next (acc + ll)
-    go (Free (ObserveLM _ bs xs re fam ys next)) acc =
-      go next (acc + lmObsLogSum bs xs re fam ys params)
-    go (Free (Potential _ _ next)) acc = go next acc   -- Potential は事前項とみなす
-    go (Free (Deterministic _ v k)) acc = go (k v) acc
-    go (Free (Data _ ys k)) acc = go (k (map realToFrac ys, ys)) acc
-    go (Free (DataIx _ is k)) acc = go (k is) acc
-    go (Free (PlateBegin _ _ next)) acc = go next acc
-    go (Free (PlateEnd next))       acc = go next acc
-
--- | For each observe node, return its distribution evaluated at the
--- current parameter values together with the observed data.
--- Gibbs サンプラーが共役構造を検出する際に、潜在変数の現在値に対する
--- 観測分布のパラメータを得るために使う (Double 特殊化版)。
---
--- 例: @y ~ Normal(mu, sigma)@ で @ps = {mu=2, sigma=0.5}@ を渡すと
--- @[(\"y\", Normal 2 0.5, [...])]@ を返す。
-runObserveDists :: Model Double r
-                -> Map Text Double
-                -> [(Text, Distribution Double, [Double])]
-runObserveDists (Pure _) _ = []
-runObserveDists (Free (Sample n _ k)) ps =
-  runObserveDists (k (Map.findWithDefault 0 n ps)) ps
-runObserveDists (Free (Observe n d ys next)) ps =
-  (n, d, ys) : runObserveDists next ps
-runObserveDists (Free (ObserveLM _ _ _ _ _ _ next)) ps =
-  -- ObserveLM は per-obs で μ が異なり単一 Distribution に収まらない。
-  -- Gibbs 共役検出 (この関数の用途) の対象外ゆえスキップ。
-  runObserveDists next ps
-runObserveDists (Free (Potential _ _ next)) ps =
-  runObserveDists next ps
-runObserveDists (Free (Deterministic _ v k)) ps =
-  runObserveDists (k v) ps
-runObserveDists (Free (Data _ ys k)) ps =
-  runObserveDists (k (ys, ys)) ps
-runObserveDists (Free (DataIx _ is k)) ps =
-  runObserveDists (k is) ps
-runObserveDists (Free (PlateBegin _ _ next)) ps = runObserveDists next ps
-runObserveDists (Free (PlateEnd next))       ps = runObserveDists next ps
-
--- | Phase 95 A6: 解析随伴 (detach) パスの適格判定 + 抽出。
--- モデルの尤度項が **ちょうど 1 個の 'MvNormal' observe** のみ (他 'Observe' /
--- 'ObserveLM' 無し) のとき、その @(μ, Σ, ys)@ を **現在の param 値で評価** して
--- 返す。 それ以外は 'Nothing' (= 呼び出し側は従来の walk+ad / vecIR 経路へ)。
---
--- 多相 (@Floating a@) ゆえ Double でも AD 型でも走らせられる: Double 版で
--- LAPACK 用の Σ⁻¹/logdet を作り (G,h 定数化)、 AD 版で surrogate @<G,Σ(θ)>@ を
--- 微分する ('Gradient.compileGradUV' の解析枝)。 walk は 'logJoint' 等と同一
--- (Sample 継続に @params Map.! name@ を流す)。 μ/Σ は Observe ノードの
--- 'Distribution' に格納された式ゆえ、 現在の param 値で lazy に具体化される。
---
--- 適格条件を **1 個の MvNormal に限定**するのは正しさのため: 尤度が MvNormal
--- 単独なら @grad(logPrior+logJac) + detach(observe)@ で厳密に総勾配を再構成できる
--- (@logJoint = logPrior + logLikelihood@・@logLikelihood = obsLogSum(MvNormal)@)。
-mvNormalObserveOf :: (Floating a, Ord a)
-                  => Model a r -> Map Text a -> Maybe ([a], [[a]], [Double])
-mvNormalObserveOf model params =
-  case go model of
-    Just [(MvNormal mu cov, ys)] -> Just (mu, cov, ys)
-    _                            -> Nothing
-  where
-    -- Observe ノードの (dist, ys) を集める。 ObserveLM が在れば失格 (Nothing)。
-    go (Pure _) = Just []
-    go (Free (Sample n _ k)) =
-      case Map.lookup n params of
-        Nothing -> Nothing              -- param 欠落 = 失格 (通常起きない)
-        Just v  -> go (k v)
-    go (Free (Observe _ d ys next)) = ((d, ys) :) <$> go next
-    go (Free (ObserveLM {}))        = Nothing          -- 構造化尤度は対象外
-    go (Free (Potential _ _ next))  = go next          -- prior 側 (logPrior が処理)
-    go (Free (Deterministic _ v k)) = go (k v)
-    go (Free (Data _ ys k))         = go (k (map realToFrac ys, ys))
-    go (Free (DataIx _ is k))       = go (k is)
-    go (Free (PlateBegin _ _ next)) = go next
-    go (Free (PlateEnd next))       = go next
-
--- | For each sample node, return @(name, prior distribution)@ in the
--- @Double@-specialized form.
--- Gibbs サンプラーの共役検出で「この潜在変数の事前は Gamma か Beta か」を
--- 判定するために使う。継続値はプレースホルダ 0 を流す。
-priorList :: Model Double r -> [(Text, Distribution Double)]
-priorList (Pure _) = []
-priorList (Free (Sample n d k)) = (n, d) : priorList (k 0)
-priorList (Free (Observe _ _ _ next)) = priorList next
-priorList (Free (ObserveLM _ _ _ _ _ _ next)) = priorList next
-priorList (Free (Potential _ _ next)) = priorList next
-priorList (Free (Deterministic _ v k)) = priorList (k v)
-priorList (Free (Data _ ys k)) = priorList (k (ys, ys))
-priorList (Free (DataIx _ is k)) = priorList (k is)
-priorList (Free (PlateBegin _ _ next)) = priorList next
-priorList (Free (PlateEnd next))       = priorList next
-
--- ---------------------------------------------------------------------------
--- 互換 API
--- ---------------------------------------------------------------------------
-
--- | パラメータ名 → 値 のマップ (constrained 空間)。
-type Params = Map Text Double
-
--- | Per-observation log-likelihood (used by WAIC / LOO-CV).
--- 各 Observe ノードのすべての観測値の logDensity を平坦リストで返す。
-perObsLogLiks :: forall r. ModelP r -> Params -> [Double]
-perObsLogLiks m params = go m []
-  where
-    go :: Model Double r -> [Double] -> [Double]
-    go (Pure _) acc = reverse acc
-    go (Free (Sample n _ k)) acc =
-      go (k (Map.findWithDefault 0 n params)) acc
-    go (Free (Observe _ d ys next)) acc =
-      let lls = case d of
-            MvNormal mu cov ->
-              let k = length mu
-              in [ mvNormalLogDensity mu cov (map realToFrac yv :: [Double])
-                 | yv <- chunksOf k ys ]
-            Multinomial nn pp ->
-              let k = length pp
-              in [ multinomialLogDensity nn pp yv | yv <- chunksOf k ys ]
-            _ -> [ logDensityObs d y | y <- ys ]
-      in go next (reverse lls ++ acc)
-    go (Free (ObserveLM _ bs xs re fam ys next)) acc =
-      let lls = lmObsLogLiks bs xs re fam ys params
-      in go next (reverse lls ++ acc)
-    go (Free (Potential _ _ next)) acc = go next acc
-    go (Free (Deterministic _ v k)) acc = go (k v) acc
-    go (Free (Data _ ys k)) acc = go (k (ys, ys)) acc
-    go (Free (DataIx _ is k)) acc = go (k is) acc
-    go (Free (PlateBegin _ _ next)) acc = go next acc
-    go (Free (PlateEnd next))       acc = go next acc
-
--- | Evaluate every 'Deterministic' node and return the resulting
--- derived-quantity @Map@.
---
--- @params@ は latent 変数 (sample) の値を表す Map。Deterministic は
--- それらから導出される量で、ここでは Double 特殊化で評価する。
-runDeterministics :: forall r. ModelP r -> Params -> Map Text Double
-runDeterministics m params = go m Map.empty
-  where
-    go :: Model Double r -> Map Text Double -> Map Text Double
-    go (Pure _) acc = acc
-    go (Free (Sample n _ k)) acc =
-      go (k (Map.findWithDefault 0 n params)) acc
-    go (Free (Observe _ _ _ next)) acc = go next acc
-    go (Free (ObserveLM _ _ _ _ _ _ next)) acc = go next acc
-    go (Free (Potential _ _ next)) acc = go next acc
-    go (Free (Deterministic n v k)) acc =
-      go (k v) (Map.insert n v acc)
-    go (Free (Data _ ys k)) acc = go (k (ys, ys)) acc
-    go (Free (DataIx _ is k)) acc = go (k is) acc
-    go (Free (PlateBegin _ _ next)) acc = go next acc
-    go (Free (PlateEnd next))       acc = go next acc
-
--- | モデル中の 'Deterministic' 宣言名を宣言順で列挙する (Phase 103)。
--- 同名の重複宣言 (plate 内反復等) は初出のみ残す。'collectNodes' は
--- Deterministic を素通しして 'Node' 化しないため専用 walker で拾う。
--- 'runDeterministics' の返す Map の key 集合と一致する (順序のみ異なる)。
-deterministicNames :: forall r. ModelP r -> [Text]
-deterministicNames m = nub (go m [])
-  where
-    go :: Model Double r -> [Text] -> [Text]
-    go (Pure _) acc = reverse acc
-    go (Free (Sample n _ k)) acc = go (k 0) acc   -- placeholder 0 (collectNodes 同型)
-    go (Free (Observe _ _ _ next)) acc = go next acc
-    go (Free (ObserveLM _ _ _ _ _ _ next)) acc = go next acc
-    go (Free (Potential _ _ next)) acc = go next acc
-    go (Free (Deterministic n v k)) acc = go (k v) (n : acc)
-    go (Free (Data _ ys k)) acc = go (k (ys, ys)) acc
-    go (Free (DataIx _ is k)) acc = go (k is) acc
-    go (Free (PlateBegin _ _ next)) acc = go next acc
-    go (Free (PlateEnd next))       acc = go next acc
-
--- | Evaluate 'runDeterministics' on every posterior sample and
--- 結果を 'chainSamples' の Map にマージした新しい Chain を返す。
--- これにより @chainVals@ / @posteriorSummary@ などのヘルパで派生量を
--- そのまま参照できる。
-augmentChainWithDeterministic :: ModelP r -> Chain -> Chain
-augmentChainWithDeterministic m ch =
-  let aug ps = Map.union (runDeterministics m ps) ps
-  in ch { chainSamples = map aug (chainSamples ch) }
-
--- | Human-readable summary of the model structure (no inference is run).
-describeModel :: ModelP r -> Text
-describeModel m = T.unlines (header : map fmtNode (collectNodes m))
-  where
-    header = "Model nodes:"
-    fmtNode n = case nodeKind n of
-      LatentN       -> "  [latent]   " <> nodeName n <> " ~ " <> nodeDist n
-      ObservedN k   -> "  [observed] " <> nodeName n <> " ~ " <> nodeDist n
-                    <> "  (n=" <> T.pack (show k) <> ")"
-      DeterministicN -> "  [determ]   " <> nodeName n <> " = " <> nodeDist n
-      DataN k        -> "  [data]     " <> nodeName n
-                    <> "  (n=" <> T.pack (show k) <> ")"
-
--- | DAG representation of the model. Edges are derived automatically by
--- 'extractDeps'.
-data ModelGraph = ModelGraph
-  { mgNodes  :: [Node]
-  , mgEdges  :: [(Text, Text)]   -- (parent, child)
-  , mgPlates :: Map Text Int     -- Phase 40: plate 名 → サイズ N
-  } deriving (Show)
-
--- | Plate 内の indexed RV (`eta_0, eta_1, …, eta_{n-1}`) を **代表 1 ノードに集約**
--- して、 PyMC `pm.model_to_graphviz` 流の true plate 描画用に変換する
--- (Phase 40-A8、 2026-05-30 追加)。
---
--- 集約条件 (heuristic):
---
--- - 同じ `nodePlates` (= plate スタック) に属する
--- - 名前が @\<prefix\>_\<digit+\>$@ パターン (末尾が _ + 数字)
--- - 同じ @prefix@ を持つノード群が 2 個以上
--- - 同じ `nodeDist` (= 分布名が一致)
---
--- 集約結果:
---
--- - 代表ノード名は @prefix@ (例: @eta_0..eta_7@ → @eta@)
--- - `nodeKind`: 元の集合内で最初の出現を維持 (LatentN / ObservedN)。
---   ObservedN の場合は観測数を全集約 (Σ)
--- - `nodeDeps`: 全集合の親集合の和 (ただし、 同じ集合内のメンバ間 deps は
---   削除 — 自己集約のため)
--- - edges: 集約後の名前で dedupe
---
--- plate 文脈外で起きる「同じ命名規則の名前衝突」 (e.g. @beta_0@ 固定効果 vs
--- @u_0@ 群効果) はこの heuristic で誤って集約されない (plate 制約)。
---
--- 元 graph をそのまま渡せば不変 (idempotent)。 plate に属さない / 単独
--- のノードは触らない。
-collapseIndexedPlateNodes :: ModelGraph -> ModelGraph
-collapseIndexedPlateNodes mg0 =
-  -- 不動点: 1 回の集約で取りこぼした多段 plate (e.g. y_0_0..y_2_1 → y_0..y_2 →
-  -- 残り index suffix を持つ → y) を順次潰す。 mgNodes 数が減らなくなれば終了。
-  let step g = collapseIndexedPlateNodesOnce g
-      iter g = let g' = step g in if length (mgNodes g') == length (mgNodes g)
-                                    then g else iter g'
-  in iter mg0
-
--- | `collapseIndexedPlateNodes` の 1 段集約 (内部、 不動点を作る材料)。
-collapseIndexedPlateNodesOnce :: ModelGraph -> ModelGraph
-collapseIndexedPlateNodesOnce mg =
-  let ns        = mgNodes mg
-      es        = mgEdges mg
-      -- 1. 各ノードについて (plate path, prefix) または Nothing を計算
-      keyOf n = case T.breakOnEnd "_" (nodeName n) of
-        (pre, digits)
-          | not (T.null pre) && not (T.null digits)
-            && T.all (`elem` ("0123456789" :: String)) digits ->
-              Just (nodePlates n, T.init pre)  -- _ を除いた prefix
-        _ -> Nothing
-      -- 2. キー単位で groupings
-      keyed = [(keyOf n, n) | n <- ns]
-      -- 3. グループ化 (Just key) のみ、 Nothing は単独
-      grouped :: Map.Map ([Text], Text) [Node]
-      grouped = Map.fromListWith (flip (++))
-        [ (k, [n]) | (Just k, n) <- keyed ]
-      -- 4. 集約候補: size ≥ 2 かつ全 nodeDist 一致
-      collapsible = Map.filter
-        (\g -> length g >= 2
-            && all (\n -> nodeDist n == nodeDist (head g)) g)
-        grouped
-      -- 5. name → 代表名 のマップ
-      nameMap :: Map.Map Text Text
-      nameMap = Map.fromList
-        [ (nodeName n, prefix)
-        | ((_plates, prefix), grp) <- Map.toList collapsible
-        , n <- grp
-        ]
-      mapName n = Map.findWithDefault n n nameMap
-      -- 6. 集約後ノード作成
-      mkRepresentative (_, prefix) grp =
-        let first = head grp
-            kind  = case nodeKind first of
-              ObservedN _ ->
-                ObservedN (sum [k | n <- grp,
-                                    let ObservedN k = nodeKind n])
-              LatentN        -> LatentN
-              DeterministicN -> DeterministicN
-              dk@(DataN _)   -> dk
-            -- 自己集約 (同じ集合のメンバへの deps) を除外
-            memberNames = Set.fromList (map nodeName grp)
-            externalDeps = Set.unions (map nodeDeps grp)
-              `Set.difference` memberNames
-            -- 親側の名前も mapName で remap (e.g. mu_0..mu_K-1 集約済の場合)
-            remappedDeps = Set.map mapName externalDeps
-        in first { nodeName = prefix
-                 , nodeKind = kind
-                 , nodeDeps = remappedDeps
-                 }
-      -- 7. ノードリスト再構築: 集約対象は代表 1 個、 非対象はそのまま
-      isInGroup n = case keyOf n of
-        Just k -> Map.member k collapsible
-        Nothing -> False
-      seenGroups :: [([Text], Text)]
-      seenGroups = []
-      walk [] _ acc = reverse acc
-      walk (n:rest) seen acc
-        | isInGroup n =
-            let Just k = keyOf n
-            in if k `elem` seen
-                 then walk rest seen acc
-                 else let rep = mkRepresentative k (collapsible Map.! k)
-                      in walk rest (k : seen) (rep : acc)
-        | otherwise = walk rest seen
-            (n { nodeDeps = Set.map mapName (nodeDeps n) } : acc)
-      newNodes = walk ns seenGroups []
-      -- 8. edges を remap + dedupe + 自己ループ除去
-      newEdges = Set.toList $ Set.fromList
-        [ (s', t')
-        | (s, t) <- es
-        , let s' = mapName s
-        , let t' = mapName t
-        , s' /= t'   -- 自己ループ除外
-        ]
-  in mg { mgNodes = newNodes, mgEdges = newEdges }
-
--- | 多相モデルから DAG を自動構築する (Track 型による依存追跡)。
---
--- 同じ名前で複数登場する Observe ノード (例: 回帰モデルで観測点ごとに
--- @observe \"y\"@ を発行する場合) は 1 つに統合される。観測数の合計と
--- 親変数集合の和をマージし、エッジも重複排除する。
-buildModelGraph :: ModelP r -> ModelGraph
-buildModelGraph m =
-  let (rawNodes, plates) = extractDeps m
-      merged   = assignDataPlates plates (mergeByName rawNodes)
-      edges    = Set.toList $ Set.fromList
-                   [ (parent, nodeName n)
-                   | n <- merged
-                   , parent <- Set.toList (nodeDeps n) ]
-  in ModelGraph merged edges plates
-  where
-    -- Phase 60.6 追補: 宣言位置が plate 外 (nodePlates = []) の DataN を、
-    -- PyMC の dims 同様「データ長 = plate サイズ」 の一意 match で plate に
-    -- 割り当てる (典型 = モデル冒頭で宣言した dataNamedX n=150 が obs(150)
-    -- cluster 内に描かれる)。 一致 plate が複数 / なし は据え置き (外に描く)。
-    -- 入れ子 plate の full path は、 既にその plate に居る他ノードの
-    -- nodePlates から逆引きする (plate 内ノードが無い場合は単独 path)。
-    assignDataPlates plates ns =
-      let paths = [ nodePlates n | n <- ns, not (null (nodePlates n)) ]
-          pathFor nm = case [ p | p <- paths, last p == nm ] of
-                         (p : _) -> p
-                         []      -> [nm]
-          assign n = case nodeKind n of
-            DataN k | null (nodePlates n) ->
-              case [ nm | (nm, sz) <- Map.toList plates, sz == k ] of
-                [nm] -> n { nodePlates = pathFor nm }
-                _    -> n
-            _ -> n
-      in map assign ns
-    -- 同名ノードを統合: ObservedN n1 + ObservedN n2 → ObservedN (n1+n2)
-    -- LatentN は最初の出現を残す。deps は和集合。
-    -- nodePlates は最初の出現のものを維持 (同名は同 plate 前提)。
-    mergeByName ns = mergeGo ns Map.empty []
-    mergeGo [] _ acc = reverse acc
-    mergeGo (n:ns) seen acc =
-      let nm = nodeName n
-      in case Map.lookup nm seen of
-           Nothing -> mergeGo ns (Map.insert nm n seen) (n : acc)
-           Just prev ->
-             -- Phase 60.4: DataN は最弱 — 同名の非 DataN ノード (典型 =
-             -- dataNamedObs "y" + observe "y" の docs 慣例) があれば吸収される
-             -- (PyMC で observed RV が data 容器を内包して表示されるのと同型)。
-             let (kind', dist', plates') =
-                   case (nodeKind prev, nodeKind n) of
-                     (ObservedN a, ObservedN b) ->
-                       (ObservedN (a + b), nodeDist prev, nodePlates prev)
-                     (DataN _, k2) -> (k2, nodeDist n, nodePlates n)
-                     (k1, _)       -> (k1, nodeDist prev, nodePlates prev)
-                 merged' = Node
-                   { nodeName = nm
-                   , nodeKind = kind'
-                   , nodeDist   = dist'
-                   , nodeDeps   = nodeDeps prev <> nodeDeps n
-                   , nodePlates = plates'
-                   }
-                 acc' = map (\x -> if nodeName x == nm then merged' else x) acc
-             in mergeGo ns (Map.insert nm merged' seen) acc'
-
-
--- ---------------------------------------------------------------------------
--- Track 評価 (logJoint の Track 特殊化)
--- ---------------------------------------------------------------------------
-
--- | Track でモデルを評価する (log joint も依存集合付きで計算)。
-runTrack :: forall r. ModelP r -> Map Text Track -> Track
-runTrack m params = logJoint (m :: Model Track r) params
diff --git a/src/Hanalyze/Model/HBM/Gradient.hs b/src/Hanalyze/Model/HBM/Gradient.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/HBM/Gradient.hs
+++ /dev/null
@@ -1,1982 +0,0 @@
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE ImpredicativeTypes #-}
-
--- |
--- Module      : Hanalyze.Model.HBM.Gradient
--- Description : HBM の AD 勾配コンパイラ層 (NUTS per-draw のホット経路)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Phase 58.8: AD 勾配コンパイラ層を 'Hanalyze.Model.HBM' 本体から分離。
--- IR (中間表現) 層の **上層** であり、 NUTS per-draw の本経路 (compileGradUV →
--- gradVecIR / hybridGradClosure) を担う最ホット モジュール。 unconstrained 空間の
--- log-joint・解析閉形式勾配 (Gaussian LM ブロック)・ハイブリッド勾配クロージャ・
--- 定数 prior 解析勾配・制約変換 (invTransformF/logJacF) を含む。
---
--- 全 top-level を export し ('module ... where' = 暗黙全公開)、 公開 API
--- (gradAD/gradADU/compileGradU/compileGradUV/compileLogPU/compileLogPUV/
--- getTransforms/logJointUnconstrained/invTransformF/logJacF) は facade
--- 'Hanalyze.Model.HBM' の export list 経由で再エクスポートされる。
-module Hanalyze.Model.HBM.Gradient where
-
-import Control.DeepSeq (NFData (..), force)
-import Control.Exception (SomeException, evaluate, try)
-import Control.Monad (forM, forM_, replicateM, when)
-import Data.List (foldl', zip4)
-import System.IO.Unsafe (unsafePerformIO)
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import qualified Data.Set as Set
-import Data.Set (Set)
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Reflection (Reifies)
-import Numeric.AD.Mode.Reverse.Double (grad, grad')
-import qualified Numeric.AD.Internal.Reverse.Double as ADRD
-import qualified System.Random.MWC as MWCBase
-import qualified System.Random.MWC.Distributions as MWC
-import System.Random.MWC (Gen)
-import Control.Monad.Primitive (PrimMonad, PrimState, stToPrim)
-
-import Control.Monad.ST (ST, runST)
-import qualified Data.Vector          as BV
-import qualified Data.Vector.Storable as VS
-import qualified Data.Vector.Storable.Mutable as VSM
-import qualified Data.Vector.Unboxed  as VU
-
--- Phase 95 A6: dense MvNormal observe の解析随伴 (detach) で Σ⁻¹/logdet を LAPACK
--- で 1 度だけ作る (cholesky を AD tape に載せない)。
-import qualified Numeric.LinearAlgebra as LA
-
-import Hanalyze.Stat.Distribution (Transform (..))
-import Hanalyze.MCMC.Core (Chain (..))
-
--- Phase 58.2: 純粋な数値・線形代数 leaf util を分離。 internal 利用に加え
--- 'lgammaApprox' / 'digamma' は export list 経由でそのまま再エクスポートされる。
-import Hanalyze.Model.HBM.Util
--- Phase 58.3/58.6a: 多相分布 ADT + 密度 + CDF を分離 (Util の上層)。 公開 API
--- (Distribution(..)/distName/logDensity/logDensityObs/obsLogSum/distCDF/logCDF/
--- logSF/MV密度群) は export list 経由でそのまま再エクスポート。 ★58.6a で事前
--- logDensity と観測 logDensityObs/obsLogSum を本体から Distribution へ集約
--- (Eval の logJoint/logPrior が logDensity を参照する back-edge を解消・密度は
--- 本来 Distribution の責務。 INLINABLE は AD cross-module inlining 維持で保持)。
-import Hanalyze.Model.HBM.Distribution
--- Phase 58.4: 分布からのサンプリング (sampleDist/sampleMvDist) を分離。
--- export list 経由でそのまま再エクスポート。 PrimMonad/mwc-random 依存・非ホット。
-import Hanalyze.Model.HBM.Sampling
--- Phase 58.5: 多相モデル DSL (Free monad + ModelF + plate + 構造検査) を分離。
--- 公開 API (Free/liftF/ModelF/Model/ModelP/sample/observe/plate/collectNodes 等)
--- は export list 経由でそのまま再エクスポート。
-import Hanalyze.Model.HBM.Model
-
--- Phase 58.6b: 依存追跡型 Track (Track/trackVar/trackConst/extractDeps) を分離。
--- Model/Distribution の上層・非ホット (DAG 抽出のみ・NUTS per-draw 非経路)。
--- export list 経由でそのまま再エクスポート。
-import Hanalyze.Model.HBM.Track
--- Phase 58.6c: 評価層 (ObserveLM 評価 + logJoint/logPrior/logLikelihood interp +
--- 互換 API runDeterministics/buildModelGraph 等 + runTrack) を分離。 Track の上層。
--- ★ホット (logJoint は AD 勾配経路)。 AD 勾配・IR (本体残置) は本モジュールを
--- forward import する。 公開 API は export list 経由でそのまま再エクスポート。
-import Hanalyze.Model.HBM.Eval
--- Phase 58.7: IR (中間表現) 層 (affine/非線形/密度 IR) を分離。 最ホット (gradVecIR)。
-import Hanalyze.Model.HBM.IR
-
--- Phase 60.7: '!!!' の依存タグは AD 勾配には無関係 (既定 id = サンプリング
--- ビット不変)。 ReverseDouble は ad の internal 型ゆえ orphan instance だが、
--- AD 経路の instantiate はこのモジュールに閉じている。
-instance TrackTag (ADRD.ReverseDouble s)
-
--- ---------------------------------------------------------------------------
--- AD 勾配
--- ---------------------------------------------------------------------------
-
--- | AD で勾配を計算する。@names@ の順で各パラメータに対する偏微分を返す。
-gradAD :: ModelP r -> [Text] -> [Double] -> [Double]
-gradAD m names xs0 = grad f xs0
-  where
-    f xs =
-      let params = Map.fromList (zip names xs)
-      in logJoint m params
-
--- | unconstrained 空間で AD 勾配を計算する (HMC 用)。
--- 各パラメータに制約変換を適用し、Jacobian 補正項込みの log-joint を微分する。
---
--- Phase 54.4a-54.6: モデルが **Gaussian-恒等リンクの 'ObserveLM' ブロック** を
--- 含む場合、 そのブロックの観測尤度勾配 (= 規模 n に比例する支配項) を解析
--- 閉形式 (∂β=Xᵀr/σ² 等) で計算し、 prior / jacobian / scalar observe /
--- 非 Gaussian LM は従来 `ad` で計算してから成分加算する (ハイブリッド)。
--- Gaussian LM を含まないモデルは従来通り全体を `ad` で微分 (後方互換)。
-gradADU :: ModelP r -> [Text] -> [Transform] -> [Double] -> [Double]
-gradADU m names trans = compileGradU m names trans
-
--- | 'compileGradUV' の list wrapper (後方互換 API)。 NUTS は vector-native の
--- 'compileGradUV' を直接使う。
-compileGradU :: forall r. ModelP r -> [Text] -> [Transform] -> ([Double] -> [Double])
-compileGradU m names trans =
-  let gv = compileGradUV m names trans
-  in VS.toList . gv . VS.fromList
-
--- | 'compileGradUV' が実際に選ぶ勾配経路のラベル (診断表示用・Phase 91 A4)。
--- 'compileGradUV' 本体の分岐順 (gaussLMBlocksAuto → synthVecIR → 全体 ad) を
--- そのまま反映する唯一の分類子。
---
--- ★**束縛済モデル** ('hbmModelSpec' 経由) を渡すこと。 生の未束縛モデル
--- ('dataNamed*' の既定 @[]@ のまま) を渡すと data 行が空になり、 Gaussian LM
--- 合成も 'collectSymRows' も 0 行となって経路判定が狂う (Phase 91 A4 実測:
--- 17-nes/12-ark は実際は Gaussian LM 閉形式経路なのに、 生モデルを渡した
--- 診断が @synthVecIR = Nothing@ と誤表示していた)。
-gradPathLabel :: ModelP r -> String
-gradPathLabel m = case gaussLMBlocksAuto m of
-  ([], _) -> case synthVecIR m of
-    Nothing | hasHmmObserve m       -> "HMM forward-backward 閉形式随伴 (Phase 92)"
-            | hasArmaObserve m      -> "ARMA(1,1) 逆向き随伴の閉形式 (Phase 101)"
-            | hasGradedIrtObserve m -> "graded response IRT 解析勾配 (Phase 101)"
-            | otherwise             -> "legacy walk+ad (全体 ad)"
-    Just _  -> "vecIR (ベクトル式 IR 高速経路)"
-  _       -> "Gaussian LM 閉形式ブロック (解析勾配)"
-
--- | Phase 92: 'gradPathLabel' 用の軽量構造判定 — 尤度が単一 'HmmForwardNormal'
--- observe か。 param 値は不要 (latent へ 0 を給餌・分布値は強制しない)。
--- 実際の経路選択は 'gradValPlan' 内の 'hmmAnalyticVG' (probe 同型) が行う。
-hasHmmObserve :: ModelP r -> Bool
-hasHmmObserve m = case go m of
-    Just [HmmForwardNormal {}] -> True
-    _                          -> False
-  where
-    go :: Model Double r' -> Maybe [Distribution Double]
-    go (Pure _) = Just []
-    go (Free (Sample _ _ k))        = go (k 0)
-    go (Free (Observe _ d _ next))  = (d :) <$> go next
-    go (Free (ObserveLM {}))        = Nothing
-    go (Free (Potential _ _ next))  = go next
-    go (Free (Deterministic _ v k)) = go (k v)
-    go (Free (Data _ ys k))         = go (k (ys, ys))
-    go (Free (DataIx _ is k))       = go (k is)
-    go (Free (PlateBegin _ _ next)) = go next
-    go (Free (PlateEnd next))       = go next
-
--- | Phase 101: 'gradPathLabel' 用の軽量構造判定 — 尤度が単一 'ArmaNormal'
--- observe か。 実際の経路選択は 'gradValPlan' 内の 'armaAnalyticVG' が行う。
-hasArmaObserve :: ModelP r -> Bool
-hasArmaObserve m = case go m of
-    Just [ArmaNormal {}] -> True
-    _                    -> False
-  where
-    go :: Model Double r' -> Maybe [Distribution Double]
-    go (Pure _) = Just []
-    go (Free (Sample _ _ k))        = go (k 0)
-    go (Free (Observe _ d _ next))  = (d :) <$> go next
-    go (Free (ObserveLM {}))        = Nothing
-    go (Free (Potential _ _ next))  = go next
-    go (Free (Deterministic _ v k)) = go (k v)
-    go (Free (Data _ ys k))         = go (k (ys, ys))
-    go (Free (DataIx _ is k))       = go (k is)
-    go (Free (PlateBegin _ _ next)) = go next
-    go (Free (PlateEnd next))       = go next
-
--- | Phase 101: 'gradPathLabel' 用の軽量構造判定 — 尤度が単一
--- 'GradedResponseIrt' observe か。 実際の経路選択は 'gradValPlan' 内の
--- 'gradedIrtAnalyticVG' が行う。
-hasGradedIrtObserve :: ModelP r -> Bool
-hasGradedIrtObserve m = case go m of
-    Just [GradedResponseIrt {}] -> True
-    _                           -> False
-  where
-    go :: Model Double r' -> Maybe [Distribution Double]
-    go (Pure _) = Just []
-    go (Free (Sample _ _ k))        = go (k 0)
-    go (Free (Observe _ d _ next))  = (d :) <$> go next
-    go (Free (ObserveLM {}))        = Nothing
-    go (Free (Potential _ _ next))  = go next
-    go (Free (Deterministic _ v k)) = go (k v)
-    go (Free (Data _ ys k))         = go (k (ys, ys))
-    go (Free (DataIx _ is k))       = go (k is)
-    go (Free (PlateBegin _ _ next)) = go next
-    go (Free (PlateEnd next))       = go next
-
--- ---------------------------------------------------------------------------
--- Phase 95 A6: dense MvNormal observe の解析随伴 (detach) 経路
--- ---------------------------------------------------------------------------
-
--- | Phase 95 A6: 尤度が **単一 'MvNormal' observe** のモデル (GP 回帰・
--- dense-MvNormal) の value+grad を **解析随伴 (detach トリック)** で計算する
--- クロージャを構築する。 'mvNormalObserveOf' が 'Just' のとき (= 適格) のみ
--- 'Just' を返し、 非適格なら 'Nothing' (呼び出し側は従来 walk+ad へ)。
---
--- === なぜ速いか
--- 現行 walk+ad は N×N Cholesky + solve + logdet を **毎 leapfrog で reverse-AD
--- tape 上に丸ごと展開**する (O(N³) のスカラー演算が各々 boxed AD ノードを alloc)。
--- 大 N で壊滅的 (§A4: N=50 で対 PyMC 62×)。 本経路は PyMC/Stan と同じく
--- **Cholesky を AD tape に載せず** Σ⁻¹/logdet を LAPACK で 1 度だけ Double 計算し、
--- @G = ∂logp/∂Σ@・@h = ∂logp/∂μ@ を定数化して surrogate @<G,Σ(θ)>+<h,μ(θ)>@ の
--- 軽量 ad (O(N²)・cholesky 無し) だけを微分する。
---
--- === 数学 (detach)
--- 1 観測 (k-vector) の @logp = -k/2·log2π - 0.5·log|Σ| - 0.5·(y-μ)ᵀΣ⁻¹(y-μ)@ に対し
--- @α = Σ⁻¹(y-μ)@、 @∂logp/∂Σ = 0.5(ααᵀ - Σ⁻¹)@、 @∂logp/∂μ = α@。 複数 chunk では
--- @G = 0.5(Σ_m α_mα_mᵀ - nCh·Σ⁻¹)@、 @h = Σ_m α_m@。 surrogate @<G,Σ(θ)>+<h,μ(θ)>@ の
--- θ 勾配は連鎖律で @∂logp/∂θ@ に厳密一致 (proto: 有限差分 2.8e-9・ad-through 2.2e-15)。
--- θ=invTransform(u) を surrogate 内に組むので ∂/∂u が直接得られる。
---
--- 総勾配 = @grad(logPrior+logJac)@ (scalar・cheap) + @detach(observe)@。
--- 値 = @(logPrior+logJac)@ + @logp_MvNormal@ (同じ LAPACK 分解から)。
---
--- === 常時 ON (次元しきい値なし)
--- 解析随伴は近似でなく厳密ゆえ正しさゲート不要。 PyMC/Stan も Cholesky Op の
--- 解析随伴を **常時**使う (次元しきい値を持たない)。 小 N (N≲40) では list 往復の
--- 定数倍で ad-through と拮抗〜僅遅だが (§A3-proto crossover ≈N40-50)、 該当する
--- shipping モデルは無い。 その帯の overhead 除去 (surrogate 配列化) は TODO (§A6)。
-mvNormalAnalyticVG
-  :: forall r. ModelP r -> [Text] -> [Transform]
-  -> Maybe (VS.Vector Double -> (Double, VS.Vector Double))
-mvNormalAnalyticVG m names trans =
-  -- 適格判定は構造のみ (param 値に依らない) — ダミー 0 で walk。
-  case mvNormalObserveOf m probeParams of
-    Nothing -> Nothing
-    Just _  -> Just closure
-  where
-    probeParams :: Map Text Double
-    probeParams = Map.fromList [ (n, 0) | n <- names ]
-
-    closure :: VS.Vector Double -> (Double, VS.Vector Double)
-    closure uv =
-      let us     = VS.toList uv
-          thetaD = [ invTransformF t u | (t, u) <- zip trans us ]
-          paramD = Map.fromList (zip names thetaD)
-      in case mvNormalObserveOf m paramD of
-           Nothing -> (fFullA us, VS.fromList (gradFullA us))  -- 適格判定と矛盾: fallback
-           Just (muD, covD, ys)
-             | k == 0 || null chunks ->                        -- 退化: fallback
-                 (fFullA us, VS.fromList (gradFullA us))
-             | otherwise ->
-                 let -- 片道 flat 化のみ (fromLists 不使用): [[Double]] を concat して
-                     -- row-major で Matrix に積む。 戻りの toLists は一切しない。
-                     sig       = LA.matrix k (concat covD)            -- N×N
-                     (inv, (lndet, _sgn)) = LA.invlndet sig
-                     muV       = LA.fromList muD
-                     ds        = [ LA.fromList (map realToFrac ym) - muV | ym <- chunks ]
-                     alphas    = [ inv LA.#> d | d <- ds ]            -- α_m = Σ⁻¹(y_m-μ)
-                     quadSum   = sum [ d LA.<.> a | (d, a) <- zip ds alphas ]
-                     kA        = fromIntegral k :: Double
-                     nChA      = fromIntegral nCh :: Double
-                     logpObs   = nChA * (negate 0.5 * kA * log (2 * pi) - 0.5 * lndet)
-                                 - 0.5 * quadSum
-                     -- G = 0.5(Σ_m α_mα_mᵀ - nCh·Σ⁻¹), h = Σ_m α_m
-                     gMat      = LA.scale 0.5
-                                   (foldl1 (+) [ LA.outer a a | a <- alphas ]
-                                    - LA.scale nChA inv)
-                     -- G/h は **flat Storable Vector のまま** (row-major)。 surrogate 側で
-                     -- index して realToFrac lift = nested list 化 (toLists) を回避。
-                     gFlat     = LA.flatten gMat :: VS.Vector Double  -- length k*k, row-major
-                     hVec      = foldl1 (+) alphas :: VS.Vector Double -- length k
-                     -- detach surrogate: ∂/∂u <G,Σ(θ(u))> + <h,μ(θ(u))>。 Σ(θ) は model が
-                     -- [[a]] を吐くので concat で 1 回 flat 化し gFlat と flat×flat 内積。
-                     surrogate :: forall a. (Floating a, Ord a, TrackTag a) => [a] -> a
-                     surrogate uu =
-                       let thetaA = [ invTransformF t u | (t, u) <- zip trans uu ]
-                           paramA = Map.fromList (zip names thetaA)
-                       in case mvNormalObserveOf m paramA of
-                            Just (muA, covA, _) ->
-                              dotFlatL gFlat (concat covA) + dotFlatL hVec muA
-                            Nothing -> 0
-                     gObs      = grad surrogate us                    -- ∂obs/∂u
-                     gRest     = gradRestA us                         -- ∂(logPrior+logJac)/∂u
-                     vRest     = fRestA us                            -- (logPrior+logJac)(u)
-                 in ( vRest + logpObs
-                    , VS.fromList (zipWith (+) gRest gObs) )
-             where k      = length muD
-                   chunks = chunksOf k ys
-                   nCh    = length chunks
-
-    -- flat Double Vector · [a] リスト内積: Double 側 (G/h) を list 化せず index して
-    -- realToFrac で lift。 xs (concat covA / muA) だけを 1 回舐める (toLists 往復回避)。
-    dotFlatL :: forall a. Floating a => VS.Vector Double -> [a] -> a
-    dotFlatL gv = go 0 0
-      where go !i !acc (x : xs) = go (i + 1) (acc + realToFrac (VS.unsafeIndex gv i) * x) xs
-            go _  !acc []       = acc
-
-    -- prior + logJac (尤度を除く) — scalar・dense 行列を含まない。
-    fRestA :: (Floating a, Ord a, TrackTag a) => [a] -> a
-    fRestA us =
-      let paramsC = Map.fromList (zip names [ invTransformF t u | (t, u) <- zip trans us ])
-          logJac  = sum [ logJacF t u | (t, u) <- zip trans us ]
-      in logPrior m paramsC + logJac
-    -- AD 側は 'logDensityRD' 注入版 (Phase 92 B3・値/勾配とも fRestA と bit 一致)
-    fRestRD :: forall s. Reifies s ADRD.Tape
-            => [ADRD.ReverseDouble s] -> ADRD.ReverseDouble s
-    fRestRD us =
-      let paramsC = Map.fromList (zip names [ invTransformF t u | (t, u) <- zip trans us ])
-          logJac  = sum [ logJacF t u | (t, u) <- zip trans us ]
-      in logPriorWith logDensityRD m paramsC + logJac
-    gradRestA :: [Double] -> [Double]
-    gradRestA = grad fRestRD
-
-    -- 適格判定が崩れた稀ケース用の完全 walk+ad fallback (従来経路と同一)。
-    fFullA :: (Floating a, Ord a, TrackTag a) => [a] -> a
-    fFullA us =
-      let paramsC = Map.fromList (zip names [ invTransformF t u | (t, u) <- zip trans us ])
-          logJac  = sum [ logJacF t u | (t, u) <- zip trans us ]
-      in logJoint m paramsC + logJac
-    gradFullA :: [Double] -> [Double]
-    gradFullA = grad fFullA
-
--- ---------------------------------------------------------------------------
--- Phase 95 B-dsl: GP (RBF) 尤度の閉形式随伴 (Cholesky を AD tape に載せない)
--- ---------------------------------------------------------------------------
-
--- | Phase 95 B-dsl: 尤度が **単一 'MvNormalGpRBF' observe** のモデルから
--- @(x, α, ρ, σ, ys)@ を現在の param 値で抽出する。 それ以外 (他 Observe/
--- ObserveLM 混在・非 GpRBF) は 'Nothing'。 walk は 'mvNormalObserveOf' と同型。
-gpRBFObserveOf :: (Floating a, Ord a)
-               => Model a r -> Map Text a -> Maybe ([a], a, a, a, [Double])
-gpRBFObserveOf model params =
-  case go model of
-    Just [(MvNormalGpRBF xs al rh sg, ys)] -> Just (xs, al, rh, sg, ys)
-    _                                      -> Nothing
-  where
-    go (Pure _) = Just []
-    go (Free (Sample n _ k)) =
-      case Map.lookup n params of
-        Nothing -> Nothing
-        Just v  -> go (k v)
-    go (Free (Observe _ d ys next)) = ((d, ys) :) <$> go next
-    go (Free (ObserveLM {}))        = Nothing
-    go (Free (Potential _ _ next))  = go next
-    go (Free (Deterministic _ v k)) = go (k v)
-    go (Free (Data _ ys k))         = go (k (map realToFrac ys, ys))
-    go (Free (DataIx _ is k))       = go (k is)
-    go (Free (PlateBegin _ _ next)) = go next
-    go (Free (PlateEnd next))       = go next
-
--- | Phase 95 B-dsl: 'MvNormalGpRBF' 尤度の value+grad を **閉形式随伴**で計算する
--- クロージャを構築する。 適格 (単一 GpRBF observe) のときのみ 'Just'。
---
--- === A案 (汎用 detach) との差 = 84% の除去
--- A案 (§mvNormalAnalyticVG) は surrogate @<G,Σ(θ)>@ を **AD で微分**するため、
--- 毎 leaf で Σ(θ) を @[[a]]@ で組み直し (profile: gpExpQuadCov 55%) その全 N²
--- ノードを reverse-AD tape に載せていた (reifyTypeable+partials 29%)。 本経路は:
---
---   1. カーネル役割 (x/α/ρ/σ) が 'MvNormalGpRBF' の型で明示されているので、
---      **∂Σ/∂θ を閉形式** (@∂Σ/∂α=2K'/α@・@∂Σ/∂ρ=K'∘d²/ρ³@・@∂Σ/∂σ=I@) で書ける。
---   2. G=∂logp/∂Σ・K'・d² は **hmatrix Matrix** (脱リスト) で計算し、
---      @g_θ = <G,∂Σ/∂θ>@ を要素積 + trace で Double 算出 (**AD tape ゼロ**)。
---   3. u への連鎖律は **軽量 surrogate** @g_α·α(u)+g_ρ·ρ(u)+g_σ·σ(u)@ を ad。
---      ad は α/ρ/σ の **3 scalar 抽出のみ** (cov は非展開・lazy)。 これで
---      「どの u が α/ρ/σ か」の対応付けを AD が自動処理する (名前直書き不要)。
---   4. 値 logp と Σ⁻¹/logdet は同じ LAPACK 分解 (@invlndet@) から。
---
--- 距離行列 d² は x (data・定数) から **build 時に 1 度だけ**作り、全 leaf で再利用。
-gpRBFAnalyticVG
-  :: forall r. ModelP r -> [Text] -> [Transform]
-  -> Maybe (VS.Vector Double -> (Double, VS.Vector Double))
-gpRBFAnalyticVG m names trans =
-  case gpRBFObserveOf m probeParams of
-    Just (xs0, _, _, _, _) | not (null xs0) -> Just (closure (buildD2 xs0))
-    _                                       -> Nothing
-  where
-    probeParams :: Map Text Double
-    probeParams = Map.fromList [ (nm, 0) | nm <- names ]
-
-    -- x (data・定数) から距離² 行列 D2_ij=(x_i-x_j)² を build 時 1 回だけ。
-    buildD2 :: [Double] -> LA.Matrix Double
-    buildD2 xs =
-      let nn = length xs
-          xv = VS.fromList xs
-      in LA.matrix nn [ let d = VS.unsafeIndex xv i - VS.unsafeIndex xv j in d * d
-                      | i <- [0 .. nn - 1], j <- [0 .. nn - 1] ]
-
-    paramMapOf :: forall a. Floating a => [a] -> Map Text a
-    paramMapOf us = Map.fromList (zip names [ invTransformF t u | (t, u) <- zip trans us ])
-
-    closure :: LA.Matrix Double -> VS.Vector Double -> (Double, VS.Vector Double)
-    closure d2 uv =
-      let us = VS.toList uv
-      in case gpRBFObserveOf m (paramMapOf us) of
-           Nothing -> (fFullA us, VS.fromList (gradFullA us))       -- 適格崩れ: fallback
-           Just (xs, alphaD, rhoD, sigmaD, ys)
-             | k == 0 || null chunks -> (fFullA us, VS.fromList (gradFullA us))
-             | otherwise ->
-                 let -- 純カーネル K'_ij = α² exp(-0.5 D2/ρ²)。exp は hmatrix の
-                     -- element-wise Floating instance (C ベクトル化) を使い、cmap の
-                     -- Haskell ラムダ per-element boxing (2500 boxed Double/leaf) を回避。
-                     kMat    = LA.scale (alphaD * alphaD)
-                                 (exp (LA.scale (negate 0.5 / (rhoD * rhoD)) d2))
-                     -- Σ = K' + (jitter+σ)I。対角に直接加算 (脱 ident: N×N を 3 パス→1)。
-                     covM    = LA.accum kMat (+) [ ((i, i), 1e-10 + sigmaD) | i <- [0 .. k - 1] ]
-                 -- covM = K'(PSD) + (1e-10+σ>0)I ゆえ本来 PD。Cholesky は LU 全逆行列より
-                 -- O(N³) 定数が軽い (PyMC/Stan と同じ経路)。ただし σ→0⁺ の悪条件で LAPACK が
-                 -- 非 PD 判定する稀ケースに備え mbChol で受け、崩れたら full-AD へ安全退避。
-                 in case LA.mbChol (LA.trustSym covM) of
-                      Nothing    -> (fFullA us, VS.fromList (gradFullA us))  -- 非 PD (稀): fallback
-                      Just uchol ->
-                       let inv     = LA.cholSolve uchol (LA.ident k)         -- Σ⁻¹ (SPD solve)
-                           lndet   = 2 * sum (map log (LA.toList (LA.takeDiag uchol)))  -- log|Σ|=2Σlog U_ii
-                           ds      = [ LA.fromList (map realToFrac yv) | yv <- chunks ]  -- (y - μ), μ=0
-                           alphas  = [ inv LA.#> d | d <- ds ]                 -- Σ⁻¹(y-μ)
-                           quadSum = sum [ d LA.<.> a | (d, a) <- zip ds alphas ]
-                           kA      = fromIntegral k :: Double
-                           nChA    = fromIntegral nCh :: Double
-                           logpObs = nChA * (negate 0.5 * kA * log (2 * pi) - 0.5 * lndet)
-                                     - 0.5 * quadSum
-                           -- 閉形式随伴 g_θ = <G, ∂Σ/∂θ>, G = 0.5(Σ_m α_mα_mᵀ - nCh·Σ⁻¹)。
-                           -- Frobenius 恒等式で G を materialize せず算出 (N×N 一時行列を全廃):
-                           --   <α_mα_mᵀ, M> = α_mᵀ M α_m  (BLAS mat-vec + dot・N² alloc なし)、
-                           --   <Σ⁻¹, M>     = <flatten Σ⁻¹, flatten M>  (BLAS ddot・temp なし)。
-                           -- ∂Σ/∂α=2K'/α・∂Σ/∂σ=I・∂Σ/∂ρ=K'∘d²/ρ³。
-                           kd2      = kMat * d2                              -- ∂Σ/∂ρ 用 Hadamard (1 回だけ)
-                           invFlat  = LA.flatten inv
-                           quadK    = sum [ a LA.<.> (kMat LA.#> a) | a <- alphas ]  -- Σ α_mᵀK'α_m
-                           quadKd2  = sum [ a LA.<.> (kd2  LA.#> a) | a <- alphas ]  -- Σ α_mᵀ(K'∘d²)α_m
-                           aaSum    = sum [ a LA.<.> a | a <- alphas ]              -- Σ α_mᵀα_m
-                           frobIK   = invFlat LA.<.> LA.flatten kMat               -- <Σ⁻¹, K'>
-                           frobIKd2 = invFlat LA.<.> LA.flatten kd2                -- <Σ⁻¹, K'∘d²>
-                           trInv    = LA.sumElements (LA.takeDiag inv)             -- tr(Σ⁻¹)
-                           gAlpha   = (quadK - nChA * frobIK) / alphaD             -- 2·<G,K'>/α
-                           gSigma   = 0.5 * (aaSum - nChA * trInv)                 -- <G,I> = tr(G)
-                           gRho     = 0.5 * (quadKd2 - nChA * frobIKd2) / (rhoD ** 3) -- <G,K'∘d²>/ρ³
-                           -- 軽量 scatter: g_θ を u へ連鎖 (ad は α/ρ/σ の抽出のみ・cov 非展開)
-                           surrogate :: forall a. (Floating a, Ord a, TrackTag a) => [a] -> a
-                           surrogate uu =
-                             case gpRBFObserveOf m (paramMapOf uu) of
-                               Just (_, al, rh, sg, _) ->
-                                 realToFrac gAlpha * al + realToFrac gRho * rh
-                                   + realToFrac gSigma * sg
-                               Nothing -> 0
-                           gObs    = grad surrogate us                        -- ∂obs/∂u
-                           gRest   = gradRestA us                             -- ∂(logPrior+logJac)/∂u
-                           vRest   = fRestA us                                -- (logPrior+logJac)(u)
-                       in ( vRest + logpObs
-                          , VS.fromList (zipWith (+) gRest gObs) )
-             where k      = length xs                                  -- GP 次元
-                   chunks = chunksOf k ys                              -- 通常 1 chunk
-                   nCh    = length chunks
-
-    -- prior + logJac (尤度除く) — scalar・dense 行列なし。
-    fRestA :: (Floating a, Ord a, TrackTag a) => [a] -> a
-    fRestA us = logPrior m (paramMapOf us)
-                  + sum [ logJacF t u | (t, u) <- zip trans us ]
-    -- AD 側は 'logDensityRD' 注入版 (Phase 92 B3・値/勾配とも fRestA と bit 一致)
-    fRestRD :: forall s. Reifies s ADRD.Tape
-            => [ADRD.ReverseDouble s] -> ADRD.ReverseDouble s
-    fRestRD us = logPriorWith logDensityRD m (paramMapOf us)
-                   + sum [ logJacF t u | (t, u) <- zip trans us ]
-    gradRestA :: [Double] -> [Double]
-    gradRestA = grad fRestRD
-
-    -- 適格崩れ時の完全 walk+ad fallback。
-    fFullA :: (Floating a, Ord a, TrackTag a) => [a] -> a
-    fFullA us = logJoint m (paramMapOf us)
-                  + sum [ logJacF t u | (t, u) <- zip trans us ]
-    gradFullA :: [Double] -> [Double]
-    gradFullA = grad fFullA
-
--- ---------------------------------------------------------------------------
--- Phase 92 A2: HMM forward 尤度の閉形式随伴 (forward-backward・AD tape ゼロ)
--- ---------------------------------------------------------------------------
-
--- | Phase 92 A2: 尤度が **単一 'HmmForwardNormal' observe** のモデルから
--- @(π_0, trans, μs, σ, ys)@ を現在の param 値で抽出する。 それ以外 (他 Observe/
--- ObserveLM 混在・非 HMM) は 'Nothing'。 walk は 'gpRBFObserveOf' と同型。
-hmmObserveOf :: (Floating a, Ord a)
-             => Model a r -> Map Text a -> Maybe ([a], [[a]], [a], a, [Double])
-hmmObserveOf model params =
-  case go model of
-    Just [(HmmForwardNormal pi0 tr mus sg, ys)] -> Just (pi0, tr, mus, sg, ys)
-    _                                           -> Nothing
-  where
-    go (Pure _) = Just []
-    go (Free (Sample n _ k)) =
-      case Map.lookup n params of
-        Nothing -> Nothing
-        Just v  -> go (k v)
-    go (Free (Observe _ d ys next)) = ((d, ys) :) <$> go next
-    go (Free (ObserveLM {}))        = Nothing
-    go (Free (Potential _ _ next))  = go next
-    go (Free (Deterministic _ v k)) = go (k v)
-    go (Free (Data _ ys k))         = go (k (map realToFrac ys, ys))
-    go (Free (DataIx _ is k))       = go (k is)
-    go (Free (PlateBegin _ _ next)) = go next
-    go (Free (PlateEnd next))       = go next
-
--- | Phase 92 A2: 'HmmForwardNormal' 尤度の value+grad を **forward-backward の
--- 閉形式随伴**で計算するクロージャを構築する。 適格 (単一 HMM observe) のとき
--- のみ 'Just'。 構成は 'gpRBFAnalyticVG' と同じ 3 段:
---
---   1. forward α / backward β を **Double 空間** (AD tape 外) で 1 回ずつ回し、
---      値 @logL = logSumExp_k α_T[k]@ と閉形式随伴
---      @∂logL/∂μ_k = Σ_t γ_t[k]·(y_t-μ_k)/σ²@ (γ_t[k]=exp(α_t+β_t-logL))・
---      @∂logL/∂T_ij = Σ_t exp(α_t[i]+emit_{t+1}[j]+β_{t+1}[j]-logL)@ (ξ 集計)・
---      @∂logL/∂π_k = γ_0[k]/π_k@・σ も同様、 を Double で算出する。
---   2. u への連鎖律は **軽量 surrogate** @Σ g_θ·θ(u)@ を ad。 ad は
---      π/T/μ/σ の **O(K²) scalar 抽出のみ** (T 長の forward loop は非展開)。
---      dirichlet の棒折り deterministic 等の合成は AD が自動処理する。
---   3. prior + logJac は 'logPrior' ベースの fRest (走査対象から尤度を除外)。
---
--- 従来 walk+ad は T×K² 個の logSumExp/logDensity を毎 leapfrog boxed AD で
--- 再評価していた (Phase 92 A1d: 数値密度系 84% + AD 6%・alloc 23.7GB/5s)。
--- 本経路は同じ O(TK²) を unboxed Double で 2 パス回すだけで tape に載せない。
-hmmAnalyticVG
-  :: forall r. ModelP r -> [Text] -> [Transform]
-  -> Maybe (VS.Vector Double -> (Double, VS.Vector Double))
-hmmAnalyticVG m names trans =
-  case hmmObserveOf m probeParams of
-    Just (pi0, tr, mus, _, ys)
-      | kDim > 0, length tr == kDim, all ((== kDim) . length) tr
-      , length mus == kDim, not (null ys) -> Just closure
-      where kDim = length pi0
-    _ -> Nothing
-  where
-    probeParams :: Map Text Double
-    probeParams = Map.fromList [ (nm, 0) | nm <- names ]
-
-    paramMapOf :: forall a. Floating a => [a] -> Map Text a
-    paramMapOf us = Map.fromList (zip names [ invTransformF t u | (t, u) <- zip trans us ])
-
-    closure :: VS.Vector Double -> (Double, VS.Vector Double)
-    closure uv =
-      let us = VS.toList uv
-      in case hmmObserveOf m (paramMapOf us) of
-           Nothing -> (fFullA us, VS.fromList (gradFullA us))       -- 適格崩れ: fallback
-           Just (pi0D, transD, musD, sgD, ys)
-             | sgD <= 0 || null ys -> (fFullA us, VS.fromList (gradFullA us))
-             | otherwise ->
-                 -- B2-① (2026-07-17): 脱リスト化 — α/β/emit を unboxed 行 vector で持ち、
-                 -- 内側 K ループは lseK (list 非 alloc の 2 パス logSumExp)。γ は非実体化。
-                 let kk  = length pi0D
-                     tT  = length ys
-                     ixs = [0 .. kk - 1]
-                     ysV  = VU.fromList ys
-                     musV = VU.fromList musD
-                     lPi0 = VU.fromList (map safeLog pi0D)
-                     lTr  = VU.fromList (map safeLog (concat transD))   -- 行優先 K×K flat
-                     lsg  = log sgD
-                     c2pi = 0.5 * log (2 * pi)
-                     emitAt t k' = let z = (VU.unsafeIndex ysV t - VU.unsafeIndex musV k') / sgD
-                                   in -0.5 * z * z - lsg - c2pi
-                     emitRows = [ VU.generate kk (emitAt t) | t <- [0 .. tT - 1] ]
-                     -- K 要素 logSumExp (max → sumexp の 2 パス・中間 list 無し)
-                     lseK f = let mx = foldl' (\acc i -> max acc (f i)) negInf ixs
-                              in if mx == negInf then negInf
-                                 else mx + log (foldl' (\acc i -> acc + exp (f i - mx)) 0 ixs)
-                     -- forward: α_t 全行を保持 (随伴の γ/ξ に使う)
-                     alpha0 = VU.zipWith (+) lPi0 (head emitRows)
-                     stepF aPrev emT = VU.generate kk $ \j ->
-                       lseK (\i -> VU.unsafeIndex aPrev i + VU.unsafeIndex lTr (i * kk + j))
-                         + VU.unsafeIndex emT j
-                     alphaRows = scanl stepF alpha0 (tail emitRows)   -- 長さ T
-                     logL = lseK (VU.unsafeIndex (last alphaRows))
-                     -- backward: β_{T-1}=0・β_t[i] = lse_j (lT_ij + emit_{t+1}[j] + β_{t+1}[j])
-                     stepB emNext bNext = VU.generate kk $ \i ->
-                       lseK (\j -> VU.unsafeIndex lTr (i * kk + j)
-                                     + VU.unsafeIndex emNext j + VU.unsafeIndex bNext j)
-                     betaRows = scanr stepB (VU.replicate kk 0) (tail emitRows)  -- 長さ T
-                     -- 閉形式随伴 (全て Double・tape ゼロ・γ_t[k] = exp(α+β-logL) は都度計算)
-                     gammaAt aR bR k' = exp (VU.unsafeIndex aR k' + VU.unsafeIndex bR k' - logL)
-                     abRows = zip3 alphaRows betaRows [0 ..]
-                     gMu = [ foldl' (\acc (aR, bR, t) ->
-                                       acc + gammaAt aR bR k'
-                                             * (VU.unsafeIndex ysV t - VU.unsafeIndex musV k')
-                                             / (sgD * sgD))
-                                    0 abRows
-                           | k' <- ixs ]
-                     gSg = foldl' (\acc (aR, bR, t) ->
-                                     foldl' (\a2 k' ->
-                                               let z = (VU.unsafeIndex ysV t
-                                                          - VU.unsafeIndex musV k') / sgD
-                                               in a2 + gammaAt aR bR k' * (z * z - 1) / sgD)
-                                            acc ixs)
-                                  0 abRows
-                     gPi0 = [ if p > 0 then gammaAt (head alphaRows) (head betaRows) k' / p else 0
-                            | (k', p) <- zip ixs pi0D ]
-                     -- ξ 集計: ∂logL/∂T_ij = Σ_{t<T-1} exp(α_t[i]+emit_{t+1}[j]+β_{t+1}[j]-logL)
-                     xiRows = zip3 alphaRows (tail emitRows) (tail betaRows)
-                     gTr = [ [ foldl' (\acc (aR, emN, bN) ->
-                                         acc + exp (VU.unsafeIndex aR i + VU.unsafeIndex emN j
-                                                      + VU.unsafeIndex bN j - logL))
-                                      0 xiRows
-                             | j <- ixs ]
-                           | i <- ixs ]
-                     -- 軽量 scatter: g_θ を u へ連鎖 (ad は π/T/μ/σ の抽出のみ・T loop 非展開)
-                     surrogate :: forall a. (Floating a, Ord a, TrackTag a) => [a] -> a
-                     surrogate uu =
-                       case hmmObserveOf m (paramMapOf uu) of
-                         Just (p0, tr', ms, s, _) ->
-                           sum (zipWith (\g v -> realToFrac g * v) gPi0 p0)
-                             + sum (zipWith (\gr r -> sum (zipWith (\g v -> realToFrac g * v) gr r))
-                                            gTr tr')
-                             + sum (zipWith (\g v -> realToFrac g * v) gMu ms)
-                             + realToFrac gSg * s
-                         Nothing -> 0
-                     -- B2-② (2026-07-17): prior+logJac と surrogate を 1 本の AD tape に合流し
-                     -- grad' で値+勾配を同時取得 (walk 4 回/eval → Double 1 + AD 1)。
-                     -- fRest の値は vComb から surrogate の Double 値を引いて復元する。
-                     -- B3: prior 密度は 'logDensityRD' 注入 (fRestRD) = 定数
-                     -- hyperparam の lgamma 正規化項を Double へ畳み込み (bit 一致)。
-                     fCombRD :: forall s. Reifies s ADRD.Tape
-                             => [ADRD.ReverseDouble s] -> ADRD.ReverseDouble s
-                     fCombRD uu = fRestRD uu + surrogate uu
-                     (vComb, gComb) = grad' fCombRD us
-                     surrAtUs = sum (zipWith (*) gPi0 pi0D)
-                                  + sum (zipWith (\gr r -> sum (zipWith (*) gr r)) gTr transD)
-                                  + sum (zipWith (*) gMu musD)
-                                  + gSg * sgD
-                 in ( (vComb - surrAtUs) + logL
-                    , VS.fromList gComb )
-
-    safeLog :: Double -> Double
-    safeLog x = if x <= 0 then negInf else log x
-
-    -- prior + logJac (尤度除く) — 'gpRBFAnalyticVG' と同じ。
-    fRestA :: (Floating a, Ord a, TrackTag a) => [a] -> a
-    fRestA us = logPrior m (paramMapOf us)
-                  + sum [ logJacF t u | (t, u) <- zip trans us ]
-    gradRestA :: [Double] -> [Double]
-    gradRestA = grad fRestA
-
-    -- fRestA の AD 特化 (Phase 92 B3): 'logDensityRD' 注入で定数 hyperparam の
-    -- lgamma 正規化項を Double へ畳み込む。 値・勾配とも fRestA と bit 一致
-    -- ('logDensityRD' の注釈参照)。
-    fRestRD :: forall s. Reifies s ADRD.Tape
-            => [ADRD.ReverseDouble s] -> ADRD.ReverseDouble s
-    fRestRD us = logPriorWith logDensityRD m (paramMapOf us)
-                   + sum [ logJacF t u | (t, u) <- zip trans us ]
-
-    -- 適格崩れ時の完全 walk+ad fallback。
-    fFullA :: (Floating a, Ord a, TrackTag a) => [a] -> a
-    fFullA us = logJoint m (paramMapOf us)
-                  + sum [ logJacF t u | (t, u) <- zip trans us ]
-    gradFullA :: [Double] -> [Double]
-    gradFullA = grad fFullA
-
--- ---------------------------------------------------------------------------
--- Phase 101 A2: ARMA(1,1) 尤度の閉形式随伴 (逆向き随伴再帰・AD tape ゼロ)
--- ---------------------------------------------------------------------------
-
--- | Phase 101 A2: 尤度が **単一 'ArmaNormal' observe** のモデルから
--- @(μ, φ, θ, σ, ys)@ を現在の param 値で抽出する。 それ以外 (他 Observe 混在・
--- 非 ARMA) は 'Nothing'。 walk は 'hmmObserveOf' と同型。
-armaObserveOf :: (Floating a, Ord a)
-              => Model a r -> Map Text a -> Maybe (a, a, a, a, [Double])
-armaObserveOf model params =
-  case go model of
-    Just [(ArmaNormal mu phi theta sg, ys)] -> Just (mu, phi, theta, sg, ys)
-    _                                       -> Nothing
-  where
-    go (Pure _) = Just []
-    go (Free (Sample n _ k)) =
-      case Map.lookup n params of
-        Nothing -> Nothing
-        Just v  -> go (k v)
-    go (Free (Observe _ d ys next)) = ((d, ys) :) <$> go next
-    go (Free (ObserveLM {}))        = Nothing
-    go (Free (Potential _ _ next))  = go next
-    go (Free (Deterministic _ v k)) = go (k v)
-    go (Free (Data _ ys k))         = go (k (map realToFrac ys, ys))
-    go (Free (DataIx _ is k))       = go (k is)
-    go (Free (PlateBegin _ _ next)) = go next
-    go (Free (PlateEnd next))       = go next
-
--- | Phase 101 A2: 'ArmaNormal' 尤度の value+grad を **逆向き 1 パスの閉形式
--- 随伴**で計算するクロージャを構築する。 適格 (単一 ArmaNormal observe) の
--- ときのみ 'Just'。 構成は 'hmmAnalyticVG' と同じ 3 段:
---
---   1. err 前向き再帰 (@e_1 = y_1 − (μ+φμ)@・@e_t = y_t − μ − φ·y_{t−1} −
---      θ·e_{t−1}@) と随伴の逆向き再帰 (@ē_t = −e_t/σ² − θ·ē_{t+1}@・
---      @ē_T = −e_T/σ²@) を **Double 空間** (AD tape 外) で 1 回ずつ回し、
---      値 @logL = Σ_t log N(e_t; 0, σ)@ と閉形式随伴
---      @∂logL/∂μ = ē_1·(−(1+φ)) − Σ_{t≥2} ē_t@・
---      @∂logL/∂φ = ē_1·(−μ) − Σ_{t≥2} ē_t·y_{t−1}@・
---      @∂logL/∂θ = −Σ_{t≥2} ē_t·e_{t−1}@・
---      @∂logL/∂σ = −T/σ + (Σ_t e_t²)/σ³@ を算出する。
---   2. u への連鎖律は **軽量 surrogate** @Σ g_θ·θ(u)@ を ad (μ/φ/θ/σ の
---      4 scalar 抽出のみ・T 長の再帰は非展開)。
---   3. prior + logJac は 'logDensityRD' 注入の fRestRD (Phase 92 B3 と同じ)。
---
--- 従来 walk+ad は T 本の 'logDensity' + mapAccumL 再帰を毎 leapfrog boxed AD
--- で再評価していた (Phase 101 A1: logDensity 31.2% + armaModel 20.9%・
--- alloc 72%)。 本経路は同じ O(T) を unboxed Double で 2 パス回すだけ。
-armaAnalyticVG
-  :: forall r. ModelP r -> [Text] -> [Transform]
-  -> Maybe (VS.Vector Double -> (Double, VS.Vector Double))
-armaAnalyticVG m names trans =
-  case armaObserveOf m probeParams of
-    Just (_, _, _, _, ys) | not (null ys) -> Just closure
-    _                                     -> Nothing
-  where
-    probeParams :: Map Text Double
-    probeParams = Map.fromList [ (nm, 0) | nm <- names ]
-
-    paramMapOf :: forall a. Floating a => [a] -> Map Text a
-    paramMapOf us = Map.fromList (zip names [ invTransformF t u | (t, u) <- zip trans us ])
-
-    closure :: VS.Vector Double -> (Double, VS.Vector Double)
-    closure uv =
-      let us = VS.toList uv
-      in case armaObserveOf m (paramMapOf us) of
-           Nothing -> (fFullA us, VS.fromList (gradFullA us))       -- 適格崩れ: fallback
-           Just (muD, phiD, thD, sgD, ys)
-             | sgD <= 0 || null ys -> (fFullA us, VS.fromList (gradFullA us))
-             | otherwise ->
-                 let ysV = VU.fromList ys
-                     tT  = VU.length ysV
-                     s2  = sgD * sgD
-                     -- forward: err 列 (unboxed・prefix 参照の constructN)
-                     errsV = VU.constructN tT $ \pre ->
-                       let i = VU.length pre
-                       in if i == 0
-                            then VU.unsafeIndex ysV 0 - (muD + phiD * muD)
-                            else VU.unsafeIndex ysV i
-                                   - (muD + phiD * VU.unsafeIndex ysV (i - 1)
-                                        + thD * VU.unsafeIndex pre (i - 1))
-                     sumE2 = VU.foldl' (\acc e -> acc + e * e) 0 errsV
-                     logL  = fromIntegral tT * (-0.5 * log (2 * pi) - log sgD)
-                               - sumE2 / (2 * s2)
-                     -- backward: 随伴 ē (suffix 参照の constructrN・ē_T = −e_T/σ²)
-                     ebarV = VU.constructrN tT $ \suf ->
-                       let t = tT - 1 - VU.length suf
-                           direct = negate (VU.unsafeIndex errsV t) / s2
-                       in if VU.null suf
-                            then direct
-                            else direct - thD * VU.unsafeIndex suf 0
-                     -- 閉形式随伴 (全て Double・tape ゼロ)
-                     gMu = VU.unsafeIndex ebarV 0 * negate (1 + phiD)
-                             - VU.ifoldl' (\acc t eb -> if t == 0 then acc else acc + eb)
-                                          0 ebarV
-                     gPhi = VU.unsafeIndex ebarV 0 * negate muD
-                              - VU.ifoldl' (\acc t eb ->
-                                              if t == 0 then acc
-                                              else acc + eb * VU.unsafeIndex ysV (t - 1))
-                                           0 ebarV
-                     gTh = negate (VU.ifoldl' (\acc t eb ->
-                                                 if t == 0 then acc
-                                                 else acc + eb * VU.unsafeIndex errsV (t - 1))
-                                              0 ebarV)
-                     gSg = negate (fromIntegral tT) / sgD + sumE2 / (s2 * sgD)
-                     -- 軽量 scatter: g_θ を u へ連鎖 (ad は μ/φ/θ/σ の 4 scalar 抽出のみ)
-                     surrogate :: forall a. (Floating a, Ord a, TrackTag a) => [a] -> a
-                     surrogate uu =
-                       case armaObserveOf m (paramMapOf uu) of
-                         Just (mu', phi', th', sg', _) ->
-                           realToFrac gMu * mu' + realToFrac gPhi * phi'
-                             + realToFrac gTh * th' + realToFrac gSg * sg'
-                         Nothing -> 0
-                     fCombRD :: forall s. Reifies s ADRD.Tape
-                             => [ADRD.ReverseDouble s] -> ADRD.ReverseDouble s
-                     fCombRD uu = fRestRD uu + surrogate uu
-                     (vComb, gComb) = grad' fCombRD us
-                     surrAtUs = gMu * muD + gPhi * phiD + gTh * thD + gSg * sgD
-                 in ( (vComb - surrAtUs) + logL
-                    , VS.fromList gComb )
-
-    -- prior + logJac (尤度除く)・'logDensityRD' 注入 — 'hmmAnalyticVG' と同じ。
-    fRestRD :: forall s. Reifies s ADRD.Tape
-            => [ADRD.ReverseDouble s] -> ADRD.ReverseDouble s
-    fRestRD us = logPriorWith logDensityRD m (paramMapOf us)
-                   + sum [ logJacF t u | (t, u) <- zip trans us ]
-
-    -- 適格崩れ時の完全 walk+ad fallback。
-    fFullA :: (Floating a, Ord a, TrackTag a) => [a] -> a
-    fFullA us = logJoint m (paramMapOf us)
-                  + sum [ logJacF t u | (t, u) <- zip trans us ]
-    gradFullA :: [Double] -> [Double]
-    gradFullA = grad fFullA
-
--- ---------------------------------------------------------------------------
--- Phase 101 A3: graded response IRT 尤度の解析勾配 (AD tape ゼロ)
--- ---------------------------------------------------------------------------
-
--- | Phase 101 A3: 尤度が **単一 'GradedResponseIrt' observe** のモデルから
--- @(θs, ncats, δs, γs, ys)@ を現在の param 値で抽出する。 walk は
--- 'armaObserveOf' と同型。
-gradedIrtObserveOf :: (Floating a, Ord a)
-                   => Model a r -> Map Text a
-                   -> Maybe ([a], [Int], [Double], [[Double]], [Double])
-gradedIrtObserveOf model params =
-  case go model of
-    Just [(GradedResponseIrt ths ncats dls gms, ys)] -> Just (ths, ncats, dls, gms, ys)
-    _                                                -> Nothing
-  where
-    go (Pure _) = Just []
-    go (Free (Sample n _ k)) =
-      case Map.lookup n params of
-        Nothing -> Nothing
-        Just v  -> go (k v)
-    go (Free (Observe _ d ys next)) = ((d, ys) :) <$> go next
-    go (Free (ObserveLM {}))        = Nothing
-    go (Free (Potential _ _ next))  = go next
-    go (Free (Deterministic _ v k)) = go (k v)
-    go (Free (Data _ ys k))         = go (k (map realToFrac ys, ys))
-    go (Free (DataIx _ is k))       = go (k is)
-    go (Free (PlateBegin _ _ next)) = go next
-    go (Free (PlateEnd next))       = go next
-
--- | Phase 101 A3: 'GradedResponseIrt' 尤度の value+grad を **解析勾配**で
--- 計算するクロージャを構築する。 適格 (単一 GradedResponseIrt observe) の
--- ときのみ 'Just'。 構成は 'armaAnalyticVG' と同じ 3 段:
---
---   1. 各 (child i, item j, grade≠−1) の @Q_k = invlogit(δ_j(θ_i−γ_jk))@ と
---      カテゴリ確率 p (隣接差) を **Double 空間**で評価し、 値
---      @logL = Σ log p@ と解析勾配 @∂logL/∂θ_i = Σ_j (dp/dθ)/p@
---      (@dQ/dθ = δ·Q(1−Q)@ の隣接差) を算出する。
---   2. u への連鎖律は **軽量 surrogate** @Σ g_i·θ_i(u)@ を ad
---      (θs の nChild scalar 抽出のみ)。
---   3. prior + logJac は 'logDensityRD' 注入の fRestRD。
---
--- 従来 walk+ad は nChild×nItem×ncat の Q/p リスト構築 (`!!` 索引込) を毎
--- leapfrog boxed AD で再評価していた (Phase 101 A1: logCatProb 64.8% time /
--- 73.2% alloc)。 本経路は同じ O(Σ ncat) を Double で 1 パス回すだけ。
-gradedIrtAnalyticVG
-  :: forall r. ModelP r -> [Text] -> [Transform]
-  -> Maybe (VS.Vector Double -> (Double, VS.Vector Double))
-gradedIrtAnalyticVG m names trans =
-  case gradedIrtObserveOf m probeParams of
-    Just (ths, ncats, dls, gms, ys)
-      | not (null ths), not (null ys)
-      , length ncats == length dls, length ncats == length gms -> Just closure
-    _ -> Nothing
-  where
-    probeParams :: Map Text Double
-    probeParams = Map.fromList [ (nm, 0) | nm <- names ]
-
-    paramMapOf :: forall a. Floating a => [a] -> Map Text a
-    paramMapOf us = Map.fromList (zip names [ invTransformF t u | (t, u) <- zip trans us ])
-
-    closure :: VS.Vector Double -> (Double, VS.Vector Double)
-    closure uv =
-      let us = VS.toList uv
-      in case gradedIrtObserveOf m (paramMapOf us) of
-           Nothing -> (fFullA us, VS.fromList (gradFullA us))       -- 適格崩れ: fallback
-           Just (thsD, ncats, dls, gms, ys)
-             | null ys -> (fFullA us, VS.fromList (gradFullA us))
-             | otherwise ->
-                 let nItem = length ncats
-                     rows  = chunksOf nItem ys
-                     -- (logL, g) を child i 毎に Double で 1 パス集計
-                     childLG th row = foldl' step (0, 0) (zip4 ncats dls gms row)
-                       where
-                         step (accL, accG) (nc, dl, gm, grD)
-                           | grD == -1 = (accL, accG)
-                           | otherwise =
-                               let gr   = round grD :: Int
-                                   kMax = nc - 1
-                                   q kk = 1 / (1 + exp (negate (dl * (th - gm !! (kk - 1)))))
-                                   dq kk = let qv = q kk in dl * qv * (1 - qv)
-                                   (p, dp)
-                                     | gr == 1   = (1 - q 1, negate (dq 1))
-                                     | gr == nc  = (q kMax, dq kMax)
-                                     | otherwise = (q (gr - 1) - q gr, dq (gr - 1) - dq gr)
-                               in (accL + log p, accG + dp / p)
-                     lgs  = [ childLG th row | (th, row) <- zip thsD rows ]
-                     logL = sum (map fst lgs)
-                     gThs = map snd lgs
-                     -- 軽量 scatter: g_i を u へ連鎖 (ad は θs の scalar 抽出のみ)
-                     surrogate :: forall a. (Floating a, Ord a, TrackTag a) => [a] -> a
-                     surrogate uu =
-                       case gradedIrtObserveOf m (paramMapOf uu) of
-                         Just (ths', _, _, _, _) ->
-                           sum (zipWith (\g v -> realToFrac g * v) gThs ths')
-                         Nothing -> 0
-                     fCombRD :: forall s. Reifies s ADRD.Tape
-                             => [ADRD.ReverseDouble s] -> ADRD.ReverseDouble s
-                     fCombRD uu = fRestRD uu + surrogate uu
-                     (vComb, gComb) = grad' fCombRD us
-                     surrAtUs = sum (zipWith (*) gThs thsD)
-                 in ( (vComb - surrAtUs) + logL
-                    , VS.fromList gComb )
-
-    -- prior + logJac (尤度除く)・'logDensityRD' 注入 — 'armaAnalyticVG' と同じ。
-    fRestRD :: forall s. Reifies s ADRD.Tape
-            => [ADRD.ReverseDouble s] -> ADRD.ReverseDouble s
-    fRestRD us = logPriorWith logDensityRD m (paramMapOf us)
-                   + sum [ logJacF t u | (t, u) <- zip trans us ]
-
-    -- 適格崩れ時の完全 walk+ad fallback。
-    fFullA :: (Floating a, Ord a, TrackTag a) => [a] -> a
-    fFullA us = logJoint m (paramMapOf us)
-                  + sum [ logJacF t u | (t, u) <- zip trans us ]
-    gradFullA :: [Double] -> [Double]
-    gradFullA = grad fFullA
-
--- | Phase 54.4b/54.6: 'gradADU' の **静的部分** (Gaussian LM ブロック抽出・
--- 設計列のベクトル化・名前→index 解決・`ad` クロージャ構築) を 1 度だけ行い、
--- unconstrained ベクトルを受けて勾配ベクトルを返すクロージャを構築する。
--- NUTS / HMC は draw ループの**外**で 1 度呼び、 全 leapfrog で再利用する。
---
--- Phase 54.6: per-op 計測 (prof-nuts-54.4e.prof) で per-call の Text-key
--- `Map.fromList` 組立 + `Map.fromListWith` 勾配集約 (compileGradU self 17.9%) と
--- vec-tape の演算毎ベクトル割当 (~52%) が残ボトルネックと確定 → 名前は compile
--- 時に index へ解決し、 勾配は ST mutable vector に解析閉形式で直接集約する
--- (Gaussian LM の勾配は ∂β_k=X_kᵀr/σ²・∂u_j=Σ_{i∈g_j}r_i/σ²・∂σ=-n/σ+sumR2/σ³
--- の閉形式ゆえ汎用 tape 不要)。
-compileGradUV :: forall r. ModelP r -> [Text] -> [Transform]
-              -> (VS.Vector Double -> VS.Vector Double)
-compileGradUV m names trans =
-  case gaussLMBlocksAuto m of
-    ([], _) -> case synthVecIR m of
-      -- Phase 95: 尤度が単一 dense MvNormal observe なら解析随伴。 Gp-RBF (B-dsl・
-      -- 閉形式随伴) を最優先、 次に汎用 MvNormal (A案・flat detach)、 いずれも
-      -- 非適格なら従来の全体 ad (後方互換)。
-      Nothing -> case gpRBFAnalyticVG m names trans of
-        Just vg -> \uv -> snd (vg uv)
-        Nothing -> case mvNormalAnalyticVG m names trans of
-          Just vg -> \uv -> snd (vg uv)
-          Nothing -> \uv -> VS.fromList (gradFull (VS.toList uv))
-      Just (gs, fams, sObs) ->                  -- 54.11: ベクトル式 IR (非線形 μ)
-        let ixOf   = Map.fromList (zip names [0 ..])
-            nP     = length names
-            transB = BV.fromList trans
-            cvi    = compileVecIR ixOf gs fams
-            famSet = Set.fromList (concat [ ms | (ms, _, _) <- fams ])
-            cps    = constPriorsOf m famSet
-            lnGroups = collectLogNormalGroups m        -- Phase 98 A3: LogNormal 群
-            lnUNames = concat [ us | (us, _, _) <- lnGroups ]
-            lnIx   = map (resolveLogNormal ixOf) lnGroups
-            exclNames = sObs `Set.union` famSet
-                        `Set.union` Set.fromList (map fst cps)
-                        `Set.union` Set.fromList lnUNames
-            noResid = residualFreeOfDensity exclNames m
-            cpIx   = [ (ixOf Map.! n, d) | (n, d) <- cps ]
-            mPriorGrad
-              | noResid   = Nothing
-              | otherwise = Just (grad (fExcl (compileResidual exclNames m) exclNames))
-        in \uv ->
-             let pc = VS.generate nP $ \i ->
-                        invTransformF (transB BV.! i) (uv `VS.unsafeIndex` i)
-                 mgc = runST $ do
-                   mg <- VSM.replicate nP 0
-                   ok <- gradVecIR cvi pc mg
-                   if ok
-                     then do
-                       mapM_ (\(i, d) ->
-                                case constPriorGradD d (pc `VS.unsafeIndex` i) of
-                                  Just g  -> VSM.modify mg (+ g) i
-                                  Nothing -> pure ()) cpIx
-                       mapM_ (\ln -> gradLogNormalIx ln pc mg) lnIx  -- A3
-                       Just <$> VS.unsafeFreeze mg
-                     else pure Nothing
-             in case mgc of
-                  -- guard 違反 (観測項が定数 -∞ の境界領域・例 invLogit の FP
-                  -- 飽和 p==1): walk+ad に per-call fallback し従来経路と同一の
-                  -- 勾配 (違反行 = 定数 → 勾配 0・他の行は有効) を返す。
-                  -- ★旧 tape (54.11-55.4) は unguarded で NaN が全勾配を汚染し
-                  -- NUTS が max-depth 迷走する潜在バグだった (56.2 で修正・
-                  -- 'constPriorGradD' の「guard 違反 = 勾配 0 で ad と一致」 と
-                  -- 同じ原則)。 境界点のみの稀ケースゆえ per-draw 影響なし。
-                  Nothing -> VS.fromList (gradFull (VS.toList uv))
-                  Just gC -> case mPriorGrad of
-                    Nothing ->
-                      VS.generate nP $ \i ->
-                        let t = transB BV.! i
-                            u = uv `VS.unsafeIndex` i
-                        in gC `VS.unsafeIndex` i * dInvTransform t u
-                           + dLogJacU t u
-                    Just priorGrad ->
-                      let pg = priorGrad (VS.toList uv)
-                      in VS.fromList
-                           [ pg_i + gC `VS.unsafeIndex` i * dInvTransform t u
-                           | (i, (pg_i, (t, u))) <- zip [0 :: Int ..]
-                               (zip pg (zip trans (VS.toList uv))) ]
-    (gbs, synthObs) ->                                    -- ハイブリッド (静的 hoist)
-      let -- 54.4c: REff (Just scale) の u-prior は解析勾配・u_j を ad から除外。
-          -- 54.4e: 定数パラメタ prior も解析勾配・ad から除外。 密度項が残らな
-          -- ければ ad クロージャを丸ごと省略 (logJac 勾配も解析式)。
-          (priorREs, cps, exclNames, cblocks, hierGroups, noResid) = analyzeGaussModel m gbs synthObs
-          ixOf   = Map.fromList (zip names [0 ..])
-          nP     = length names
-          transB = BV.fromList trans                      -- boxed (Storable 不可)
-          cbIx   = map (resolveLMBlock ixOf) cblocks
-          reIx   = [ ReffPriorIx (VU.fromList (map (ixOf Map.!) uNames))
-                                 (ixOf Map.! scaleName)
-                   | (uNames, scaleName) <- priorREs ]
-          hniIx  = map (resolveHierNormal ixOf) hierGroups
-          cpIx   = [ (ixOf Map.! n, d) | (n, d) <- cps ]
-          mPriorGrad                                      -- 残 prior 等の ad (fallback)
-            | noResid   = Nothing
-            | otherwise = Just (grad (fExcl (compileResidual exclNames m) exclNames))
-      in hybridGradClosure nP transB trans
-           (\pc mg -> do
-              mapM_ (\cb -> gradLMBlockIx cb pc mg) cbIx
-              mapM_ (\ri -> gradReffPriorIx ri pc mg) reIx
-              mapM_ (\hn -> gradHierNormalIx hn pc mg) hniIx
-              mapM_ (\(i, d) ->
-                       case constPriorGradD d (pc `VS.unsafeIndex` i) of
-                         Just g  -> VSM.modify mg (+ g) i
-                         Nothing -> pure ()) cpIx)
-           mPriorGrad
-  where
-    gradFull = grad fFull
-    fFull us =
-      let paramsC = Map.fromList
-            [ (n, invTransformF t u) | (n, t, u) <- zip3 names trans us ]
-          logJac  = sum [ logJacF t u | (t, u) <- zip trans us ]
-      in logJoint m paramsC + logJac
-    fExcl mcr excl us =
-      let paramsC = Map.fromList
-            [ (n, invTransformF t u) | (n, t, u) <- zip3 names trans us ]
-          logJac  = sum [ logJacF t u | (t, u) <- zip trans us ]
-      in residualExcl mcr excl m paramsC + logJac
-
--- | Phase 87.2b: 'compileGradUV' の value-and-grad 融合版 (JAX @value_and_grad@
--- 相当)。 返り値 = (logπ(u) (logJac 込・'compileLogPUV' と同値)・∇logπ(u)
--- ('compileGradUV' と同値))。 NUTS の葉は leapfrog 最終勾配とエネルギー (logπ)
--- を同一点で別々に評価していた (prof 実測で葉 logPi が全体の 19%) — 本閉包は
--- forward pass を 1 度だけ走らせて両方を返す。 経路分岐・fallback 意味論は
--- 'compileGradUV' / 'compileLogPUV' と対 (vecIR guard 違反 = 値 -∞ +
--- 勾配 walk+ad fallback)。
-compileGradValUV :: forall r. ModelP r -> [Text] -> [Transform]
-                 -> (VS.Vector Double -> (Double, VS.Vector Double))
-compileGradValUV m names trans =
-  case gradValPlan m names trans of
-    GVPure f -> f
-    GVVecIR sz prep core finish -> \uv ->
-      let pc  = prep uv
-          mvg = runST $ do
-            ar  <- VSM.unsafeNew sz
-            adj <- VSM.unsafeNew sz
-            core ar adj pc
-      in finish uv pc mvg
-
--- | Phase 90 A11-4①: 'compileGradValUV' の monadic 版。 vecIR 経路の作業
--- バッファ (forward arena + 随伴 arena・13-traffic 実測で 34k セル × 2 =
--- 葉勾配 0.139ms 中の確保 0.031ms + GC churn) を **閉包生成時に 1 度だけ**
--- 確保し、 全 leapfrog 呼出で再利用する。 閉包は chain ごとに生成される
--- ('nutsStream' 内) ため、 chain 横断 spark 並列 ('nutsChainsPure') とも
--- 干渉しない。 返る値/勾配は毎回 fresh に freeze されるので alias しない。
--- 非 vecIR 経路 (walk+ad / ハイブリッド) は従来 pure 閉包をそのまま包む。
-compileGradValUVM :: forall m r. PrimMonad m
-                  => ModelP r -> [Text] -> [Transform]
-                  -> m (VS.Vector Double -> m (Double, VS.Vector Double))
-compileGradValUVM m names trans =
-  case gradValPlan m names trans of
-    GVPure f -> pure (\uv -> pure (f uv))
-    GVVecIR sz prep core finish -> do
-      ar  <- VSM.unsafeNew sz
-      adj <- VSM.unsafeNew sz
-      pure $ \uv -> do
-        let pc = prep uv
-        mvg <- stToPrim (core ar adj pc)
-        pure (finish uv pc mvg)
-
--- | Phase 90 A11-4①: 'compileGradValUV' / 'compileGradValUVM' が共有する
--- 静的解析結果。 vecIR 経路のみ per-call の arena/adj 確保をバッファ注入
--- (prep / core / finish の 3 分割) に分離し、 pure 版 (毎回確保・従来意味論)
--- と monadic 版 (chain 閉包で 1 回確保) が同一 per-call コードを共有する。
-data GradValPlan
-  = GVPure (VS.Vector Double -> (Double, VS.Vector Double))
-    -- ^ walk+ad fallback / ハイブリッド経路 (arena 非使用・従来 pure 閉包)。
-  | GVVecIR
-      !Int                                    -- ^ arena/adj サイズ ('vpSize')
-      (VS.Vector Double -> VS.Vector Double)  -- ^ prep: uv → pc (invTransform)
-      (forall s. VSM.MVector s Double -> VSM.MVector s Double
-                 -> VS.Vector Double
-                 -> ST s (Maybe (Double, VS.Vector Double)))
-        -- ^ core: ar adj pc → (値, constrained 勾配)。 guard 違反 = Nothing。
-      (VS.Vector Double -> VS.Vector Double
-                 -> Maybe (Double, VS.Vector Double)
-                 -> (Double, VS.Vector Double))
-        -- ^ finish: uv pc mvg → 最終 (logπ, ∇logπ) (chain rule + fallback)。
-
-gradValPlan :: forall r. ModelP r -> [Text] -> [Transform] -> GradValPlan
-gradValPlan m names trans =
-  case gaussLMBlocksAuto m of
-    ([], _) -> case synthVecIR m of
-      -- Phase 95: 尤度が単一 dense MvNormal observe なら解析随伴 (pure 閉包 = GVPure)。
-      -- Phase 92: 単一 HmmForwardNormal observe も同様 (forward-backward 閉形式)。
-      -- Phase 101: 単一 ArmaNormal observe も同様 (逆向き随伴再帰の閉形式)。
-      -- HMM → ARMA → Gp-RBF (B-dsl・閉形式) → 汎用 MvNormal (A案) → 従来の全体 walk+ad。
-      Nothing -> case hmmAnalyticVG m names trans of
-        Just vg -> GVPure vg
-        Nothing -> case armaAnalyticVG m names trans of
-          Just vg -> GVPure vg
-          Nothing -> case gradedIrtAnalyticVG m names trans of
-           Just vg -> GVPure vg
-           Nothing -> case gpRBFAnalyticVG m names trans of
-            Just vg -> GVPure vg
-            Nothing -> case mvNormalAnalyticVG m names trans of
-              Just vg -> GVPure vg
-              Nothing -> GVPure $ \uv ->        -- 後方互換: 全体を walk + ad (融合なし)
-                let us = VS.toList uv
-                in (fFull us, VS.fromList (gradFull us))
-      Just (gs, fams, sObs) ->                -- ベクトル式 IR (compileGradUV と同静的)
-        let ixOf   = Map.fromList (zip names [0 ..])
-            nP     = length names
-            transB = BV.fromList trans
-            cvi    = compileVecIR ixOf gs fams
-            famSet = Set.fromList (concat [ ms | (ms, _, _) <- fams ])
-            cps    = constPriorsOf m famSet
-            lnGroups = collectLogNormalGroups m        -- Phase 98 A3: LogNormal 群
-            lnUNames = concat [ us | (us, _, _) <- lnGroups ]
-            lnIx   = map (resolveLogNormal ixOf) lnGroups
-            exclNames = sObs `Set.union` famSet
-                        `Set.union` Set.fromList (map fst cps)
-                        `Set.union` Set.fromList lnUNames
-            noResid = residualFreeOfDensity exclNames m
-            cpIx   = [ (ixOf Map.! n, d) | (n, d) <- cps ]
-            mPrior                             -- (勾配, 値) の対 (fExcl は logJac 込)
-              | noResid   = Nothing
-              | otherwise = let mcr = compileResidual exclNames m
-                            in Just (grad (fExcl mcr exclNames), fExcl mcr exclNames)
-            prep uv = VS.generate nP $ \i ->
-                        invTransformF (transB BV.! i) (uv `VS.unsafeIndex` i)
-            core :: forall s. VSM.MVector s Double -> VSM.MVector s Double
-                 -> VS.Vector Double -> ST s (Maybe (Double, VS.Vector Double))
-            core ar adj pc = do
-              mg <- VSM.replicate nP 0
-              mv <- gradVecIRValWith cvi ar adj pc mg
-              case mv of
-                Nothing -> pure Nothing
-                Just v  -> do
-                  mapM_ (\(i, d) ->
-                           case constPriorGradD d (pc `VS.unsafeIndex` i) of
-                             Just g  -> VSM.modify mg (+ g) i
-                             Nothing -> pure ()) cpIx
-                  mapM_ (\ln -> gradLogNormalIx ln pc mg) lnIx  -- A3
-                  gv <- VS.unsafeFreeze mg
-                  pure (Just (v, gv))
-            finish uv pc mvg = case mvg of
-              -- guard 違反: 値 = -∞ ('vecIRValue' と同一)・勾配 = walk+ad
-              -- fallback ('compileGradUV' と同一)。
-              Nothing -> ((-1) / 0, VS.fromList (gradFull (VS.toList uv)))
-              Just (vIR, gC) ->
-                let cpVal = sum [ logDensity d (pc `VS.unsafeIndex` i)
-                                | (i, d) <- cpIx ]
-                          + sum [ valueLogNormalIx ln pc | ln <- lnIx ]  -- A3
-                in case mPrior of
-                  Nothing ->
-                    let logJac = sum [ logJacF (transB BV.! i)
-                                               (uv `VS.unsafeIndex` i)
-                                     | i <- [0 .. nP - 1] ]
-                        g = VS.generate nP $ \i ->
-                              let t = transB BV.! i
-                                  u = uv `VS.unsafeIndex` i
-                              in gC `VS.unsafeIndex` i * dInvTransform t u
-                                 + dLogJacU t u
-                    in (vIR + cpVal + logJac, g)
-                  Just (priorGrad, priorVal) ->
-                    let us = VS.toList uv
-                        pg = priorGrad us
-                        g = VS.fromList
-                              [ pg_i + gC `VS.unsafeIndex` i * dInvTransform t u
-                              | (i, (pg_i, (t, u))) <- zip [0 :: Int ..]
-                                  (zip pg (zip trans us)) ]
-                    in (vIR + cpVal + priorVal us, g)
-        in GVVecIR (vpSize (cvProg cvi)) prep core finish
-    (gbs, synthObs) -> GVPure $               -- ハイブリッド (compileGradUV と同静的)
-      let (priorREs, cps, exclNames, cblocks, hierGroups, noResid) = analyzeGaussModel m gbs synthObs
-          ixOf   = Map.fromList (zip names [0 ..])
-          nP     = length names
-          transB = BV.fromList trans
-          cbIx   = map (resolveLMBlock ixOf) cblocks
-          reIx   = [ ReffPriorIx (VU.fromList (map (ixOf Map.!) uNames))
-                                 (ixOf Map.! scaleName)
-                   | (uNames, scaleName) <- priorREs ]
-          hniIx  = map (resolveHierNormal ixOf) hierGroups
-          cpIx   = [ (ixOf Map.! n, d) | (n, d) <- cps ]
-          mPrior
-            | noResid   = Nothing
-            | otherwise = let mcr = compileResidual exclNames m
-                          in Just (grad (fExcl mcr exclNames), fExcl mcr exclNames)
-      in hybridGradValClosure nP transB trans
-           (\pc mg -> do
-              mapM_ (\cb -> gradLMBlockIx cb pc mg) cbIx
-              mapM_ (\ri -> gradReffPriorIx ri pc mg) reIx
-              mapM_ (\hn -> gradHierNormalIx hn pc mg) hniIx
-              mapM_ (\(i, d) ->
-                       case constPriorGradD d (pc `VS.unsafeIndex` i) of
-                         Just g  -> VSM.modify mg (+ g) i
-                         Nothing -> pure ()) cpIx)
-           (\pc -> sum [ valueLMBlockIx cb pc | cb <- cbIx ]
-                   + sum [ valueReffPriorIx ri pc | ri <- reIx ]
-                   + sum [ valueHierNormalIx hn pc | hn <- hniIx ]
-                   + sum [ logDensity d (pc `VS.unsafeIndex` i)
-                         | (i, d) <- cpIx ])
-           mPrior
-  where
-    gradFull = grad fFull
-    fFull us =
-      let paramsC = Map.fromList
-            [ (n, invTransformF t u) | (n, t, u) <- zip3 names trans us ]
-          logJac  = sum [ logJacF t u | (t, u) <- zip trans us ]
-      in logJoint m paramsC + logJac
-    fExcl mcr excl us =
-      let paramsC = Map.fromList
-            [ (n, invTransformF t u) | (n, t, u) <- zip3 names trans us ]
-          logJac  = sum [ logJacF t u | (t, u) <- zip trans us ]
-      in residualExcl mcr excl m paramsC + logJac
-
--- | Phase 87.2b: 'hybridGradClosure' の value-and-grad 融合版。 解析勾配
--- (@gradC@) に加えて解析**値** (@valC@ = 'compileLogPUV' ハイブリッド経路の
--- analytic と同一) を計算し、 (値 (logJac 込)・勾配) を返す。
-hybridGradValClosure
-  :: Int -> BV.Vector Transform -> [Transform]
-  -> (forall s. VS.Vector Double -> VSM.MVector s Double -> ST s ())
-  -> (VS.Vector Double -> Double)
-  -> Maybe ([Double] -> [Double], [Double] -> Double)
-  -> (VS.Vector Double -> (Double, VS.Vector Double))
-hybridGradValClosure nP transB trans gradC valC mPrior = \uv ->
-  let pc = VS.generate nP $ \i ->
-             invTransformF (transB BV.! i) (uv `VS.unsafeIndex` i)
-      gC = runST $ do
-        mg <- VSM.replicate nP 0
-        gradC pc mg
-        VS.unsafeFreeze mg
-      aVal = valC pc
-  in case mPrior of
-       Nothing ->
-         let logJac = sum [ logJacF (transB BV.! i) (uv `VS.unsafeIndex` i)
-                          | i <- [0 .. nP - 1] ]
-             g = VS.generate nP $ \i ->
-                   let t = transB BV.! i
-                       u = uv `VS.unsafeIndex` i
-                   in gC `VS.unsafeIndex` i * dInvTransform t u + dLogJacU t u
-         in (aVal + logJac, g)
-       Just (priorGrad, priorVal) ->            -- fExcl は logJac 込
-         let us = VS.toList uv
-             pg = priorGrad us
-             g = VS.fromList
-                   [ p + gC `VS.unsafeIndex` i * dInvTransform t u
-                   | (i, (p, (t, u))) <- zip [0 :: Int ..]
-                       (zip pg (zip trans us)) ]
-         in (aVal + priorVal us, g)
-
--- | 'compileGradUV' の per-call 本体 (Phase 54.11 で affine 経路と IR 経路の
--- 共有部を関数化): unconstrained ベクトル → constrained 値 → 解析/ベクトル
--- 経路の constrained 勾配 (@gradC@ が mutable ベクトルへ加算) → chain rule。
--- @mPriorGrad@ = 残差 ad クロージャ ('Nothing' = 密度項が残らず logJac も解析)。
-hybridGradClosure
-  :: Int -> BV.Vector Transform -> [Transform]
-  -> (forall s. VS.Vector Double -> VSM.MVector s Double -> ST s ())
-  -> Maybe ([Double] -> [Double])
-  -> (VS.Vector Double -> VS.Vector Double)
-hybridGradClosure nP transB trans gradC mPriorGrad = \uv ->
-  let pc = VS.generate nP $ \i ->
-             invTransformF (transB BV.! i) (uv `VS.unsafeIndex` i)
-      gC = runST $ do                            -- constrained 空間の解析勾配
-        mg <- VSM.replicate nP 0
-        gradC pc mg
-        VS.unsafeFreeze mg
-  in case mPriorGrad of
-       Nothing ->                                -- ad 完全省略 (logJac 解析)
-         VS.generate nP $ \i ->
-           let t = transB BV.! i
-               u = uv `VS.unsafeIndex` i
-           in gC `VS.unsafeIndex` i * dInvTransform t u + dLogJacU t u
-       Just priorGrad ->                         -- 残りは ad (chain は ad 内)
-         let pg = priorGrad (VS.toList uv)
-         in VS.fromList
-              [ p + gC `VS.unsafeIndex` i * dInvTransform t u
-              | (i, (p, (t, u))) <- zip [0 :: Int ..]
-                  (zip pg (zip trans (VS.toList uv))) ]
-
--- | Phase 54.4e: **定数パラメタ prior** の解析勾配 @d logDensity(d, θ)/dθ@
--- (constrained 空間)。 @Nothing@ = 未対応分布 (従来 `ad` に fallback)。
---
--- prior のパラメタが他 latent に依存しない (extractDeps で deps ∅) latent に
--- のみ使う前提 (パラメタを定数として θ でだけ微分する)。 各分岐は 'logDensity'
--- の実装・ガードと対にしてある: ガード違反域では 'logDensity' が定数 negInf を
--- 返し `ad` の勾配は 0 になるので、 ここでも 0 を返して一致させる。
-constPriorGradD :: Distribution Double -> Double -> Maybe Double
-constPriorGradD d x = case d of
-  Normal mu sig
-    | sig <= 0           -> Just 0
-    | otherwise          -> Just (negate (x - mu) / (sig * sig))
-  Exponential rate
-    | x < 0 || rate <= 0 -> Just 0
-    | otherwise          -> Just (negate rate)
-  Gamma shape rate
-    | x <= 0 || shape <= 0 || rate <= 0 -> Just 0
-    | otherwise          -> Just ((shape - 1) / x - rate)
-  Beta alpha beta
-    | x <= 0 || x >= 1 || alpha <= 0 || beta <= 0 -> Just 0
-    | otherwise          -> Just ((alpha - 1) / x - (beta - 1) / (1 - x))
-  Uniform lo hi
-    | hi <= lo || x < lo || x > hi -> Just 0
-    | otherwise          -> Just 0
-  StudentT df mu sig
-    | df <= 0 || sig <= 0 -> Just 0
-    | otherwise          ->
-        let z = x - mu
-        in Just (negate ((df + 1) * z) / (df * sig * sig + z * z))
-  Cauchy loc sc
-    | sc <= 0            -> Just 0
-    | otherwise          ->
-        let z = x - loc
-        in Just (negate (2 * z) / (sc * sc + z * z))
-  HalfNormal sig
-    | sig <= 0 || x < 0  -> Just 0
-    | otherwise          -> Just (negate x / (sig * sig))
-  HalfCauchy sc
-    | sc <= 0 || x < 0   -> Just 0
-    | otherwise          -> Just (negate (2 * x) / (sc * sc + x * x))
-  LogNormal mu sig
-    | sig <= 0 || x <= 0 -> Just 0
-    | otherwise          ->
-        Just (negate (1 + (log x - mu) / (sig * sig)) / x)
-  InverseGamma alpha beta
-    | alpha <= 0 || beta <= 0 || x <= 0 -> Just 0
-    | otherwise          -> Just (negate (alpha + 1) / x + beta / (x * x))
-  Weibull kShape lam
-    | kShape <= 0 || lam <= 0 || x <= 0 -> Just 0
-    | otherwise          ->
-        Just ((kShape - 1) / x - (kShape / lam) * (x / lam) ** (kShape - 1))
-  Pareto alpha xm
-    | alpha <= 0 || xm <= 0 || x < xm -> Just 0
-    | otherwise          -> Just (negate (alpha + 1) / x)
-  _ -> Nothing
-
--- | 'logJacF' の u 微分 (Phase 54.4e: ad 省略時に解析で加算)。 'logJacF' と対。
-dLogJacU :: Transform -> Double -> Double
-dLogJacU UnconstrainedT _ = 0
-dLogJacU PositiveT      _ = 1
-dLogJacU UnitIntervalT  u = let s = 1 / (1 + exp (-u)) in 1 - 2 * s
-
--- | Phase 54.4e: @excl@ 除外後の walk に log-density 寄与が残らないか。
--- 残らなければ 'compileGradU' は `ad` クロージャを丸ごと省略でき
--- (reflection tape 生成 = profile の 18.9% がゼロに)、 'compileLogPU' は
--- Free walk 自体を省略できる。 scalar 'Observe' は名前が @excl@ になければ
--- False (Phase 54.8: 自動合成で吸収済みの Observe は除外扱い)。
--- 'Potential' があれば常に False (従来 ad / walk 経路に fallback・正しさ担保)。
-residualFreeOfDensity :: Set Text -> Model Double r -> Bool
-residualFreeOfDensity excl = go
-  where
-    go (Pure _) = True
-    go (Free (Sample n _ k)) = n `Set.member` excl && go (k 0)
-    go (Free (Observe n _ _ next)) = n `Set.member` excl && go next
-    go (Free (ObserveLM nm _ _ _ _ _ next)) = nm `Set.member` excl && go next
-    -- Phase 90 A10: vecIR ('VGPot') に吸収済みの potential は残差に数えない。
-    go (Free (Potential n _ next)) = n `Set.member` excl && go next
-    go (Free (Deterministic _ v k)) = go (k v)
-    go (Free (Data _ ys k)) = go (k (ys, ys))
-    go (Free (DataIx _ is k)) = go (k is)
-    go (Free (PlateBegin _ _ next)) = go next
-    go (Free (PlateEnd next)) = go next
-
--- | Phase 54.4e: 'compileGradU' / 'compileLogPU' 共通の静的解析。
--- Gaussian LM ブロック群から (ブロック名, 解析 u-prior, 定数パラメタ prior,
--- 除外集合, 前処理済みブロック, residual 空フラグ) を 1 度だけ求める。
--- @synthObs@ (Phase 54.8) = 自動合成ブロックに吸収済みの scalar 'Observe' 名
--- (除外集合に合流させ、 residual walk で二重加算しない)。
-analyzeGaussModel
-  :: ModelP r
-  -> [(Text, [Text], [[Double]], [REff], Text, [Double])]
-  -> Set Text                              -- synthObs (吸収済 scalar Observe 名)
-  -> ( [([Text], Text)]                    -- priorREs (uNames, scaleName)
-     , [(Text, Distribution Double)]       -- constPriors
-     , Set Text                            -- exclNames
-     , [CompiledLMBlock]
-     , [([Text], Text, Text)]              -- Phase 93: 階層 Normal 群 (uNames, μ名, τ名)
-     , Bool )                              -- residual に密度項が残らないか
-analyzeGaussModel m gbs synthObs =
-  let blockNames = [ bn | (bn, _, _, _, _, _) <- gbs ]
-      priorREs   = [ (uNames, scaleName)
-                   | (_, _, _, res, _, _) <- gbs
-                   , REff uNames _ (Just scaleName) _ _ <- res ]
-      exclUNames = concat [ uNames | (uNames, _) <- priorREs ]
-      -- Phase 93: 非ゼロ latent 平均の階層 Normal prior 群 (mean-0 reff とは disjoint)。
-      -- u_i の prior を解析勾配 ('gradHierNormalIx') で扱い残差 ad から外す。
-      -- μ・τ は自身の prior を持つので cps 側に残す (u のみ除外)。
-      hierGroups = collectHierNormalGroups m
-      hierUNames = concat [ us | (us, _, _) <- hierGroups ]
-      -- 定数パラメタ prior。 u_j (REff / 階層群 経由で解析済) は除く。
-      cps = constPriorsOf m (Set.fromList (exclUNames ++ hierUNames))
-      exclNames = Set.fromList (blockNames ++ exclUNames ++ hierUNames ++ map fst cps)
-                  `Set.union` synthObs
-      cblocks   = [ compileLMBlock (bs, xs, re, sn, ys)
-                  | (_, bs, xs, re, sn, ys) <- gbs ]
-      noResid   = residualFreeOfDensity exclNames m
-  in (priorREs, cps, exclNames, cblocks, hierGroups, noResid)
-
--- | 定数パラメタ prior の抽出 (Phase 54.4e): extractDeps で親 latent 無し
--- (deps ∅) かつ解析勾配対応分布の latent。 @exclSet@ = 別経路 (REff 族 /
--- 54.11 IR 族) で扱う latent は除く。 54.4e/54.11 で共有。
-constPriorsOf :: ModelP r -> Set Text -> [(Text, Distribution Double)]
-constPriorsOf m exclSet =
-  let (depNodes, _) = extractDeps m
-      latentDeps = Map.fromList [ (nodeName nd, nodeDeps nd)
-                                | nd <- depNodes, nodeKind nd == LatentN ]
-  in [ (n, dist)
-     | (n, dist) <- priorList m
-     , not (Set.member n exclSet)
-     , Just deps <- [Map.lookup n latentDeps], Set.null deps
-     , Just _ <- [constPriorGradD dist 0.5] ]
-
--- | Phase 54.4d: logp **値** 評価のコンパイル ('compileGradU' の値版)。
---
--- NUTS は tree node ごとにエネルギー (logp の値) を評価する。 54.4c 時点の
--- cost-centre profile で、 勾配は vec 化済みなのに値評価が Free walk +
--- per-obs スカラ 'logDensityObs' のままで per-draw の 46% を占めると判明
--- (`prof-nuts-54.4c.prof`)。 本関数は 'compileGradU' と同じ静的前処理
--- ('CompiledLMBlock') を 1 度だけ行い、 unconstrained ベクトルを受けて
--- log-joint + log-jacobian を返すクロージャを構築する:
---
---   * Gaussian-恒等リンク 'ObserveLM' ブロックの観測尤度値 → 素な Double
---     ベクトル演算 ('valueCompiledLMBlock'・tape 不要)
---   * @REff (Just scale)@ の u-prior 値 → 解析式 ('reffPriorValue')
---   * 残り (他 prior / scalar observe / 非 Gauss LM / jacobian)
---     → 'logJointExclBlocks' の Double walk
---
--- Gaussian LM を含まないモデルは従来 'logJointUnconstrained' 相当に fallback
--- (後方互換)。 数値は 'logJointUnconstrained' と一致 (test で担保)。
-compileLogPU :: forall r. ModelP r -> [Text] -> [Transform] -> ([Double] -> Double)
-compileLogPU m names trans =
-  let lv = compileLogPUV m names trans
-  in lv . VS.fromList
-
--- | 'compileLogPU' の vector-native 版 (Phase 54.6)。 NUTS のエネルギー評価が
--- 直接使う。 名前は compile 時に index へ解決し、 per-call は Storable vector
--- 上の素な Double 演算のみ (Text-key Map 組立なし)。
-compileLogPUV :: forall r. ModelP r -> [Text] -> [Transform]
-              -> (VS.Vector Double -> Double)
-compileLogPUV m names trans =
-  case gaussLMBlocksAuto m of
-    ([], _) -> case synthVecIR m of
-      Nothing -> fFull . VS.toList                     -- 後方互換: 従来の walk 評価
-      Just (gs, fams, sObs) ->                 -- 54.11: ベクトル式 IR (非線形 μ)
-        let ixOf   = Map.fromList (zip names [0 ..])
-            nP     = length names
-            transB = BV.fromList trans
-            cvi    = compileVecIR ixOf gs fams
-            famSet = Set.fromList (concat [ ms | (ms, _, _) <- fams ])
-            cps    = constPriorsOf m famSet
-            lnGroups = collectLogNormalGroups m        -- Phase 98 A3: LogNormal 群
-            lnUNames = concat [ us | (us, _, _) <- lnGroups ]
-            lnIx   = map (resolveLogNormal ixOf) lnGroups
-            exclNames = sObs `Set.union` famSet
-                        `Set.union` Set.fromList (map fst cps)
-                        `Set.union` Set.fromList lnUNames
-            noResid = residualFreeOfDensity exclNames m
-            cpIx   = [ (ixOf Map.! n, d) | (n, d) <- cps ]
-            mResid
-              | noResid   = Nothing
-              | otherwise = Just (residualExcl (compileResidual exclNames m) exclNames m)
-        in hybridLogPClosure nP transB names
-             (\pc -> vecIRValue cvi pc
-                     + sum [ logDensity d (pc `VS.unsafeIndex` i)
-                           | (i, d) <- cpIx ]
-                     + sum [ valueLogNormalIx ln pc | ln <- lnIx ])  -- A3
-             mResid
-    (gbs, synthObs) ->
-      let -- 54.4c/54.4e と同じ静的解析: u-prior は解析値・定数パラメタ prior は
-          -- 直接 logDensity・残りだけ walk。 密度項が残らなければ Free walk 自体を
-          -- 省略する (モデル再構築 = reNormal の Text 名生成等も消える)。
-          (priorREs, cps, exclNames, cblocks, hierGroups, noResid) = analyzeGaussModel m gbs synthObs
-          ixOf   = Map.fromList (zip names [0 ..])
-          nP     = length names
-          transB = BV.fromList trans
-          cbIx   = map (resolveLMBlock ixOf) cblocks
-          reIx   = [ ReffPriorIx (VU.fromList (map (ixOf Map.!) uNames))
-                                 (ixOf Map.! scaleName)
-                   | (uNames, scaleName) <- priorREs ]
-          hniIx  = map (resolveHierNormal ixOf) hierGroups
-          cpIx   = [ (ixOf Map.! n, d) | (n, d) <- cps ]
-          mResid                                       -- 残 walk (fallback のみ)
-            | noResid   = Nothing
-            | otherwise = Just (residualExcl (compileResidual exclNames m) exclNames m)
-      in hybridLogPClosure nP transB names
-           (\pc -> sum [ valueLMBlockIx cb pc | cb <- cbIx ]
-                   + sum [ valueReffPriorIx ri pc | ri <- reIx ]
-                   + sum [ valueHierNormalIx hn pc | hn <- hniIx ]
-                   + sum [ logDensity d (pc `VS.unsafeIndex` i)
-                         | (i, d) <- cpIx ])
-           mResid
-  where
-    fFull us =
-      let paramsC = Map.fromList
-            [ (n, invTransformF t u) | (n, t, u) <- zip3 names trans us ]
-          logJac  = sum [ logJacF t u | (t, u) <- zip trans us ]
-      in logJoint m paramsC + logJac
-
--- | 'compileLogPUV' の per-call 本体 (Phase 54.11 で affine 経路と IR 経路の
--- 共有部を関数化): unconstrained ベクトル → constrained 値 → 解析/ベクトル
--- 経路の log-density 値 (@analytic@) + 残差 walk (@mResid@) + log-jacobian。
-hybridLogPClosure
-  :: Int -> BV.Vector Transform -> [Text]
-  -> (VS.Vector Double -> Double)
-  -> Maybe (Map Text Double -> Double)
-  -> (VS.Vector Double -> Double)
-hybridLogPClosure nP transB names analytic mResid = \uv ->
-  let pc = VS.generate nP $ \i ->
-             invTransformF (transB BV.! i) (uv `VS.unsafeIndex` i)
-      logJac = sum [ logJacF (transB BV.! i) (uv `VS.unsafeIndex` i)
-                   | i <- [0 .. nP - 1] ]
-      residV = case mResid of
-        Nothing -> 0
-        Just rv -> rv (Map.fromList (zip names (VS.toList pc)))
-  in residV + analytic pc + logJac
-
--- | invTransform の導関数 dθ/du (chain rule 用)。 'invTransformF' と対。
-dInvTransform :: Transform -> Double -> Double
-dInvTransform UnconstrainedT _ = 1
-dInvTransform PositiveT      u = exp u
-dInvTransform UnitIntervalT  u = let s = 1 / (1 + exp (-u)) in s * (1 - s)
-
--- | モデル中の Gaussian-恒等リンク 'ObserveLM' ブロックを収集する
--- (ブロック名 / β 名 / 設計行列 / ランダム効果 / σ 名 / 観測 ys)。 非 Gaussian は除外。
-gaussLMBlocks :: ModelP r -> [(Text, [Text], [[Double]], [REff], Text, [Double])]
-gaussLMBlocks m = go m []
-  where
-    go (Pure _) acc = reverse acc
-    go (Free f) acc = case f of
-      Sample _ _ k        -> go (k 0) acc
-      Observe _ _ _ next  -> go next acc
-      ObserveLM nm bs xs re fam ys next ->
-        case fam of
-          LMGaussian sn -> go next ((nm, bs, xs, re, sn, ys) : acc)
-          _             -> go next acc
-      Potential _ _ next  -> go next acc
-      Deterministic _ v k -> go (k v) acc
-      Data _ ys k         -> go (k (ys, ys)) acc
-      DataIx _ is k       -> go (k is) acc
-      PlateBegin _ _ next -> go next acc
-      PlateEnd next       -> go next acc
-
--- | 'gaussLMBlocks' + Phase 54.8 自動合成。 明示 'ObserveLM' ブロックに、
--- per-obs scalar 'Observe' から自動合成したブロックを連結して返す
--- (合成に吸収した scalar Observe 名集合も返す → 'analyzeGaussModel' で除外)。
-gaussLMBlocksAuto
-  :: ModelP r
-  -> ([(Text, [Text], [[Double]], [REff], Text, [Double])], Set Text)
-gaussLMBlocksAuto m =
-  let (sblocks, sObs) = synthGaussLMBlocks m
-  in (gaussLMBlocks m ++ sblocks, sObs)
-
-
--- | 'logJoint' と同じだが、 名前が @excl@ に含まれる項を **加算しない**:
---
---   * @excl@ に含まれる 'ObserveLM' ブロックの観測尤度 (vec-tape 経路で別計算)
---   * @excl@ に含まれる scalar 'Observe' の観測尤度
---     (Phase 54.8: 'synthGaussLMBlocks' が合成ブロックへ吸収済みのもの)
---   * @excl@ に含まれる 'Sample' ノードの prior log-density
---     (Phase 54.4c: 群効果 @u_j@ の prior を解析勾配経路で別計算するため)。
---     値は継続に必要なので 'Sample' 自体は walk するが log-density は足さない。
-logJointExclBlocks :: (Floating a, Ord a)
-                   => Set Text -> Model a r -> Map Text a -> a
-logJointExclBlocks excl model params = go model 0
-  where
-    go (Pure _) acc = acc
-    go (Free (Sample n d k)) acc =
-      case Map.lookup n params of
-        Nothing -> negInf
-        Just v
-          | n `Set.member` excl -> go (k v) acc
-          | otherwise           -> go (k v) (acc + logDensity d v)
-    go (Free (Observe n d ys next)) acc
-      | n `Set.member` excl = go next acc
-      | otherwise           = go next (acc + obsLogSum d ys)
-    go (Free (ObserveLM nm bs xs re fam ys next)) acc
-      | nm `Set.member` excl = go next acc
-      | otherwise            = go next (acc + lmObsLogSum bs xs re fam ys params)
-    -- Phase 90 A10: vecIR ('VGPot') に吸収済みの potential は二重加算しない。
-    go (Free (Potential n v next)) acc
-      | n `Set.member` excl = go next acc
-      | otherwise           = go next (acc + v)
-    go (Free (Deterministic _ v k)) acc = go (k v) acc
-    go (Free (Data _ ys k)) acc = go (k (map realToFrac ys, ys)) acc
-    go (Free (DataIx _ is k)) acc = go (k is) acc
-    go (Free (PlateBegin _ _ next)) acc = go next acc
-    go (Free (PlateEnd next))       acc = go next acc
-
--- | Phase 98 A2: excl 吸収後の残余 log-density。 'compileResidual' が成功すれば
--- flat 畳み込み ('residualValueA'・Free walk 無し)、 失敗すれば従来の
--- 'logJointExclBlocks' walk に fallback する。 呼び出し側は @mcr@ を 1 度だけ
--- ('compileResidual' で) 構築して値/勾配の両クロージャに渡す ('CompiledResidual'
--- は 'SExp' 保持の純データなので型非依存で共有できる)。
-residualExcl :: (Floating a, Ord a)
-             => Maybe CompiledResidual -> Set Text -> Model a r -> Map Text a -> a
-residualExcl (Just cr) _    _ params = residualValueA cr params
-residualExcl Nothing   excl m params = logJointExclBlocks excl m params
-
--- | Phase 54.4b: Gaussian-恒等リンク 'ObserveLM' ブロックの **静的部分**を 1 度
--- だけ前処理した中間表現。 NUTS の draw ループの外で構築し全 leapfrog で再利用する
--- ことで、 設計列のベクトル化 (@row !! k@ = O(n·p²)) や群 id の unbox 変換・ys の
--- Storable 化といった「値に依らず draw 間で不変な仕事」 を毎勾配評価から外す。
-data CompiledLMBlock = CompiledLMBlock
-  { clbBetas :: ![Text]                          -- ^ β パラメタ名 (列順)
-  , clbCols  :: ![VS.Vector Double]              -- ^ 設計列 (p 本・各 length n)
-  , clbReff  :: ![([Text], Int, VU.Vector Int, Maybe (VS.Vector Double))]
-    -- ^ (u 名, nG, gids, per-row 重み) のランダム効果 (重み Nothing = 全 1)
-  , clbSname :: !Text                            -- ^ σ パラメタ名
-  , clbYs    :: !(VS.Vector Double)              -- ^ 観測 (length n)
-  , clbN     :: !Int
-  , clbP     :: !Int
-  }
-
--- | 'gaussLMBlocks' の 1 ブロックを 'CompiledLMBlock' に前処理する (静的・1 回)。
-compileLMBlock :: ([Text], [[Double]], [REff], Text, [Double]) -> CompiledLMBlock
-compileLMBlock (betaNames, designX, reffs, sName, ys) =
-  let p    = length betaNames
-      n    = length ys
-      cols = [ VS.fromList [ row !! k | row <- designX ] | k <- [0 .. p - 1] ]
-      reff = [ (uNames, length uNames, VU.fromList gids, fmap VS.fromList mw)
-             | REff uNames gids _ mw _ <- reffs ]
-  in CompiledLMBlock betaNames cols reff sName (VS.fromList ys) n p
-
--- | Phase 54.6: 'CompiledLMBlock' の名前参照を param index に解決した形。
--- compile 時に 1 度だけ作り、 per-call は Storable vector への index 参照のみ
--- (Text-key Map lookup なし)。
-data CompiledLMBlockIx = CompiledLMBlockIx
-  { cliBetaIx :: !(VU.Vector Int)                       -- ^ β の param index (列順)
-  , cliXMat   :: !(VS.Vector Double)                    -- ^ 設計行列 row-major (n×p・X[i*p+k])
-  , cliCols   :: !(BV.Vector (VS.Vector Double))        -- ^ 設計列 (∂β dot 用・O(1) 添字)
-  , cliReff   :: ![(VU.Vector Int, Int, VU.Vector Int, Maybe (VS.Vector Double))]
-    -- ^ (u indices, nG, gids, per-row 重み)。 重み Nothing = 全 1 (Phase 54.10)
-  , cliSIx    :: !Int                                   -- ^ σ の param index
-  , cliYs     :: !(VS.Vector Double)                    -- ^ 観測 (length n)
-  , cliN      :: !Int
-  , cliP      :: !Int
-  }
-
--- | 'CompiledLMBlock' の名前を index に解決する (静的・1 回)。 Phase 54.7a で
--- row-major 設計行列も前計算 (残差ループのキャッシュ局所性 + リスト走査排除)。
-resolveLMBlock :: Map Text Int -> CompiledLMBlock -> CompiledLMBlockIx
-resolveLMBlock ixOf clb =
-  let n = clbN clb
-      p = clbP clb
-      cols = clbCols clb
-  in CompiledLMBlockIx
-    { cliBetaIx = VU.fromList [ ixOf Map.! nm | nm <- clbBetas clb ]
-    , cliXMat   = VS.generate (n * p) $ \ix ->
-                    let (i, k) = ix `divMod` p
-                    in (cols !! k) `VS.unsafeIndex` i
-    , cliCols   = BV.fromList cols
-    , cliReff   = [ (VU.fromList [ ixOf Map.! nm | nm <- uNames ], nG, gids, mw)
-                  | (uNames, nG, gids, mw) <- clbReff clb ]
-    , cliSIx    = ixOf Map.! clbSname clb
-    , cliYs     = clbYs clb
-    , cliN      = n
-    , cliP      = p
-    }
-
--- | Phase 93: 階層 Normal 群 (uNames, μ名, τ名) の名前を param index に解決する
--- ('resolveLMBlock' と同様に compile 時 1 回)。
-resolveHierNormal :: Map Text Int -> ([Text], Text, Text) -> HierNormalIx
-resolveHierNormal ixOf (uNames, meanName, scaleName) = HierNormalIx
-  { hniUIx     = VU.fromList [ ixOf Map.! nm | nm <- uNames ]
-  , hniMeanIx  = ixOf Map.! meanName
-  , hniScaleIx = ixOf Map.! scaleName
-  }
-
--- | 残差 @r_i = y_i - Σ_k β_k X_ik - Σ_re u^{re}[gid_i]@ と @Σr²@ を
--- **1 パスの手動ループ** で計算する (Phase 54.7a: (a)-0 実測で per-call
--- ~48-82KB の割当が本物と確定 — `VS.generate` 内のリスト fold・`zip`/`toList`
--- の毎回再構築・dot/sumR2 の中間ベクトルが原因。 unboxed アキュムレータの
--- 明示ループ + row-major X で割当を r 1 本に削減)。
-lmResidualS :: CompiledLMBlockIx -> VS.Vector Double -> (VS.Vector Double, Double)
-lmResidualS blk pc = runST $ do
-  let n   = cliN blk
-      p   = cliP blk
-      xm  = cliXMat blk
-      ys  = cliYs blk
-      res = cliReff blk
-      bv  = VS.generate p (\k -> pc `VS.unsafeIndex` (cliBetaIx blk `VU.unsafeIndex` k))
-  mr <- VSM.unsafeNew n
-  let goObs !i !acc
-        | i >= n    = pure acc
-        | otherwise = do
-            let base = i * p
-                goK !k !s
-                  | k >= p    = s
-                  | otherwise = goK (k + 1)
-                      (s + bv `VS.unsafeIndex` k * (xm `VS.unsafeIndex` (base + k)))
-                reS = foldl' (\ !a (uix, _, gids, mw) ->
-                                let u = pc `VS.unsafeIndex` (uix `VU.unsafeIndex`
-                                          (gids `VU.unsafeIndex` i))
-                                in a + case mw of
-                                         Nothing -> u
-                                         Just w  -> w `VS.unsafeIndex` i * u) 0 res
-                ri  = ys `VS.unsafeIndex` i - goK 0 0 - reS
-            VSM.unsafeWrite mr i ri
-            goObs (i + 1) (acc + ri * ri)
-  sumR2 <- goObs 0 0
-  r <- VS.unsafeFreeze mr
-  pure (r, sumR2)
-
--- | 前処理済みブロックの観測尤度 @Σ_i logDensityObs(Normal η_i σ) y_i@ の
--- **constrained 空間**での勾配を解析閉形式で mutable 勾配ベクトルに加算する
--- (Phase 54.6: Gaussian-恒等リンクは閉形式が書けるので汎用 tape 不要):
---
--- > ∂/∂β_k = X_kᵀ r / σ²
--- > ∂/∂u_j = (Σ_{i: gid_i=j} w_i·r_i) / σ²   (scatter・O(n)・重み無しは w_i=1)
--- > ∂/∂σ   = -n/σ + (Σ r²)/σ³
---
--- Phase 54.7a: dot / scatter とも unboxed アキュムレータの明示ループ
--- (中間ベクトル・`VU.convert`・`accumulate` 割当なし)。
-gradLMBlockIx :: CompiledLMBlockIx -> VS.Vector Double
-              -> VSM.MVector s Double -> ST s ()
-gradLMBlockIx blk pc mg = do
-  let sigma = pc `VS.unsafeIndex` cliSIx blk
-      s2    = sigma * sigma
-      n     = cliN blk
-      (r, sumR2) = lmResidualS blk pc
-      n'    = fromIntegral n
-  forM_ [0 .. cliP blk - 1] $ \k -> do
-    let c = cliCols blk `BV.unsafeIndex` k
-        dot !i !acc
-          | i >= n    = acc
-          | otherwise = dot (i + 1)
-              (acc + c `VS.unsafeIndex` i * r `VS.unsafeIndex` i)
-    VSM.modify mg (+ (dot 0 0 / s2)) (cliBetaIx blk `VU.unsafeIndex` k)
-  forM_ (cliReff blk) $ \(uix, nG, gids, mw) -> do
-    macc <- VSM.replicate nG 0
-    let scat !i
-          | i >= n    = pure ()
-          | otherwise = do
-              let g  = gids `VU.unsafeIndex` i
-                  ri = r `VS.unsafeIndex` i
-                  wr = case mw of
-                         Nothing -> ri
-                         Just w  -> w `VS.unsafeIndex` i * ri
-              v <- VSM.unsafeRead macc g
-              VSM.unsafeWrite macc g (v + wr)
-              scat (i + 1)
-    scat 0
-    forM_ [0 .. nG - 1] $ \j -> do
-      gj <- VSM.unsafeRead macc j
-      VSM.modify mg (+ (gj / s2)) (uix `VU.unsafeIndex` j)
-  VSM.modify mg (+ (negate n' / sigma + sumR2 / (s2 * sigma))) (cliSIx blk)
-
--- | 前処理済みブロックの観測尤度の **値**
--- @-n/2·log2π - n·logσ - Σr²/(2σ²)@。 Phase 54.7a: r を materialize せず
--- sumR2 だけを 1 パスの明示ループで累積 (割当ゼロ)。
--- guard (σ≤0 → -∞) は 'logDensityObs' の Normal 分岐と一致させる。
-valueLMBlockIx :: CompiledLMBlockIx -> VS.Vector Double -> Double
-valueLMBlockIx blk pc
-  | sigma <= 0 = negInf
-  | otherwise  =
-      negate (0.5 * n' * log (2 * pi)) - n' * log sigma
-        - sumR2 / (2 * sigma * sigma)
-  where
-    sigma = pc `VS.unsafeIndex` cliSIx blk
-    n     = cliN blk
-    p     = cliP blk
-    n'    = fromIntegral n
-    xm    = cliXMat blk
-    ys    = cliYs blk
-    res   = cliReff blk
-    bv    = VS.generate p (\k -> pc `VS.unsafeIndex` (cliBetaIx blk `VU.unsafeIndex` k))
-    sumR2 = goObs 0 0
-    goObs !i !acc
-      | i >= n    = acc
-      | otherwise =
-          let base = i * p
-              goK !k !s
-                | k >= p    = s
-                | otherwise = goK (k + 1)
-                    (s + bv `VS.unsafeIndex` k * (xm `VS.unsafeIndex` (base + k)))
-              reS = foldl' (\ !a (uix, _, gids, mw) ->
-                              let u = pc `VS.unsafeIndex` (uix `VU.unsafeIndex`
-                                        (gids `VU.unsafeIndex` i))
-                              in a + case mw of
-                                       Nothing -> u
-                                       Just w  -> w `VS.unsafeIndex` i * u) 0 res
-              ri  = ys `VS.unsafeIndex` i - goK 0 0 - reS
-          in goObs (i + 1) (acc + ri * ri)
-
--- | Phase 54.4c/54.6: 群効果 prior @u_j ~ Normal(0, τ)@ の index 解決形。
-data ReffPriorIx = ReffPriorIx
-  { rpiUIx     :: !(VU.Vector Int)   -- ^ u_j の param index (長さ nG)
-  , rpiScaleIx :: !Int               -- ^ τ の param index
-  }
-
--- | 群効果 prior の **constrained 空間**での解析勾配を mutable 勾配ベクトルに
--- 加算する (`ad` のスカラ tape を回避):
---
--- > log p(u | τ) = -nG/2·log(2π) - nG·log τ - (Σ u_j²)/(2τ²)
--- > ∂/∂u_j = -u_j / τ²
--- > ∂/∂τ   = -nG/τ + (Σ u_j²)/τ³
---
--- τ 成分は τ 自身の prior (解析 or `ad` 経路) と加算合流する。 unconstrained への
--- chain rule ('dInvTransform') は呼出側で適用する。
-gradReffPriorIx :: ReffPriorIx -> VS.Vector Double -> VSM.MVector s Double -> ST s ()
-gradReffPriorIx (ReffPriorIx uix six) pc mg = do
-  let tau  = pc `VS.unsafeIndex` six
-      tau2 = tau * tau
-      nG   = VU.length uix
-  sumU2 <- VU.foldM' (\ !acc i -> do
-                        let u = pc `VS.unsafeIndex` i
-                        VSM.modify mg (+ (negate u / tau2)) i
-                        pure (acc + u * u)) 0 uix
-  VSM.modify mg (+ (negate (fromIntegral nG) / tau + sumU2 / (tau2 * tau))) six
-
--- | 群効果 prior の log-density 和の **値** ('gradReffPriorIx' の値版)。
--- guard (τ≤0 → -∞) は 'logDensity' の Normal 分岐と一致させる。
-valueReffPriorIx :: ReffPriorIx -> VS.Vector Double -> Double
-valueReffPriorIx (ReffPriorIx uix six) pc
-  | tau <= 0  = negInf
-  | otherwise =
-      negate (0.5 * nG' * log (2 * pi)) - nG' * log tau
-        - sumU2 / (2 * tau * tau)
-  where
-    tau   = pc `VS.unsafeIndex` six
-    nG'   = fromIntegral (VU.length uix)
-    sumU2 = VU.foldl' (\ !acc i -> let u = pc `VS.unsafeIndex` i
-                                   in acc + u * u) 0 uix
-
--- | Phase 93: **非ゼロ latent 平均**の階層 Normal prior の解析勾配経路。
--- 'ReffPriorIx' (mean-0 専用) の一般化で、 平均 μ・スケール τ とも latent の
--- @u_i ~ Normal(μ, τ)@ 群を扱う (rats の @alpha[i]~Normal(muAlpha,sigmaAlpha)@ 等)。
-data HierNormalIx = HierNormalIx
-  { hniUIx     :: !(VU.Vector Int)   -- ^ u_i の param index (長さ nG)
-  , hniMeanIx  :: !Int               -- ^ μ の param index
-  , hniScaleIx :: !Int               -- ^ τ の param index
-  }
-
--- | 'HierNormalIx' の **constrained 空間**での解析勾配を mutable 勾配ベクトルに
--- 加算する (`ad` のスカラ tape を回避):
---
--- > log p(u | μ, τ) = -nG/2·log(2π) - nG·log τ - (Σ (u_i-μ)²)/(2τ²)
--- > ∂/∂u_i = -(u_i - μ) / τ²
--- > ∂/∂μ   =  (Σ (u_i - μ)) / τ²
--- > ∂/∂τ   = -nG/τ + (Σ (u_i-μ)²)/τ³
---
--- μ・τ 成分は各自の prior (解析 or `ad` 経路) と加算合流する。 unconstrained への
--- chain rule ('dInvTransform') は呼出側で適用する。
-gradHierNormalIx :: HierNormalIx -> VS.Vector Double -> VSM.MVector s Double -> ST s ()
-gradHierNormalIx (HierNormalIx uix mIx sIx) pc mg = do
-  let mu   = pc `VS.unsafeIndex` mIx
-      tau  = pc `VS.unsafeIndex` sIx
-      tau2 = tau * tau
-      nG   = VU.length uix
-  (sumD, sumD2) <-
-    VU.foldM' (\ (!accD, !accD2) i -> do
-                 let u = pc `VS.unsafeIndex` i
-                     d = u - mu
-                 VSM.modify mg (+ (negate d / tau2)) i
-                 pure (accD + d, accD2 + d * d)) (0, 0) uix
-  VSM.modify mg (+ (sumD / tau2)) mIx
-  VSM.modify mg (+ (negate (fromIntegral nG) / tau + sumD2 / (tau2 * tau))) sIx
-
--- | 'HierNormalIx' の log-density 和の **値** ('gradHierNormalIx' の値版)。
--- guard (τ≤0 → -∞) は 'logDensity' の Normal 分岐と一致させる。
-valueHierNormalIx :: HierNormalIx -> VS.Vector Double -> Double
-valueHierNormalIx (HierNormalIx uix mIx sIx) pc
-  | tau <= 0  = negInf
-  | otherwise =
-      negate (0.5 * nG' * log (2 * pi)) - nG' * log tau
-        - sumD2 / (2 * tau * tau)
-  where
-    mu    = pc `VS.unsafeIndex` mIx
-    tau   = pc `VS.unsafeIndex` sIx
-    nG'   = fromIntegral (VU.length uix)
-    sumD2 = VU.foldl' (\ !acc i -> let d = pc `VS.unsafeIndex` i - mu
-                                   in acc + d * d) 0 uix
-
--- ---------------------------------------------------------------------------
--- Phase 98 A3: LogNormal 群 prior の解析勾配 ('HierNormalIx' の LogNormal 版)
--- ---------------------------------------------------------------------------
--- @a_i ~ LogNormal(μ, σ)@ 群 (μ = 定数 or 単一 latent・σ = 単一 latent) の値/勾配を
--- 解析式で扱い、 vecIR 経路の残余 reverse-AD tape (irt-2pl で ~30%time/~85%alloc) を消す。
-
--- | 'collectLogNormalGroups' の結果を param index へ解決した中間表現。
--- μ が定数なら @hlnMeanIx = Left c@、 latent なら @Right ix@。
-data LogNormalIx = LogNormalIx
-  { hlnUIx     :: !(VU.Vector Int)     -- ^ a_i の param index (長さ nG)
-  , hlnMeanIx  :: !(Either Double Int) -- ^ μ (定数 or param index)
-  , hlnScaleIx :: !Int                 -- ^ σ の param index
-  }
-
-resolveLogNormal :: Map Text Int -> ([Text], Either Double Text, Text) -> LogNormalIx
-resolveLogNormal ixOf (uNames, mean, scaleName) = LogNormalIx
-  { hlnUIx     = VU.fromList [ ixOf Map.! nm | nm <- uNames ]
-  , hlnMeanIx  = either Left (Right . (ixOf Map.!)) mean
-  , hlnScaleIx = ixOf Map.! scaleName
-  }
-
--- | 'LogNormalIx' の **constrained 空間**での解析勾配を mutable 勾配ベクトルに
--- 加算する (`ad` のスカラ tape を回避)。 L_i = log a_i, d_i = L_i - μ として:
---
--- > log p(a | μ, σ) = -nG/2·log(2π) - nG·log σ - Σ L_i - (Σ d_i²)/(2σ²)
--- > ∂/∂a_i = -(1 + d_i/σ²) / a_i
--- > ∂/∂μ   =  (Σ d_i) / σ²          (μ が latent のときのみ)
--- > ∂/∂σ   = -nG/σ + (Σ d_i²)/σ³
---
--- unconstrained への chain rule ('dInvTransform') は呼出側で適用する。
-gradLogNormalIx :: LogNormalIx -> VS.Vector Double -> VSM.MVector s Double -> ST s ()
-gradLogNormalIx (LogNormalIx uix meanIx sIx) pc mg = do
-  let mu   = either id (pc `VS.unsafeIndex`) meanIx
-      sig  = pc `VS.unsafeIndex` sIx
-      sig2 = sig * sig
-      nG   = VU.length uix
-  (sumD, sumD2) <-
-    VU.foldM' (\ (!accD, !accD2) i -> do
-                 let a = pc `VS.unsafeIndex` i
-                     d = log a - mu
-                 VSM.modify mg (+ (negate (1 + d / sig2) / a)) i
-                 pure (accD + d, accD2 + d * d)) (0, 0) uix
-  case meanIx of
-    Right mIx -> VSM.modify mg (+ (sumD / sig2)) mIx
-    Left _    -> pure ()
-  VSM.modify mg (+ (negate (fromIntegral nG) / sig + sumD2 / (sig2 * sig))) sIx
-
--- | 'LogNormalIx' の log-density 和の **値** ('gradLogNormalIx' の値版)。
--- guard (σ≤0 / a_i≤0 → -∞) は 'logDensity' の LogNormal 分岐と一致させる
--- (a は PositiveT 変換で a>0 だが安全のため一致させる)。
-valueLogNormalIx :: LogNormalIx -> VS.Vector Double -> Double
-valueLogNormalIx (LogNormalIx uix meanIx sIx) pc
-  | sig <= 0                      = negInf
-  | VU.any (\i -> pc `VS.unsafeIndex` i <= 0) uix = negInf
-  | otherwise =
-      negate (0.5 * nG' * log (2 * pi)) - nG' * log sig - sumL
-        - sumD2 / (2 * sig * sig)
-  where
-    mu    = either id (pc `VS.unsafeIndex`) meanIx
-    sig   = pc `VS.unsafeIndex` sIx
-    nG'   = fromIntegral (VU.length uix)
-    (sumL, sumD2) =
-      VU.foldl' (\ (!aL, !aD2) i ->
-                   let l = log (pc `VS.unsafeIndex` i)
-                       d = l - mu
-                   in (aL + l, aD2 + d * d)) (0, 0) uix
-
--- ---------------------------------------------------------------------------
--- 制約変換 (Floating 多相版)
--- ---------------------------------------------------------------------------
-
--- | unconstrained → constrained 変換 (Floating 多相)。
---
--- > UnconstrainedT: θ = u
--- > PositiveT:      θ = exp(u)
--- > UnitIntervalT:  θ = sigmoid(u) = 1/(1+exp(-u))
-invTransformF :: Floating a => Transform -> a -> a
-invTransformF UnconstrainedT u = u
-invTransformF PositiveT      u = exp u
-invTransformF UnitIntervalT  u = 1 / (1 + exp (-u))
-
--- | log |∂θ/∂u| — Jacobian 行列式の対数 (Floating 多相)。
-logJacF :: Floating a => Transform -> a -> a
-logJacF UnconstrainedT _ = 0
-logJacF PositiveT      u = u                       -- log(exp u) = u
-logJacF UnitIntervalT  u =
-  let p = 1 / (1 + exp (-u))
-  in log p + log (1 - p)                           -- log σ(u)(1-σ(u))
-
--- | 各 latent 変数の事前分布から制約変換を自動検出する。 分布名→変換の表は
--- 'nameToTransform' (@HBM.Distribution@) に一元化 (probe 側 'vecIRProbeOK' と
--- 同一 source)。
-getTransforms :: ModelP r -> Map Text Transform
-getTransforms m = Map.fromList
-  [ (nodeName n, nameToTransform (nodeDist n))
-  | n <- collectNodes m
-  , nodeKind n == LatentN
-  ]
-
--- | unconstrained 空間における log-joint (Jacobian 補正込み)。
--- Jacobian 補正で確率密度の積分を保存する。
-logJointUnconstrained :: forall a r. (Floating a, Ord a)
-                      => Model a r
-                      -> [Text]      -- ^ パラメータ順序
-                      -> [Transform] -- ^ 各パラメータの変換種別
-                      -> Map Text a  -- ^ unconstrained パラメータ値
-                      -> a
-logJointUnconstrained m names trans paramsU =
-  let paramsC = Map.fromList
-        [ (n, invTransformF t (Map.findWithDefault 0 n paramsU))
-        | (n, t) <- zip names trans ]
-      logJac  = sum
-        [ logJacF t (Map.findWithDefault 0 n paramsU)
-        | (n, t) <- zip names trans ]
-  in logJoint m paramsC + logJac
diff --git a/src/Hanalyze/Model/HBM/IR.hs b/src/Hanalyze/Model/HBM/IR.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/HBM/IR.hs
+++ /dev/null
@@ -1,3055 +0,0 @@
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE ImpredicativeTypes #-}
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE TupleSections #-}
-
--- |
--- Module      : Hanalyze.Model.HBM.IR
--- Description : HBM の中間表現 (IR) 層 (affine 追跡・SExp/UExp コンパイル)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Phase 58.7: IR (中間表現) 層を 'Hanalyze.Model.HBM' から分離。
---
--- AD 勾配の高速経路で使う **中間表現** (記述層 Model / 評価層 Eval の上層):
---
---   * affine 追跡 ('AffV') による per-obs 手書き Gaussian モデルの自動
---     ObserveLM 化 (Phase 54.8・'synthGaussLMBlocks')
---   * 非線形 μ の「スカラ式 IR」 ('SExp') → 「ベクトル式 IR」 ('UExp') 合成
---     (Phase 54.11/55・'synthVecIR' / 'compileVecIR')
---   * 観測密度の IR 式化 ('VecObsIR' → 'CompiledVecIR') と arena 上の値/勾配
---     評価 ('vecIRValue' / 'gradVecIR'・Phase 56.2)
---
--- ★**最ホット**: NUTS per-draw の勾配本経路 ('gradVecIR')。 monolith では AD 勾配
--- コンパイラ ('compileGradUV' 本体残置) と同一モジュールで inline されていた。
--- 境界跨ぎ inline 喪失を防ぐため定義と一緒に INLINABLE/SPECIALIZE を移送。 依存は
--- 下層 Model / Distribution / Eval (lmObsLogSum) / Util のみ (一方向)。
---
--- export list は省略 (内部実装層)。 公開 surface (synthGaussLMBlocks / synthVecIR)
--- は facade 'Hanalyze.Model.HBM' の export list が制御する。
-module Hanalyze.Model.HBM.IR where
-
-import Control.DeepSeq (NFData (..), force)
-import Control.Exception (SomeAsyncException (..), SomeException, evaluate,
-                          fromException, throwIO, try)
-import Control.Monad (forM, forM_, replicateM, when)
-import Data.IORef (IORef, modifyIORef', newIORef, readIORef, writeIORef)
-import Data.List (foldl')
-import System.IO.Unsafe (unsafePerformIO)
-import System.Mem.StableName (StableName, hashStableName, makeStableName)
-import qualified Data.IntMap.Strict as IM
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import qualified Data.Set as Set
-import Data.Set (Set)
-import Data.Text (Text)
-import qualified Data.Text as T
-import Numeric.AD.Mode.Reverse.Double (grad)
-import Control.Monad.Primitive (PrimMonad, PrimState)
-
-import Control.Monad.ST (ST, runST)
-import qualified Data.Vector          as BV
-import qualified Data.Vector.Storable as VS
-import qualified Data.Vector.Storable.Mutable as VSM
-import qualified Data.Vector.Unboxed  as VU
-
-import Hanalyze.Stat.Distribution (Transform (..), fromUnconstrained)
-import Hanalyze.MCMC.Core (Chain (..))
-
-import Hanalyze.Model.HBM.Util
-import Hanalyze.Model.HBM.Distribution
-import Hanalyze.Model.HBM.Sampling
-import Hanalyze.Model.HBM.Model
-import Hanalyze.Model.HBM.Track
-import Hanalyze.Model.HBM.Eval
-
--- ---------------------------------------------------------------------------
--- Phase 54.8: per-obs 手書きモデルの自動 ObserveLM 化 (M1 救済)
--- ---------------------------------------------------------------------------
-
--- | affine 追跡値 (Phase 54.8)。 latent 値の場所に流して、 式が
--- @Σ coeff_i · latent_i + offset@ (係数は定数) の形に留まるかを追跡する。
--- 非線形演算 (latent 同士の積・exp 等) が掛かった時点で 'NA' に落ちる。
-data AffV
-  = AffC !Double               -- ^ 定数
-  | AffL !(Map Text Double) !Double  -- ^ Σ coeff·latent + offset (affine)
-  | NA                         -- ^ 非 affine (追跡断念)
-
--- Phase 60.7: '!!!' の依存タグは IR 抽出には無関係 (既定 id)。
-instance TrackTag AffV
-
--- | 非定数値の比較 = 値依存分岐。 構造抽出は分岐の片側しか見られないため
--- 誤抽出になる → error poison で walk 全体を失敗させ、 呼出側の
--- @try/evaluate/force@ で捕捉して fallback する (安全網①)。
-affPoison :: a
-affPoison = error "AffV: non-constant comparison (value-dependent branch)"
-
-instance Eq AffV where
-  AffC a == AffC b = a == b
-  _      == _      = affPoison
-
-instance Ord AffV where
-  compare (AffC a) (AffC b) = compare a b
-  compare _        _        = affPoison
-
-instance Num AffV where
-  AffC a   + AffC b   = AffC (a + b)
-  AffC a   + AffL m c = AffL m (a + c)
-  AffL m c + AffC a   = AffL m (c + a)
-  AffL m1 c1 + AffL m2 c2 = AffL (Map.unionWith (+) m1 m2) (c1 + c2)
-  _ + _ = NA
-  AffC a   * AffC b   = AffC (a * b)
-  AffC a   * AffL m c = scaleAffV a m c
-  AffL m c * AffC a   = scaleAffV a m c
-  _ * _ = NA
-  negate (AffC a)   = AffC (negate a)
-  negate (AffL m c) = AffL (Map.map negate m) (negate c)
-  negate NA         = NA
-  abs (AffC a) = AffC (abs a)
-  abs _        = NA
-  signum (AffC a) = AffC (signum a)
-  signum _        = NA
-  fromInteger = AffC . fromInteger
-
--- | 定数倍。 0 倍は affine 情報ごと消えて定数 0 (係数 0 の死に列を作らない)。
-scaleAffV :: Double -> Map Text Double -> Double -> AffV
-scaleAffV a m c
-  | a == 0    = AffC 0
-  | otherwise = AffL (Map.map (a *) m) (a * c)
-
-instance Fractional AffV where
-  AffC a   / AffC b = AffC (a / b)
-  AffL m c / AffC b = scaleAffV (recip b) m c
-  _        / _      = NA
-  recip (AffC a) = AffC (recip a)
-  recip _        = NA
-  fromRational = AffC . fromRational
-
-instance Floating AffV where
-  pi = AffC pi
-  exp   = affLift1 exp
-  log   = affLift1 log
-  sqrt  = affLift1 sqrt
-  sin   = affLift1 sin
-  cos   = affLift1 cos
-  tan   = affLift1 tan
-  asin  = affLift1 asin
-  acos  = affLift1 acos
-  atan  = affLift1 atan
-  sinh  = affLift1 sinh
-  cosh  = affLift1 cosh
-  tanh  = affLift1 tanh
-  asinh = affLift1 asinh
-  acosh = affLift1 acosh
-  atanh = affLift1 atanh
-
--- | 超越関数: 定数には適用、 latent が絡んだら非 affine。
-affLift1 :: (Double -> Double) -> AffV -> AffV
-affLift1 f (AffC a) = AffC (f a)
-affLift1 _ _        = NA
-
--- | Phase 54.8: per-obs 手書き scalar 'Observe' 群から Gaussian LM ブロックを
--- **自動合成**する。 返り値は (合成ブロック群, 吸収した Observe ノード名集合)。
--- 検出できない / 安全網に掛かった場合は @([], ∅)@ (従来経路に fallback)。
---
--- 仕組み: 'Sample' の継続に @AffL {name:1} 0@ を給餌して model を walk し、
--- @Observe nm (Normal μ σ) ys@ の μ が affine・σ が単一 latent (係数 1・offset 0)
--- の行を収集する。 定数 offset は ys 側に畳む (Normal は y−μ のみに依存)。
--- σ 名ごとに 1 ブロックへまとめ、 prior が @Normal(0, τ)@ (τ 単一 latent) を
--- 共有し **各行にちょうど 1 つ**現れる latent 族を 'REff' gather に昇格する
--- (dense one-hot は O(nG·n) で階層に逆効果 — 54.4a 計測)。 係数は任意で、
--- per-row 重みとして 'REff' に載せる (Phase 54.10: random slope @v_g·x_i@ も
--- gather 化。 全 1 なら重みスロットは @Nothing@ = 従来の random intercept)。
--- 族抽出に失敗した latent は dense β 列のまま (正しいが遅い・安全方向)。
---
--- 安全網 2 段: ① 'AffV' の Eq/Ord は非定数比較で error poison →
--- 'unsafePerformIO' + 'try' + 'force' で捕捉し全体 fallback (値依存分岐モデルの
--- 誤抽出防止・Nonlinear 系の前例に同じ)。 ② 合成ブロックの観測尤度を probe
--- 2 点で walk 評価 ('obsOnlySum') と突合し、 不一致なら fallback。
-synthGaussLMBlocks
-  :: ModelP r
-  -> ([(Text, [Text], [[Double]], [REff], Text, [Double])], Set Text)
-synthGaussLMBlocks m = unsafePerformIO $ do
-  r <- try (evaluate (force (synthGaussLMWalk m)))
-  pure $ case r :: Either SomeException
-                    ([(Text, [Text], [[Double]], [REff], Text, [Double])], Set Text) of
-    Left _  -> ([], Set.empty)
-    Right v@(blocks, obsNames)
-      | null blocks          -> ([], Set.empty)
-      | synthProbeOK m blocks obsNames -> v
-      | otherwise            -> ([], Set.empty)
-{-# NOINLINE synthGaussLMBlocks #-}
-
--- | 'synthGaussLMBlocks' の純粋部 (walk + 族抽出)。 poison は遅延に潜むので
--- 呼出側が force してから使う。
-synthGaussLMWalk
-  :: ModelP r
-  -> ([(Text, [Text], [[Double]], [REff], Text, [Double])], Set Text)
-synthGaussLMWalk m =
-  let (rows, priors) = collectAffRows m
-      sigmas = ordNubT [ sn | (_, _, _, sn, _) <- rows ]
-      blocks = [ synthBlock priors sn [ r | r@(_, _, _, sn', _) <- rows, sn' == sn ]
-               | sn <- sigmas ]
-      obsNames = Set.fromList [ nm | (nm, _, _, _, _) <- rows ]
-  in (blocks, obsNames)
-  where
-    ordNubT = go Set.empty
-      where go _ [] = []
-            go seen (x:xs)
-              | x `Set.member` seen = go seen xs
-              | otherwise           = x : go (Set.insert x seen) xs
-
--- | model を 'AffV' で walk し、 合成可能な行 (Observe 名, μ 係数, μ offset,
--- σ 名, 観測値) と latent prior のスケール検出 (@Normal(0, τ)@ → @Just τ@) を集める。
-collectAffRows
-  :: Model AffV r
-  -> ([(Text, Map Text Double, Double, Text, Double)], Map Text (Maybe Text))
-collectAffRows = go [] Map.empty
-  where
-    go rows priors (Pure _) = (reverse rows, priors)
-    go rows priors (Free f) = case f of
-      Sample n d k ->
-        let sc = case d of
-                   Normal (AffC 0) (AffL tm 0)
-                     | [(tn, 1)] <- Map.toList tm -> Just tn
-                   _ -> Nothing
-        in go rows (Map.insert n sc priors) (k (AffL (Map.singleton n 1) 0))
-      Observe nm (Normal mu sg) ys next
-        | AffL sm 0 <- sg, [(sn, 1)] <- Map.toList sm
-        , Just (cs, off) <- affParts mu ->
-            go ([ (nm, cs, off, sn, y) | y <- ys ] ++ rows) priors next
-      Observe _ _ _ next  -> go rows priors next
-      ObserveLM _ _ _ _ _ _ next -> go rows priors next
-      Potential _ _ next  -> go rows priors next
-      Deterministic _ v k -> go rows priors (k v)
-      Data _ ys k         -> go rows priors (k (map realToFrac ys, ys))
-      DataIx _ is k       -> go rows priors (k is)
-      PlateBegin _ _ next -> go rows priors next
-      PlateEnd next       -> go rows priors next
-    affParts (AffC c)   = Just (Map.empty, c)
-    affParts (AffL m c) = Just (m, c)
-    affParts NA         = Nothing
-
--- | Phase 93: 非ゼロ **latent 平均** の階層 Normal prior を検出する。
--- @u_i ~ Normal(μ, τ)@ で μ・τ **ともに単一 latent** (係数 1・offset 0) の
--- 'Sample' を集め、 (μ, τ) の組ごとに出現順を保って群化する。 返り値の各要素は
--- @(u 名の群, μ 名, τ 名)@。
---
--- 平均が定数 (@AffC 0@) の reff は既存の mean-0 解析経路 ('ReffPriorIx') が
--- 扱うのでここでは検出しない (μ が 'AffL' でないと不一致)。 係数≠1・多項・
--- offset≠0 の平均や非 affine な μ/τ も対象外 (残差 ad に残す安全側)。
--- rats の @alpha[i]~Normal(muAlpha,sigmaAlpha)@ / @beta[i]~Normal(muBeta,sigmaBeta)@
--- のような varying-intercept/slope の中心化階層 prior を解析勾配へ載せるための検出器。
-collectHierNormalGroups :: Model AffV r -> [([Text], Text, Text)]
-collectHierNormalGroups = regroup . go
-  where
-    go (Pure _) = []
-    go (Free f) = case f of
-      Sample n d k ->
-        let hit = case d of
-                    Normal (AffL mm 0) (AffL tm 0)
-                      | [(mn, 1)] <- Map.toList mm
-                      , [(tn, 1)] <- Map.toList tm -> Just (n, mn, tn)
-                    _ -> Nothing
-            rest = go (k (AffL (Map.singleton n 1) 0))
-        in maybe rest (: rest) hit
-      Observe _ _ _ next          -> go next
-      ObserveLM _ _ _ _ _ _ next  -> go next
-      Potential _ _ next          -> go next
-      Deterministic _ v k         -> go (k v)
-      Data _ ys k                 -> go (k (map realToFrac ys, ys))
-      DataIx _ is k               -> go (k is)
-      PlateBegin _ _ next         -> go next
-      PlateEnd next               -> go next
-    -- (μ,τ) ごとに、u の出現順を保って群化する。
-    regroup hits =
-      [ ([ u | (u, mn', tn') <- hits, mn' == mn, tn' == tn ], mn, tn)
-      | (mn, tn) <- ordNub [ (mn, tn) | (_, mn, tn) <- hits ] ]
-    ordNub = goN Set.empty
-      where goN _ [] = []
-            goN s (x:xs) | x `Set.member` s = goN s xs
-                         | otherwise        = x : goN (Set.insert x s) xs
-
--- | Phase 98 A3: @a_i ~ LogNormal(μ, σ)@ 群を検出する ('collectHierNormalGroups' の
--- LogNormal 版)。μ は定数 (@Left c@・例 irt-2pl の 0) か単一 latent (@Right mn@)、
--- σ は単一 latent (@AffL {sn:1} 0@)。σ 定数は 'constPriorsOf' が拾うのでここでは対象外。
--- 返り値 = [(u 名, μ, σ 名)]。vecIR 経路で解析勾配 ('gradLogNormalIx') に載せ残差 ad から
--- 外す (irt-2pl の 20-項 LogNormal prior が reverse-AD tape を張っていたのを解消)。
-collectLogNormalGroups :: Model AffV r -> [([Text], Either Double Text, Text)]
-collectLogNormalGroups = regroup . go
-  where
-    go (Pure _) = []
-    go (Free f) = case f of
-      Sample n d k ->
-        let hit = case d of
-                    LogNormal muA (AffL tm 0)
-                      | [(tn, 1)] <- Map.toList tm
-                      , Just mean <- affMean muA -> Just (n, mean, tn)
-                    _ -> Nothing
-            rest = go (k (AffL (Map.singleton n 1) 0))
-        in maybe rest (: rest) hit
-      Observe _ _ _ next          -> go next
-      ObserveLM _ _ _ _ _ _ next  -> go next
-      Potential _ _ next          -> go next
-      Deterministic _ v k         -> go (k v)
-      Data _ ys k                 -> go (k (map realToFrac ys, ys))
-      DataIx _ is k               -> go (k is)
-      PlateBegin _ _ next         -> go next
-      PlateEnd next               -> go next
-    -- μ = 定数 (AffC c / offset のみの AffL) か単一 latent (係数 1・offset 0)。
-    affMean (AffC c)                        = Just (Left c)
-    affMean (AffL mm c)
-      | Map.null mm                         = Just (Left c)
-      | [(mn, 1)] <- Map.toList mm, c == 0  = Just (Right mn)
-    affMean _                               = Nothing
-    -- (μ, σ) ごとに u の出現順を保って群化する。
-    regroup hits =
-      [ ([ u | (u, mean', sn') <- hits, mean' == mean, sn' == sn ], mean, sn)
-      | (mean, sn) <- ordNub [ (mean, sn) | (_, mean, sn) <- hits ] ]
-    ordNub = goN Set.empty
-      where goN _ [] = []
-            goN s (x:xs) | x `Set.member` s = goN s xs
-                         | otherwise        = x : goN (Set.insert x s) xs
-
--- | 1 つの σ 名グループから (ブロック名, β 名, X, REff 族, σ 名, ys') を合成する。
-synthBlock
-  :: Map Text (Maybe Text)
-  -> Text
-  -> [(Text, Map Text Double, Double, Text, Double)]
-  -> (Text, [Text], [[Double]], [REff], Text, [Double])
-synthBlock priors sn rows =
-  let coeffs   = [ cs | (_, cs, _, _, _) <- rows ]
-      latents  = Set.toAscList (Set.unions (map Map.keysSet coeffs))
-      -- 族候補 (Phase 54.10 で「係数常 1」 を撤廃): prior = Normal(0, τ) 検出済
-      -- なら係数任意。 係数は per-row 重みとして 'REff' に載せる (random slope)。
-      isCand l = maybe False (/= Nothing) (Map.lookup l priors)
-      -- スケール τ ごとに族を貪欲抽出: 全行にちょうど 1 つ現れる族のみ採用。
-      famsByTau = Map.fromListWith (++)
-        [ (tn, [l]) | l <- latents, isCand l
-        , Just (Just tn) <- [Map.lookup l priors] ]
-      accepted = [ (tn, Set.toAscList (Set.fromList ls))
-                 | (tn, ls) <- Map.toList famsByTau
-                 , let fam = Set.fromList ls
-                 , all (\cs -> length (filter (`Map.member` cs) (Set.toList fam)) == 1)
-                       coeffs ]
-      famSet   = Set.fromList (concatMap snd accepted)
-      reffs    = [ let gws = [ gwOf fam cs | cs <- coeffs ]
-                       ws  = map snd gws
-                       mw  = if all (== 1) ws then Nothing else Just ws
-                   in REff fam (map fst gws) (Just tn) mw Nothing
-                 | (tn, fam) <- accepted ]
-      -- 各行で族中ちょうど 1 つ現れる latent の (族内 index, 係数 = 重み)。
-      gwOf fam cs = head [ (j, cs Map.! l) | (j, l) <- zip [0 ..] fam
-                         , l `Map.member` cs ]
-      betas    = [ l | l <- latents, not (l `Set.member` famSet) ]
-      xs       = [ [ Map.findWithDefault 0 b cs | b <- betas ] | cs <- coeffs ]
-      ys'      = [ y - off | (_, _, off, _, y) <- rows ]
-  in ("__synth_lm_" <> sn, betas, xs, reffs, sn, ys')
-
--- | 安全網② (Phase 54.8): 合成ブロックの観測尤度を、 元 model の walk 評価
--- ('obsOnlySum' = 吸収した scalar Observe だけ足す) と probe 2 点で突合する。
--- prior は足さないので guard 起因の ±∞ で比較が壊れない。 probe 値は
--- per-param に変えて係数の取り違えも検出する (全 latent 正値 → σ guard 安全)。
-synthProbeOK
-  :: ModelP r
-  -> [(Text, [Text], [[Double]], [REff], Text, [Double])]
-  -> Set Text -> Bool
-synthProbeOK m blocks obsNames = all check [(0.5, 0.07), (1.3, 0.11)]
-  where
-    names = sampleNames m
-    check (base, step) =
-      let pm = Map.fromList [ (n, base + step * fromIntegral i)
-                            | (n, i) <- zip names [0 :: Int ..] ]
-          ref = obsOnlySum obsNames m pm
-          syn = sum [ lmObsLogSum bs xs re (LMGaussian sn) ys pm
-                    | (_, bs, xs, re, sn, ys) <- blocks ]
-      in abs (ref - syn) <= 1e-9 * (1 + abs ref)
-
--- | 名前が @sel@ に含まれる scalar 'Observe' の log-likelihood **だけ**を足す
--- walk (Phase 54.8 probe 用)。
-obsOnlySum :: Set Text -> Model Double r -> Map Text Double -> Double
-obsOnlySum sel model params = go model 0
-  where
-    go (Pure _) acc = acc
-    go (Free (Sample n _ k)) acc = go (k (Map.findWithDefault 0 n params)) acc
-    go (Free (Observe n d ys next)) acc
-      | n `Set.member` sel = go next (acc + obsLogSum d ys)
-      | otherwise          = go next acc
-    go (Free (ObserveLM _ _ _ _ _ _ next)) acc = go next acc
-    go (Free (Potential _ _ next)) acc = go next acc
-    go (Free (Deterministic _ v k)) acc = go (k v) acc
-    go (Free (Data _ ys k)) acc = go (k (ys, ys)) acc
-    go (Free (DataIx _ is k)) acc = go (k is) acc
-    go (Free (PlateBegin _ _ next)) acc = go next acc
-    go (Free (PlateEnd next)) acc = go next acc
-
--- ---------------------------------------------------------------------------
--- Phase 54.11: 非線形 μ のベクトル式 IR (M5/M6 救済)
--- ---------------------------------------------------------------------------
---
--- 54.8 の AffV (affine 限定) の代役として、 latent 値の場所に **スカラ式ノード**
--- ('SExp') を給餌して model を walk し、 per-obs scalar @Observe (Normal μ σ)@ の
--- μ 式 (非線形可) を行ごとに収集する。 行間で式の形が同型 (定数 leaf だけが行
--- ごとに違う) なら定数列をベクトル leaf に束ねて「ベクトル式 IR」 ('UExp') へ
--- 持ち上げ (μ⃗ = f(θ, x⃗))、 評価は VecAD の vector-op tape で行う (勾配) /
--- 素な Double ベクトル演算で行う (値)。 階層 prior (a_g ~ Normal(m, τ) の族) の
--- スカラ密度も同 IR に乗せる (M6 要件・54.9)。
---
--- IR 持ち上げ + 静的解析は compile 時 1 回・draw 間で再利用する (54.4b 前例)。
--- VecAD tape 自体は per-call 構築 (spike `bench-hbm-vecir` の実測はこの構築込み)。
-
--- | スカラ単項演算子 ('SExp' の節)。 導関数は 'sUnD' と対。
-data SUn
-  = SNegO | SAbsO | SSignumO | SExpO | SLogO | SSqrtO | SRecipO
-  | SSinO | SCosO | STanO | SAsinO | SAcosO | SAtanO
-  | SSinhO | SCoshO | STanhO | SAsinhO | SAcoshO | SAtanhO
-  | SLgammaO   -- ^ log Γ (Phase 56.2・密度 IR 用。 'Floating' 経由では現れない)
-  deriving (Eq, Ord, Show)
-
--- | 'SUn' の評価関数を known-function として継続に渡す CPS dispatcher。
--- Phase 105 A3: arena 実行ループが @f = sUnF op@ で closure を束縛してから
--- 要素毎に間接呼出すると GHC が unbox できず per-element boxing が出る
--- (irt-2pl prof で sUnF/sBinF/sUnD 計 30.5% time・38.9% alloc)。 call site を
--- INLINE 展開して op の case をループの外に出し、 各分岐を known-function の
--- 特殊化 unboxed ループに落とす。 演算内容・FP 順序は不変 (= posterior bit 一致)。
-withSUnF :: SUn -> ((Double -> Double) -> r) -> r
-withSUnF o k = case o of
-  SNegO    -> k negate
-  SAbsO    -> k abs
-  SSignumO -> k signum
-  SExpO    -> k exp
-  SLogO    -> k log
-  SSqrtO   -> k sqrt
-  SRecipO  -> k recip
-  SSinO    -> k sin
-  SCosO    -> k cos
-  STanO    -> k tan
-  SAsinO   -> k asin
-  SAcosO   -> k acos
-  SAtanO   -> k atan
-  SSinhO   -> k sinh
-  SCoshO   -> k cosh
-  STanhO   -> k tanh
-  SAsinhO  -> k asinh
-  SAcoshO  -> k acosh
-  SAtanhO  -> k atanh
-  SLgammaO -> k lgammaApprox
-{-# INLINE withSUnF #-}
-
-sUnF :: SUn -> Double -> Double
-sUnF o = withSUnF o id
-
--- | 'sUnF' の導関数の CPS dispatcher ('withSUnF' と同じ意図)。
-withSUnD :: SUn -> ((Double -> Double) -> r) -> r
-withSUnD o k = case o of
-  SNegO    -> k (const (-1))
-  SAbsO    -> k signum
-  SSignumO -> k (const 0)
-  SExpO    -> k exp
-  SLogO    -> k recip
-  SSqrtO   -> k (\x -> 0.5 / sqrt x)
-  SRecipO  -> k (\x -> negate (recip (x * x)))
-  SSinO    -> k cos
-  SCosO    -> k (negate . sin)
-  STanO    -> k (\x -> let t = tan x in 1 + t * t)
-  SAsinO   -> k (\x -> 1 / sqrt (1 - x * x))
-  SAcosO   -> k (\x -> negate (1 / sqrt (1 - x * x)))
-  SAtanO   -> k (\x -> 1 / (1 + x * x))
-  SSinhO   -> k cosh
-  SCoshO   -> k sinh
-  STanhO   -> k (\x -> let t = tanh x in 1 - t * t)
-  SAsinhO  -> k (\x -> 1 / sqrt (x * x + 1))
-  SAcoshO  -> k (\x -> 1 / sqrt (x * x - 1))
-  SAtanhO  -> k (\x -> 1 / (1 - x * x))
-  -- digamma でなく項別微分: 評価関数 lgammaApprox の AD 微分 (walk+ad fallback /
-  -- 参照勾配) とビット近傍一致させる (digamma だと z=12 境界で ~1.3e-9 ズレ・56.4)
-  SLgammaO -> k lgammaApproxDeriv
-{-# INLINE withSUnD #-}
-
--- | 'sUnF' の導関数。
-sUnD :: SUn -> Double -> Double
-sUnD o = withSUnD o id
-
--- | スカラ二項演算子 ('SExp' の節)。
--- | Phase 90 A3: 'SMaxO' は Mixture/ZeroInflatedBinomial の log-sum-exp を
--- 数値安定に組むための elementwise max (勾配は winner-take-all の
--- subgradient・'gradVecIRGo' 参照)。 'SExp' の 'Num' インスタンス経由では
--- 構築しない (Num に max が無い) — 'logSumExp2' からのみ直接 'RU2 SMaxO' で
--- 使う。
-data SBin = SAddO | SSubO | SMulO | SDivO | SMaxO
-  deriving (Eq, Ord, Show)
-
--- | 二項演算子の CPS dispatcher ('withSUnF' と同じ意図)。
-withSBinF :: SBin -> ((Double -> Double -> Double) -> r) -> r
-withSBinF o k = case o of
-  SAddO -> k (+)
-  SSubO -> k (-)
-  SMulO -> k (*)
-  SDivO -> k (/)
-  SMaxO -> k max
-{-# INLINE withSBinF #-}
-
-sBinF :: SBin -> Double -> Double -> Double
-sBinF o = withSBinF o id
-
--- | スカラ式 IR。 latent 値の場所に流して式の木を構築する (AffV と違い
--- 非線形演算も leaf に潜らず木に残る)。 定数同士は即畳み込む ('sc1'/'sc2') ので
--- データ由来の値は常に 'SC' leaf に正規化され、 行間の形状照合が成立する。
-data SExp
-  = SC !Double          -- ^ 定数 (データ・リテラル)
-  | SV !Text            -- ^ latent 参照
-  | S1 !SUn SExp
-  | S2 !SBin SExp SExp
-
-instance NFData SExp where
-  rnf (SC x)     = rnf x
-  rnf (SV n)     = rnf n
-  rnf (S1 o e)   = o `seq` rnf e
-  rnf (S2 o a b) = o `seq` rnf a `seq` rnf b
-
--- Phase 60.7: '!!!' の依存タグは IR 抽出には無関係 (既定 id)。
-instance TrackTag SExp
-
--- | 非定数値の比較 = 値依存分岐 → error poison (54.8 の AffV と同じ安全網①)。
-symPoison :: a
-symPoison = error "SExp: non-constant comparison (value-dependent branch)"
-
-instance Eq SExp where
-  SC a == SC b = a == b
-  _    == _    = symPoison
-
-instance Ord SExp where
-  compare (SC a) (SC b) = compare a b
-  compare _        _    = symPoison
-
--- | 定数畳み込み付きノード構築。
-sc1 :: SUn -> SExp -> SExp
-sc1 o (SC a) = SC (sUnF o a)
-sc1 o e      = S1 o e
-
-sc2 :: SBin -> SExp -> SExp -> SExp
-sc2 o (SC a) (SC b) = SC (sBinF o a b)
-sc2 o a b           = S2 o a b
-
-instance Num SExp where
-  (+) = sc2 SAddO
-  (-) = sc2 SSubO
-  (*) = sc2 SMulO
-  negate = sc1 SNegO
-  abs    = sc1 SAbsO
-  signum = sc1 SSignumO
-  fromInteger = SC . fromInteger
-
-instance Fractional SExp where
-  (/) = sc2 SDivO
-  recip = sc1 SRecipO
-  fromRational = SC . fromRational
-
-instance Floating SExp where
-  pi    = SC pi
-  exp   = sc1 SExpO
-  log   = sc1 SLogO
-  sqrt  = sc1 SSqrtO
-  sin   = sc1 SSinO
-  cos   = sc1 SCosO
-  tan   = sc1 STanO
-  asin  = sc1 SAsinO
-  acos  = sc1 SAcosO
-  atan  = sc1 SAtanO
-  sinh  = sc1 SSinhO
-  cosh  = sc1 SCoshO
-  tanh  = sc1 STanhO
-  asinh = sc1 SAsinhO
-  acosh = sc1 SAcoshO
-  atanh = sc1 SAtanhO
-
--- | 構造一致 (total・poison しない。 族 prior の同型判定用)。
-sexpEq :: SExp -> SExp -> Bool
-sexpEq (SC a)     (SC b)     = a == b
-sexpEq (SV a)     (SV b)     = a == b
-sexpEq (S1 o a)   (S1 p b)   = o == p && sexpEq a b
-sexpEq (S2 o a c) (S2 p b d) = o == p && sexpEq a b && sexpEq c d
-sexpEq _          _          = False
-
--- | 式中の latent 参照名。
-sexpVars :: SExp -> Set Text
-sexpVars (SC _)     = Set.empty
-sexpVars (SV n)     = Set.singleton n
-sexpVars (S1 _ e)   = sexpVars e
-sexpVars (S2 _ a b) = sexpVars a `Set.union` sexpVars b
-
--- | μ 式の「形の指紋」 (Phase 55.2)。 演算子木の形と leaf の SC/SV 区別のみで、
--- 値・名前は含めない。 同一 σ 下で式形が混在しても指紋ごとに独立のグループとして
--- 'unifyMany' に掛けるためのキー (形違いで σ グループ丸ごと drop しない)。
--- 「全行同一 SV」 と「行で異なる SV (族 gather)」 の区別は従来どおり unify 側の仕事。
-sexpShape :: SExp -> String
-sexpShape (SC _)     = "c"
-sexpShape (SV _)     = "v"
-sexpShape (S1 o e)   = show o ++ '(' : sexpShape e ++ ")"
-sexpShape (S2 o a b) = show o ++ '(' : sexpShape a ++ ',' : sexpShape b ++ ")"
-
--- | σ 式の「名前付き指紋」 (Phase 55.3)。 'sexpShape' と違い SV は latent 名を
--- 含める: σ 側は名前が違えば別グループに分ける (σ leaf を行で混ぜて族 gather に
--- 持ち上げると、 族 prior 条件を満たさない σ 同士の合流でグループ全体が drop する
--- 退行が起き得るため、 σ は保守的に「同一式 (定数値のみ行依存可)」 でキーする)。
--- heteroscedastic (例 @exp(g0 + g1·z_i)@) は名前が全行同一・データ定数だけ行で
--- 違う形なので、 このキーで 1 グループに揃い 'unifyMany' が UC 列に持ち上げる。
-sexpKeyNamed :: SExp -> String
-sexpKeyNamed (SC _)     = "c"
-sexpKeyNamed (SV n)     = "v:" ++ T.unpack n
-sexpKeyNamed (S1 o e)   = show o ++ '(' : sexpKeyNamed e ++ ")"
-sexpKeyNamed (S2 o a b) =
-  show o ++ '(' : sexpKeyNamed a ++ ',' : sexpKeyNamed b ++ ")"
-
--- | scalar 'Observe' 行の分布部 (Phase 55.4)。 IR 化対象の分布のみ。
---
--- ★分布追加チェックリスト (Phase 56.1 転記・1 分布 = 6 箇所・1 commit):
---   1. 'collectSymRows' に Observe 分岐 (+観測値定義域チェック → 域外行を含む
---      グループは収集時に弾く = walk の -∞ 縮退を残す安全方向)
---   2. 'keyOf' に family タグ (位置-尺度系は scale 側を 'sexpKeyNamed')
---   3. 'tryGroup' の unify 分岐
---   4. 'VecGroupSrc' / 'VecObsIR' ctor (+NFData) + 観測値定数の compile 時前計算
---   5. 密度式 + 値 guard ('logDensityObs' の該当分岐と完全一致。 56.2 後は
---      densityIR の式のみ・勾配は記号微分で自動)
---   6. test: 吸収確認 + 値 1e-9 + 勾配 ad 1e-9 + 中心差分 1e-4 + fallback 確認。
---      probe 点 (0.5/1.3) の定義域を分布別に確認 (link 経由は構造上域内・
---      パラメタ latent 直で域外なら fallback = 既知制限)
-data SymDist
-  = SDGauss SExp SExp   -- ^ Normal μ σ (σ は任意式・55.3)
-  | SDPois  SExp        -- ^ Poisson λ (λ は任意式・GLM log link は exp が式に入る)
-  | SDBern  SExp        -- ^ Bernoulli p (同・invLogit が式に入る)
-  | SDStudT !Double SExp SExp
-    -- ^ StudentT ν μ σ (56.3。 ν は SC 定数のみ吸収 = lgamma 項が定数化。
-    -- ν latent は fallback・計画の scope どおり)
-  | SDCauchy SExp SExp  -- ^ Cauchy x₀ γ (56.3)
-  | SDLogis SExp SExp   -- ^ Logistic μ s (56.3)
-  | SDGumbel SExp SExp  -- ^ Gumbel μ β (56.3)
-  | SDExpo SExp         -- ^ Exponential rate (56.4。 y ≥ 0 は収集時に確認)
-  | SDWeib SExp SExp    -- ^ Weibull k λ (56.4。 y > 0 は収集時に確認)
-  | SDLogN SExp SExp    -- ^ LogNormal μ σ (56.4。 y > 0 は収集時に確認)
-  | SDGamma SExp SExp   -- ^ Gamma α rate (56.4。 y > 0 は収集時に確認)
-  | SDBeta SExp SExp    -- ^ Beta α β (56.4。 0 < y < 1 は収集時に確認)
-  | SDBinom !Int SExp   -- ^ Binomial n p (56.5。 n は ctor 定数・
-                        --   0 ≤ round y ≤ n は収集時に確認)
-  | SDGeom SExp         -- ^ Geometric p (56.5。 round y ≥ 1 は収集時に確認)
-  | SDNegBin SExp SExp  -- ^ NegativeBinomial μ α (56.5。 y ≥ 0 は収集時に確認)
-  | SDMixNorm2 SExp SExp SExp SExp SExp SExp
-    -- ^ Mixture [w1,w2] [Normal μ1 σ1, Normal μ2 σ2] (Phase 90 A3。 2成分
-    -- Normal混合限定 — 任意分布族・K成分への一般化は対象外。 w1 w2 は
-    -- 'Distribution.hs' の Mixture 定義どおり Σw で自動正規化するので
-    -- w1+w2=1 を仮定しない (w1 w2 μ1 σ1 μ2 σ2)。
-  | SDZIBinom !Int SExp SExp
-    -- ^ ZeroInflatedBinomial n ψ p (Phase 90 A3。 n は ctor 定数・
-    -- 0 ≤ round y ≤ n は収集時に確認)
-
--- | model を 'SExp' で walk し、 scalar @Observe@ 行 (Observe 名, 分布部, 観測値)
--- と latent prior を集める ('collectAffRows' の 54.11 版)。 Phase 55.3 で σ を
--- 任意式に、 55.4 で Normal 限定 → Poisson / Bernoulli にも拡張。 他のノードは
--- 素通し (residual walk に残す)。
-collectSymRows
-  :: Model SExp r
-  -> ([(Text, SymDist, Double)], Map Text (Distribution SExp))
-collectSymRows = go [] Map.empty
-  where
-    go rows priors (Pure _) = (reverse rows, priors)
-    go rows priors (Free f) = case f of
-      Sample n d k -> go rows (Map.insert n d priors) (k (SV n))
-      Observe nm (Normal mu sg) ys next ->
-        go ([ (nm, SDGauss mu sg, y) | y <- ys ] ++ rows) priors next
-      Observe nm (Poisson lam) ys next ->
-        go ([ (nm, SDPois lam, y) | y <- ys ] ++ rows) priors next
-      Observe nm (Bernoulli p) ys next ->
-        go ([ (nm, SDBern p, y) | y <- ys ] ++ rows) priors next
-      -- 56.3 位置-尺度系 (support = ℝ → 観測値定義域チェック不要)。
-      -- StudentT は ν=SC かつ ν>0 のみ (ν≤0 は walk の -∞ を残す安全方向)。
-      Observe nm (StudentT (SC nu) mu sg) ys next | nu > 0 ->
-        go ([ (nm, SDStudT nu mu sg, y) | y <- ys ] ++ rows) priors next
-      Observe nm (Cauchy loc sc) ys next ->
-        go ([ (nm, SDCauchy loc sc, y) | y <- ys ] ++ rows) priors next
-      Observe nm (Logistic mu s) ys next ->
-        go ([ (nm, SDLogis mu s, y) | y <- ys ] ++ rows) priors next
-      Observe nm (Gumbel mu be) ys next ->
-        go ([ (nm, SDGumbel mu be, y) | y <- ys ] ++ rows) priors next
-      -- 56.4 正値・区間系 (観測値定義域チェックは tryGroup の ysV 検査で)。
-      Observe nm (Exponential rate) ys next ->
-        go ([ (nm, SDExpo rate, y) | y <- ys ] ++ rows) priors next
-      Observe nm (Weibull k lam) ys next ->
-        go ([ (nm, SDWeib k lam, y) | y <- ys ] ++ rows) priors next
-      Observe nm (LogNormal mu sg) ys next ->
-        go ([ (nm, SDLogN mu sg, y) | y <- ys ] ++ rows) priors next
-      Observe nm (Gamma sh rt) ys next ->
-        go ([ (nm, SDGamma sh rt, y) | y <- ys ] ++ rows) priors next
-      Observe nm (Beta al be) ys next ->
-        go ([ (nm, SDBeta al be, y) | y <- ys ] ++ rows) priors next
-      -- 56.5 離散系。
-      Observe nm (Binomial n p) ys next ->
-        go ([ (nm, SDBinom n p, y) | y <- ys ] ++ rows) priors next
-      Observe nm (Geometric p) ys next ->
-        go ([ (nm, SDGeom p, y) | y <- ys ] ++ rows) priors next
-      Observe nm (NegativeBinomial mu al) ys next ->
-        go ([ (nm, SDNegBin mu al, y) | y <- ys ] ++ rows) priors next
-      -- Phase 90 A3: 2成分 Normal 混合限定 (04-low-dim-gauss-mix の
-      -- log_mix(θ, normal_lpdf(μ1,σ1), normal_lpdf(μ2,σ2)) と同型)。
-      -- 任意分布族・3成分以上は対象外 (素通し → residual walk+ad)。
-      Observe nm (Mixture [w1, w2] [Normal mu1 sg1, Normal mu2 sg2]) ys next ->
-        go ([ (nm, SDMixNorm2 w1 w2 mu1 sg1 mu2 sg2, y) | y <- ys ] ++ rows) priors next
-      Observe nm (ZeroInflatedBinomial n psi p) ys next ->
-        go ([ (nm, SDZIBinom n psi p, y) | y <- ys ] ++ rows) priors next
-      Observe _ _ _ next  -> go rows priors next
-      ObserveLM _ _ _ _ _ _ next -> go rows priors next
-      Potential _ _ next  -> go rows priors next
-      Deterministic _ v k -> go rows priors (k v)
-      Data _ ys k         -> go rows priors (k (map realToFrac ys, ys))
-      DataIx _ is k       -> go rows priors (k is)
-      PlateBegin _ _ next -> go rows priors next
-      PlateEnd next       -> go rows priors next
-
--- | model を 'SExp' で walk し、 raw 'Potential' の (名前, 式) を出現順に
--- 集める (Phase 90 A10)。 'collectSymRows' と同じ給餌 (latent = @SV n@)。
-collectSymPots :: Model SExp r -> [(Text, SExp)]
-collectSymPots = go []
-  where
-    go acc (Pure _) = reverse acc
-    go acc (Free f) = case f of
-      Sample n _ k        -> go acc (k (SV n))
-      Observe _ _ _ next  -> go acc next
-      ObserveLM _ _ _ _ _ _ next -> go acc next
-      Potential nm v next -> go ((nm, v) : acc) next
-      Deterministic _ v k -> go acc (k v)
-      Data _ ys k         -> go acc (k (map realToFrac ys, ys))
-      DataIx _ is k       -> go acc (k is)
-      PlateBegin _ _ next -> go acc next
-      PlateEnd next       -> go acc next
-
--- ===========================================================================
--- Phase 98 A2: 残余 log-joint の flat compile (Free AST 再解釈の廃止)
--- ===========================================================================
--- 'logJointExclBlocks' (Gradient.hs) は excl 吸収後の残余 log-density を求める
--- ため 'Model a r' の Free 構造を毎勾配評価で頭から walk する。 vecIR arena に
--- 吸収し切れない項 (例 06-irt-2pl: `a` の LogNormal 事前分布) が残るモデルでは、
--- 大量の吸収済み Observe plate まで「継続のため素通り walk」する純オーバーヘッド
--- が支配する (Phase 98 A1c prof: logJointExclBlocks = 31.5% time / 41.7% alloc・
--- Free monad `>>=`/`fmap` が十数億 entry)。
---
--- 本 IR は残余を **1 度の symbolic walk で flat 化**し ('CompiledResidual')、 全
--- leapfrog で「非吸収項の畳み込み」だけを行う (Free walk 廃止)。 'CompiledLMBlock'
--- の残余版に相当。 'SExp' 保持の純データなので値 (Double) と勾配 (AD 型) の双方で
--- 共有できる ('residualValueA' が多相)。
-
--- | 残余 log-joint の非吸収項を出現順に flat 化した中間表現。
-data CompiledResidual = CompiledResidual
-  { crPriors :: ![(Text, Distribution SExp)]     -- ^ 非吸収 Sample: logDensity d (params!n)
-  , crObs    :: ![(Distribution SExp, [Double])] -- ^ 非吸収 Observe: obsLogSum d ys
-  , crPots   :: ![SExp]                          -- ^ 非吸収 Potential: 式値
-  }
-
--- | 'sUnF' の 'Floating' 一般化 (density IR 専用の 'SLgammaO' を除く — SLgammaO は
--- 'Floating SExp' インスタンス経由では現れず 'Distribution SExp' に入らない)。
-sUnG :: Floating a => SUn -> a -> a
-sUnG SNegO    = negate
-sUnG SAbsO    = abs
-sUnG SSignumO = signum
-sUnG SExpO    = exp
-sUnG SLogO    = log
-sUnG SSqrtO   = sqrt
-sUnG SRecipO  = recip
-sUnG SSinO    = sin
-sUnG SCosO    = cos
-sUnG STanO    = tan
-sUnG SAsinO   = asin
-sUnG SAcosO   = acos
-sUnG SAtanO   = atan
-sUnG SSinhO   = sinh
-sUnG SCoshO   = cosh
-sUnG STanhO   = tanh
-sUnG SAsinhO  = asinh
-sUnG SAcoshO  = acosh
-sUnG SAtanhO  = atanh
-sUnG SLgammaO = error "sUnG: SLgammaO は残余 SExp には現れない (compileResidual の不変条件)"
-
--- | 'sBinF' の 'Floating'+'Ord' 一般化。
-sBinG :: (Floating a, Ord a) => SBin -> a -> a -> a
-sBinG SAddO = (+)
-sBinG SSubO = (-)
-sBinG SMulO = (*)
-sBinG SDivO = (/)
-sBinG SMaxO = max
-
--- | 'SExp' を任意の 'Floating' 型で評価する (latent 参照は @lookupVar@ 経由)。
--- 'CompiledResidual' の per-eval 評価に使う (SExp 木の畳み込み・Free walk 無し)。
-evalSExpA :: (Floating a, Ord a) => (Text -> a) -> SExp -> a
-evalSExpA lookupVar = ev
-  where
-    ev (SC x)     = realToFrac x
-    ev (SV n)     = lookupVar n
-    ev (S1 o e)   = sUnG o (ev e)
-    ev (S2 o a b) = sBinG o (ev a) (ev b)
-
--- | 残余 (excl 吸収後) を 1 度の symbolic walk で 'CompiledResidual' に flat 化。
--- compiled 経路で忠実再現できない残余 (非吸収 'ObserveLM') があれば 'Nothing' を
--- 返し、 呼び出し側は従来の 'logJointExclBlocks' walk に fallback する。
--- 'Deterministic'/'Data' は walk 時に 'SExp' へインライン展開されるので収集式の
--- 'SV' は必ず sampled latent を指す (per-eval の params に存在)。
-compileResidual :: Set Text -> Model SExp r -> Maybe CompiledResidual
-compileResidual excl = go [] [] []
-  where
-    go ps os pots (Pure _) =
-      Just (CompiledResidual (reverse ps) (reverse os) (reverse pots))
-    go ps os pots (Free f) = case f of
-      Sample n d k
-        | n `Set.member` excl -> go ps os pots (k (SV n))
-        | otherwise           -> go ((n, d) : ps) os pots (k (SV n))
-      Observe n d ys next
-        | n `Set.member` excl -> go ps os pots next
-        | otherwise           -> go ps ((d, ys) : os) pots next
-      ObserveLM nm _ _ _ _ _ next
-        | nm `Set.member` excl -> go ps os pots next
-        | otherwise            -> Nothing   -- 非吸収 ObserveLM は flat 化不可 → fallback
-      Potential n v next
-        | n `Set.member` excl -> go ps os pots next
-        | otherwise           -> go ps os (v : pots) next
-      Deterministic _ v k -> go ps os pots (k v)
-      Data _ ys k         -> go ps os pots (k (map realToFrac ys, ys))
-      DataIx _ is k       -> go ps os pots (k is)
-      PlateBegin _ _ next -> go ps os pots next
-      PlateEnd next       -> go ps os pots next
-
--- | 'CompiledResidual' の per-eval 評価 (Free walk 無し・flat list の畳み込み)。
--- 'logJointExclBlocks excl m params' と同値 (同じ 'logDensity'/'obsLogSum'・同じ
--- params)。 sampled latent が params に無い場合は 'logJointExclBlocks' と同じく
--- -∞ (安全網)。
-residualValueA :: (Floating a, Ord a) => CompiledResidual -> Map Text a -> a
-residualValueA cr params = priorSum + obsSum + potSum
-  where
-    ev = evalSExpA (\n -> Map.findWithDefault 0 n params)
-    priorSum = sum [ case Map.lookup n params of
-                       Nothing -> negInf
-                       Just v  -> logDensity (fmap ev d) v
-                   | (n, d) <- crPriors cr ]
-    obsSum   = sum [ obsLogSum (fmap ev d) ys | (d, ys) <- crObs cr ]
-    potSum   = sum [ ev v | v <- crPots cr ]
-
--- | ベクトル式 IR。 'unifyMany' が行ごとの 'SExp' を束ねた結果で、 leaf は
--- スカラ (行に依らない) かベクトル (行ごとに値が違う) のいずれか。
-data UExp
-  = UK !Double                  -- ^ 全行同一の定数 (スカラ)
-  | UC !(VS.Vector Double)      -- ^ 行ごとの定数列 (データ列・長さ n)
-  | UV !Text                    -- ^ 全行同一の latent (スカラ・broadcast)
-  | UG ![Text] !(VU.Vector Int) -- ^ 族 gather: 行 i は member[gids_i] (長さ n)
-  | U1 !SUn UExp
-  | U2 !SBin UExp UExp
-  | USum UExp                   -- ^ Σ (ベクトル → スカラ)。 Phase 90 A10:
-                                --   raw potential 内の同型 Σ チェーンの
-                                --   ベクトル化に使う ('absorbPot')。 中身は
-                                --   行依存 ('uexpIsVec') であること
-
-instance NFData UExp where
-  rnf (UK v)     = rnf v
-  rnf (UC v)     = v `seq` ()
-  rnf (UV n)     = rnf n
-  rnf (UG ms g)  = rnf ms `seq` g `seq` ()
-  rnf (U1 o e)   = o `seq` rnf e
-  rnf (U2 o a b) = o `seq` rnf a `seq` rnf b
-  rnf (USum e)   = rnf e
-
--- | 行ごとのスカラ式を 1 本のベクトル式に持ち上げる (形状照合)。
--- 演算子木が全行同型で、 leaf が「全行 SC」「全行同一 SV」「行ごとに違う SV
--- (→ 族 gather 候補)」 のいずれかに揃う場合のみ成功。
-unifyMany :: [SExp] -> Maybe UExp
-unifyMany []          = Nothing
-unifyMany es@(e0 : _) = case e0 of
-  SC _ -> do
-    vs <- mapM (\e -> case e of SC v -> Just v; _ -> Nothing) es
-    Just $ case vs of
-      (v : rest) | all (== v) rest -> UK v
-      _                            -> UC (VS.fromList vs)
-  SV _ -> do
-    ns <- mapM (\e -> case e of SV n -> Just n; _ -> Nothing) es
-    Just $ case ns of
-      (n0 : rest) | all (== n0) rest -> UV n0
-      _ ->
-        let mems = Set.toAscList (Set.fromList ns)
-            ixm  = Map.fromList (zip mems [0 :: Int ..])
-        in UG mems (VU.fromList [ ixm Map.! n | n <- ns ])
-  S1 o _ -> do
-    cs <- mapM (\e -> case e of S1 o' c | o' == o -> Just c; _ -> Nothing) es
-    U1 o <$> unifyMany cs
-  S2 o _ _ -> do
-    ps <- mapM (\e -> case e of S2 o' a b | o' == o -> Just (a, b); _ -> Nothing) es
-    U2 o <$> unifyMany (map fst ps) <*> unifyMany (map snd ps)
-
--- | IR 中のスカラ latent 参照 (出現順・重複あり)。
-uexpScalNames :: UExp -> [Text]
-uexpScalNames (UV n)     = [n]
-uexpScalNames (U1 _ e)   = uexpScalNames e
-uexpScalNames (U2 _ a b) = uexpScalNames a ++ uexpScalNames b
-uexpScalNames (USum e)   = uexpScalNames e
-uexpScalNames _          = []
-
--- | IR 中の族 gather の member リスト (出現順・重複あり)。
-uexpFamilies :: UExp -> [[Text]]
-uexpFamilies (UG ms _)   = [ms]
-uexpFamilies (U1 _ e)    = uexpFamilies e
-uexpFamilies (U2 _ a b)  = uexpFamilies a ++ uexpFamilies b
-uexpFamilies (USum e)    = uexpFamilies e
-uexpFamilies _           = []
-
--- | 式が行依存 (ベクトル形) か ('ruIsVec' の 'UExp' 版・Phase 90 A10)。
--- 'USum' は Σ 済みなのでスカラ。
--- Phase 104: 'ruIsVec' と同じ共有無視走査の残党 (absorbPot 経路で同じ指数
--- 爆発があり得る) のため、同時に StableName memo walk 化 (詳細は 'ruIsVec')。
-uexpIsVec :: UExp -> Bool
-uexpIsVec e0 = unsafePerformIO $ do
-  memo <- newIORef IM.empty
-  let go x0 = do
-        x  <- evaluate x0
-        sn <- makeStableName x
-        let h = hashStableName sn
-        mm <- readIORef memo
-        case lookup sn =<< IM.lookup h mm of
-          Just r  -> pure r
-          Nothing -> do
-            r <- case x of
-              UC _     -> pure True
-              UG _ _   -> pure True
-              U1 _ e   -> go e
-              U2 _ a b -> (||) <$> go a <*> go b
-              _        -> pure False
-            modifyIORef' memo (IM.insertWith (++) h [(sn, r)])
-            pure r
-  go e0
-{-# NOINLINE uexpIsVec #-}
-
--- | 出現順を保つ重複排除。
-ordNubO :: Ord a => [a] -> [a]
-ordNubO = go Set.empty
-  where
-    go _ [] = []
-    go seen (x : xs)
-      | x `Set.member` seen = go seen xs
-      | otherwise           = x : go (Set.insert x seen) xs
-
--- | IR グループ (unify 後・compile 前)。 family ごとに観測密度の組み方が違う
--- (Phase 55.4 で Gaussian 限定 → Poisson / Bernoulli を追加)。
-data VecGroupSrc
-  = VGGauss !UExp !UExp !(VS.Vector Double)  -- ^ μ IR, σ IR, ys
-  | VGPois  !UExp !(VS.Vector Double)        -- ^ λ IR, ys (全行 y ≥ 0 を確認済)
-  | VGBern  !UExp !(VS.Vector Double)        -- ^ p IR, ys (全行 round y ∈ {0,1})
-  | VGStudT !Double !UExp !UExp !(VS.Vector Double)
-    -- ^ ν (SC 定数), μ IR, σ IR, ys (56.3)
-  | VGCauchy !UExp !UExp !(VS.Vector Double)  -- ^ x₀ IR, γ IR, ys (56.3)
-  | VGLogis !UExp !UExp !(VS.Vector Double)   -- ^ μ IR, s IR, ys (56.3)
-  | VGGumbel !UExp !UExp !(VS.Vector Double)  -- ^ μ IR, β IR, ys (56.3)
-  | VGExpo !UExp !(VS.Vector Double)          -- ^ rate IR, ys (全行 y ≥ 0・56.4)
-  | VGWeib !UExp !UExp !(VS.Vector Double)    -- ^ k IR, λ IR, ys (全行 y > 0・56.4)
-  | VGLogN !UExp !UExp !(VS.Vector Double)    -- ^ μ IR, σ IR, ys (全行 y > 0・56.4)
-  | VGGamma !UExp !UExp !(VS.Vector Double)   -- ^ α IR, rate IR, ys (全行 y > 0・56.4)
-  | VGBeta !UExp !UExp !(VS.Vector Double)    -- ^ α IR, β IR, ys (全行 0<y<1・56.4)
-  | VGBinom !(VS.Vector Double) !UExp !(VS.Vector Double)
-    -- ^ n 列 (行対応), p IR, ys (0≤k≤n・56.5。 Phase 94 で n を行対応 Vector 化 =
-    -- n 別の group 分裂を解消し 1 group にまとめる)
-  | VGGeom !UExp !(VS.Vector Double)          -- ^ p IR, ys (round y ≥ 1・56.5)
-  | VGNegBin !UExp !UExp !(VS.Vector Double)  -- ^ μ IR, α IR, ys (y ≥ 0・56.5)
-  | VGMixNorm2 !UExp !UExp !UExp !UExp !UExp !UExp !(VS.Vector Double)
-    -- ^ w1 IR, w2 IR, μ1 IR, σ1 IR, μ2 IR, σ2 IR, ys (Phase 90 A3・2成分限定)
-  | VGZIBinom !(VS.Vector Double) !UExp !UExp !(VS.Vector Double)
-    -- ^ n 列 (行対応), ψ IR, p IR, ys (0≤k≤n・Phase 90 A3。 Phase 94 で n を
-    -- 行対応 Vector 化)
-  | VGPot !UExp
-    -- ^ raw `potential` 項 (Phase 90 A10)。 scalar 形の UExp (内部の同型
-    -- Σ チェーンは 'USum' でベクトル化済・'absorbPot')。 値 = 式そのもの
-    -- (ys なし・guard なし = walk の 'Potential' 加算と同値)
-
-instance NFData VecGroupSrc where
-  rnf (VGGauss u sg ys)    = rnf u `seq` rnf sg `seq` ys `seq` ()
-  rnf (VGPois u ys)        = rnf u `seq` ys `seq` ()
-  rnf (VGBern u ys)        = rnf u `seq` ys `seq` ()
-  rnf (VGStudT nu u sg ys) = nu `seq` rnf u `seq` rnf sg `seq` ys `seq` ()
-  rnf (VGCauchy u sc ys)   = rnf u `seq` rnf sc `seq` ys `seq` ()
-  rnf (VGLogis u s ys)     = rnf u `seq` rnf s `seq` ys `seq` ()
-  rnf (VGGumbel u be ys)   = rnf u `seq` rnf be `seq` ys `seq` ()
-  rnf (VGExpo u ys)        = rnf u `seq` ys `seq` ()
-  rnf (VGWeib k u ys)      = rnf k `seq` rnf u `seq` ys `seq` ()
-  rnf (VGLogN u sg ys)     = rnf u `seq` rnf sg `seq` ys `seq` ()
-  rnf (VGGamma sh u ys)    = rnf sh `seq` rnf u `seq` ys `seq` ()
-  rnf (VGBeta al u ys)     = rnf al `seq` rnf u `seq` ys `seq` ()
-  rnf (VGBinom nv u ys)    = nv `seq` rnf u `seq` ys `seq` ()
-  rnf (VGGeom u ys)        = rnf u `seq` ys `seq` ()
-  rnf (VGNegBin u al ys)   = rnf u `seq` rnf al `seq` ys `seq` ()
-  rnf (VGMixNorm2 w1 w2 m1 s1 m2 s2 ys) =
-    rnf w1 `seq` rnf w2 `seq` rnf m1 `seq` rnf s1 `seq` rnf m2 `seq`
-    rnf s2 `seq` ys `seq` ()
-  rnf (VGZIBinom nv psi p ys) = nv `seq` rnf psi `seq` rnf p `seq` ys `seq` ()
-  rnf (VGPot u)              = rnf u
-
--- | 'synthVecIR' の結果: (グループ列, 族 prior (members, m, τ),
--- 吸収した scalar Observe / raw potential 名集合 = residual walk から
--- 除外すべき名前)。 σ は Phase 55.3 から 'UExp'
--- (スカラ式なら値はスカラ・UC を含む行依存式なら heteroscedastic ベクトル密度)。
-type VecIRSrc =
-  ( [VecGroupSrc]
-  , [([Text], SExp, SExp)]
-  , Set Text )
-
--- ===========================================================================
--- Phase 90 A8: 式 DAG 化 (共有保存 hash-consing) — synthVecIR 指数ハングの根治
--- ===========================================================================
---
--- 従来の合成解析 ('sexpShape'/'sexpVars'/'unifyMany'/'rnf' 等) は 'SExp' を
--- 素朴な木として walk していたが、 ユーザコードの let 共有 (RK4 等の逐次再帰で
--- 前状態を複数回参照する形) を無視すると訪問回数が「経路数」 (深さに対し指数)
--- に比例して爆発する (A6 実測: RK4 深さ5で DAG 352 ノード vs 経路 6.8×10¹¹)。
--- ここでは StableName (heap 同一性) + 構造 intern (hash-consing) で式を一度
--- だけ明示的 DAG (ノード表 + ID) に変換し、 以後の解析を全て ID ベース
--- O(distinct ノード数) で行う。 形状クラス・自由変数集合はノード生成時に
--- bottom-up で確定する (子 ID は常に親より先に intern 済み)。
-
--- | 'SExp' の DAG ノード (子は intern 済み ID)。 構造 intern のキー =
--- 「構造が等しい ⇔ ID が等しい」 が成立する ('sexpEq'/'sexpKeyNamed' の代替)。
-data SNode = NC !Double | NV !Text | N1 !SUn !Int | N2 !SBin !Int !Int
-  deriving (Eq, Ord)
-
--- | latent 名を消した形状クラスのキー ('sexpShape' の代替。 SV は全て KV に
--- 潰れる = 名前違いの行が同一形状クラスに揃い族 gather 候補になる)。
-data ShapeKey = KC | KV | K1 !SUn !Int | K2 !SBin !Int !Int
-  deriving (Eq, Ord)
-
--- | 名前付き指紋のキー ('sexpKeyNamed' の代替)。 SV は latent 名を保持・
--- **SC は値を無視して同一クラスに潰す** (行ごとに違うデータ定数だけの σ 式を
--- 1 グループに束ね、 unify が UC 列へ持ち上げる Phase 55.3 仕様)。 構造
--- intern ID ('sexpEq' 相当・定数値まで厳密) とは役割が違う点に注意。
-data NamedKey = MC | MV !Text | M1 !SUn !Int | M2 !SBin !Int !Int
-  deriving (Eq, Ord)
-
--- | intern 状態。 memo は 2 段: heap 同一性 (StableName・共有 thunk の再走査
--- 防止) と構造 ('SNode'・等価だが別 heap の部分式を同一 ID に合流)。
-data SDagSt = SDagSt
-  { sdStable :: !(IM.IntMap [(StableName SExp, Int)])
-  , sdStruct :: !(Map SNode Int)
-  , sdNodes  :: !(IM.IntMap SNode)             -- ^ ID → ノード
-  , sdShapes :: !(Map ShapeKey Int)            -- ^ 形状 intern
-  , sdShape  :: !(IM.IntMap Int)               -- ^ ID → 形状クラス ID
-  , sdNamedKs :: !(Map NamedKey Int)           -- ^ 名前付き指紋 intern
-  , sdNamed  :: !(IM.IntMap Int)               -- ^ ID → 名前付き指紋 ID
-  , sdVars   :: !(IM.IntMap (Set Text))        -- ^ ID → 自由 latent 集合
-  , sdNext   :: !Int
-  , sdUnify  :: !(Map [Int] UExp)
-    -- ^ unify memo (ID 列 → 'UExp')。 **全 group 共有** = 同一部分式列は
-    -- 同一 'UExp' heap オブジェクトに合流し、 出力も共有付き DAG になる
-    -- (garch11 のような group 跨ぎ共有が下流 'compileVecIR' の identity
-    -- memo で 1 回だけコンパイルされるために必須)。
-  }
-
-newSDag :: IO (IORef SDagSt)
-newSDag = newIORef (SDagSt IM.empty Map.empty IM.empty Map.empty
-                            IM.empty Map.empty IM.empty IM.empty 0 Map.empty)
-
-sdagNodeOf :: SDagSt -> Int -> SNode
-sdagNodeOf st i = sdNodes st IM.! i
-
-sdagShapeOf :: SDagSt -> Int -> Int
-sdagShapeOf st i = sdShape st IM.! i
-
-sdagNamedOf :: SDagSt -> Int -> Int
-sdagNamedOf st i = sdNamed st IM.! i
-
-sdagVarsOf :: SDagSt -> Int -> Set Text
-sdagVarsOf st i = sdVars st IM.! i
-
--- | 'SExp' を DAG に intern して ID を返す。 各 heap ノードの訪問は 1 回
--- (StableName memo)・poison ('symPoison' 等の error thunk) はここで顕在化
--- する ('synthVecIR' の try が捕捉する範囲内で呼ぶこと)。
-internS :: IORef SDagSt -> SExp -> IO Int
-internS ref = go
-  where
-    go e0 = do
-      e  <- evaluate e0
-      sn <- makeStableName e
-      let h = hashStableName sn
-      st <- readIORef ref
-      case lookup sn =<< IM.lookup h (sdStable st) of
-        Just i  -> pure i
-        Nothing -> do
-          nd <- case e of
-            SC v     -> pure (NC v)
-            SV n     -> pure (NV n)
-            S1 o a   -> N1 o <$> go a
-            S2 o a b -> N2 o <$> go a <*> go b
-          st1 <- readIORef ref
-          i <- case Map.lookup nd (sdStruct st1) of
-            Just j  -> pure j
-            Nothing -> do
-              let j     = sdNext st1
-                  shKey = case nd of
-                    NC _     -> KC
-                    NV _     -> KV
-                    N1 o a   -> K1 o (sdShape st1 IM.! a)
-                    N2 o a b -> K2 o (sdShape st1 IM.! a) (sdShape st1 IM.! b)
-                  (shId, shapes') = case Map.lookup shKey (sdShapes st1) of
-                    Just s  -> (s, sdShapes st1)
-                    Nothing -> let s = Map.size (sdShapes st1)
-                               in (s, Map.insert shKey s (sdShapes st1))
-                  nmKey = case nd of
-                    NC _     -> MC
-                    NV n     -> MV n
-                    N1 o a   -> M1 o (sdNamed st1 IM.! a)
-                    N2 o a b -> M2 o (sdNamed st1 IM.! a) (sdNamed st1 IM.! b)
-                  (nmId, nameds') = case Map.lookup nmKey (sdNamedKs st1) of
-                    Just s  -> (s, sdNamedKs st1)
-                    Nothing -> let s = Map.size (sdNamedKs st1)
-                               in (s, Map.insert nmKey s (sdNamedKs st1))
-                  vs = case nd of
-                    NC _     -> Set.empty
-                    NV n     -> Set.singleton n
-                    N1 _ a   -> sdVars st1 IM.! a
-                    N2 _ a b -> (sdVars st1 IM.! a) `Set.union` (sdVars st1 IM.! b)
-              writeIORef ref st1
-                { sdStruct = Map.insert nd j (sdStruct st1)
-                , sdNodes  = IM.insert j nd (sdNodes st1)
-                , sdShapes = shapes'
-                , sdShape  = IM.insert j shId (sdShape st1)
-                , sdNamedKs = nameds'
-                , sdNamed  = IM.insert j nmId (sdNamed st1)
-                , sdVars   = IM.insert j vs (sdVars st1)
-                , sdNext   = j + 1 }
-              pure j
-          modifyIORef' ref $ \s ->
-            s { sdStable = IM.insertWith (++) h [(sn, i)] (sdStable s) }
-          pure i
-
--- | 'unifyMany' の DAG 版: 行ごとの ID で lockstep 再帰し、 位置 (= ID 列)
--- ごとに結果 'UExp' を memo する。 同一 ID 列は同一 'UExp' オブジェクトに
--- 合流するので出力も共有付き DAG (leaf 判定・失敗条件は 'unifyMany' と同一)。
-unifyManyD :: IORef SDagSt -> [SExp] -> IO (Maybe UExp)
-unifyManyD ref es = mapM (internS ref) es >>= goIds
-  where
-    goIds [] = pure Nothing
-    goIds is = do
-      st <- readIORef ref
-      case Map.lookup is (sdUnify st) of
-        Just u  -> pure (Just u)
-        Nothing -> do
-          mu <- case map (sdagNodeOf st) is of
-            nds@(NC _ : _) -> pure $ do
-              vs <- mapM (\n -> case n of NC v -> Just v; _ -> Nothing) nds
-              Just $ case vs of
-                (v : rest) | all (== v) rest -> UK v
-                _                            -> UC (VS.fromList vs)
-            nds@(NV _ : _) -> pure $ do
-              ns <- mapM (\n -> case n of NV nm -> Just nm; _ -> Nothing) nds
-              Just $ case ns of
-                (n0 : rest) | all (== n0) rest -> UV n0
-                _ ->
-                  let mems = Set.toAscList (Set.fromList ns)
-                      ixm  = Map.fromList (zip mems [0 :: Int ..])
-                  in UG mems (VU.fromList [ ixm Map.! n | n <- ns ])
-            nds@(N1 o _ : _) ->
-              case mapM (\n -> case n of N1 o' c | o' == o -> Just c
-                                         _                 -> Nothing) nds of
-                Nothing -> pure Nothing
-                Just cs -> fmap (U1 o) <$> goIds cs
-            nds@(N2 o _ _ : _) ->
-              case mapM (\n -> case n of N2 o' a b | o' == o -> Just (a, b)
-                                         _                   -> Nothing) nds of
-                Nothing -> pure Nothing
-                Just ps -> do
-                  ma <- goIds (map fst ps)
-                  case ma of
-                    Nothing -> pure Nothing
-                    Just ua -> fmap (U2 o ua) <$> goIds (map snd ps)
-            [] -> pure Nothing
-          case mu of
-            Nothing -> pure Nothing
-            Just u  -> do
-              u' <- evaluate u
-              modifyIORef' ref $ \s -> s { sdUnify = Map.insert is u' (sdUnify s) }
-              pure (Just u')
-
--- | 'UExp' の scalar leaf 名と族 gather member リストを**初出順**で収集する
--- ('uexpScalNames'/'uexpFamilies' の共有保存版)。 memo (visited 集合) を
--- IORef で外から渡し、 複数式・複数 group を跨いで 1 本の memo で走る =
--- 共有部分式は 1 回だけ訪問。 収集結果を 'ordNubO' に掛ける用途では
--- スキップされた再訪問分は重複除去されるだけなので結果は木 walk と一致する。
-uexpLeavesIO :: IORef (IM.IntMap [StableName UExp]) -> UExp
-             -> IO ([Text], [[Text]])
-uexpLeavesIO seenRef = go
-  where
-    go u0 = do
-      u  <- evaluate u0
-      sn <- makeStableName u
-      let h = hashStableName sn
-      seen <- readIORef seenRef
-      if maybe False (elem sn) (IM.lookup h seen)
-        then pure ([], [])
-        else do
-          modifyIORef' seenRef (IM.insertWith (++) h [sn])
-          case u of
-            UK _     -> pure ([], [])
-            UC _     -> pure ([], [])
-            UV n     -> pure ([n], [])
-            UG ms _  -> pure ([], [ms])
-            U1 _ e   -> go e
-            U2 _ a b -> do
-              (s1, f1) <- go a
-              (s2, f2) <- go b
-              pure (s1 ++ s2, f1 ++ f2)
-            USum e   -> go e
-
--- | 'absorbPot' が 'USum' 化を試みる加算チェーンの最小項数 (Phase 90 A10)。
--- これ未満の和はスカラ 'U2' 連鎖のまま持つ (コスト無視できる規模)。
-potSumThreshold :: Int
-potSumThreshold = 8
-
--- | Phase 90 A10: raw `potential` 式を scalar 'UExp' へ吸収する。
--- 大きな同型加算チェーン (項数 ≥ 'potSumThreshold') は 'unifyManyD' で
--- ベクトル化して 'USum' へ落とす (チェーン中の定数項は畳んで加算)。
--- 吸収できない構造 (unify 失敗・行依存にならない縮退 Σ 等) は Nothing =
--- その potential ごと残差 ad に残す (安全方向・値は walk と同値のまま)。
--- 走査は StableName memo で共有保存 (A8 の教訓: 素朴な木 walk は共有式で
--- 指数爆発)。
-absorbPot :: IORef SDagSt
-          -> IORef (IM.IntMap [(StableName SExp, Maybe UExp)])
-          -> SExp -> IO (Maybe UExp)
-absorbPot ref memoRef = go
-  where
-    go e0 = do
-      e  <- evaluate e0
-      sn <- makeStableName e
-      let h = hashStableName sn
-      mm <- readIORef memoRef
-      case lookup sn =<< IM.lookup h mm of
-        Just r  -> pure r
-        Nothing -> do
-          r <- build e
-          modifyIORef' memoRef (IM.insertWith (++) h [(sn, r)])
-          pure r
-    build e = case e of
-      SC v -> pure (Just (UK v))
-      SV n -> pure (Just (UV n))
-      S2 SAddO _ _ -> do
-        terms <- flat e []
-        let (cs, ts) = foldr part (0, []) terms
-            part t (c, acc) = case t of
-              SC v -> (c + v, acc)
-              _    -> (c, t : acc)
-        if length ts >= potSumThreshold
-          then do
-            mu <- unifyManyD ref ts
-            case mu of
-              Just u | uexpIsVec u ->
-                pure (Just (if cs == 0 then USum u
-                            else U2 SAddO (USum u) (UK cs)))
-              -- 巨大チェーンをスカラ連鎖のまま素通しすると compile 側が
-              -- 肥大するため、 unify 不能なら吸収ごと断念 (残差 ad へ)。
-              _ -> pure Nothing
-          else bin e
-      S1 o a -> fmap (U1 o) <$> go a
-      S2 {}  -> bin e
-    bin (S2 o a b) = do
-      ma <- go a
-      case ma of
-        Nothing -> pure Nothing
-        Just ua -> fmap (U2 o ua) <$> go b
-    bin _ = pure Nothing
-    -- 加算 spine の平坦化 (foldl 'sum' 由来の深い左スパイン・O(項数))。
-    flat e0 acc = do
-      e <- evaluate e0
-      case e of
-        S2 SAddO a b -> flat a =<< flat b acc
-        _            -> pure (e : acc)
-
--- | Phase 54.11: per-obs 手書き scalar 'Observe' 群から「ベクトル式 IR」 を
--- **自動合成**する ('synthGaussLMBlocks' の非線形版)。 検出できない / 安全網に
--- 掛かった場合は 'Nothing' (従来経路に fallback)。
---
--- 安全網 2 段 (54.8 と同じ): ① 'SExp' の Eq/Ord は非定数比較で error poison →
--- 'unsafePerformIO' + 'try' で捕捉し全体 fallback (poison は 'internS' の
--- 走査中に顕在化する)。 async 例外 (timeout / Ctrl-C 等) は fallback にせず
--- **透過** (Phase 90 A6: 飲み込むとハングの中断が「fallback」に誤報告される
--- ことを実測確認)。 ② IR の値評価 (観測尤度 + 族 prior) を probe 2 点で
--- walk 評価 ('obsOnlySum' + 'priorOnlySum') と突合し、 不一致なら fallback。
-synthVecIR :: ModelP r -> Maybe VecIRSrc
-synthVecIR m = unsafePerformIO $ do
-  r <- try (synthVecIRWalkIO m)
-  case r :: Either SomeException VecIRSrc of
-    Left e
-      | Just (SomeAsyncException _) <- fromException e -> throwIO e
-      | otherwise -> pure Nothing
-    Right v@(gs, _, _)
-      | null gs          -> pure Nothing
-      | vecIRProbeOK m v -> pure (Just v)
-      | otherwise        -> pure Nothing
-{-# NOINLINE synthVecIR #-}
-
--- | 互換 wrapper (旧 pure 版と同じ表面)。 内部は 'synthVecIRWalkIO'。
-synthVecIRWalk :: ModelP r -> VecIRSrc
-synthVecIRWalk = unsafePerformIO . synthVecIRWalkIO
-{-# NOINLINE synthVecIRWalk #-}
-
--- | 'synthVecIR' の合成部 (walk + 形状照合 + 族抽出)。 Phase 90 A8 で共有保存
--- DAG ('internS'/'unifyManyD') ベースに全面改修 — 解析は全て ID 経由
--- O(distinct ノード数) で、 RK4 のような深い自己参照式でも指数爆発しない。
--- 照合に失敗した σ グループは丸ごと残す (residual ad に fallback・安全方向)。
--- 結果の式部分は構築時に正格化済み (旧実装の「呼出側が force」 は不要 —
--- 共有 DAG に rnf を掛けると経路数比例で逆に爆発するため**禁止**)。
-synthVecIRWalkIO :: ModelP r -> IO VecIRSrc
-synthVecIRWalkIO m = do
-  let (rows, priors) = collectSymRows m
-  ref      <- newSDag
-  leafSeen <- newIORef IM.empty
-      -- 族条件: 全 member の prior が構造同一の Normal(m, τ) で、 m/τ が member
-      -- 自身を参照しない (ベクトル化密度 -nG·logτ - Σ(a_j-m)²/(2τ²) が成立する形)。
-      -- 構造同一判定 ('sexpEq' 相当) は intern ID の等値。
-  let famOf ms = case mapM (`Map.lookup` priors) ms of
-        Just ds@(Normal m0 t0 : _) -> do
-          i0 <- internS ref m0
-          j0 <- internS ref t0
-          oks <- forM ds $ \d -> case d of
-            Normal mm tt -> do
-              im <- internS ref mm
-              jt <- internS ref tt
-              pure (im == i0 && jt == j0)
-            _ -> pure False
-          st <- readIORef ref
-          pure $ if and oks
-                    && Set.null ((sdagVarsOf st i0 `Set.union` sdagVarsOf st j0)
-                                 `Set.intersection` Set.fromList ms)
-                 then Just (ms, m0, t0) else Nothing
-        _ -> pure Nothing
-      -- IO 上の Maybe 連結 (MaybeT 相当の局所定義・unify 失敗の短絡用)。
-      mIO >>=? k = mIO >>= maybe (pure Nothing) k
-      okIf cond g = pure (if cond then Just g else Nothing)
-      -- family 別の unify + 観測値の妥当性 (値 guard を walk と一致させるため、
-      -- 観測値側の guard に掛かる行を含むグループは吸収しない = walk が -∞ を
-      -- 返す縮退ケースをそのまま残す安全方向)。
-      tryGroup grows = do
-        let ysV = VS.fromList [ y | (_, _, y) <- grows ]
-        mg <- case [ d | (_, d, _) <- grows ] of
-          ds@(SDGauss{} : _) ->
-            unifyManyD ref [ mu | SDGauss mu _ <- ds ] >>=? \u ->
-            unifyManyD ref [ sg | SDGauss _ sg <- ds ] >>=? \sgU ->
-            pure (Just (VGGauss u sgU ysV))
-          ds@(SDPois{} : _) ->
-            unifyManyD ref [ lam | SDPois lam <- ds ] >>=? \u ->
-            okIf (VS.all (>= 0) ysV) (VGPois u ysV)
-          ds@(SDBern{} : _) ->
-            unifyManyD ref [ p | SDBern p <- ds ] >>=? \u ->
-            okIf (VS.all (\y -> let k = round y :: Int in k == 0 || k == 1) ysV)
-                 (VGBern u ysV)
-          ds@(SDStudT nu _ _ : _) ->
-            unifyManyD ref [ mu | SDStudT _ mu _ <- ds ] >>=? \u ->
-            unifyManyD ref [ sg | SDStudT _ _ sg <- ds ] >>=? \sgU ->
-            pure (Just (VGStudT nu u sgU ysV))
-          ds@(SDCauchy{} : _) ->
-            unifyManyD ref [ loc | SDCauchy loc _ <- ds ] >>=? \u ->
-            unifyManyD ref [ sc | SDCauchy _ sc <- ds ] >>=? \scU ->
-            pure (Just (VGCauchy u scU ysV))
-          ds@(SDLogis{} : _) ->
-            unifyManyD ref [ mu | SDLogis mu _ <- ds ] >>=? \u ->
-            unifyManyD ref [ s | SDLogis _ s <- ds ] >>=? \sU ->
-            pure (Just (VGLogis u sU ysV))
-          ds@(SDGumbel{} : _) ->
-            unifyManyD ref [ mu | SDGumbel mu _ <- ds ] >>=? \u ->
-            unifyManyD ref [ be | SDGumbel _ be <- ds ] >>=? \beU ->
-            pure (Just (VGGumbel u beU ysV))
-          ds@(SDExpo{} : _) ->
-            unifyManyD ref [ rate | SDExpo rate <- ds ] >>=? \u ->
-            okIf (VS.all (>= 0) ysV) (VGExpo u ysV)
-          ds@(SDWeib{} : _) ->
-            unifyManyD ref [ k | SDWeib k _ <- ds ] >>=? \kU ->
-            unifyManyD ref [ lam | SDWeib _ lam <- ds ] >>=? \u ->
-            okIf (VS.all (> 0) ysV) (VGWeib kU u ysV)
-          ds@(SDLogN{} : _) ->
-            unifyManyD ref [ mu | SDLogN mu _ <- ds ] >>=? \u ->
-            unifyManyD ref [ sg | SDLogN _ sg <- ds ] >>=? \sgU ->
-            okIf (VS.all (> 0) ysV) (VGLogN u sgU ysV)
-          ds@(SDGamma{} : _) ->
-            unifyManyD ref [ sh | SDGamma sh _ <- ds ] >>=? \shU ->
-            unifyManyD ref [ rt | SDGamma _ rt <- ds ] >>=? \u ->
-            okIf (VS.all (> 0) ysV) (VGGamma shU u ysV)
-          ds@(SDBeta{} : _) ->
-            unifyManyD ref [ al | SDBeta al _ <- ds ] >>=? \alU ->
-            unifyManyD ref [ be | SDBeta _ be <- ds ] >>=? \u ->
-            okIf (VS.all (\y -> y > 0 && y < 1) ysV) (VGBeta alU u ysV)
-          ds@(SDBinom{} : _) ->
-            unifyManyD ref [ p | SDBinom _ p <- ds ] >>=? \u ->
-            let nsV = VS.fromList [ fromIntegral n | SDBinom n _ <- ds ]
-                -- Phase 94: n を行対応化したので、 各行を自分の n で域内判定
-                -- (旧: 先頭行の n を全行に流用 = merge 前提が単一 n だった)。
-                domOk = and [ let k = round y :: Int in k >= 0 && k <= round nn
-                            | (nn, y) <- zip (VS.toList nsV) (VS.toList ysV) ]
-            in okIf domOk (VGBinom nsV u ysV)
-          ds@(SDGeom{} : _) ->
-            unifyManyD ref [ p | SDGeom p <- ds ] >>=? \u ->
-            okIf (VS.all (\y -> (round y :: Int) >= 1) ysV) (VGGeom u ysV)
-          ds@(SDNegBin{} : _) ->
-            unifyManyD ref [ mu | SDNegBin mu _ <- ds ] >>=? \u ->
-            unifyManyD ref [ al | SDNegBin _ al <- ds ] >>=? \alU ->
-            okIf (VS.all (>= 0) ysV) (VGNegBin u alU ysV)
-          ds@(SDMixNorm2{} : _) ->
-            unifyManyD ref [ w1 | SDMixNorm2 w1 _ _ _ _ _ <- ds ] >>=? \w1U ->
-            unifyManyD ref [ w2 | SDMixNorm2 _ w2 _ _ _ _ <- ds ] >>=? \w2U ->
-            unifyManyD ref [ m1 | SDMixNorm2 _ _ m1 _ _ _ <- ds ] >>=? \m1U ->
-            unifyManyD ref [ s1 | SDMixNorm2 _ _ _ s1 _ _ <- ds ] >>=? \s1U ->
-            unifyManyD ref [ m2 | SDMixNorm2 _ _ _ _ m2 _ <- ds ] >>=? \m2U ->
-            unifyManyD ref [ s2 | SDMixNorm2 _ _ _ _ _ s2 <- ds ] >>=? \s2U ->
-            pure (Just (VGMixNorm2 w1U w2U m1U s1U m2U s2U ysV))
-          ds@(SDZIBinom{} : _) ->
-            unifyManyD ref [ psi | SDZIBinom _ psi _ <- ds ] >>=? \psiU ->
-            unifyManyD ref [ p | SDZIBinom _ _ p <- ds ] >>=? \pU ->
-            let nsV = VS.fromList [ fromIntegral n | SDZIBinom n _ _ <- ds ]
-                domOk = and [ let k = round y :: Int in k >= 0 && k <= round nn
-                            | (nn, y) <- zip (VS.toList nsV) (VS.toList ysV) ]
-            in okIf domOk (VGZIBinom nsV psiU pU ysV)
-          [] -> pure Nothing
-        case mg of
-          Nothing -> pure Nothing
-          Just g  -> do
-            -- Phase 90 A5: family absorb (prior のベクトル化) は likelihood 側の
-            -- vecIR 吸収と独立の最適化。 famOf に失敗した family は fams から
-            -- 単に除外し (absorb しない)、 その prior は既存の `constPriorsOf`
-            -- (`Gradient.hs`) 経由で扱わせる。 leaf 収集の memo (leafSeen) は
-            -- group 跨ぎ共有 — スキップされた再訪問分の family は前の group が
-            -- 同一 (ms, m0, τ0) を famsAll に登録済みなので結果は不変。
-            famLs <- concatMap snd <$> mapM (uexpLeavesIO leafSeen) (vgExprAll g)
-            famRs <- mapM famOf (ordNubO famLs)
-            let fams = [ f | Just f <- famRs ]
-            pure (Just (g, fams, Set.fromList [ nm | (nm, _, _) <- grows ]))
-      -- Phase 55.2-56.3 のグループキー (family タグ + σ 名前付き指紋 + μ 形状)
-      -- を DAG の ID で表現: 名前付き指紋 = 構造 intern ID ('sexpKeyNamed' と
-      -- 同値)、 形状 = 形状クラス ID ('sexpShape' と同値)。 String 指紋は
-      -- 長さが式の展開サイズ (= 経路数) 比例で指数爆発するため廃止 (A6)。
-      key tag named shaped = do
-        nids <- mapM (internS ref) named
-        si   <- internS ref shaped
-        st   <- readIORef ref
-        pure (tag :: String, map (sdagNamedOf st) nids, sdagShapeOf st si)
-      keyOf d = case d of
-        SDGauss mu sg    -> key "g" [sg] mu
-        SDPois  lam      -> key "p" [] lam
-        SDBern  p        -> key "b" [] p
-        SDStudT nu mu sg -> key ("t:" ++ show nu) [sg] mu
-        SDCauchy loc sc  -> key "cy" [sc] loc
-        SDLogis mu s     -> key "lg" [s] mu
-        SDGumbel mu be   -> key "gb" [be] mu
-        SDExpo rate      -> key "e" [] rate
-        SDWeib k lam     -> key "w" [k] lam
-        SDLogN mu sg     -> key "ln" [sg] mu
-        SDGamma sh rt    -> key "ga" [sh] rt
-        SDBeta al be     -> key "be" [al] be
-        SDBinom _ p      -> key "bi:" [] p   -- Phase 94: n を key から除外 (行対応 Vector 化で 1 group に merge)
-        SDGeom p         -> key "ge" [] p
-        SDNegBin mu al   -> key "nb" [al] mu
-        SDMixNorm2 w1 w2 m1 s1 m2 s2 -> key "mx2" [w1, w2, s1, m2, s2] m1
-        SDZIBinom _ psi p -> key "zb:" [psi] p   -- Phase 94: n を key から除外
-  rowsK <- forM rows $ \r@(_, d, _) -> (,) r <$> keyOf d
-  let gkeys = ordNubO (map snd rowsK)
-  cands <- concat <$> forM gkeys (\gk -> do
-             mc <- tryGroup [ r | (r, gk') <- rowsK, gk' == gk ]
-             pure (maybe [] (: []) mc))
-  -- Phase 90 A10: raw potential の吸収。 吸収成功した potential は
-  -- 'VGPot' グループ + 吸収名集合 (第3成分) に合流する。 吸収できない
-  -- potential はここに現れない = 従来どおり残差 ad が担う。
-  -- ★potential の gather は族の**部分集合** member list になり得る
-  -- (icar の node1/node2 等) ため、 leaf family を famOf に掛けると
-  -- Observe 群由来の全体族と member が重複し disjoint チェックで全体
-  -- fallback してしまう。 potential 側では族 prior 吸収を行わない —
-  -- 族に吸収されなかった latent の prior は 'constPriorsOf'
-  -- (`Gradient.hs`) が per-scalar 解析勾配で拾うので残差ゼロは保たれる。
-  potMemo <- newIORef IM.empty
-  potRs <- forM (collectSymPots m) $ \(nm, e) -> do
-    mu <- absorbPot ref potMemo e
-    pure (fmap ((,) nm) mu)
-  let pots = [ p | Just p <- potRs ]
-  let famsAll = Map.toList (Map.fromList
-                  [ (ms, (mx, tx)) | (_, fs, _) <- cands, (ms, mx, tx) <- fs ])
-      famNames = concatMap fst famsAll
-      disjoint = length famNames == Set.size (Set.fromList famNames)
-  evaluate $ if not disjoint
-    then ([], [], Set.empty)   -- 族 member が重複 (二重計上の危険) → 全体 fallback
-    else ( [ g | (g, _, _) <- cands ] ++ [ VGPot u | (_, u) <- pots ]
-         , [ (ms, mx, tx) | (ms, (mx, tx)) <- famsAll ]
-         , Set.unions [ obs | (_, _, obs) <- cands ]
-           `Set.union` Set.fromList (map fst pots) )
-
--- | グループ中の全 'UExp' フィールド (出現順)。 Phase 90 A3: 従来の
--- 'vgExpr1'/'vgExpr2' (最大2フィールド限定) を、 Mixture (6フィールド) 等
--- 任意個数のフィールドを持つ family にも対応できる形に一般化した
--- (呼び出し側 'compileVecIR' の scalNames/vecLists 収集は 1 パスに統合)。
-vgExprAll :: VecGroupSrc -> [UExp]
-vgExprAll (VGGauss u sg _)      = [u, sg]
-vgExprAll (VGPois u _)          = [u]
-vgExprAll (VGBern u _)          = [u]
-vgExprAll (VGStudT _ u sg _)    = [u, sg]
-vgExprAll (VGCauchy u sc _)     = [u, sc]
-vgExprAll (VGLogis u s _)       = [u, s]
-vgExprAll (VGGumbel u be _)     = [u, be]
-vgExprAll (VGExpo u _)          = [u]
-vgExprAll (VGWeib k u _)        = [u, k]
-vgExprAll (VGLogN u sg _)       = [u, sg]
-vgExprAll (VGGamma sh u _)      = [u, sh]
-vgExprAll (VGBeta al u _)       = [u, al]
-vgExprAll (VGBinom _ u _)       = [u]
-vgExprAll (VGGeom u _)          = [u]
-vgExprAll (VGNegBin u al _)     = [u, al]
-vgExprAll (VGMixNorm2 w1 w2 m1 s1 m2 s2 _) = [w1, w2, m1, s1, m2, s2]
-vgExprAll (VGZIBinom _ psi p _) = [psi, p]
-vgExprAll (VGPot u)             = [u]
-
--- | グループ中の族 gather member リスト (全 'UExp' フィールドから)。
-vgFamilies :: VecGroupSrc -> [[Text]]
-vgFamilies = concatMap uexpFamilies . vgExprAll
-
--- | 名前が @sel@ に含まれる raw 'Potential' の値**だけ**を足す walk
--- (Phase 90 A10 probe 用・'obsOnlySum' の potential 版)。
-potOnlySum :: Set Text -> Model Double r -> Map Text Double -> Double
-potOnlySum sel model params = go model 0
-  where
-    go (Pure _) acc = acc
-    go (Free (Sample n _ k)) acc = go (k (Map.findWithDefault 0 n params)) acc
-    go (Free (Observe _ _ _ next)) acc = go next acc
-    go (Free (ObserveLM _ _ _ _ _ _ next)) acc = go next acc
-    go (Free (Potential n v next)) acc
-      | n `Set.member` sel = go next (acc + v)
-      | otherwise          = go next acc
-    go (Free (Deterministic _ v k)) acc = go (k v) acc
-    go (Free (Data _ ys k)) acc = go (k (ys, ys)) acc
-    go (Free (DataIx _ is k)) acc = go (k is) acc
-    go (Free (PlateBegin _ _ next)) acc = go next acc
-    go (Free (PlateEnd next)) acc = go next acc
-
--- | 名前が @sel@ に含まれる 'Sample' の prior log-density **だけ**を足す walk
--- (Phase 54.11 probe 用・'obsOnlySum' の prior 版)。
-priorOnlySum :: Set Text -> Model Double r -> Map Text Double -> Double
-priorOnlySum sel model params = go model 0
-  where
-    go (Pure _) acc = acc
-    go (Free (Sample n d k)) acc =
-      let v = Map.findWithDefault 0 n params
-      in go (k v) (if n `Set.member` sel then acc + logDensity d v else acc)
-    go (Free (Observe _ _ _ next)) acc = go next acc
-    go (Free (ObserveLM _ _ _ _ _ _ next)) acc = go next acc
-    go (Free (Potential _ _ next)) acc = go next acc
-    go (Free (Deterministic _ v k)) acc = go (k v) acc
-    go (Free (Data _ ys k)) acc = go (k (ys, ys)) acc
-    go (Free (DataIx _ is k)) acc = go (k is) acc
-    go (Free (PlateBegin _ _ next)) acc = go next acc
-    go (Free (PlateEnd next)) acc = go next acc
-
--- | 安全網② (Phase 54.11): IR の値 (観測尤度 + 族 prior) を、 元 model の
--- walk 評価と probe 2 点で突合する (54.8 'synthProbeOK' と同じ流儀。
--- probe 値は per-param に変えて係数取り違えも検出・全 latent 正値で guard 安全)。
-vecIRProbeOK :: ModelP r -> VecIRSrc -> Bool
-vecIRProbeOK m (gs, fams, obsNames) = all check [(0.5, 0.07), (1.3, 0.11)]
-  where
-    names  = sampleNames m
-    -- 各 latent の prior 分布 → 制約変換種別。 probe 点を **その latent の台**
-    -- (正値 / 単位区間) に写すため。 素の base+step は有界台の latent
-    -- (Beta 等) で域外になり sqrt(1-pc²) 等が NaN 化 → 誤 fallback していた
-    -- (Phase 80.2)。 'fromUnconstrained' で恒等 / exp / sigmoid を通し常に域内。
-    --
-    -- Phase 90 A3: @base + step·i@ の @i@ は全 latent 通し番号なので、
-    -- 個体ごとの random effect 等で latent 数が多い階層モデル (M=385 等) では
-    -- 添字の大きい latent の probe 値が発散する (例: i=387 → 27.6)。
-    -- unconstrained (Normal 等・変換なし) latent は exp/log の非線形演算を
-    -- 経由すると `exp(-eps)` アンダーフロー → `log(1-p) = -Infinity` →
-    -- `ref - syn = -Inf - (-Inf) = NaN` で誤って probe 不一致 (05-mh で実測
-    -- 発覚)。 通し番号を法 16 で折り返し、 添字が多くても probe 値の広がりを
-    -- 一定に保つ (元の「係数取り違え検出のため異なる値を使う」意図は
-    -- 16 通りの相異なる値で十分に保たれる)。
-    (_, priors) = collectSymRows m
-    trOf n = maybe UnconstrainedT distToTransform (Map.lookup n priors)
-    ixOf   = Map.fromList (zip names [0 ..])
-    cvi    = compileVecIR ixOf gs fams
-    famSet = Set.fromList (concat [ ms | (ms, _, _) <- fams ])
-    check (base, step) =
-      let pm = Map.fromList
-                 [ (n, fromUnconstrained (trOf n) (base + step * fromIntegral (i `mod` 16)))
-                 | (n, i) <- zip names [0 :: Int ..] ]
-          pc = VS.fromList [ pm Map.! n | n <- names ]
-          ref = obsOnlySum obsNames m pm + priorOnlySum famSet m pm
-                + potOnlySum obsNames m pm   -- 吸収済み raw potential (A10)
-          syn = vecIRValue cvi pc
-      in abs (ref - syn) <= 1e-9 * (1 + abs ref)
-
--- | index 解決済みのベクトル式 IR ノード。 latent 参照は leaf **位置**
--- ('cvScalIx' / 'cvVecIxs' の添字) に解決済み (per-call の Text lookup なし)。
-data RUExp
-  = RUK !Double
-  | RUC !(VS.Vector Double)
-  | RUV !Int                    -- ^ scalar leaf 位置
-  | RUG !Int !(VU.Vector Int)   -- ^ vector leaf 位置 + gids (gather・長さ = 行数)
-  | RUVec !Int                  -- ^ vector leaf そのもの (族 prior 用・Phase 56.2)
-  | RU1 !SUn RUExp
-  | RU2 !SBin RUExp RUExp
-  | RUSum RUExp                 -- ^ Σ (ベクトル → スカラ・Phase 56.2)
-  deriving (Eq, Ord)            -- ^ compile 時 hash-consing (CSE) 用
-
--- | 式が行依存 (ベクトル形) か (compile 時に静的に決まる)。
--- Phase 104: 素朴な構造再帰は共有 DAG 上で**経路数**に比例して走り、
--- garch11 の σ 逐次再帰 (sPrev² = 2 参照 × T 段 → Σ2^t 経路) で指数ハング
--- した (prof 99.8% time・entries 2^30)。 StableName memo walk で
--- O(distinct)/呼出に是正 ('compileVecIR' と同流儀・引数決定的なので参照透過)。
-ruIsVec :: RUExp -> Bool
-ruIsVec e0 = unsafePerformIO $ do
-  memo <- newIORef IM.empty
-  let go x0 = do
-        x  <- evaluate x0
-        sn <- makeStableName x
-        let h = hashStableName sn
-        mm <- readIORef memo
-        case lookup sn =<< IM.lookup h mm of
-          Just r  -> pure r
-          Nothing -> do
-            r <- case x of
-              RUC{}     -> pure True
-              RUG{}     -> pure True
-              RUVec{}   -> pure True
-              RU1 _ e   -> go e
-              RU2 _ a b -> (||) <$> go a <*> go b
-              _         -> pure False
-            modifyIORef' memo (IM.insertWith (++) h [(sn, r)])
-            pure r
-  go e0
-{-# NOINLINE ruIsVec #-}
-
-infixl 6 .+#, .-#
-infixl 7 .*#, ./#
--- | 密度 IR 構築用の局所演算子 (Phase 56.2・export しない)。
--- Phase 85.3: 恒等演算 (x·1 / x+0 / x-0 / x÷1) は構築時に畳む =
--- 'ru2Smart'。 μ 合成 (designHBMProgram) が汎用に作る @0 + coef·x@ 連鎖が
--- radon で 919 セル級ベクトル命令 20 本中 6 本 (~29%) を占めると 85.1 prof の
--- 命令列 dump で実測されたため。 x·0→0 は IEEE 非保存 (x=Inf/NaN で NaN) ゆえ
--- 畳まない。
-(.+#), (.-#), (.*#), (./#) :: RUExp -> RUExp -> RUExp
-(.+#) = ru2Smart SAddO
-(.-#) = ru2Smart SSubO
-(.*#) = ru2Smart SMulO
-(./#) = ru2Smart SDivO
-
--- | 'RU2' の恒等演算畳み込み smart constructor (Phase 85.3)。
--- 定数同士は即値化 (SExp の 'sc2' と同じ流儀)。
-ru2Smart :: SBin -> RUExp -> RUExp -> RUExp
-ru2Smart o (RUK a) (RUK b) = RUK (sBinF o a b)
-ru2Smart SAddO (RUK 0) b = b
-ru2Smart SAddO a (RUK 0) = a
-ru2Smart SSubO a (RUK 0) = a
-ru2Smart SMulO (RUK 1) b = b
-ru2Smart SMulO a (RUK 1) = a
-ru2Smart SDivO a (RUK 1) = a
-ru2Smart o a b = RU2 o a b
-
--- | 'RU1' の smart constructor (Phase 90 A11-4②・命令融合)。
---
---   * 定数は即値化 ('ru2Smart' と同流儀)。
---   * **@log(exp x) → x@ の代数畳み込み** (F3b): GLM log-link で観測密度が
---     @Σ y·log(λ)@・@λ = exp(η)@ を組むと @log(exp η)@ の往復が 1 命令
---     (観測長ぶんの `SLogO` pass + その backward) として残る。 これを恒等に
---     畳んで η を直接使う (`Σ y·η − exp η` = Poisson の log-link 標準形)。
---     数学的に厳密な恒等 (exp は常に正・log(exp x)=x)。 FP では ulp 差が出る
---     ため **draws は変わる** (回帰判定は PyMC 事後突合・A11-5 と別 gate)。
---   ⚠ @exp(log x) → x@ は x>0 でしか成立せず (log(負)=NaN) 一般には不正 =
---     畳まない。 log∘exp のみ。
-ru1Smart :: SUn -> RUExp -> RUExp
-ru1Smart o (RUK a)          = RUK (sUnF o a)
-ru1Smart SLogO (RU1 SExpO x) = x
-ru1Smart o e                = RU1 o e
-
--- | Phase 54.11 の前処理済み IR ('CompiledLMBlock' の IR 版)。 compile 時に
--- 1 度だけ作り、 per-call は leaf 値の差し替え + tape/値評価のみ。
--- | index 解決 + 観測値由来の定数前計算済みのグループ (Phase 55.4)。
-data VecObsIR
-  = VOGauss !RUExp !RUExp !(VS.Vector Double)
-    -- ^ (μ IR, σ IR, ys)。 σ IR が行依存 (RUC を含む) なら heteroscedastic
-    -- ベクトル密度 (Phase 55.3)
-  | VOPois !RUExp !(VS.Vector Double) !Double
-    -- ^ (λ IR, ys, Σ log y_i! 前計算)。 logp = Σ(y_i·logλ_i - λ_i) - Σlog y_i!
-    -- (y は定数なので factorial 項は compile 時前計算・勾配に寄与しない)
-  | VOBern !RUExp !(VS.Vector Double)
-    -- ^ (p IR, yb)。 yb = round 済 0/1 列。 logp = Σ(yb_i·log p_i +
-    -- (1-yb_i)·log(1-p_i)) ('logDensityObs' の round 分岐を係数化)
-  | VOStudT !Double !RUExp !RUExp !(VS.Vector Double)
-    -- ^ (ν 定数, μ IR, σ IR, ys)。 lgamma 項は ν=SC なので compile 時定数
-    -- (56.3。 'lgammaApprox' で walk と完全一致)
-  | VOCauchy !RUExp !RUExp !(VS.Vector Double)
-    -- ^ (x₀ IR, γ IR, ys)。 logp = -n·logπ - Σlogγ - Σlog(1+z_i²) (56.3)
-  | VOLogis !RUExp !RUExp !(VS.Vector Double)
-    -- ^ (μ IR, s IR, ys)。 logp = -Σz_i - Σlog s - 2·Σlog(1+exp(-z_i)) (56.3)
-  | VOGumbel !RUExp !RUExp !(VS.Vector Double)
-    -- ^ (μ IR, β IR, ys)。 logp = -Σlog β - Σz_i - Σexp(-z_i) (56.3)
-  | VOExpo !RUExp !(VS.Vector Double)
-    -- ^ (rate IR, ys)。 logp = Σlog rate_i - Σ rate_i·y_i (56.4)
-  | VOWeib !RUExp !RUExp !(VS.Vector Double)
-    -- ^ (k IR, λ IR, log ys 前計算)。 (y/λ)^k は exp(k·(log y - log λ)) で
-    -- 初等化 (`**` と ulp 差のみ・56.4)
-  | VOLogN !RUExp !RUExp !(VS.Vector Double) !Double
-    -- ^ (μ IR, σ IR, log ys 前計算, -Σlog y 定数)。 密度 = Gaussian ノード
-    -- ('VOGauss' の densityIR) 再利用 + 定数 (56.4 計画どおり)
-  | VOGamma !RUExp !RUExp !(VS.Vector Double) !(VS.Vector Double)
-    -- ^ (α IR, rate IR, ys, log ys 前計算)。 lgammaΓ(α) は 'SLgammaO'
-    -- (値 lgammaApprox / 導関数 lgammaApproxDeriv・56.4 初使用)
-  | VOBeta !RUExp !RUExp !(VS.Vector Double) !(VS.Vector Double)
-    -- ^ (α IR, β IR, log ys, log (1-ys) 前計算)。 56.4
-  | VOBinom !RUExp !(VS.Vector Double) !(VS.Vector Double) !Double
-    -- ^ (p IR, k 列 (raw y・walk の kA と一致), n-k 列, Σ logC(n,round y) 定数)。
-    -- Bernoulli 式の係数一般化 (56.5)
-  | VOGeom !RUExp !(VS.Vector Double)
-    -- ^ (p IR, k 列 (raw y))。 logp = Σ(k_i-1)·log(1-p_i) + Σlog p_i (56.5)
-  | VONegBin !RUExp !RUExp !(VS.Vector Double) !Double
-    -- ^ (μ IR, α IR, k 列 (raw y), Σ lgammaΓ(k_i+1) 定数)。 lgammaΓ(k_i+α) は
-    -- 'SLgammaO' の elementwise 適用 (56.5 本命)
-  | VOPot !RUExp
-    -- ^ raw `potential` 項 (Phase 90 A10)。 scalar 形 (内部 Σ は 'RUSum')。
-    -- logp 寄与 = 式の値そのもの・guard なし (walk の 'Potential' 加算と同値)
-  | VOMixNorm2 !RUExp !RUExp !RUExp !RUExp !RUExp !RUExp !(VS.Vector Double)
-    -- ^ (w1 IR, w2 IR, μ1 IR, σ1 IR, μ2 IR, σ2 IR, ys)。 Phase 90 A3・
-    -- 2成分 Normal 混合限定。 logp_i = logsumexp(logw1-logtotal+lpdf1_i,
-    -- logw2-logtotal+lpdf2_i) ('Distribution.hs' の Mixture と数式一致)
-  | VOZIBinom !(VS.Vector Double) !RUExp !RUExp !(VS.Vector Double) !(VS.Vector Double)
-              !(VS.Vector Double) !(VS.Vector Double)
-    -- ^ (n 列 (行対応・Phase 94), ψ IR, p IR, mask0 列 (y=0なら1), y 列 (raw), n-y 列,
-    -- logC(n,y) 列)。 Phase 90 A3。 y==0/y>0 の分岐は compile 時に mask 列へ
-    -- 落とし、 両方の分岐式を全行 elementwise に計算してから mask で選択
-    -- (group 分割はしない — family gather の disjoint 検査を壊さない安全設計)
-
-data CompiledVecIR = CompiledVecIR
-  { cvProg   :: !VecProgram
-    -- ^ 値 + 勾配の静的命令列 (compile 時 1 回生成・Phase 56.2 = per-call の
-    -- tape 構築を撤去し「tape を compile 時に固定」)
-  , cvScalIx :: !(VU.Vector Int)
-    -- ^ scalar leaf → param index
-  , cvVecIxs :: ![VU.Vector Int]
-    -- ^ vector leaf → member param indices
-  }
-
-
--- | 'VecIRSrc' を param index に解決する (静的・1 回)。 Phase 90 A8:
--- 'UExp'/'SExp' → 'RUExp' の変換と leaf 収集を identity memo (StableName) で
--- 共有保存し、 命令列生成は intern 済み DAG ('RUNode') 上で行う (旧実装は
--- 全て共有無視の木 walk + 'Map' 構造キーの CSE = 深い共有式で指数)。 表面は
--- pure のまま ('Gradient.hs' の呼出互換・引数に対し決定的なので参照透過)。
-compileVecIR
-  :: Map Text Int
-  -> [VecGroupSrc] -> [([Text], SExp, SExp)]
-  -> CompiledVecIR
-compileVecIR ixOf gs fams = unsafePerformIO (compileVecIRIO ixOf gs fams)
-{-# NOINLINE compileVecIR #-}
-
-compileVecIRIO
-  :: Map Text Int
-  -> [VecGroupSrc] -> [([Text], SExp, SExp)]
-  -> IO CompiledVecIR
-compileVecIRIO ixOf gs fams = do
-  -- leaf 収集 (初出順・memo は全式共有 = ordNubO 後の結果は旧木 walk と同一)
-  seenRef   <- newIORef IM.empty
-  leafPairs <- mapM (uexpLeavesIO seenRef) [ e | g <- gs, e <- vgExprAll g ]
-  sdag <- newSDag
-  famVars <- forM fams $ \(_, mx, tx) -> do
-    im <- internS sdag mx
-    it <- internS sdag tx
-    st <- readIORef sdag
-    pure (Set.toList (sdagVarsOf st im `Set.union` sdagVarsOf st it))
-  let scalNames = ordNubO (concatMap fst leafPairs ++ concat famVars)
-      vecLists  = ordNubO (concatMap snd leafPairs
-                           ++ [ ms | (ms, _, _) <- fams ])
-      sPos = Map.fromList (zip scalNames [0 :: Int ..])
-      vPos = Map.fromList (zip vecLists [0 :: Int ..])
-  -- UExp/SExp → RUExp (identity memo で共有保存・'ru2Smart' の畳みは従来どおり)
-  rUMemo <- newIORef IM.empty
-  rSMemo <- newIORef IM.empty
-  let rU u0 = do
-        u  <- evaluate u0
-        sn <- makeStableName u
-        let h = hashStableName sn
-        mm <- readIORef rUMemo
-        case lookup sn =<< IM.lookup h mm of
-          Just r  -> pure r
-          Nothing -> do
-            r <- case u of
-              UK v       -> pure (RUK v)
-              UC v       -> pure (RUC v)
-              UV n       -> pure (RUV (sPos Map.! n))
-              UG ms gids -> pure (RUG (vPos Map.! ms) gids)
-              U1 o e     -> RU1 o <$> rU e
-              U2 o a b   -> ru2Smart o <$> rU a <*> rU b
-              USum e     -> RUSum <$> rU e
-            r' <- evaluate r
-            modifyIORef' rUMemo (IM.insertWith (++) h [(sn, r')])
-            pure r'
-      rS e0 = do
-        e  <- evaluate e0
-        sn <- makeStableName e
-        let h = hashStableName sn
-        mm <- readIORef rSMemo
-        case lookup sn =<< IM.lookup h mm of
-          Just r  -> pure r
-          Nothing -> do
-            r <- case e of
-              SC v     -> pure (RUK v)
-              SV n     -> pure (RUV (sPos Map.! n))
-              S1 o x   -> RU1 o <$> rS x
-              S2 o a b -> ru2Smart o <$> rS a <*> rS b
-            r' <- evaluate r
-            modifyIORef' rSMemo (IM.insertWith (++) h [(sn, r')])
-            pure r'
-      cgOf g = case g of
-        VGGauss u sg ys -> VOGauss <$> rU u <*> rU sg <*> pure ys
-        VGPois u ys ->
-          (\r -> VOPois r ys
-             (VS.sum (VS.map (logFactorial . (round :: Double -> Int)) ys)))
-          <$> rU u
-        VGBern u ys ->
-          (\r -> VOBern r (VS.map (\y -> fromIntegral (round y :: Int)) ys))
-          <$> rU u
-        VGStudT nu u sg ys -> VOStudT nu <$> rU u <*> rU sg <*> pure ys
-        VGCauchy u sc ys -> VOCauchy <$> rU u <*> rU sc <*> pure ys
-        VGLogis u s ys -> VOLogis <$> rU u <*> rU s <*> pure ys
-        VGGumbel u be ys -> VOGumbel <$> rU u <*> rU be <*> pure ys
-        VGExpo u ys -> (\r -> VOExpo r ys) <$> rU u
-        VGWeib k u ys ->
-          (\rk r -> VOWeib rk r (VS.map log ys)) <$> rU k <*> rU u
-        VGLogN u sg ys ->
-          let lys = VS.map log ys
-          in (\r rsg -> VOLogN r rsg lys (negate (VS.sum lys)))
-             <$> rU u <*> rU sg
-        VGGamma sh u ys ->
-          (\rsh r -> VOGamma rsh r ys (VS.map log ys)) <$> rU sh <*> rU u
-        VGBeta al u ys ->
-          (\ral r -> VOBeta ral r (VS.map log ys)
-                       (VS.map (\y -> log (1 - y)) ys))
-          <$> rU al <*> rU u
-        VGBinom nv u ys ->
-          (\r -> VOBinom r ys (VS.zipWith (-) nv ys)
-             (VS.sum (VS.zipWith (\nn y -> logBinomCoeff (round nn) (round y)) nv ys)))
-          <$> rU u
-        VGGeom u ys -> (\r -> VOGeom r ys) <$> rU u
-        VGNegBin u al ys ->
-          (\r ral -> VONegBin r ral ys
-             (VS.sum (VS.map (\y -> lgammaApprox (y + 1)) ys)))
-          <$> rU u <*> rU al
-        VGMixNorm2 w1 w2 m1 s1 m2 s2 ys ->
-          (\a b c d e f -> VOMixNorm2 a b c d e f ys)
-          <$> rU w1 <*> rU w2 <*> rU m1 <*> rU s1 <*> rU m2 <*> rU s2
-        VGZIBinom nv psi p ys ->
-          (\rpsi rp -> VOZIBinom nv rpsi rp
-             (VS.map (\y -> if round y == (0 :: Int) then 1 else 0) ys)
-             ys
-             (VS.zipWith (-) nv ys)
-             (VS.zipWith (\nn y -> logBinomCoeff (round nn) (round y)) nv ys))
-          <$> rU psi <*> rU p
-        VGPot u -> VOPot <$> rU u
-  gd <- map densityIR <$> mapM cgOf gs
-  fd <- forM fams $ \(ms, mx, tx) ->
-          famDensityIR (vPos Map.! ms) (length ms) <$> rS mx <*> rS tx
-  let obj  = foldl1 (RU2 SAddO) (map fst gd ++ map fst fd)
-      grds = concatMap snd gd ++ concatMap snd fd
-  -- RUExp → intern 済み DAG → 命令列 (構造 intern が旧 CSE cache と同じ
-  -- 重複排除を構造キー比較なしで与える)
-  rud  <- newRUDag
-  k0   <- internRU rud (RUK 0)
-  objI <- internRU rud obj
-  gIs  <- forM grds $ \(k, ge) -> (,) k <$> internRU rud ge
-  st   <- readIORef rud
-  pure CompiledVecIR
-    { cvProg   = compileVecProgramD (map length vecLists) (rudNodes st)
-                                    k0 objI gIs
-    , cvScalIx = VU.fromList [ ixOf Map.! n | n <- scalNames ]
-    , cvVecIxs = [ VU.fromList [ ixOf Map.! n | n <- ms ] | ms <- vecLists ]
-    }
-
--- ---------------------------------------------------------------------------
--- Phase 56.2: 観測密度の IR 式化 + 静的命令列 (記号 reverse-mode・arena 実行)
--- ---------------------------------------------------------------------------
-
--- | 値側 guard の種別 (勾配側は unguarded・54.11 の前例どおり)。
-data GuardKind = GPos | GUnit
-
--- | 数値安定な 2 項 log-sum-exp: log(exp a + exp b) = max(a,b) + log(1+exp(-|a-b|))。
--- Phase 90 A3: Mixture (log_mix)・ZeroInflatedBinomial の x=0 分岐で使う。
--- 'SMaxO' (勾配は winner-take-all subgradient) を経由するので、 常に有限差分
--- (|a-b| は必ず ≥0) のみ exp する = オーバーフロー安全。
-logSumExp2 :: RUExp -> RUExp -> RUExp
-logSumExp2 a b =
-  RU2 SMaxO a b .+# RU1 SLogO (RUK 1 .+# RU1 SExpO (RU1 SNegO (RU1 SAbsO (a .-# b))))
-
--- | family 別の観測密度を IR 式として組む (Phase 56.2)。 式・guard とも
--- 'logDensityObs' の該当分岐と値一致 (test/probe で担保)。 旧 groupVal /
--- groupNode (手書き tape ノード) の置換 — 勾配は記号微分で自動。
-densityIR :: VecObsIR -> (RUExp, [(GuardKind, RUExp)])
-densityIR g = case g of
-  -- raw potential (Phase 90 A10): 式値そのまま・guard なし。
-  VOPot re -> (re, [])
-  -- Gaussian: -n/2·log2π - (n·logσ + Σr²/(2σ²)) (σ スカラ) /
-  --           -n/2·log2π - Σlogσ_i - Σ(r_i/σ_i)²/2 (σ 行依存・55.3)
-  VOGauss mu sge ys ->
-    let n' = fromIntegral (VS.length ys) :: Double
-        c0 = RUK (negate (0.5 * n' * log (2 * pi)))
-        r  = RUC ys .-# mu
-    in if ruIsVec sge
-       then ( c0 .-# RUSum (RU1 SLogO sge)
-                 .-# (RUSum (let t = r ./# sge in t .*# t) ./# RUK 2)
-            , [(GPos, sge)] )
-       else ( c0 .-# (RUK n' .*# RU1 SLogO sge)
-                 .-# (RUSum (r .*# r) ./# (RUK 2 .*# sge .*# sge))
-            , [(GPos, sge)] )
-  -- Poisson: Σ(y_i·logλ_i - λ_i) - Σlog y_i! (lfk 前計算・y は raw = kA 一致)
-  VOPois lam ys lfk ->
-    let n' = fromIntegral (VS.length ys) :: Double
-        -- Phase 90 A11-4② (F3b): log(exp η) を η に畳む (log-link 標準形)。
-        logLam = ru1Smart SLogO lam
-    in if ruIsVec lam
-       then ( RUSum (RUC ys .*# logLam) .-# RUSum lam .-# RUK lfk
-            , [(GPos, lam)] )
-       else ( (RUK (VS.sum ys) .*# logLam)
-                .-# (RUK n' .*# lam) .-# RUK lfk
-            , [(GPos, lam)] )
-  -- Bernoulli: Σ(yb·log p + (1-yb)·log(1-p)) (yb = round 済 0/1 定数)
-  VOBern p yb ->
-    let n' = fromIntegral (VS.length yb) :: Double
-        c1 = VS.sum yb
-        omp = RUK 1 .-# p
-    in if ruIsVec p
-       then ( RUSum (RUC yb .*# RU1 SLogO p)
-                .+# RUSum (RUC (VS.map (1 -) yb) .*# RU1 SLogO omp)
-            , [(GUnit, p)] )
-       else ( (RUK c1 .*# RU1 SLogO p) .+# (RUK (n' - c1) .*# RU1 SLogO omp)
-            , [(GUnit, p)] )
-  -- StudentT (ν=SC・56.3): n·[lgamma((ν+1)/2) - lgamma(ν/2) - ½log(νπ)]
-  --   - Σlogσ - ((ν+1)/2)·Σ log(1 + z_i²/ν)。 ν≤0 は収集時に排除済。
-  VOStudT nu mu sge ys ->
-    let n' = fromIntegral (VS.length ys) :: Double
-        c0 = RUK (n' * (lgammaApprox ((nu + 1) / 2) - lgammaApprox (nu / 2)
-                        - 0.5 * log (nu * pi)))
-        z  = zOf mu sge ys
-    in ( c0 .-# sumLogScale (VS.length ys) sge
-            .-# (RUK ((nu + 1) / 2)
-                   .*# RUSum (RU1 SLogO (RUK 1 .+# (z .*# z ./# RUK nu))))
-       , [(GPos, sge)] )
-  -- Cauchy (56.3): -n·logπ - Σlogγ - Σ log(1 + z_i²)
-  VOCauchy loc sce ys ->
-    let n' = fromIntegral (VS.length ys) :: Double
-        z  = zOf loc sce ys
-    in ( RUK (negate (n' * log pi)) .-# sumLogScale (VS.length ys) sce
-            .-# RUSum (RU1 SLogO (RUK 1 .+# z .*# z))
-       , [(GPos, sce)] )
-  -- Logistic (56.3): -Σz_i - Σlog s - 2·Σ log(1 + exp(-z_i))
-  VOLogis mu se ys ->
-    let z = zOf mu se ys
-    in ( RU1 SNegO (RUSum z) .-# sumLogScale (VS.length ys) se
-            .-# (RUK 2
-                   .*# RUSum (RU1 SLogO (RUK 1 .+# RU1 SExpO (RU1 SNegO z))))
-       , [(GPos, se)] )
-  -- Gumbel (56.3): -Σlog β - Σz_i - Σ exp(-z_i)
-  VOGumbel mu bee ys ->
-    let z = zOf mu bee ys
-    in ( RU1 SNegO (sumLogScale (VS.length ys) bee)
-            .-# RUSum z .-# RUSum (RU1 SExpO (RU1 SNegO z))
-       , [(GPos, bee)] )
-  -- Exponential (56.4): Σ log rate_i - Σ rate_i·y_i (y ≥ 0 は収集時確認済)
-  VOExpo rate ys ->
-    ( sumLogScale (VS.length ys) rate
-        .-# (if ruIsVec rate then RUSum (rate .*# RUC ys)
-             else rate .*# RUK (VS.sum ys))
-    , [(GPos, rate)] )
-  -- Weibull (56.4): Σlog k - Σlog λ + Σ(k-1)·z_i - Σ exp(k·z_i)
-  -- (z_i = log y_i - log λ_i。 walk の (y/λ)**k と ulp 差のみ)
-  VOWeib k lam lys ->
-    let n = VS.length lys
-        z = RUC lys .-# RU1 SLogO lam
-    in ( sumLogScale n k .-# sumLogScale n lam
-            .+# RUSum ((k .-# RUK 1) .*# z)
-            .-# RUSum (RU1 SExpO (k .*# z))
-       , [(GPos, k), (GPos, lam)] )
-  -- LogNormal (56.4): N(log y⃗ | μ, σ) - Σlog y (Gaussian ノード再利用・guard も流用)
-  VOLogN mu sge lys c ->
-    let (e, gds) = densityIR (VOGauss mu sge lys)
-    in (RUK c .+# e, gds)
-  -- Gamma (56.4): Σ(α-1)·log y - Σ rate·y + Σ α·log rate - Σ lgammaΓ(α)
-  VOGamma al rate ys lys ->
-    let n = VS.length ys
-    in ( RUSum ((al .-# RUK 1) .*# RUC lys)
-            .-# (if ruIsVec rate then RUSum (rate .*# RUC ys)
-                 else rate .*# RUK (VS.sum ys))
-            .+# sumOf n (al .*# RU1 SLogO rate)
-            .-# sumOf n (RU1 SLgammaO al)
-       , [(GPos, al), (GPos, rate)] )
-  -- Beta (56.4): Σ(α-1)·log y + Σ(β-1)·log(1-y) - Σ[lgΓα + lgΓβ - lgΓ(α+β)]
-  VOBeta al be lys l1ys ->
-    let n = VS.length lys
-    in ( RUSum ((al .-# RUK 1) .*# RUC lys)
-            .+# RUSum ((be .-# RUK 1) .*# RUC l1ys)
-            .-# sumOf n (RU1 SLgammaO al .+# RU1 SLgammaO be
-                           .-# RU1 SLgammaO (al .+# be))
-       , [(GPos, al), (GPos, be)] )
-  -- Binomial (56.5): ΣlogC + Σk_i·log p_i + Σ(n-k_i)·log(1-p_i)
-  -- (Bernoulli の yb/1-yb 係数を k/n-k に一般化・logC は compile 時定数)
-  VOBinom p kv nkv lc ->
-    let omp = RUK 1 .-# p
-    in if ruIsVec p
-       then ( RUK lc .+# RUSum (RUC kv .*# RU1 SLogO p)
-                .+# RUSum (RUC nkv .*# RU1 SLogO omp)
-            , [(GUnit, p)] )
-       else ( RUK lc .+# (RUK (VS.sum kv) .*# RU1 SLogO p)
-                .+# (RUK (VS.sum nkv) .*# RU1 SLogO omp)
-            , [(GUnit, p)] )
-  -- Geometric (56.5): Σ(k_i-1)·log(1-p_i) + Σlog p_i (round y ≥ 1 は収集時確認済)
-  VOGeom p kv ->
-    let n' = fromIntegral (VS.length kv) :: Double
-        omp = RUK 1 .-# p
-    in if ruIsVec p
-       then ( RUSum ((RUC kv .-# RUK 1) .*# RU1 SLogO omp)
-                .+# RUSum (RU1 SLogO p)
-            , [(GUnit, p)] )
-       else ( (RUK (VS.sum kv - n') .*# RU1 SLogO omp)
-                .+# (RUK n' .*# RU1 SLogO p)
-            , [(GUnit, p)] )
-  -- NegativeBinomial (56.5 本命): p = α/(α+μ) として
-  -- Σ lgΓ(k_i+α) - Σ lgΓ(α) - Σ lgΓ(k_i+1) + Σ α·log p_i + Σ k_i·log(1-p_i)
-  -- (lgΓ(k_i+α) は SLgammaO の elementwise・lgΓ(k_i+1) は compile 時定数)
-  VONegBin mu al kv lgk1 ->
-    let n = VS.length kv
-        p = al ./# (al .+# mu)
-    in ( RUSum (RU1 SLgammaO (RUC kv .+# al))
-            .-# sumOf n (RU1 SLgammaO al)
-            .-# RUK lgk1
-            .+# sumOf n (al .*# RU1 SLogO p)
-            .+# RUSum (RUC kv .*# RU1 SLogO (RUK 1 .-# p))
-       , [(GPos, mu), (GPos, al)] )
-  -- Mixture (Phase 90 A3・2成分 Normal 限定): 'Distribution.hs' の
-  -- @logDensity (Mixture ws comps) x = logSumExpA [log(w_k/Σw)+logDensity d_k x]@
-  -- と数式一致 (w1+w2=1 を仮定せず Σw で正規化)。 各成分の Gaussian 対数密度
-  -- (per-row) を 'gaussLpdfElem' で作り、 'logSumExp2' で数値安定に合成。
-  VOMixNorm2 w1 w2 m1 s1 m2 s2 ys ->
-    let total  = w1 .+# w2
-        logw1  = RU1 SLogO w1 .-# RU1 SLogO total
-        logw2  = RU1 SLogO w2 .-# RU1 SLogO total
-        lpdf1  = gaussLpdfElem m1 s1 ys
-        lpdf2  = gaussLpdfElem m2 s2 ys
-        perRow = logSumExp2 (logw1 .+# lpdf1) (logw2 .+# lpdf2)
-    in ( RUSum perRow
-       , [(GPos, w1), (GPos, w2), (GPos, s1), (GPos, s2)] )
-  -- ZeroInflatedBinomial (Phase 90 A3): 'Distribution.hs' の
-  -- @logDensity (ZeroInflatedBinomial n psi p) x@ と数式一致。 y==0/y>0 の
-  -- データ分岐は group を分けず mask 列で elementwise に選択する (family
-  -- gather の disjoint 検査を壊さない安全設計・A1 調査で確定した方針)。
-  -- branch0 は「もしこの行が y=0 だったら」の仮想値を **行ごとの n** (nv) で
-  -- 計算する (nmy=n-y_i は y=0 行以外で n と異なるため使えず、 専用の n 列 nv を
-  -- 使う。 Phase 94 で n を行対応化 = n 別 group 分裂を解消)。
-  VOZIBinom nv psi p mask0 yv nmy logc ->
-    let omp     = RUK 1 .-# p
-        ompsi   = RUK 1 .-# psi
-        branch0 = logSumExp2 (RU1 SLogO psi)
-                    (RU1 SLogO ompsi .+# (RUC nv .*# RU1 SLogO omp))
-        branch1 = RU1 SLogO ompsi .+# RUC logc
-                    .+# (RUC yv .*# RU1 SLogO p) .+# (RUC nmy .*# RU1 SLogO omp)
-        perRow  = (RUC mask0 .*# branch0) .+# ((RUK 1 .-# RUC mask0) .*# branch1)
-    in ( RUSum perRow, [(GUnit, psi), (GUnit, p)] )
-  where
-    -- 位置-尺度系の共通形 (56.3): z⃗ = (y⃗ - μ)/s (y⃗ は RUC 定数なので常にベクトル形)
-    zOf mu sge ys = (RUC ys .-# mu) ./# sge
-    -- Σ e: e がスカラ式なら n·e に畳む (走査回避・56.4 で一般化)
-    sumOf n e
-      | ruIsVec e = RUSum e
-      | otherwise = RUK (fromIntegral n) .*# e
-    -- Σ log s (スカラ時 n·log s・Gauss の 55.3 と同型)
-    sumLogScale n sge = sumOf n (RU1 SLogO sge)
-    -- Phase 90 A3 (Mixture 用): Gaussian 対数密度の **行ごとの値**
-    -- (-0.5·log2π - logσ - (y-μ)²/(2σ²))。 'VOGauss' の densityIR と違い
-    -- ここでは Σ を取らずベクトルのまま返す (logSumExp2 で行ごとに混合してから
-    -- 最後に 1 回だけ Σ する必要があるため)。
-    gaussLpdfElem mu sge ys =
-      let r = RUC ys .-# mu
-      in RUK (negate (0.5 * log (2 * pi))) .-# RU1 SLogO sge
-             .-# ((r .*# r) ./# (RUK 2 .*# sge .*# sge))
-
--- | 族 prior 密度の IR 式: -nG/2·log2π - nG·logτ - Σ(a_j-m)²/(2τ²)。
-famDensityIR :: Int -> Int -> RUExp -> RUExp -> (RUExp, [(GuardKind, RUExp)])
-famDensityIR vp nG mx tx =
-  let nG' = fromIntegral nG :: Double
-      c0  = RUK (negate (0.5 * nG' * log (2 * pi)))
-      ra  = RUVec vp .-# mx
-  in ( c0 .-# (RUK nG' .*# RU1 SLogO tx)
-          .-# (RUSum (ra .*# ra) ./# (RUK 2 .*# tx .*# tx))
-     , [(GPos, tx)] )
-
--- | 静的命令列の 1 命令。 slot i = 命令 i の結果 (SSA/ANF・共有保存)。
--- 全 slot の形 (スカラ / 長さ n) は compile 時に確定し、 1 本の unboxed arena に
--- オフセット解決して敷き詰める (boxed 中間表現なし)。
-data VInstr
-  = VIK !Double                          -- ^ スカラ定数
-  | VIKV !(VS.Vector Double)             -- ^ ベクトル定数 (データ列)
-  | VILeafS !Int                         -- ^ scalar leaf p の値
-  | VILeafV !Int                         -- ^ vector leaf p (member 列そのもの)
-  | VIGath !Int !(VU.Vector Int) !Int    -- ^ gather (vector leaf p, gids, 行数)
-  | VIUn !SUn !Int
-  | VIBin !SBin !Int !Int                -- ^ broadcast は形 (静的) で解決
-  | VISum !Int                           -- ^ Σ (ベクトル → スカラ)
-  -- Phase 85.3-ii: superinstruction (radon 命令列 dump 由来の頻出パターンを
-  -- compile 時に融合。 pass 数と中間 slot を削減 = 85.3a spike の融合利得)
-  | VIAxpy !Int !Int !Int                -- ^ out = a + s·v (a: slot・len 0 は
-                                         --   broadcast、 s: スカラ slot、 v: ベクトル slot)
-  | VIAxpyC !Int !Int !(VS.Vector Double)
-                                         -- ^ 同上・v がデータ列定数 (VIKV copy 消滅)
-  | VISumSqD !Int !Int                   -- ^ out(スカラ) = Σ (x_j − m_j)²
-                                         --   (x/m: slot・スカラ側は broadcast)
-  | VISumSqC !(VS.Vector Double) !Int    -- ^ 同上・x がデータ列定数
-  -- Phase 85.3-iv: RE 連鎖の gather 内蔵化 + 3 項融合 (radon 残 pass の削減)
-  | VIMulG !Int !Int !(VU.Vector Int) !Int
-                                         -- ^ out = s·gather(p) (s: スカラ slot・
-                                         --   gather は VIGath と同形で命令内蔵 =
-                                         --   gather の実体化 pass 消滅)
-  | VIAxpyG !Int !Int !Int !(VU.Vector Int) !Int
-                                         -- ^ out = a + s·gather(p)
-  | VIMulVC !Int !Int !(VS.Vector Double)
-                                         -- ^ out = s·v⊙c (スカラ×ベクトル×データ列定数)
-  | VISumSqC2 !(VS.Vector Double) !Int !Int
-                                         -- ^ out(スカラ) = Σ (c_j − m1_j − m2_j)²
-                                         --   (m1/m2: ベクトル slot・和の実体化 pass 消滅)
-  | VISumSqDGG !Int !(VU.Vector Int) !Int !(VU.Vector Int) !Int
-                                         -- ^ Phase 90 A11-4② (F2): out(スカラ) =
-                                         --   Σ (φ[px·gx_j] − φ[pm·gm_j])²。 gather 2 本を
-                                         --   SumSqD に内蔵 (ICAR ペア差分・5461 セルの
-                                         --   gather 実体化 2 本を消す)。 gather 値は pc
-                                         --   から直読み・随伴は param へ直 scatter
-
--- | compile 済みの値+勾配プログラム (Phase 56.2)。 生成は 1 回・per-call は
--- forward (値) / forward+backward (勾配) の実行のみ = per-call の tape 構築を
--- 撤去し「tape を compile 時に固定」。 arena は per-call 確保 (共有 mutable
--- なし = 'nutsChainsPure' の spark 並列と整合)。
-data VecProgram = VecProgram
-  { vpInstrs  :: !(BV.Vector VInstr)
-  , vpOff     :: !(VU.Vector Int)        -- ^ slot → arena オフセット
-  , vpLen     :: !(VU.Vector Int)        -- ^ slot → 0 (スカラ) / n (ベクトル)
-  , vpSize    :: !Int                    -- ^ arena 総長
-  , vpObj     :: !Int                    -- ^ 目的 (log-density 和) の slot
-  , vpGuards  :: ![(GuardKind, Int)]     -- ^ 値側 guard (slot 参照)
-  }
-
--- ===========================================================================
--- Phase 90 A8: 'RUExp' の DAG intern + 共有保存の命令列生成
--- ===========================================================================
-
--- | 'RUExp' の DAG ノード (子は intern 済み ID)。
-data RUNode
-  = RNK !Double
-  | RNC !(VS.Vector Double)
-  | RNV !Int
-  | RNG !Int !(VU.Vector Int)
-  | RNVec !Int
-  | RN1 !SUn !Int
-  | RN2 !SBin !Int !Int
-  | RNSum !Int
-  deriving (Eq, Ord)
-
-data RUDagSt = RUDagSt
-  { rudStable :: !(IM.IntMap [(StableName RUExp, Int)])
-  , rudStruct :: !(Map RUNode Int)
-  , rudNodes  :: !(IM.IntMap RUNode)
-  , rudNext   :: !Int
-  }
-
-newRUDag :: IO (IORef RUDagSt)
-newRUDag = newIORef (RUDagSt IM.empty Map.empty IM.empty 0)
-
--- | 'RUExp' を DAG に intern して ID を返す ('internS' の RUExp 版)。
--- 構造 intern が旧 'compileVecProgram' の @Map RUExp Int@ CSE と同じ重複排除を
--- 与える (旧実装はキー比較が構造 walk = 共有木で経路数比例、 こちらは
--- 子 ID 比較のみで O(ノード数 · log))。
-internRU :: IORef RUDagSt -> RUExp -> IO Int
-internRU ref = go
-  where
-    go e0 = do
-      e  <- evaluate e0
-      sn <- makeStableName e
-      let h = hashStableName sn
-      st <- readIORef ref
-      case lookup sn =<< IM.lookup h (rudStable st) of
-        Just i  -> pure i
-        Nothing -> do
-          nd <- case e of
-            RUK v      -> pure (RNK v)
-            RUC v      -> pure (RNC v)
-            RUV p      -> pure (RNV p)
-            RUG p gids -> pure (RNG p gids)
-            RUVec p    -> pure (RNVec p)
-            RU1 o x    -> RN1 o <$> go x
-            RU2 o a b  -> RN2 o <$> go a <*> go b
-            RUSum x    -> RNSum <$> go x
-          st1 <- readIORef ref
-          i <- case Map.lookup nd (rudStruct st1) of
-            Just j  -> pure j
-            Nothing -> do
-              let j = rudNext st1
-              writeIORef ref st1
-                { rudStruct = Map.insert nd j (rudStruct st1)
-                , rudNodes  = IM.insert j nd (rudNodes st1)
-                , rudNext   = j + 1 }
-              pure j
-          modifyIORef' ref $ \s ->
-            s { rudStable = IM.insertWith (++) h [(sn, i)] (rudStable s) }
-          pure i
-
--- | 'compileVecProgram' の DAG 版 (Phase 90 A8)。 ノードは intern 済み ID で
--- 参照し、 CSE cache は ID → slot の 'IM.IntMap'。 superinstruction 融合
--- (85.3-ii/iv) の構造判定は ID 経由の 1 段 lookup。 意味は旧実装と同一 —
--- 「構造等値 ⇔ ID 等値」 が intern で保証されるため、 Σ(x−m)² 融合の
--- @r1 == r2@ も ID 比較で厳密に旧構造比較と一致する。
-compileVecProgramD
-  :: [Int]              -- ^ vector leaf 長
-  -> IM.IntMap RUNode   -- ^ ID → ノード (子 ID < 親 ID)
-  -> Int                -- ^ @RUK 0@ の ID (Σx² 融合で m 側が無い時の代用)
-  -> Int                -- ^ 目的 (log-density 和) root ID
-  -> [(GuardKind, Int)] -- ^ guard root ID
-  -> VecProgram
-compileVecProgramD vecLens nodes k0 objI guardIs =
-  let nodeOf i = nodes IM.! i
-      isVecA = IM.foldlWithKey'
-        (\mp i nd -> IM.insert i
-           (case nd of
-              RNC{}     -> True
-              RNG{}     -> True
-              RNVec{}   -> True
-              RN1 _ a   -> mp IM.! a
-              RN2 _ a b -> (mp IM.! a) || (mp IM.! b)
-              _         -> False) mp)
-        IM.empty nodes
-      isVec i = isVecA IM.! i
-      emit ins l i (cache, acc, lens, n) =
-        (n, (IM.insert i n cache, ins : acc, BV.snoc lens l, n + 1 :: Int))
-      -- Phase 85.3-ii: a + s·v (AXPY) の融合対象判定。
-      mulSV i = case nodeOf i of
-        RN2 SMulO p q
-          | not (isVec p), isVec q -> Just (p, q)
-          | isVec p, not (isVec q) -> Just (q, p)
-        _ -> Nothing
-      axpyMatch a b = case mulSV b of
-        Just (se, ve) -> Just (a, se, ve)
-        Nothing       -> case mulSV a of
-          Just (se, ve) -> Just (b, se, ve)
-          Nothing       -> Nothing
-      -- Phase 85.3-iv: スカラ × gather (VIMulG 判定)
-      mulSG x y = case (nodeOf x, nodeOf y) of
-        (_, RNG p gids) | not (isVec x) -> Just (x, p, gids)
-        (RNG p gids, _) | not (isVec y) -> Just (y, p, gids)
-        _ -> Nothing
-      -- Phase 85.3-iv: (スカラ×ベクトル) ⊙ データ列定数 (VIMulVC 判定)
-      mulVC x y = case (nodeOf x, nodeOf y) of
-        (RNC c, _) -> goVC c y
-        (_, RNC c) -> goVC c x
-        _          -> Nothing
-        where
-          goVC c mi = case nodeOf mi of
-            RN2 SMulO p q
-              | not (isVec p), isVec q -> Just (p, q, c)
-              | isVec p, not (isVec q) -> Just (q, p, c)
-            _ -> Nothing
-      comp i st@(cache, _, _, _) = case IM.lookup i cache of
-        Just sl -> (sl, st)
-        Nothing -> case nodeOf i of
-          RNK v      -> emit (VIK v) 0 i st
-          RNC v      -> emit (VIKV v) (VS.length v) i st
-          RNV p      -> emit (VILeafS p) 0 i st
-          RNVec p    -> emit (VILeafV p) (vecLens !! p) i st
-          RNG p gids ->
-            emit (VIGath p gids (VU.length gids)) (VU.length gids) i st
-          RN1 o x    ->
-            let (sx, st1@(_, _, lens1, _)) = comp x st
-            in emit (VIUn o sx) (lens1 BV.! sx) i st1
-          -- Phase 85.3-ii: Σ(x−m)² / Σx² を 1 命令に融合。
-          RNSum mI | RN2 SMulO r1 r2 <- nodeOf mI, r1 == r2, isVec r1 ->
-            let (xe, me) = case nodeOf r1 of
-                  RN2 SSubO x mm -> (x, mm)
-                  _              -> (r1, k0)
-            in case nodeOf xe of
-                 RNC c | RN2 SAddO m1 m2 <- nodeOf me, isVec m1, isVec m2 ->
-                   let (s1, st1) = comp m1 st
-                       (s2, st2) = comp m2 st1
-                   in emit (VISumSqC2 c s1 s2) 0 i st2
-                 RNC c ->
-                   let (sm, st1) = comp me st
-                   in emit (VISumSqC c sm) 0 i st1
-                 -- Phase 90 A11-4② (F2): Σ(gather − gather)² は gather 2 本を
-                 -- SumSqD 命令に内蔵し 5461 セルの arena 実体化を消す (ICAR)。
-                 _ | RNG px gx <- nodeOf xe, RNG pm gm <- nodeOf me
-                   , VU.length gx == VU.length gm ->
-                     emit (VISumSqDGG px gx pm gm (VU.length gx)) 0 i st
-                 _ ->
-                   let (sx, st1) = comp xe st
-                       (sm, st2) = comp me st1
-                   in emit (VISumSqD sx sm) 0 i st2
-          -- Phase 85.3-ii: a + s·v → VIAxpy。 85.3-iv: v が gather なら VIAxpyG。
-          RN2 SAddO a b | Just (ae, se, ve) <- axpyMatch a b ->
-            let (sa, st1) = comp ae st
-                (ss, st2) = comp se st1
-            in case nodeOf ve of
-                 RNC c -> emit (VIAxpyC sa ss c) (VS.length c) i st2
-                 RNG p gids ->
-                   emit (VIAxpyG sa ss p gids (VU.length gids))
-                        (VU.length gids) i st2
-                 _ ->
-                   let (sv, st3@(_, _, lens3, _)) = comp ve st2
-                   in emit (VIAxpy sa ss sv) (lens3 BV.! sv) i st3
-          -- Phase 85.3-iv: スカラ×gather → VIMulG。
-          RN2 SMulO a b | Just (se, p, gids) <- mulSG a b ->
-            let (ss, st1) = comp se st
-            in emit (VIMulG ss p gids (VU.length gids)) (VU.length gids) i st1
-          -- Phase 85.3-iv: (スカラ×ベクトル)⊙データ列定数 → VIMulVC。
-          RN2 SMulO a b | Just (se, ve, c) <- mulVC a b ->
-            let (ss, st1) = comp se st
-                (sv, st2) = comp ve st1
-            in emit (VIMulVC ss sv c) (VS.length c) i st2
-          RN2 o a b  ->
-            let (sa, st1) = comp a st
-                (sb, st2@(_, _, lens2, _)) = comp b st1
-            in emit (VIBin o sa sb) (max (lens2 BV.! sa) (lens2 BV.! sb)) i st2
-          RNSum x    -> let (sx, st1) = comp x st in emit (VISum sx) 0 i st1
-      st0 = (IM.empty :: IM.IntMap Int, [], BV.empty, 0)
-      (sObj, st1) = comp objI st0
-      (gss, (_, accF, lensF, _)) =
-        foldl (\(gacc, st) (k, gi) ->
-                 let (sl, st') = comp gi st in (gacc ++ [(k, sl)], st'))
-              ([], st1) guardIs
-      lensL = BV.toList lensF
-      offs  = scanl (+) 0 (map (max 1) lensL)   -- スカラ slot は 1 セル
-  in VecProgram
-    { vpInstrs  = BV.fromList (reverse accF)
-    , vpOff     = VU.fromList (init offs)
-    , vpLen     = VU.fromList lensL
-    , vpSize    = last offs
-    , vpObj     = sObj
-    , vpGuards  = gss
-    }
-
--- | 'RUExp' (目的 + guard 式) を命令列へ。 leaf は重複排除・それ以外は木のまま
--- (密度式は小さいので CSE なしで十分。 随伴は slot 単位で共有されるため
--- 記号微分でも式膨張しない)。
-compileVecProgram :: [Int] -> RUExp -> [(GuardKind, RUExp)] -> VecProgram
-compileVecProgram vecLens obj guards =
-  let emit ins l e (cache, acc, lens, n) =
-        (n, (Map.insert e n cache, ins : acc, BV.snoc lens l, n + 1 :: Int))
-      -- Phase 85.3-ii: a + s·v (AXPY) の融合対象判定。 加数のどちらかが
-      -- (スカラ × ベクトル) 積なら Just (残りの加数, スカラ式, ベクトル式)。
-      mulSV (RU2 SMulO p q)
-        | not (ruIsVec p), ruIsVec q = Just (p, q)
-        | ruIsVec p, not (ruIsVec q) = Just (q, p)
-      mulSV _ = Nothing
-      axpyMatch a b = case mulSV b of
-        Just (se, ve) -> Just (a, se, ve)
-        Nothing       -> case mulSV a of
-          Just (se, ve) -> Just (b, se, ve)
-          Nothing       -> Nothing
-      -- Phase 85.3-iv: スカラ × gather (VIMulG 判定)
-      mulSG x y = case (x, y) of
-        (s, RUG p gids) | not (ruIsVec s) -> Just (s, p, gids)
-        (RUG p gids, s) | not (ruIsVec s) -> Just (s, p, gids)
-        _ -> Nothing
-      -- Phase 85.3-iv: (スカラ×ベクトル) ⊙ データ列定数 (VIMulVC 判定)
-      mulVC x y = case (x, y) of
-        (RUC c, m) -> goVC c m
-        (m, RUC c) -> goVC c m
-        _          -> Nothing
-        where goVC c (RU2 SMulO p q)
-                | not (ruIsVec p), ruIsVec q = Just (p, q, c)
-                | ruIsVec p, not (ruIsVec q) = Just (q, p, c)
-              goVC _ _ = Nothing
-      comp e st@(cache, _, _, _) = case Map.lookup e cache of
-        Just sl -> (sl, st)
-        Nothing -> case e of
-          RUK v      -> emit (VIK v) 0 e st
-          RUC v      -> emit (VIKV v) (VS.length v) e st
-          RUV p      -> emit (VILeafS p) 0 e st
-          RUVec p    -> emit (VILeafV p) (vecLens !! p) e st
-          RUG p gids ->
-            emit (VIGath p gids (VU.length gids)) (VU.length gids) e st
-          RU1 o x    ->
-            let (sx, st1@(_, _, lens1, _)) = comp x st
-            in emit (VIUn o sx) (lens1 BV.! sx) e st1
-          -- Phase 85.3-ii: Σ(x−m)² / Σx² を 1 命令に融合 (residual→二乗→Σ の
-          -- 3 pass → 1 pass・中間 slot 消滅)。 x がデータ列定数なら VIKV copy
-          -- ごと消す。 ruIsVec 条件はスカラ RUSum の従来意味 (0) を保存。
-          RUSum (RU2 SMulO r1 r2) | r1 == r2, ruIsVec r1 ->
-            let (xe, me) = case r1 of
-                  RU2 SSubO x m -> (x, m)
-                  x             -> (x, RUK 0)
-            in case xe of
-                 -- 85.3-iv: Σ(c − (m1+m2))² は和の実体化も畳む (radon の
-                 -- 固定効果 μ + RE 項の和がここに来る)
-                 RUC c | RU2 SAddO m1 m2 <- me, ruIsVec m1, ruIsVec m2 ->
-                   let (s1, st1) = comp m1 st
-                       (s2, st2) = comp m2 st1
-                   in emit (VISumSqC2 c s1 s2) 0 e st2
-                 RUC c ->
-                   let (sm, st1) = comp me st
-                   in emit (VISumSqC c sm) 0 e st1
-                 _ ->
-                   let (sx, st1) = comp xe st
-                       (sm, st2) = comp me st1
-                   in emit (VISumSqD sx sm) 0 e st2
-          -- Phase 85.3-ii: a + s·v → VIAxpy (2 pass → 1 pass)。
-          -- 85.3-iv: v が gather ならそれも内蔵 (VIAxpyG)。
-          RU2 SAddO a b | Just (ae, se, ve) <- axpyMatch a b ->
-            let (sa, st1) = comp ae st
-                (ss, st2) = comp se st1
-            in case ve of
-                 RUC c -> emit (VIAxpyC sa ss c) (VS.length c) e st2
-                 RUG p gids ->
-                   emit (VIAxpyG sa ss p gids (VU.length gids))
-                        (VU.length gids) e st2
-                 _     ->
-                   let (sv, st3@(_, _, lens3, _)) = comp ve st2
-                   in emit (VIAxpy sa ss sv) (lens3 BV.! sv) e st3
-          -- Phase 85.3-iv: スカラ×gather → VIMulG (gather 実体化の消滅)。
-          RU2 SMulO a b | Just (se, p, gids) <- mulSG a b ->
-            let (ss, st1) = comp se st
-            in emit (VIMulG ss p gids (VU.length gids)) (VU.length gids) e st1
-          -- Phase 85.3-iv: (スカラ×ベクトル)⊙データ列定数 → VIMulVC。
-          RU2 SMulO a b | Just (se, ve, c) <- mulVC a b ->
-            let (ss, st1) = comp se st
-                (sv, st2) = comp ve st1
-            in emit (VIMulVC ss sv c) (VS.length c) e st2
-          RU2 o a b  ->
-            let (sa, st1) = comp a st
-                (sb, st2@(_, _, lens2, _)) = comp b st1
-            in emit (VIBin o sa sb) (max (lens2 BV.! sa) (lens2 BV.! sb)) e st2
-          RUSum x    -> let (sx, st1) = comp x st in emit (VISum sx) 0 e st1
-      st0 = (Map.empty :: Map RUExp Int, [], BV.empty, 0)
-      (sObj, st1) = comp obj st0
-      (gss, (_, accF, lensF, _)) =
-        foldl (\(gacc, st) (k, ge) ->
-                 let (sl, st') = comp ge st in (gacc ++ [(k, sl)], st'))
-              ([], st1) guards
-      lensL = BV.toList lensF
-      offs  = scanl (+) 0 (map (max 1) lensL)   -- スカラ slot は 1 セル
-  in VecProgram
-    { vpInstrs  = BV.fromList (reverse accF)
-    , vpOff     = VU.fromList (init offs)
-    , vpLen     = VU.fromList lensL
-    , vpSize    = last offs
-    , vpObj     = sObj
-    , vpGuards  = gss
-    }
-
--- | forward 実行: 全 slot の値を 1 本の arena に書く (ST・per-call 確保)。
-forwardArena
-  :: CompiledVecIR -> VS.Vector Double -> ST s (VSM.MVector s Double)
-forwardArena cvi pc = do
-  ar <- VSM.unsafeNew (vpSize (cvProg cvi))
-  forwardArenaInto cvi pc ar
-  pure ar
-
--- | 'forwardArena' の呼出側バッファ版 (Phase 90 A11-4①: NUTS 葉勾配の
--- per-call arena 確保 (34k セル級) を chain 閉包での 1 回確保 + 再利用に
--- 変える)。 全 slot を毎回上書きするため zero-fill 不要。
-forwardArenaInto
-  :: CompiledVecIR -> VS.Vector Double -> VSM.MVector s Double -> ST s ()
-forwardArenaInto cvi pc ar = do
-  let prog   = cvProg cvi
-      instrs = vpInstrs prog
-      offV   = vpOff prog
-      lenV   = vpLen prog
-      misB   = BV.fromList (cvVecIxs cvi)
-      scal p = pc `VS.unsafeIndex` (cvScalIx cvi `VU.unsafeIndex` p)
-  let off i = offV `VU.unsafeIndex` i
-      len i = lenV `VU.unsafeIndex` i
-      rd  = VSM.unsafeRead ar
-      wr  = VSM.unsafeWrite ar
-      step i = do
-        let o = off i
-        case instrs BV.! i of
-          VIK v     -> wr o v
-          VIKV v    ->
-            let go !j | j >= VS.length v = pure ()
-                      | otherwise = do
-                          wr (o + j) (v `VS.unsafeIndex` j)
-                          go (j + 1)
-            in go 0
-          VILeafS p -> wr o (scal p)
-          VILeafV p ->
-            let mis = misB BV.! p
-                go !j | j >= VU.length mis = pure ()
-                      | otherwise = do
-                          wr (o + j)
-                            (pc `VS.unsafeIndex` (mis `VU.unsafeIndex` j))
-                          go (j + 1)
-            in go 0
-          VIGath p gids n ->
-            let mis = misB BV.! p
-                go !r | r >= n = pure ()
-                      | otherwise = do
-                          wr (o + r) (pc `VS.unsafeIndex`
-                            (mis `VU.unsafeIndex` (gids `VU.unsafeIndex` r)))
-                          go (r + 1)
-            in go 0
-          -- Phase 105 A3: withSUnF/withSBinF (INLINE CPS) で op の case を
-          -- ループ外に出し、 known-function の特殊化 unboxed ループに落とす
-          -- (closure 間接呼出の per-element boxing 排除。 FP 順序不変)。
-          VIUn op x -> withSUnF op $ \f -> do
-            let xo = off x
-            case len i of
-              0 -> rd xo >>= wr o . f
-              n ->
-                let go !j | j >= n = pure ()
-                          | otherwise = do
-                              v <- rd (xo + j)
-                              wr (o + j) (f v)
-                              go (j + 1)
-                in go 0
-          VIBin op x y -> withSBinF op $ \f -> do
-            let xo = off x
-                yo = off y
-            case (len x, len y) of
-              (0, 0) -> do
-                a <- rd xo
-                b <- rd yo
-                wr o (f a b)
-              (0, n) -> do
-                a <- rd xo
-                let go !j | j >= n = pure ()
-                          | otherwise = do
-                              b <- rd (yo + j)
-                              wr (o + j) (f a b)
-                              go (j + 1)
-                go 0
-              (n, 0) -> do
-                b <- rd yo
-                let go !j | j >= n = pure ()
-                          | otherwise = do
-                              a <- rd (xo + j)
-                              wr (o + j) (f a b)
-                              go (j + 1)
-                go 0
-              (n, _) ->
-                let go !j | j >= n = pure ()
-                          | otherwise = do
-                              a <- rd (xo + j)
-                              b <- rd (yo + j)
-                              wr (o + j) (f a b)
-                              go (j + 1)
-                in go 0
-          VISum x -> do
-            let xo = off x
-                n  = len x
-                go !acc !j | j >= n    = wr o acc
-                           | otherwise = do
-                               v <- rd (xo + j)
-                               go (acc + v) (j + 1)
-            go 0 0
-          -- Phase 85.3-ii superinstruction
-          VIAxpy a s v -> do
-            sv <- rd (off s)
-            let vo = off v
-                n  = len i
-            case len a of
-              0 -> do
-                av <- rd (off a)
-                let go !j | j >= n = pure ()
-                          | otherwise = do
-                              b <- rd (vo + j)
-                              wr (o + j) (av + sv * b)
-                              go (j + 1)
-                go 0
-              _ -> do
-                let ao = off a
-                    go !j | j >= n = pure ()
-                          | otherwise = do
-                              av <- rd (ao + j)
-                              b  <- rd (vo + j)
-                              wr (o + j) (av + sv * b)
-                              go (j + 1)
-                go 0
-          VIAxpyC a s c -> do
-            sv <- rd (off s)
-            let n = len i
-            case len a of
-              0 -> do
-                av <- rd (off a)
-                let go !j | j >= n = pure ()
-                          | otherwise = do
-                              wr (o + j) (av + sv * (c `VS.unsafeIndex` j))
-                              go (j + 1)
-                go 0
-              _ -> do
-                let ao = off a
-                    go !j | j >= n = pure ()
-                          | otherwise = do
-                              av <- rd (ao + j)
-                              wr (o + j) (av + sv * (c `VS.unsafeIndex` j))
-                              go (j + 1)
-                go 0
-          VISumSqD x m -> do
-            let xo = off x
-                mo = off m
-                bx = len x /= 0
-                bm = len m /= 0
-                n  = max (len x) (len m)
-                go !acc !j
-                  | j >= n = wr o acc
-                  | otherwise = do
-                      a <- rd (if bx then xo + j else xo)
-                      b <- rd (if bm then mo + j else mo)
-                      let d = a - b
-                      go (acc + d * d) (j + 1)
-            go 0 0
-          VISumSqC c m -> do
-            let mo = off m
-                bm = len m /= 0
-                n  = VS.length c
-                go !acc !j
-                  | j >= n = wr o acc
-                  | otherwise = do
-                      b <- rd (if bm then mo + j else mo)
-                      let d = c `VS.unsafeIndex` j - b
-                      go (acc + d * d) (j + 1)
-            go 0 0
-          -- Phase 85.3-iv superinstruction
-          VIMulG s p gids n -> do
-            sv <- rd (off s)
-            let mis = misB BV.! p
-                go !j | j >= n = pure ()
-                      | otherwise = do
-                          wr (o + j) (sv * (pc `VS.unsafeIndex`
-                            (mis `VU.unsafeIndex` (gids `VU.unsafeIndex` j))))
-                          go (j + 1)
-            go 0
-          VIAxpyG a s p gids n -> do
-            sv <- rd (off s)
-            let mis = misB BV.! p
-                gv j = pc `VS.unsafeIndex`
-                         (mis `VU.unsafeIndex` (gids `VU.unsafeIndex` j))
-            case len a of
-              0 -> do
-                av <- rd (off a)
-                let go !j | j >= n = pure ()
-                          | otherwise = do
-                              wr (o + j) (av + sv * gv j)
-                              go (j + 1)
-                go 0
-              _ -> do
-                let ao = off a
-                    go !j | j >= n = pure ()
-                          | otherwise = do
-                              av <- rd (ao + j)
-                              wr (o + j) (av + sv * gv j)
-                              go (j + 1)
-                go 0
-          VIMulVC s v c -> do
-            sv <- rd (off s)
-            let vo = off v
-                n  = VS.length c
-                go !j | j >= n = pure ()
-                      | otherwise = do
-                          b <- rd (vo + j)
-                          wr (o + j) (sv * b * (c `VS.unsafeIndex` j))
-                          go (j + 1)
-            go 0
-          VISumSqC2 c m1 m2 -> do
-            let m1o = off m1
-                m2o = off m2
-                n   = VS.length c
-                go !acc !j
-                  | j >= n = wr o acc
-                  | otherwise = do
-                      b1 <- rd (m1o + j)
-                      b2 <- rd (m2o + j)
-                      let d = c `VS.unsafeIndex` j - b1 - b2
-                      go (acc + d * d) (j + 1)
-            go 0 0
-          -- Phase 90 A11-4② (F2): gather 2 本内蔵の Σ(φ_a − φ_b)²。 gather 値は
-          -- pc から直読み (VIGath forward と同経路)・arena 実体化なし。
-          VISumSqDGG px gx pm gm n -> do
-            let misx = misB BV.! px
-                mism = misB BV.! pm
-                go !acc !j
-                  | j >= n = wr o acc
-                  | otherwise = do
-                      let a = pc `VS.unsafeIndex`
-                                (misx `VU.unsafeIndex` (gx `VU.unsafeIndex` j))
-                          b = pc `VS.unsafeIndex`
-                                (mism `VU.unsafeIndex` (gm `VU.unsafeIndex` j))
-                          d = a - b
-                      go (acc + d * d) (j + 1)
-            go 0 0
-      loop !i | i >= BV.length instrs = pure ()
-              | otherwise = step i >> loop (i + 1)
-  loop 0
-
--- | IR の log-density **値** (観測尤度 + 族 prior)。 guard (σ/τ/λ ≤ 0・
--- p ∉ (0,1) → -∞) は 'logDensityObs' / 'logDensity' の該当分岐と一致。
-vecIRValue :: CompiledVecIR -> VS.Vector Double -> Double
-vecIRValue cvi pc = runST $ do
-  let prog = cvProg cvi
-  ar <- forwardArena cvi pc
-  ok <- arenaGuardsOK prog ar
-  if ok then VSM.unsafeRead ar (vpOff prog `VU.unsafeIndex` vpObj prog)
-        else pure negInf
-
--- | Phase 87.2b: 'gradVecIR' の value-and-grad 融合版。 forward arena を 1 度
--- だけ構築し、 log-density **値** (objective slot・'vecIRValue' と同一) と
--- constrained 勾配 (mg へ加算・'gradVecIR' と同一) を同時に返す。 NUTS の葉が
--- leapfrog 最終勾配と同一点でエネルギー (logπ) を別途評価していた重複
--- (prof 実測 19%) を除去するためのエントリポイント。 guard 違反 = Nothing
--- (呼出側が 値 -∞ / 勾配 walk+ad fallback で従来意味論と一致させる)。
-gradVecIRVal :: CompiledVecIR -> VS.Vector Double -> VSM.MVector s Double
-             -> ST s (Maybe Double)
-gradVecIRVal cvi pc mg = do
-  let sz = vpSize (cvProg cvi)
-  ar  <- VSM.unsafeNew sz
-  adj <- VSM.unsafeNew sz
-  gradVecIRValWith cvi ar adj pc mg
-
--- | 'gradVecIRVal' の呼出側バッファ版 (Phase 90 A11-4①)。 @ar@ / @adj@ は
--- 長さ 'vpSize' の作業バッファで、 呼出間で再利用してよい (初期化不要・
--- 毎回全上書き / zero-fill される)。 NUTS の葉勾配 closure が chain ごとに
--- 1 度だけ確保して全 leapfrog で使い回すためのエントリポイント。
-gradVecIRValWith :: CompiledVecIR
-                 -> VSM.MVector s Double -> VSM.MVector s Double
-                 -> VS.Vector Double -> VSM.MVector s Double
-                 -> ST s (Maybe Double)
-gradVecIRValWith cvi ar adj pc mg = do
-  let prog = cvProg cvi
-  forwardArenaInto cvi pc ar
-  ok <- arenaGuardsOK prog ar
-  if not ok
-    then pure Nothing
-    else do
-      v <- VSM.unsafeRead ar (vpOff prog `VU.unsafeIndex` vpObj prog)
-      gradVecIRGoWith cvi pc ar adj mg
-      pure (Just v)
-
--- | forward arena 上で値側 guard を検査 (vecIRValue / gradVecIR 共有)。
-arenaGuardsOK :: VecProgram -> VSM.MVector s Double -> ST s Bool
-arenaGuardsOK prog ar = fmap and (mapM gOK (vpGuards prog))
-  where
-    gOK (k, sl) = do
-      let o = vpOff prog `VU.unsafeIndex` sl
-          n = max 1 (vpLen prog `VU.unsafeIndex` sl)
-          chk = case k of
-            GPos  -> (> 0)
-            GUnit -> \pv -> pv > 0 && pv < 1
-          go !j | j >= n    = pure True
-                | otherwise = do
-                    v <- VSM.unsafeRead ar (o + j)
-                    if chk v then go (j + 1) else pure False
-      go 0
-
--- | IR の constrained 勾配を mutable 勾配ベクトルへ**直接**加算する (Phase 56.2:
--- 記号 reverse-mode・arena backward)。 forward arena と同形の随伴 arena に
--- 逆順伝播し、 leaf 随伴は param 位置へその場で scatter。 命令列・形・
--- オフセットは compile 時に固定済み = per-call の tape 構築なし。 勾配側は
--- unguarded (54.11 の前例どおり・-∞ 状態は NUTS が値側で棄却する)。
--- unconstrained への chain rule は呼出側。
-gradVecIR :: CompiledVecIR -> VS.Vector Double -> VSM.MVector s Double
-          -> ST s Bool
-gradVecIR cvi pc mg = do
-  let prog = cvProg cvi
-  ar <- forwardArena cvi pc
-  ok <- arenaGuardsOK prog ar
-  if not ok then pure False
-            else gradVecIRGo cvi pc ar mg >> pure True
-
--- | 'gradVecIR' の backward 本体 (guard 通過後)。 Phase 85.3-iv: gather 内蔵
--- 命令 (VIMulG/VIAxpyG) が gather 値を読むため pc (constrained params) を取る。
-gradVecIRGo
-  :: CompiledVecIR -> VS.Vector Double -> VSM.MVector s Double
-  -> VSM.MVector s Double -> ST s ()
-gradVecIRGo cvi pc ar mg = do
-  adj <- VSM.unsafeNew (vpSize (cvProg cvi))
-  gradVecIRGoWith cvi pc ar adj mg
-
--- | 'gradVecIRGo' の呼出側 adj バッファ版 (Phase 90 A11-4①)。 zero-fill は
--- 本関数が行う (旧 @VSM.replicate (vpSize prog) 0@ と同値) ため、 呼出側は
--- 確保のみで初期化不要。
-gradVecIRGoWith
-  :: CompiledVecIR -> VS.Vector Double -> VSM.MVector s Double
-  -> VSM.MVector s Double -> VSM.MVector s Double -> ST s ()
-gradVecIRGoWith cvi pc ar adj mg = do
-  let prog   = cvProg cvi
-      instrs = vpInstrs prog
-      offV   = vpOff prog
-      lenV   = vpLen prog
-      misB   = BV.fromList (cvVecIxs cvi)
-      nSlots = BV.length instrs
-  VSM.set adj 0
-  let off i = offV `VU.unsafeIndex` i
-      len i = lenV `VU.unsafeIndex` i
-      rdV = VSM.unsafeRead ar
-      rdA = VSM.unsafeRead adj
-      addA o d = VSM.unsafeModify adj (+ d) o
-      addG ix d = VSM.unsafeModify mg (+ d) ix
-      step i = do
-        let o = off i
-        case instrs BV.! i of
-          VIK _  -> pure ()
-          VIKV _ -> pure ()
-          VILeafS p -> do
-            d <- rdA o
-            addG (cvScalIx cvi `VU.unsafeIndex` p) d
-          VILeafV p ->
-            let mis = misB BV.! p
-                go !j | j >= len i = pure ()
-                      | otherwise = do
-                          d <- rdA (o + j)
-                          addG (mis `VU.unsafeIndex` j) d
-                          go (j + 1)
-            in go 0
-          VIGath p gids n ->
-            let mis = misB BV.! p
-                go !r | r >= n = pure ()
-                      | otherwise = do
-                          d <- rdA (o + r)
-                          addG (mis `VU.unsafeIndex`
-                                  (gids `VU.unsafeIndex` r)) d
-                          go (r + 1)
-            in go 0
-          -- Phase 105 A3: withSUnD (INLINE CPS) で特殊化 (forward 側と同じ意図)。
-          VIUn op x -> withSUnD op $ \df -> do
-            let xo = off x
-            case len i of
-              0 -> do
-                a <- rdA o
-                v <- rdV xo
-                addA xo (df v * a)
-              n ->
-                let go !j | j >= n = pure ()
-                          | otherwise = do
-                              a <- rdA (o + j)
-                              v <- rdV (xo + j)
-                              addA (xo + j) (df v * a)
-                              go (j + 1)
-                in go 0
-          VIBin op x y -> do
-            let xo = off x
-                yo = off y
-                n  = max 1 (len i)
-                bx = len x /= 0   -- x がベクトルか
-                by = len y /= 0
-                xi j = if bx then xo + j else xo
-                yi j = if by then yo + j else yo
-            case op of
-              SAddO ->
-                let go !j | j >= n = pure ()
-                          | otherwise = do
-                              a <- rdA (o + j)
-                              addA (xi j) a
-                              addA (yi j) a
-                              go (j + 1)
-                in go 0
-              SSubO ->
-                let go !j | j >= n = pure ()
-                          | otherwise = do
-                              a <- rdA (o + j)
-                              addA (xi j) a
-                              addA (yi j) (negate a)
-                              go (j + 1)
-                in go 0
-              SMulO ->
-                let go !j | j >= n = pure ()
-                          | otherwise = do
-                              a  <- rdA (o + j)
-                              vx <- rdV (xi j)
-                              vy <- rdV (yi j)
-                              addA (xi j) (a * vy)
-                              addA (yi j) (a * vx)
-                              go (j + 1)
-                in go 0
-              SDivO ->
-                let go !j | j >= n = pure ()
-                          | otherwise = do
-                              a  <- rdA (o + j)
-                              vx <- rdV (xi j)
-                              vy <- rdV (yi j)
-                              addA (xi j) (a / vy)
-                              addA (yi j) (negate (a * vx / (vy * vy)))
-                              go (j + 1)
-                in go 0
-              -- winner-take-all subgradient (tie は測度0・x側に付与で十分)。
-              SMaxO ->
-                let go !j | j >= n = pure ()
-                          | otherwise = do
-                              a  <- rdA (o + j)
-                              vx <- rdV (xi j)
-                              vy <- rdV (yi j)
-                              if vx >= vy
-                                then addA (xi j) a
-                                else addA (yi j) a
-                              go (j + 1)
-                in go 0
-          VISum x -> do
-            a <- rdA o
-            let xo = off x
-                n  = len x
-                go !j | j >= n = pure ()
-                      | otherwise = addA (xo + j) a >> go (j + 1)
-            go 0
-          -- Phase 85.3-ii superinstruction: out = a + s·v の随伴 =
-          -- adj a += g (スカラ a は Σg)・adj s += Σ g·v・adj v += g·s。
-          VIAxpy a s v -> do
-            sv <- rdV (off s)
-            let vo = off v
-                ao = off a
-                n  = len i
-            case len a of
-              0 ->
-                let go !ga !gs !j
-                      | j >= n = addA ao ga >> addA (off s) gs
-                      | otherwise = do
-                          g  <- rdA (o + j)
-                          bv <- rdV (vo + j)
-                          addA (vo + j) (g * sv)
-                          go (ga + g) (gs + g * bv) (j + 1)
-                in go 0 0 0
-              _ ->
-                let go !gs !j
-                      | j >= n = addA (off s) gs
-                      | otherwise = do
-                          g  <- rdA (o + j)
-                          bv <- rdV (vo + j)
-                          addA (ao + j) g
-                          addA (vo + j) (g * sv)
-                          go (gs + g * bv) (j + 1)
-                in go 0 0
-          VIAxpyC a s c -> do
-            let ao = off a
-                n  = len i
-            case len a of
-              0 ->
-                let go !ga !gs !j
-                      | j >= n = addA ao ga >> addA (off s) gs
-                      | otherwise = do
-                          g <- rdA (o + j)
-                          go (ga + g) (gs + g * (c `VS.unsafeIndex` j)) (j + 1)
-                in go 0 0 0
-              _ ->
-                let go !gs !j
-                      | j >= n = addA (off s) gs
-                      | otherwise = do
-                          g <- rdA (o + j)
-                          addA (ao + j) g
-                          go (gs + g * (c `VS.unsafeIndex` j)) (j + 1)
-                in go 0 0
-          -- out = Σ(x−m)² の随伴 = adj x_j += 2(x_j−m_j)·g・adj m_j −= 同
-          -- (スカラ側は Σ を単発加算)。 2(x−m)g は旧 (r·r 同一 slot 2 加算 +
-          -- SSubO 伝播) と IEEE 同値 (x+x ≡ 2x)。
-          VISumSqD x m -> do
-            g <- rdA o
-            let xo = off x
-                mo = off m
-                bx = len x /= 0
-                bm = len m /= 0
-                n  = max (len x) (len m)
-                go !sx !sm !j
-                  | j >= n = do
-                      if bx then pure () else addA xo sx
-                      if bm then pure () else addA mo sm
-                  | otherwise = do
-                      a <- rdV (if bx then xo + j else xo)
-                      b <- rdV (if bm then mo + j else mo)
-                      let d = 2 * (a - b) * g
-                      if bx then addA (xo + j) d          else pure ()
-                      if bm then addA (mo + j) (negate d) else pure ()
-                      go (if bx then sx else sx + d)
-                         (if bm then sm else sm - d) (j + 1)
-            go 0 0 0
-          VISumSqC c m -> do
-            g <- rdA o
-            let mo = off m
-                bm = len m /= 0
-                n  = VS.length c
-                go !sm !j
-                  | j >= n = if bm then pure () else addA mo sm
-                  | otherwise = do
-                      b <- rdV (if bm then mo + j else mo)
-                      let d = 2 * (c `VS.unsafeIndex` j - b) * g
-                      if bm then addA (mo + j) (negate d) >> go sm (j + 1)
-                            else go (sm - d) (j + 1)
-            go 0 0
-          -- Phase 85.3-iv superinstruction: gather 内蔵命令の随伴は leaf
-          -- (param) へ直接 scatter ('VIGath' backward と同じ) + gather 値は
-          -- pc から読む。
-          VIMulG s p gids n -> do
-            sv <- rdV (off s)
-            let mis = misB BV.! p
-                go !gs !j
-                  | j >= n = addA (off s) gs
-                  | otherwise = do
-                      g <- rdA (o + j)
-                      let ix = mis `VU.unsafeIndex` (gids `VU.unsafeIndex` j)
-                      addG ix (g * sv)
-                      go (gs + g * (pc `VS.unsafeIndex` ix)) (j + 1)
-            go 0 0
-          VIAxpyG a s p gids n -> do
-            sv <- rdV (off s)
-            let mis = misB BV.! p
-                ao  = off a
-            case len a of
-              0 ->
-                let go !ga !gs !j
-                      | j >= n = addA ao ga >> addA (off s) gs
-                      | otherwise = do
-                          g <- rdA (o + j)
-                          let ix = mis `VU.unsafeIndex` (gids `VU.unsafeIndex` j)
-                          addG ix (g * sv)
-                          go (ga + g) (gs + g * (pc `VS.unsafeIndex` ix)) (j + 1)
-                in go 0 0 0
-              _ ->
-                let go !gs !j
-                      | j >= n = addA (off s) gs
-                      | otherwise = do
-                          g <- rdA (o + j)
-                          let ix = mis `VU.unsafeIndex` (gids `VU.unsafeIndex` j)
-                          addA (ao + j) g
-                          addG ix (g * sv)
-                          go (gs + g * (pc `VS.unsafeIndex` ix)) (j + 1)
-                in go 0 0
-          VIMulVC s v c -> do
-            sv <- rdV (off s)
-            let vo = off v
-                n  = VS.length c
-                go !gs !j
-                  | j >= n = addA (off s) gs
-                  | otherwise = do
-                      g <- rdA (o + j)
-                      b <- rdV (vo + j)
-                      let cj = c `VS.unsafeIndex` j
-                      addA (vo + j) (g * sv * cj)
-                      go (gs + g * b * cj) (j + 1)
-            go 0 0
-          VISumSqC2 c m1 m2 -> do
-            g <- rdA o
-            let m1o = off m1
-                m2o = off m2
-                n   = VS.length c
-                go !j | j >= n = pure ()
-                      | otherwise = do
-                          b1 <- rdV (m1o + j)
-                          b2 <- rdV (m2o + j)
-                          let d = 2 * (c `VS.unsafeIndex` j - b1 - b2) * g
-                          addA (m1o + j) (negate d)
-                          addA (m2o + j) (negate d)
-                          go (j + 1)
-            go 0
-          -- Phase 90 A11-4② (F2): 随伴 = ∂/∂φ_a[Σ(φ_a−φ_b)²] = 2(φ_a−φ_b)·g を
-          -- param へ直 scatter (φ_b は −同)。 gather 値は pc から直読み。
-          VISumSqDGG px gx pm gm n -> do
-            g <- rdA o
-            let misx = misB BV.! px
-                mism = misB BV.! pm
-                go !j | j >= n = pure ()
-                      | otherwise = do
-                          let ixa = misx `VU.unsafeIndex` (gx `VU.unsafeIndex` j)
-                              ixb = mism `VU.unsafeIndex` (gm `VU.unsafeIndex` j)
-                              d = 2 * (pc `VS.unsafeIndex` ixa
-                                       - pc `VS.unsafeIndex` ixb) * g
-                          addG ixa d
-                          addG ixb (negate d)
-                          go (j + 1)
-            go 0
-      loop !i | i < 0     = pure ()
-              | otherwise = step i >> loop (i - 1)
-  VSM.unsafeWrite adj (off (vpObj prog)) 1
-  loop (nSlots - 1)
diff --git a/src/Hanalyze/Model/HBM/Interp.hs b/src/Hanalyze/Model/HBM/Interp.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/HBM/Interp.hs
+++ /dev/null
@@ -1,1760 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE FlexibleContexts #-}
-
--- |
--- Module      : Hanalyze.Model.HBM.Interp
--- Description : HBM dialog DSL の評価系 (interpreter) と NUTS 設定・結果整形
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- HBM dialog DSL の評価系 (interpreter) + NUTS 設定 reader + 結果整形。
---
--- Phase 27.5 (2026-05-31) step 1: canvas-backend @フロントエンド app.Analysis.HBM@
--- から eval/interp/curve コアを移設。 frontend が backend 統一 parser から得た
--- @program_ast@ を streaming sidecar が直接 interpret して実モデルを構築できる
--- よう、 DSL の評価系をライブラリ層 (hanalyze) に置く。
---
--- 本 module は **canvas wire 型 (AnalysisRequest 等) にも text parser
--- (DSL frontend) にも依存しない**。 依存は 'Hanalyze.Model.HBM.Ast' (AST) +
--- @Hanalyze.Model.HBM@ (ModelP/Distribution) + @Hanalyze.Stat.*@ /
--- @Hanalyze.MCMC.*@ + aeson / hmatrix のみ。
---
--- text → AST 変換 (@parseHbmText@ 経路) と canvas 専用の @runHbm@ /
--- @buildDataMap@ / @ProgramInfo@ 解決は canvas-backend 側に残す。
-module Hanalyze.Model.HBM.Interp
-  ( -- * core types
-    DataMap
-  , Column (..)
-  , colDoubles
-  , colLength
-  , colLevels
-  , lookupDoubles
-  , EnvA
-  , Value (..)
-  , PlateCtx (..)
-  , topCtx
-  , TopBind (..)
-  , ParamSummary (..)
-  , HbmMeanCurve (..)
-    -- * evaluation
-  , hasColRef
-  , liftD
-  , builtinTable
-  , asNum
-  , asBool
-  , asList
-  , asMatrix
-  , evalScalar
-  , evalValue
-  , evalDist
-  , buildTopEnv
-    -- * plate / groups (GLMM forEachGroup)
-  , matchForEachGroup
-  , lamBodyToStmts
-  , retToStmts
-  , groupValsIn
-  , rowsForGroup
-  , groupSuffix
-  , groupSuffixFor
-    -- * validation / interpretation
-  , inferTransforms
-  , validateAst
-  , preprocessAliases
-  , validateStmts
-  , interpStmts
-  , observeNodeMap
-    -- * NUTS config readers
-  , readChainCount
-  , readNutsConfig
-    -- * result shaping
-  , paramSummaryMulti
-  , fmtSummary
-  , round4
-  , takeEvery
-  , summaryToJson
-  , hbmMeanCurveToJson
-  , extractObserveMeans
-  , collectCols
-  , percentileOf
-  , computeMeanCurves
-    -- * WAIC / LOO / posterior predictive
-  , ObsDistSet (..)
-  , computeObsDists
-  , pointwiseLogLik
-  , finitePointwiseLogLik
-    -- * Phase 44: multi-column observe (observeMV) WAIC / PPC
-  , MvObsDistSet (..)
-  , computeMvObsDists
-  , pointwiseLogLikMv
-  , reconstructMatrixComb
-  , MatrixCombSpec (..)
-    -- * model graph plate aggregation (GLMM forEachGroup)
-  , GraphPlate (..)
-  , plateRenameMap
-  , collectGraphPlates
-  , collapsePlateGraph
-  ) where
-
-import Control.Monad (forM_, when)
-import Data.Char (isAlpha, isAlphaNum)
-import Data.List (sort, nub, transpose)
-import Data.Maybe (fromMaybe)
-import qualified Data.Set as Set
-import qualified Data.Aeson as A
-import qualified Data.Aeson.Key as Key
-import qualified Data.Aeson.KeyMap as KM
-import qualified Data.Map.Strict as Map
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Numeric.LinearAlgebra as LA
-
-import qualified Hanalyze.MCMC.Core as MC
-import qualified Hanalyze.MCMC.NUTS as NUTS
-import qualified Hanalyze.Model.HBM as HBM
-import qualified Hanalyze.Stat.Distribution as HD
-import qualified Hanalyze.Stat.MCMC as SMC
-
-import Hanalyze.Model.HBM.Ast
-  ( Expr (..)
-  , Lit (..)
-  , Bind (..)
-  , DoStmt (..)
-  , collectApp
-  , Err
-  )
-
--- ===========================================================================
--- Interpreter コア型
--- ===========================================================================
-
--- | 列参照を含む式かどうか(observe の dist 引数に列が混じったら per-row 展開する)。
-hasColRef :: Expr -> Bool
-hasColRef = go
-  where
-    go (ECol _)       = True
-    go (EApp f x)     = go f || go x
-    go (ENeg e)       = go e
-    go (EOp _ a b)    = go a || go b
-    go (EList xs)     = any go xs
-    go (ELet bs e)    = any (go . bindValue) bs || go e
-    go (EIf c a b)    = go c || go a || go b
-    go (ELam _ b)     = go b
-    go (EDo _ _)      = False  -- nested do 不可
-    go (ELit _)       = False
-    go (EVar _)       = False
-
--- 'Err' (= Either Text) は Hanalyze.Model.HBM.Ast から import。
-
--- | スカラ式評価。col 参照は許可されるなら row idx を指定。Nothing なら無効でエラー。
-liftD :: Floating a => Double -> a
-liftD d = fromRational (toRational d)
-
--- | 環境: sample / let / top-level で束縛された値 (数値・真偽・関数)。
--- Phase 27 §F-3a で scalar から 'Value' に拡張 (ユーザ定義関数を持てる)。
-type EnvA a = Map.Map Text (Value a)
-
--- | データ列の値 (Phase 41)。 数値列 (連続/整数) と categorical 列
--- (factor = level 辞書 + 整数 code) を区別する sum 型。 'Numeric' は従来の
--- @[Double]@ 相当で後方互換、 'Factor' は R の factor / PyMC coords 相当
--- (level 出現順に 0,1,2,... の code を振る)。
-data Column
-  = Numeric ![Double]                                     -- ^ 連続 / 整数列
-  | Factor  { facLevels :: ![Text], facCodes :: ![Int] }  -- ^ categorical
-  deriving (Eq, Show)
-
--- | データ列の Map。 列名 → 'Column'。
-type DataMap = Map.Map Text Column
-
--- | 列を @[Double]@ として見る (群比較 / 数値 observe / mean-curve 用)。
--- 'Numeric' はそのまま、 'Factor' は code を Double 化 (0,1,2,...)。 既存の
--- 数値ロジックはこの accessor 経由で Factor も透過に扱える。
-colDoubles :: Column -> [Double]
-colDoubles (Numeric xs)  = xs
-colDoubles (Factor _ cs) = map fromIntegral cs
-
--- | 列長 (行数)。
-colLength :: Column -> Int
-colLength (Numeric xs)  = length xs
-colLength (Factor _ cs) = length cs
-
--- | 'Factor' なら level 辞書 (出現順)、 'Numeric' なら Nothing。
-colLevels :: Column -> Maybe [Text]
-colLevels (Factor ls _) = Just ls
-colLevels (Numeric _)   = Nothing
-
--- | 列を @[Double]@ として引く (無ければ空)。 旧 @Map.findWithDefault [] k dm@ の
--- 'Column' 対応版。
-lookupDoubles :: Text -> DataMap -> [Double]
-lookupDoubles k = maybe [] colDoubles . Map.lookup k
-
--- | Phase 27 §F-3a: モデル本体評価中の値。 Double 閉の数値 + 真偽 +
--- 一階クロージャ (ユーザ定義関数 / lambda) + 組込関数マーカ + 遅延エラー
--- (top-level 値束縛の評価失敗を lookup まで遅延運搬する)。
-data Value a
-  = VNum a
-  | VBool Bool
-  | VList [Value a]                -- Phase 42: list リテラル ([e₁, …, eₙ])。 多値分布
-                                   --   (Categorical [probs] / OrderedLogistic eta [cuts])
-                                   --   の list 引数評価の土台。 要素はスカラ閉前提。
-  | VClosure (EnvA a) [Text] Expr  -- 捕捉環境, 残り仮引数, 本体 (一階・カリー化)
-  | VBuiltin Text Int              -- 組込関数 (名前, arity)。 適用時に builtinTable で解決
-  | VErr Text                      -- 遅延エラー (top-level 値 thunk の評価失敗)
-
--- | Phase 27 §F-3a: 組込数学関数ホワイトリスト (name → (arity, impl))。
--- すべて Double 上で閉じる純関数 (IO/import なし)。 GLM リンク
--- (invLogit/logistic) もここに含む。 必要に応じ追加。
-builtinTable :: forall a. (Floating a, Ord a) => Map.Map Text (Int, [a] -> a)
-builtinTable = Map.fromList
-  [ ("exp",      (1, \xs -> exp (head xs)))
-  , ("log",      (1, \xs -> log (head xs)))
-  , ("log1p",    (1, \xs -> log (1 + head xs)))
-  , ("sqrt",     (1, \xs -> sqrt (head xs)))
-  , ("abs",      (1, \xs -> abs (head xs)))
-  , ("signum",   (1, \xs -> signum (head xs)))
-  , ("recip",    (1, \xs -> recip (head xs)))
-  , ("negate",   (1, \xs -> negate (head xs)))
-  , ("tanh",     (1, \xs -> tanh (head xs)))
-  , ("sin",      (1, \xs -> sin (head xs)))
-  , ("cos",      (1, \xs -> cos (head xs)))
-  , ("logistic", (1, \xs -> 1 / (1 + exp (negate (head xs)))))
-  , ("invLogit", (1, \xs -> 1 / (1 + exp (negate (head xs)))))
-  , ("min",      (2, \xs -> min (xs !! 0) (xs !! 1)))
-  , ("max",      (2, \xs -> max (xs !! 0) (xs !! 1)))
-  ]
-
--- | Phase 43: list builtin (VList→VList) の名前集合。 スカラ 'builtinTable'
--- (= @[a] -> a@) には乗らないので別管理。 softmax 多項ロジット
--- (@Categorical (softmax [η₀, η₁, …])@) で多クラス線形予測子を確率に変換する。
-listBuiltins :: Set.Set Text
-listBuiltins = Set.fromList ["softmax"]
-
--- | 安定 softmax (= @exp(xₖ − max x) / Σ@)。 識別性のため基準クラスは η=0 を
--- 明示的に並べる前提 (例 @softmax [0, b₁·x, b₂·x]@)。 空リストはエラー。
-softmaxList :: forall a. (Floating a, Ord a) => [a] -> Err [a]
-softmaxList [] = Left "softmax: 空リストには適用できません (クラス数 ≥ 1 の [..] が必要です)"
-softmaxList xs =
-  let m  = maximum xs
-      es = map (\x -> exp (x - m)) xs
-      s  = sum es
-  in Right (map (/ s) es)
-
--- | Value を数値に落とす (= 親切な日本語エラー)。
-asNum :: Value a -> Err a
-asNum (VNum x)     = Right x
-asNum (VBool _)    = Left "真偽値が数値の位置に現れました (比較式を算術に混ぜていませんか)"
-asNum (VClosure{}) = Left "関数が数値の位置に現れました (引数不足か、 適用し忘れ)"
-asNum (VBuiltin n _) = Left ("組込関数 " <> n <> " が数値の位置に現れました (引数を渡してください)")
-asNum (VList _)    = Left "リストが数値の位置に現れました (list 引数はスカラとして使えません)"
-asNum (VErr msg)   = Left msg
-
--- | Value を真偽に落とす。
-asBool :: Value a -> Err Bool
-asBool (VBool b)    = Right b
-asBool (VNum _)     = Left "数値が真偽の位置に現れました (if の条件は比較式である必要があります)"
-asBool (VClosure{}) = Left "関数が真偽の位置に現れました"
-asBool (VBuiltin n _) = Left ("組込関数 " <> n <> " が真偽の位置に現れました")
-asBool (VList _)    = Left "リストが真偽の位置に現れました"
-asBool (VErr msg)   = Left msg
-
--- | Value を list に落とす (Phase 42: 多値分布の list 引数評価)。 各要素は
--- 'asNum' でスカラに落とせる前提。
-asList :: Value a -> Err [Value a]
-asList (VList xs)    = Right xs
-asList (VNum _)      = Left "数値がリストの位置に現れました (list 引数には [..] を渡してください)"
-asList (VBool _)     = Left "真偽値がリストの位置に現れました"
-asList (VClosure{})  = Left "関数がリストの位置に現れました"
-asList (VBuiltin n _) = Left ("組込関数 " <> n <> " がリストの位置に現れました")
-asList (VErr msg)    = Left msg
-
--- | Value を行列 ([[a]]) に落とす (Phase 44: MvNormal の cov / lkjCorrCholesky の
--- 行列値を評価する用途)。 VList-of-VList を期待し、 各内側 VList の長さ一致と
--- 数値性を検査する。 list 操作で書くのは DSL スカラ値 'Value' 上の小さい構造
--- 変換のためで、 hmatrix Matrix 経路ではない (density 計算側の choleskyL /
--- forwardSub は既存実装を流用)。
-asMatrix :: Value a -> Err [[a]]
-asMatrix v = do
-  rows <- asList v
-  mat  <- mapM (\r -> asList r >>= mapM asNum) rows
-  case mat of
-    []        -> Left "行列が空です (cov / 行列引数には [[..],..] が必要です)"
-    (r0 : rs)
-      | all ((== length r0) . length) rs -> Right mat
-      | otherwise -> Left "行列の各行の長さが揃っていません (cov は正方行列である必要があります)"
-
--- | スカラ式評価 (= 数値を返す)。 'evalValue' の薄いラッパ。 col 参照は
--- mi=Just i なら行 i、 Nothing なら不可。
-evalScalar
-  :: forall a. (Floating a, Ord a)
-  => EnvA a -> DataMap -> Maybe Int -> Expr -> Err a
-evalScalar env dataMap mi e = evalValue env dataMap mi e >>= asNum
-
--- | Phase 27 §F-3a: 式を 'Value' に評価する interpreter 中核。
--- 適用 (EApp) / if (EIf) / 比較・論理 (EOp) / lambda (ELam) / let を解釈。
--- ユーザ定義関数 (一階・カリー化) と組込数学関数を呼べる。
--- 再帰なし (total) ・ IO/ADT/型クラスなし。
-evalValue
-  :: forall a. (Floating a, Ord a)
-  => EnvA a -> DataMap -> Maybe Int -> Expr -> Err (Value a)
-evalValue env dataMap mi = go
-  where
-    go :: Expr -> Err (Value a)
-    go (ELit (LNumber d)) = Right (VNum (liftD d))
-    go (ELit (LBool b))   = Right (VBool b)
-    go (ELit (LText t))   = Left ("文字列リテラル \"" <> t <> "\" は数式の中では使えません")
-    go (EVar n) = case Map.lookup n env of
-      Just v  -> Right v
-      Nothing -> case Map.lookup n (builtinTable :: Map.Map Text (Int, [a] -> a)) of
-        Just (ar, _) -> Right (VBuiltin n ar)
-        Nothing
-          -- Phase 43: list builtin (softmax 等、 VList→VList) はスカラ
-          -- builtinTable に乗らないので別途 VBuiltin として解決する。
-          | n `Set.member` listBuiltins -> Right (VBuiltin n 1)
-          | otherwise -> Left ("未定義の変数です: " <> n)
-    go (ECol c) = case mi of
-      Nothing -> Left ("列参照 '" <> c <> "' は行ごとの評価文脈 (observe の per-row) でのみ使えます")
-      Just i  -> case Map.lookup c dataMap of
-        Just col -> case drop i (colDoubles col) of
-          (x : _) -> Right (VNum (liftD x))
-          []      -> Left ("列のインデックスが範囲外です: " <> c <> "[" <> T.pack (show i) <> "]")
-        Nothing -> Left ("未知の列です: " <> c)
-    go (ENeg e) = do v <- go e; x <- asNum v; Right (VNum (negate x))
-    go (EOp op a b) = evalOp op a b
-    go (EIf c a b) = do
-      cv <- go c
-      cond <- asBool cv
-      if cond then go a else go b
-    go (ELet bs body) = do
-      env' <- foldEnv env bs
-      evalValue env' dataMap mi body
-    go (ELam x body) = Right (VClosure env [x] body)
-    go appE@(EApp _ _) =
-      let (h, args) = spine appE []
-      in do
-        hv <- go h
-        argVs <- mapM go args
-        applyValue hv argVs
-    -- Phase 42: list リテラルを VList に評価。 多値分布 (Categorical /
-    -- OrderedLogistic) の list 引数で使う。 要素は順次スカラ評価される。
-    go (EList xs) = VList <$> mapM go xs
-    go (EDo _ _) = Left "入れ子の do ブロックは未対応です"
-
-    -- 適用の脊柱を平坦化: f x y → (f, [x, y])。
-    spine :: Expr -> [Expr] -> (Expr, [Expr])
-    spine (EApp f x) acc = spine f (x : acc)
-    spine e acc = (e, acc)
-
-    -- 値を引数列に適用 (一階・カリー化)。
-    applyValue :: Value a -> [Value a] -> Err (Value a)
-    applyValue v [] = Right v
-    -- Phase 43: list builtin (softmax)。 VList を取り VList を返す (多項ロジット:
-    -- Categorical (softmax [η₀, η₁, …]))。 スカラ builtinTable とは別経路。
-    applyValue (VBuiltin "softmax" _) args = case args of
-      [VList xs] -> do ns <- mapM asNum xs; VList . map VNum <$> softmaxList ns
-      [_]        -> Left "softmax はリスト引数 ([..]) が必要です"
-      _          -> Left ("softmax はリスト引数 1 個が必要ですが " <> T.pack (show (length args)) <> " 個でした")
-    applyValue (VBuiltin name ar) args =
-      case Map.lookup name (builtinTable :: Map.Map Text (Int, [a] -> a)) of
-        Nothing -> Left ("内部エラー: 未知の組込関数 " <> name)
-        Just (_, impl)
-          | length args == ar -> do ns <- mapM asNum args; Right (VNum (impl ns))
-          | length args <  ar -> Left ("組込関数 " <> name <> " は引数 "
-              <> T.pack (show ar) <> " 個が必要ですが " <> T.pack (show (length args))
-              <> " 個でした (部分適用は未対応)")
-          | otherwise -> Left ("組込関数 " <> name <> " に引数が多すぎます (必要 "
-              <> T.pack (show ar) <> " 個)")
-    applyValue (VClosure cenv params body) args = applyClosure cenv params body args
-    applyValue (VNum _) _  = Left "数値を関数として適用しています (関数ではない値に引数を渡しています)"
-    applyValue (VBool _) _ = Left "真偽値を関数として適用しています"
-    applyValue (VList _) _ = Left "リストを関数として適用しています"
-    applyValue (VErr m) _  = Left m
-
-    -- クロージャ適用: 引数を仮引数に順次束縛。 仮引数が尽きたら (= 完全
-    -- 適用) 本体を評価して残り引数をさらに適用、 引数が尽きたが仮引数が
-    -- 残るなら部分適用 (VClosure を返す)。 節順が重要: 完全適用
-    -- (ps=[], args=[]) は本体評価を先に判定する。
-    applyClosure :: EnvA a -> [Text] -> Expr -> [Value a] -> Err (Value a)
-    applyClosure cenv [] body args = do
-      r <- evalValue cenv dataMap mi body
-      applyValue r args
-    applyClosure cenv ps body [] = Right (VClosure cenv ps body)
-    applyClosure cenv (p : ps) body (a : as) =
-      applyClosure (Map.insert p a cenv) ps body as
-
-    evalOp :: Text -> Expr -> Expr -> Err (Value a)
-    evalOp op a b = case op of
-      "+"  -> num2 (+)
-      "-"  -> num2 (-)
-      "*"  -> num2 (*)
-      "/"  -> num2 (/)
-      "**" -> num2 (**)
-      "^"  -> num2 (**)   -- DSL では Double 冪 (整数冪に限定しない)
-      "==" -> cmp (==)
-      "/=" -> cmp (/=)
-      "<"  -> cmp (<)
-      "<=" -> cmp (<=)
-      ">"  -> cmp (>)
-      ">=" -> cmp (>=)
-      "&&" -> bool2 (&&)
-      "||" -> bool2 (||)
-      _    -> Left ("モデル中で未対応の演算子です: " <> op)
-      where
-        num2 f  = do av <- asNum =<< go a; bv <- asNum =<< go b; Right (VNum (f av bv))
-        cmp f   = do av <- asNum =<< go a; bv <- asNum =<< go b; Right (VBool (f av bv))
-        bool2 f = do av <- asBool =<< go a; bv <- asBool =<< go b; Right (VBool (f av bv))
-
-    foldEnv :: EnvA a -> [Bind] -> Err (EnvA a)
-    foldEnv e [] = Right e
-    foldEnv e (Bind n v : rest) = do
-      vv <- evalValue e dataMap mi v
-      foldEnv (Map.insert n vv e) rest
-
--- ===========================================================================
--- Phase 27 §F-3a: top-level 束縛環境
--- ===========================================================================
-
--- | 1 つの top-level 値/関数束縛 (= ユーザが model と並べて書く
--- `tmpvar = 1` / `linkfunc x = log x`)。 model (= do-block 束縛) は含まない。
-data TopBind = TopBind
-  { tbName   :: Text
-  , tbParams :: [Text]
-  , tbBody   :: Expr
-  } deriving (Show)
-
--- | top-level 束縛から評価環境を組む。 値束縛 (引数なし) は env の中で
--- 遅延評価して 'Value' に (相互参照は laziness で解決、 再帰は無い前提)。
--- 関数束縛 (引数あり) は env を捕捉した 'VClosure' に。 評価失敗は 'VErr'
--- として運び、 実際に参照された時にエラーを出す。
-buildTopEnv :: forall a. (Floating a, Ord a) => DataMap -> [TopBind] -> EnvA a
-buildTopEnv dataMap binds = env
-  where
-    env :: EnvA a
-    env = Map.fromList [ (tbName b, toVal b) | b <- binds ]
-    toVal b
-      | null (tbParams b) = case evalValue env dataMap Nothing (tbBody b) of
-          Right v -> v
-          Left e  -> VErr e
-      | otherwise = VClosure env (tbParams b) (tbBody b)
-
--- ===========================================================================
--- Phase 27 §F-3c: GLMM plate (forEachGroup)
--- ===========================================================================
---
--- `forEachGroup "gcol" $ \g -> do { … }` は群列 gcol の distinct 値ごとに
--- 内部 do-block を展開する専用構文。 native ModelP の
--- `forM_ groups $ \j -> do { theta <- sample ("theta_"++show j) …; observe … }`
--- (= Phase37 demo randomSlope/multiLevel) を AST 経由で再現する。
---   * sample / observe 名は群値で suffix を付け、 群ごとに別 latent にする。
---   * observe は当該群の行のみを対象にする (= 行サブセット)。
---   * lambda 引数 g は群値 (Double) に束縛 (式で使える)。
---   * nest 可能 (forEachGroup の中に forEachGroup): suffix 連結 + 行積集合。
-
--- | plate 評価コンテキスト。 top-level は 'topCtx' = ("", 全行) で従来挙動を保つ。
-data PlateCtx = PlateCtx
-  { pcSuffix :: Text          -- sample/observe 名に付ける suffix (例 "_1_2")
-  , pcRows   :: Maybe [Int]    -- observe 対象行 (Nothing = 全行)
-  }
-
-topCtx :: PlateCtx
-topCtx = PlateCtx "" Nothing
-
--- | `forEachGroup "gcol" (\g -> do { … })` を検出し (群列, 引数名, 内部 stmts)。
-matchForEachGroup :: Expr -> Maybe (Text, Text, [DoStmt])
-matchForEachGroup e = case collectApp e of
-  Right ("forEachGroup", [ELit (LText gcol), ELam param body]) ->
-    Just (gcol, param, lamBodyToStmts body)
-  _ -> Nothing
-
--- | lambda 本体を DoStmt 列に。 do なら stmts + 末尾式、 それ以外は単一 DoExpr。
-lamBodyToStmts :: Expr -> [DoStmt]
-lamBodyToStmts (EDo stmts ret) = stmts ++ retToStmts ret
-lamBodyToStmts other           = [DoExpr other]
-
--- | do-block 末尾式: pure/return は捨て、 それ以外 (observe 等) は DoExpr に。
-retToStmts :: Expr -> [DoStmt]
-retToStmts r = case r of
-  EApp (EVar "pure") _   -> []
-  EApp (EVar "return") _ -> []
-  ELit (LBool _)         -> []
-  _                      -> [DoExpr r]
-
--- | ctx の対象行に限定した群列の distinct 値 (昇順)。
-groupValsIn :: DataMap -> Text -> Maybe [Int] -> [Double]
-groupValsIn dm gcol mrows =
-  let col  = lookupDoubles gcol dm
-      idxs = fromMaybe [0 .. length col - 1] mrows
-  in sort (nub [ col !! i | i <- idxs, i >= 0, i < length col ])
-
--- | ctx の対象行のうち 群列 == gval の行 index。
-rowsForGroup :: DataMap -> Text -> Double -> Maybe [Int] -> [Int]
-rowsForGroup dm gcol gval mrows =
-  let col  = lookupDoubles gcol dm
-      idxs = fromMaybe [0 .. length col - 1] mrows
-  in [ i | i <- idxs, i >= 0, i < length col, col !! i == gval ]
-
--- | 群値を name suffix に (整数なら "_3"、 非整数なら小数表記)。
--- 群列は整数コード前提なので通常は整数 suffix。
-groupSuffix :: Double -> Text
-groupSuffix g
-  | g == fromIntegral (round g :: Integer) = "_" <> T.pack (show (round g :: Integer))
-  | otherwise                              = "_" <> T.pack (show g)
-
--- | 群 suffix (Phase 41.4)。 群列が 'Factor' で code が level を指し、 その
--- level が安全な識別子 (先頭英字/下線 + 英数字/下線のみ) なら可読 suffix
--- "_<level>" (例 "_setosa")、 それ以外は 'groupSuffix' (数値 code suffix) に
--- フォールバック。 charset / 衝突安全のため不安全な level は code に落とす。
--- interpStmts / collectObsInstances / plateRenameMap の 3 経路で同一規律を使う
--- 必要がある (node 名が一致しないと観測値/グラフが噛み合わない)。
-groupSuffixFor :: Maybe Column -> Double -> Text
-groupSuffixFor (Just (Factor levels _)) g
-  | i >= 0, i < length levels, isSafeIdent (levels !! i) = "_" <> (levels !! i)
-  where i = round g :: Int
-groupSuffixFor _ g = groupSuffix g
-
--- | node 名 suffix に使える安全な識別子か (先頭英字/下線、 以降英数字/下線)。
-isSafeIdent :: Text -> Bool
-isSafeIdent t = case T.uncons t of
-  Nothing          -> False
-  Just (c0, rest)  -> (isAlpha c0 || c0 == '_')
-                        && T.all (\c -> isAlphaNum c || c == '_') rest
-
--- | Distribution AST を Hanalyze.Model.HBM.Distribution に変換。
--- mi が Nothing なら列参照不可。 mi=Just i なら行 i で評価。
-evalDist
-  :: forall a. (Floating a, Ord a)
-  => EnvA a -> DataMap -> Maybe Int -> Expr -> Err (HBM.Distribution a)
-evalDist env dataMap mi expr = do
-  (name, args) <- collectApp expr
-  case (name, args) of
-    ("Normal",     [m, s])    -> mk2 HBM.Normal m s
-    ("HalfNormal", [s])       -> mk1 HBM.HalfNormal s
-    ("Beta",       [a, b])    -> mk2 HBM.Beta a b
-    ("Gamma",      [s, r])    -> mk2 HBM.Gamma s r
-    ("Exponential", [r])      -> mk1 HBM.Exponential r
-    ("Poisson",    [l])       -> mk1 HBM.Poisson l
-    ("Bernoulli",  [p])       -> mk1 HBM.Bernoulli p
-    ("Uniform",    [l, h])    -> mk2 HBM.Uniform l h
-    ("StudentT",   [df, m, s]) -> mk3 HBM.StudentT df m s
-    ("Cauchy",     [l, s])    -> mk2 HBM.Cauchy l s
-    ("HalfCauchy", [s])       -> mk1 HBM.HalfCauchy s
-    ("LogNormal",  [m, s])    -> mk2 HBM.LogNormal m s
-    -- Phase 42: 多値 categorical 応答。 list 引数は VList 経由でスカラ列に
-    -- 評価する (observe は factor code 0..K-1)。 2 値応答は Bernoulli +
-    -- factor code 0/1 で Phase 41.5 対応済。
-    ("Categorical", [probs])  -> HBM.Categorical <$> evalList probs
-    ("OrderedLogistic", [eta, cuts]) -> HBM.OrderedLogistic <$> eval eta <*> evalList cuts
-    -- Phase 44: 多変量正規 (観測専用)。 mu = 平均ベクトル ([a])、 cov = full Σ
-    -- (VList-of-VList → [[a]])。 observeMV 経由で k-vector を観測する。 cov の
-    -- 正定値性は density 評価時 (choleskyL→-∞) 任せ、 ここでは正方性のみ検査。
-    ("MvNormal", [mu, cov]) -> do
-      muV  <- evalList mu
-      covV <- evalMat cov
-      let k = length muV
-      when (length covV /= k)
-        (Left ("MvNormal: 平均ベクトル長 " <> T.pack (show k)
-               <> " と共分散の行数 " <> T.pack (show (length covV)) <> " が一致しません"))
-      when (any ((/= k) . length) covV)
-        (Left ("MvNormal: 共分散は " <> T.pack (show k) <> "×" <> T.pack (show k)
-               <> " 正方行列である必要があります"))
-      Right (HBM.MvNormal muV covV)
-    -- Phase 44: scale vector σ + 相関 Cholesky L パラメタ化 (観測専用)。 L は
-    -- lkjCorrCholesky bind 由来の VList-of-VList。 covariance = (diag σ·L)(diag σ·L)ᵀ。
-    ("MvNormalChol", [mu, sigma, lExpr]) -> do
-      muV    <- evalList mu
-      sigmaV <- evalList sigma
-      lV     <- evalMat lExpr
-      let k = length muV
-      when (length sigmaV /= k)
-        (Left ("MvNormalChol: 平均ベクトル長 " <> T.pack (show k)
-               <> " と scale ベクトル長 " <> T.pack (show (length sigmaV)) <> " が一致しません"))
-      when (length lV /= k || any ((/= k) . length) lV)
-        (Left ("MvNormalChol: 相関 Cholesky L は " <> T.pack (show k) <> "×"
-               <> T.pack (show k) <> " 行列である必要があります"))
-      Right (HBM.MvNormalChol muV sigmaV lV)
-    -- Phase 45: 混合分布 (スカラ単一列、 観測は scalar observe)。 第 1 引数 =
-    -- 重みベクトル ([a]、 literal `[0.3,0.7]` or dirichlet 由来 VList ref、 既存
-    -- evalList で評価)、 第 2 引数 = 成分分布リスト (EList の各要素を **再帰
-    -- evalDist** = `[Distribution a]`)。 component 数 K は EList 要素数で静的決定。
-    -- MvNormal (Phase 44) と異なり multi-column ではない (logDensity はスカラ x)。
-    ("Mixture", [weights, EList distExprs]) -> do
-      ws    <- evalList weights
-      comps <- mapM (evalDist env dataMap mi) distExprs
-      when (null comps)
-        (Left "Mixture: 成分分布が空です (第 2 引数に少なくとも 1 つの分布が必要)")
-      when (length ws /= length comps)
-        (Left ("Mixture: 重み数 " <> T.pack (show (length ws))
-               <> " と成分分布数 " <> T.pack (show (length comps)) <> " が一致しません"))
-      Right (HBM.Mixture ws comps)
-    _ -> Left ("Unsupported distribution: " <> name <> " with " <> T.pack (show (length args)) <> " args")
-  where
-    eval = evalScalar env dataMap mi
-    -- list 引数 ([a]): EList を VList に評価 → 各要素をスカラに。
-    evalList e = evalValue env dataMap mi e >>= asList >>= mapM asNum
-    -- 行列引数 ([[a]]): VList-of-VList に評価 → 'asMatrix' で正方性検査。
-    evalMat e = evalValue env dataMap mi e >>= asMatrix
-    mk1 f a       = f <$> eval a
-    mk2 f a b     = f <$> eval a <*> eval b
-    mk3 f a b c   = f <$> eval a <*> eval b <*> eval c
-
--- | k-vector 観測を取る多変量分布か。 @observeMV@ (Phase 44) はこれらのみ
--- 受理し、 scalar 分布が渡されたら親切エラーにする。 obsLogSum
--- (HBM.hs:987) が chunk 処理する分布と対応する。
-isMultivariateDist :: HBM.Distribution a -> Bool
-isMultivariateDist d = HBM.distName d `elem`
-  [ "MvNormal", "MvNormalChol", "MvStudentT"
-  , "Multinomial", "DirichletMultinomial", "Wishart" ]
-
--- 'collectApp' は Hanalyze.Model.HBM.Ast から import。
-
--- ===========================================================================
--- Phase 43: list 値 Model combinator (latent vector を返す DoBind)
--- ===========================================================================
---
--- 現 'DoBind' は scalar @sample@ 専用 (= @x <- Dist …@ で 1 値)。 だが
--- @cuts <- orderedCuts "cut" 2 (-2) 1@ や @probs <- dirichlet "pi" [1,1,1]@ の
--- ように **latent vector を返す** Model combinator (HBM.orderedCuts /
--- HBM.dirichlet、 いずれも @Model a [a]@) は scalar に乗らない。 これらは
--- 'evalDist' (= Distribution を返す) とは別経路で、 DoBind の RHS を
--- 'matchListComb' で検出し、 Model モナドで実行して 'VList' に束縛する。
---
--- 消費側 (@OrderedLogistic eta cuts@ / @Categorical probs@) は env 内で cuts /
--- probs が VList に束縛されるので Phase 42 の evalList (evalValue >>= asList >>=
--- mapM asNum) がそのまま解決する (本機構の追加は **bind 側のみ**)。
-
--- | 検出した list 値 combinator 呼び出し (引数は評価済 = base 名 + 構築情報)。
--- 結果ベクトルの長さは引数から静的に決まる ('listCombLen')。
-data ListComb a
-  = OrderedCutsComb Text Int a a   -- ^ name, nCuts (= K-1 ≥ 1), cMin, HalfNormal scale
-  | DirichletComb   Text [a]       -- ^ name, α 集中度ベクトル (長さ K ≥ 2)
-
--- | combinator が返すベクトルの長さ (validateStmts の placeholder VList 長 /
--- interpStmts は実値から決まるので参照不要)。
-listCombLen :: ListComb a -> Int
-listCombLen (OrderedCutsComb _ n _ _) = n
-listCombLen (DirichletComb _ as)      = length as
-
--- | base 名に plate suffix を付ける (forEachGroup 内で群ごとに別 latent にする)。
-listCombSuffix :: Text -> ListComb a -> ListComb a
-listCombSuffix suf (OrderedCutsComb nm n cm sc) = OrderedCutsComb (nm <> suf) n cm sc
-listCombSuffix suf (DirichletComb nm as)        = DirichletComb (nm <> suf) as
-
--- | DoBind の RHS が list 値 combinator (orderedCuts / dirichlet) なら
--- 引数を評価して 'ListComb' に。 combinator でなければ 'Nothing' (= scalar
--- sample 経路へ)。 名前は文字列リテラル必須、 nCuts は数値リテラル必須
--- (静的に長さを決めるため。 Phase 43 当面の制約、 doc 想定リスク参照)。
-matchListComb
-  :: forall a. (Floating a, Ord a)
-  => EnvA a -> DataMap -> Expr -> Maybe (Err (ListComb a))
-matchListComb env dataMap expr = case collectApp expr of
-  Right ("orderedCuts", [nameE, nCutsE, cMinE, scaleE]) -> Just $ do
-    nm <- textLit "orderedCuts" nameE
-    n  <- intLit  "orderedCuts" nCutsE
-    when (n < 1) (Left "orderedCuts: カット数 (第 2 引数) は 1 以上である必要があります")
-    cm <- evalScalar env dataMap Nothing cMinE
-    sc <- evalScalar env dataMap Nothing scaleE
-    Right (OrderedCutsComb nm n cm sc)
-  Right ("dirichlet", [nameE, alphasE]) -> Just $ do
-    nm <- textLit "dirichlet" nameE
-    as <- evalValue env dataMap Nothing alphasE >>= asList >>= mapM asNum
-    when (length as < 2) (Left "dirichlet: α ベクトル (第 2 引数) は長さ 2 以上の [..] である必要があります")
-    Right (DirichletComb nm as)
-  _ -> Nothing
-  where
-    textLit _ (ELit (LText t)) = Right t
-    textLit fn _ = Left (fn <> " の名前引数 (第 1 引数) は文字列リテラルである必要があります")
-    intLit :: Text -> Expr -> Err Int
-    intLit _ (ELit (LNumber d)) = Right (round d)
-    intLit fn _ = Left (fn <> " のカット数引数 (第 2 引数) は数値リテラルである必要があります (変数経由は未対応)")
-
--- | 'ListComb' を実際の Model アクション (latent vector を sample) に。
-runListComb :: forall a. (Floating a, Ord a) => ListComb a -> HBM.Model a [a]
-runListComb (OrderedCutsComb nm n cm sc) = HBM.orderedCuts nm n cm sc
-runListComb (DirichletComb nm as)        = HBM.dirichlet nm as
-
--- ===========================================================================
--- Phase 44: 行列値 Model combinator (latent 相関行列を返す DoBind)
--- ===========================================================================
---
--- 'ListComb' (Phase 43、 @Model a [a]@) の行列版。 @lkjCorrCholesky@ は
--- @Model a [[a]]@ で k×k 下三角の相関 Cholesky 因子 L を返す latent
--- combinator。 @L <- lkjCorrCholesky "L" 2 2.0@ を 'VList'-of-'VList' に束縛し、
--- 消費側 ('MvNormalChol' の第 3 引数) は env 内の VList-of-VList を 'asMatrix'
--- で解決する。 内部 latent (@L_pc*@ / @L_L*@ 等) は Model が自動登録する
--- (DSL は latent を再実装しない)。
-
--- | 検出した行列値 combinator 呼び出し (引数評価済)。
-data MatrixComb a
-  = LkjCholComb Text Int a   -- ^ name, dim k (≥ 2), eta (LKJ 集中度)
-
--- | combinator が返す行列の次元 k (validateStmts の placeholder 用)。
-matrixCombDim :: MatrixComb a -> Int
-matrixCombDim (LkjCholComb _ k _) = k
-
--- | base 名に plate suffix を付ける (群ごとに別 latent にする)。
-matrixCombSuffix :: Text -> MatrixComb a -> MatrixComb a
-matrixCombSuffix suf (LkjCholComb nm k eta) = LkjCholComb (nm <> suf) k eta
-
--- | DoBind の RHS が行列値 combinator (lkjCorrCholesky) なら引数を評価して
--- 'MatrixComb' に。 combinator でなければ 'Nothing'。 名前は文字列リテラル、
--- 次元 k は数値リテラル必須 (静的に行列サイズを決めるため)。
-matchMatrixComb
-  :: forall a. (Floating a, Ord a)
-  => EnvA a -> DataMap -> Expr -> Maybe (Err (MatrixComb a))
-matchMatrixComb env dataMap expr = case collectApp expr of
-  Right ("lkjCorrCholesky", [nameE, kE, etaE]) -> Just $ do
-    nm  <- textLit nameE
-    k   <- intLit  kE
-    when (k < 2) (Left "lkjCorrCholesky: 次元 (第 2 引数) は 2 以上である必要があります")
-    eta <- evalScalar env dataMap Nothing etaE
-    Right (LkjCholComb nm k eta)
-  _ -> Nothing
-  where
-    textLit (ELit (LText t)) = Right t
-    textLit _ = Left "lkjCorrCholesky の名前引数 (第 1 引数) は文字列リテラルである必要があります"
-    intLit :: Expr -> Err Int
-    intLit (ELit (LNumber d)) = Right (round d)
-    intLit _ = Left "lkjCorrCholesky の次元引数 (第 2 引数) は数値リテラルである必要があります (変数経由は未対応)"
-
--- | 'MatrixComb' を実際の Model アクション (latent 相関行列を sample) に。
-runMatrixComb :: forall a. (Floating a, Ord a) => MatrixComb a -> HBM.Model a [[a]]
-runMatrixComb (LkjCholComb nm k eta) = HBM.lkjCorrCholesky nm k eta
-
--- | Phase 9.1d-5: stmts を walk して各 latent 変数(DoBind の左辺)に対する
--- Transform を返す。 constrained 空間で 0 初期化は PositiveT で log 0 = -∞
--- 発散するため、 streaming endpoint で transform 別に初期値を選ぶのに使う。
-inferTransforms :: [DoStmt] -> Map.Map Text HD.Transform
-inferTransforms = Map.fromList . concatMap extract
-  where
-    extract (DoBind name distExpr) = case collectApp distExpr of
-      Right (dname, _) -> [(name, distNameToTransform dname)]
-      _ -> [(name, HD.UnconstrainedT)]
-    extract _ = []
-
-    distNameToTransform "Normal"       = HD.UnconstrainedT
-    distNameToTransform "StudentT"     = HD.UnconstrainedT
-    distNameToTransform "Cauchy"       = HD.UnconstrainedT
-    distNameToTransform "Uniform"      = HD.UnconstrainedT
-    distNameToTransform "HalfNormal"   = HD.PositiveT
-    distNameToTransform "HalfCauchy"   = HD.PositiveT
-    distNameToTransform "Gamma"        = HD.PositiveT
-    distNameToTransform "Exponential"  = HD.PositiveT
-    distNameToTransform "LogNormal"    = HD.PositiveT
-    distNameToTransform "InverseGamma" = HD.PositiveT
-    distNameToTransform "Weibull"      = HD.PositiveT
-    distNameToTransform "Beta"         = HD.UnitIntervalT
-    distNameToTransform "Bernoulli"    = HD.UnitIntervalT
-    distNameToTransform _              = HD.UnconstrainedT
-
--- ===========================================================================
--- AST → Model モナド構築
--- ===========================================================================
-
--- | EDo 内の各 stmt を Model モナドに翻訳。実装は 'forall a' のもとに
--- 動作する必要があるが、Err は Haskell の純粋値なので外側で先に検査して
--- Model 構築は失敗しない前提にする。エラーは事前検証で全部捕まえる方針。
---
--- ここでは「事前検証ありで、検証通過後に Model を直接組み上げる」設計。
--- ModelP は forall を含む rank-1 polymorphic 型なので Either に直接乗らない
--- (ImpredicativeTypes を避けるため)。validate と build を分離する。
-validateAst :: [TopBind] -> Expr -> DataMap -> Either Text [DoStmt]
-validateAst topBinds body0 dataMap = do
-  rawStmts <- case body0 of
-    -- Phase 13 §9.3c-2: frontend parser は最終 DoExpr を ret として分離する
-    -- (hanalyze 慣行で「observe が最後 + pure 省略」 が許される)。 ret が
-    -- pure / return のときは捨て、 それ以外(例: observe)は body 末尾に
-    -- DoExpr として戻して扱う。
-    EDo s ret ->
-      let isDiscard = case ret of
-            EApp (EVar "pure") _ -> True
-            EApp (EVar "return") _ -> True
-            ELit (LBool _) -> True   -- frontend の implicit ret fallback
-            _ -> False
-          full = if isDiscard then s else s ++ [DoExpr ret]
-      in Right full
-    _ -> Left "Model body must be a do-block (`do { ... }`)"
-  -- Phase 26.1 §A-2 (2026-05-27): `x <- Data "label" expr` を pre-process で
-  -- substitute (= 列 alias 経路)。 詳細は streaming bridge/.../HbmAst.hs の同名
-  -- 関数 doc 参照。 hanalyze の pm.Data 厳密対応 (withData 経由) は将来 phase。
-  let stmts = preprocessAliases rawStmts
-  validateStmts topBinds dataMap stmts
-  pure stmts
-
--- | Phase 26.1 §A-2 alias 経路 (2026-05-27 王道方針に切替): Haskell の `let`
--- を syntactic alias として扱う pre-processing。 spec §4.2 と整合
--- (= ∀LIC∃Code は Haskell サブセット、 `let x = col "..."` で Expression
--- Language alias)。 詳細は streaming bridge/.../HbmAst.hs の同名関数 doc 参照。
-preprocessAliases :: [DoStmt] -> [DoStmt]
-preprocessAliases = go Map.empty
-  where
-    go _ [] = []
-    go aliases (s : rest) = case extractLetAliases aliases s of
-      Just (newAliases, mStmt) -> case mStmt of
-        Nothing   -> go newAliases rest
-        Just stmt -> stmt : go newAliases rest
-      Nothing ->
-        substituteStmt aliases s : go aliases rest
-
-    extractLetAliases
-      :: Map.Map Text Expr -> DoStmt
-      -> Maybe (Map.Map Text Expr, Maybe DoStmt)
-    extractLetAliases aliases (DoLet bs) =
-      let (newAliases, kept) = foldl step (aliases, []) bs
-          step (al, acc) (Bind n v) =
-            let substV = substitute al v
-            in if hasColRef substV
-                 then (Map.insert n substV al, acc)
-                 else (al, acc ++ [Bind n substV])
-      in Just (newAliases, if null kept then Nothing else Just (DoLet kept))
-    extractLetAliases _ _ = Nothing
-
-    substituteStmt :: Map.Map Text Expr -> DoStmt -> DoStmt
-    substituteStmt aliases (DoBind n v) = DoBind n (substitute aliases v)
-    substituteStmt aliases (DoLet bs)   = DoLet (map (substBind aliases) bs)
-    substituteStmt aliases (DoExpr e)   = DoExpr (substitute aliases e)
-
-    substBind :: Map.Map Text Expr -> Bind -> Bind
-    substBind aliases (Bind n v) = Bind n (substitute aliases v)
-
-    substitute :: Map.Map Text Expr -> Expr -> Expr
-    substitute env = go'
-      where
-        go' (EVar n) = case Map.lookup n env of
-          Just e  -> e
-          Nothing -> EVar n
-        go' (EOp op a b) = EOp op (go' a) (go' b)
-        go' (EApp f x)   = EApp (go' f) (go' x)
-        go' (ENeg e)     = ENeg (go' e)
-        go' (ELet bs body) = ELet (map (substBind env) bs) (go' body)
-        go' (EList xs)   = EList (map go' xs)
-        go' (EIf c a b)  = EIf (go' c) (go' a) (go' b)
-        go' (ELam x b)   = ELam x (go' b)
-        go' (EDo s r)    = EDo s r   -- 入れ子 do は触らない
-        go' x            = x
-
--- | 静的検証(変数名スコープ / 列存在)。型は (Double, Double) 環境で一度評価して
--- 実行時エラーが起きないかを確認する。
-validateStmts :: [TopBind] -> DataMap -> [DoStmt] -> Err ()
-validateStmts topBinds dataMap stmts0 = go (buildTopEnv dataMap topBinds) stmts0
-  where
-    go :: EnvA Double -> [DoStmt] -> Err ()
-    go _env [] = Right ()
-    -- Phase 43: RHS が list 値 combinator (orderedCuts / dirichlet) なら、
-    -- 構造検証 + 結果長 K の placeholder VList を束縛する (Model モナドが無い
-    -- 検証経路では実行できないため。 後続 Categorical/OrderedLogistic の検証が
-    -- 通るように長さだけ合わせる)。
-    go env (DoBind name distExpr : rest)
-      | Just ecomb <- matchListComb env dataMap distExpr :: Maybe (Err (ListComb Double)) = do
-          comb <- ecomb
-          let k = listCombLen comb
-          go (Map.insert name (VList (replicate k (VNum 0.0))) env) rest
-    -- Phase 44: 行列値 combinator は k×k placeholder VList-of-VList を束縛する
-    -- (Model モナドが無い検証経路では実行できないため、 次元だけ合わせる)。
-    go env (DoBind name distExpr : rest)
-      | Just ecomb <- matchMatrixComb env dataMap distExpr :: Maybe (Err (MatrixComb Double)) = do
-          comb <- ecomb
-          let k = matrixCombDim comb
-          go (Map.insert name (VList (replicate k (VList (replicate k (VNum 0.0))))) env) rest
-    go env (DoBind name distExpr : rest) = do
-      _ <- if hasColRef distExpr
-             then Left ("sample distribution cannot reference data column: " <> name)
-             else evalDist env dataMap Nothing distExpr :: Err (HBM.Distribution Double)
-      go (Map.insert name (VNum 0.0) env) rest
-    go env (DoLet bs : rest) = do
-      env' <- foldEnv env bs
-      go env' rest
-    go env (DoExpr e : rest)
-      -- Phase 27 §F-3c: forEachGroup は群列の存在を確認し、 内部 stmts を
-      -- 代表コンテキスト (param = 0) で検証する (行サブセットは検証不要)。
-      | Just (gcol, param, inner) <- matchForEachGroup e = do
-          when (Map.notMember gcol dataMap)
-            (Left ("forEachGroup の群列が見つかりません: " <> gcol))
-          go (Map.insert param (VNum 0) env) inner
-          go env rest
-      | otherwise = do
-          -- observe の構文形を検査
-          validateObserve env e
-          go env rest
-
-    foldEnv e [] = Right e
-    foldEnv e (Bind n v : rest) = do
-      vv <- evalValue e dataMap Nothing v
-      foldEnv (Map.insert n vv e) rest
-
-    validateObserve env e = do
-      (fname, args) <- collectApp e
-      case (fname, args) of
-        ("observe", [ELit (LText _obsName), distExpr, dataRef]) -> do
-          colName <- requireColRef dataRef
-          when (Map.notMember colName dataMap) (Left ("Unknown column in observe: " <> colName))
-          if hasColRef distExpr
-            then do
-              -- 列参照あり: 各行で評価できることを確認(row 0 でテスト)
-              _ <- evalDist env dataMap (Just 0) distExpr :: Err (HBM.Distribution Double)
-              pure ()
-            else do
-              _ <- evalDist env dataMap Nothing distExpr :: Err (HBM.Distribution Double)
-              pure ()
-        -- Phase 44: multi-column observe。 第 3 引数は観測列リスト ['y1','y2',..]。
-        -- 全列の存在 + 列数 ≥ 2 + 列長一致 + dist が多変量かを検査する。 dist の
-        -- μ/cov は行不変前提なので row 参照不可 (Nothing) で評価する。
-        ("observeMV", [ELit (LText _obsName), distExpr, EList colRefs]) -> do
-          cols <- mapM requireColRef colRefs
-          when (length cols < 2)
-            (Left "observeMV: 観測列は 2 列以上必要です (1 列なら scalar observe を使ってください)")
-          forM_ cols $ \c ->
-            when (Map.notMember c dataMap) (Left ("Unknown column in observeMV: " <> c))
-          let lens = map (length . (`lookupDoubles` dataMap)) cols
-          when (any (/= head lens) (tail lens))
-            (Left "observeMV: 観測列の長さが揃っていません (k-vector 組成には全列同長が必要です)")
-          d <- evalDist env dataMap Nothing distExpr :: Err (HBM.Distribution Double)
-          when (not (isMultivariateDist d))
-            (Left ("observeMV: 第 2 引数は多変量分布 (MvNormal 等) が必要ですが、 scalar 分布 "
-                   <> HBM.distName d <> " が渡されました"))
-        ("observeMV", _) ->
-          Left "observeMV: 第 3 引数は観測列リスト ['y1','y2',..] が必要です"
-        ("pure", _) -> Right ()   -- pure x は最終行用
-        ("return", _) -> Right ()
-        _ -> Left ("Unsupported statement: " <> fname)
-
-    requireColRef (ECol n) = Right n
-    requireColRef _ = Left "observe's third argument must be a column reference 'colname'"
-
--- | 検証通過後の Model 構築(polymorphic in a)。エラーは想定外なので
--- error で落とす(validation で漏れたバグは fail-fast)。
-interpStmts :: [TopBind] -> DataMap -> [DoStmt] -> HBM.ModelP ()
-interpStmts topBinds dataMap stmts = goM topCtx (buildTopEnv dataMap topBinds) stmts
-  where
-    goM :: forall a. (Floating a, Ord a) => PlateCtx -> EnvA a -> [DoStmt] -> HBM.Model a ()
-    goM _ _env [] = pure ()
-    -- Phase 43: list 値 combinator (orderedCuts / dirichlet) は scalar sample
-    -- ではなく Model アクションを実行して latent vector を VList 束縛する。
-    -- base 名に plate suffix を付け、 forEachGroup 内で群ごとに別 latent にする。
-    goM ctx env (DoBind name distExpr : rest)
-      | Just ecomb <- matchListComb env dataMap distExpr = do
-          let comb = case ecomb of
-                Right c -> listCombSuffix (pcSuffix ctx) c
-                Left e  -> error (T.unpack e)
-          xs <- runListComb comb
-          goM ctx (Map.insert name (VList (map VNum xs)) env) rest
-    -- Phase 44: 行列値 combinator (lkjCorrCholesky) は latent 相関 Cholesky 因子を
-    -- Model で実行し、 VList-of-VList に束縛する (MvNormalChol の L 引数で消費)。
-    goM ctx env (DoBind name distExpr : rest)
-      | Just ecomb <- matchMatrixComb env dataMap distExpr = do
-          let comb = case ecomb of
-                Right c -> matrixCombSuffix (pcSuffix ctx) c
-                Left e  -> error (T.unpack e)
-          m <- runMatrixComb comb
-          goM ctx (Map.insert name (VList (map (VList . map VNum) m)) env) rest
-    goM ctx env (DoBind name distExpr : rest) = do
-      let dist = case evalDist env dataMap Nothing distExpr of
-            Right d -> d
-            Left e -> error (T.unpack e)
-      x <- HBM.sample (name <> pcSuffix ctx) dist
-      goM ctx (Map.insert name (VNum x) env) rest
-    goM ctx env (DoLet bs : rest) = do
-      let env' = foldlBinds env bs
-      goM ctx env' rest
-    goM ctx env (DoExpr e : rest)
-      -- Phase 27 §F-3c: forEachGroup は群ごとに内部 do-block を展開する。
-      | Just (gcol, param, inner) <- matchForEachGroup e = do
-          let gvals = groupValsIn dataMap gcol (pcRows ctx)
-          forM_ gvals $ \gval -> do
-            let ctx' = ctx
-                  { pcSuffix = pcSuffix ctx <> groupSuffixFor (Map.lookup gcol dataMap) gval
-                  , pcRows   = Just (rowsForGroup dataMap gcol gval (pcRows ctx))
-                  }
-                env' = Map.insert param (VNum (liftD gval)) env
-            goM ctx' env' inner
-          goM ctx env rest
-      | otherwise = do
-          execObserve ctx env e
-          goM ctx env rest
-
-    foldlBinds :: forall a. (Floating a, Ord a) => EnvA a -> [Bind] -> EnvA a
-    foldlBinds e [] = e
-    foldlBinds e (Bind n v : rs) =
-      let vv = case evalValue e dataMap Nothing v of
-            Right x  -> x
-            Left err -> VErr err  -- 検証通過しているはずなので通常来ない
-      in foldlBinds (Map.insert n vv e) rs
-
-    execObserve :: forall a. (Floating a, Ord a) => PlateCtx -> EnvA a -> Expr -> HBM.Model a ()
-    execObserve ctx env e = case collectApp e of
-      Right ("observe", [ELit (LText obsName), distExpr, ECol colName]) ->
-        let ys      = lookupDoubles colName dataMap
-            allRows = [0 .. length ys - 1]
-            rows    = fromMaybe allRows (pcRows ctx)  -- 群コンテキストなら当該群の行
-            nm      = obsName <> pcSuffix ctx
-        in if hasColRef distExpr
-             then do
-               -- per-row distribution。 対象行のみ observeColumns でまとめる。
-               let pairs = [ (case evalDist env dataMap (Just i) distExpr of
-                                Right d -> d
-                                Left _ -> error "validation should have caught this"
-                             , [ys !! i])
-                           | i <- rows, i >= 0, i < length ys
-                           ]
-               HBM.observeColumns nm pairs
-             else do
-               let dist = case evalDist env dataMap Nothing distExpr of
-                     Right d -> d
-                     Left _ -> error "validation should have caught this"
-               HBM.observe nm dist [ ys !! i | i <- rows, i >= 0, i < length ys ]
-      -- Phase 44: multi-column observe。 dist (μ/cov) は行不変なので 1 回だけ
-      -- 評価し、 観測列だけを行ごとに k-vector に組んで HBM.observeMV に流す。
-      Right ("observeMV", [ELit (LText obsName), distExpr, EList colRefs]) ->
-        let cols    = [ c | ECol c <- colRefs ]
-            colVecs = map (`lookupDoubles` dataMap) cols
-            n       = if null colVecs then 0 else minimum (map length colVecs)
-            allRows = [0 .. n - 1]
-            rows    = fromMaybe allRows (pcRows ctx)
-            nm      = obsName <> pcSuffix ctx
-            dist    = case evalDist env dataMap Nothing distExpr of
-                        Right d -> d
-                        Left _  -> error "validation should have caught this"
-            obss    = [ [ cv !! i | cv <- colVecs ] | i <- rows, i >= 0, i < n ]
-        in HBM.observeMV nm dist obss
-      Right ("pure", _) -> pure ()
-      Right ("return", _) -> pure ()
-      _ -> pure ()  -- validateObserve で弾く想定
-
--- ===========================================================================
--- NUTS 設定 reader
--- ===========================================================================
-
--- | extra から NUTS 設定を読む(欠落時はデフォルト)。
-readChainCount :: A.Object -> Int
-readChainCount o = case KM.lookup (Key.fromText "hbmChains") o of
-  Just (A.Number n) -> let v = floor (realToFrac n :: Double) in max 1 (min 16 v)
-  _ -> 4
-
-readNutsConfig :: A.Object -> NUTS.NUTSConfig
-readNutsConfig o =
-  let
-    def = NUTS.defaultNUTSConfig
-    getInt :: Text -> Int
-    getInt k = case KM.lookup (Key.fromText k) o of
-      Just (A.Number n) -> floor (realToFrac n :: Double)
-      _ -> 0
-    getDbl :: Text -> Double
-    getDbl k = case KM.lookup (Key.fromText k) o of
-      Just (A.Number n) -> realToFrac n
-      _ -> 0
-    getBool' k = case KM.lookup (Key.fromText k) o of
-      Just (A.Bool b) -> b
-      _ -> False
-  in def
-    { NUTS.nutsIterations    = if getInt "hbmIterations" > 0 then getInt "hbmIterations" else NUTS.nutsIterations def
-    , NUTS.nutsBurnIn        = max 0 (getInt "hbmBurnIn")
-    , NUTS.nutsStepSize      = if getDbl "hbmStepSize" > 0 then getDbl "hbmStepSize" else NUTS.nutsStepSize def
-    , NUTS.nutsMaxDepth      = if getInt "hbmMaxDepth" > 0 then getInt "hbmMaxDepth" else NUTS.nutsMaxDepth def
-    , NUTS.nutsAdaptStepSize = getBool' "hbmAdaptStepSize"
-    , NUTS.nutsTargetAccept  = let v = getDbl "hbmTargetAccept" in if v > 0 && v < 1 then v else NUTS.nutsTargetAccept def
-    , NUTS.nutsAdaptMass     = getBool' "hbmAdaptMass"
-    }
-
--- | Phase 13 §9.3c-2: observe ノード名 → 観測列名 のマッピングを取り出す。
--- DSL 構文 @observe "NAME" DIST 'COL'@ から (NAME, COL) を抽出。
--- frontend で「observe ノード "y" の観測値はどの列か」 を解決する用途。
-observeNodeMap :: [DoStmt] -> [(Text, Text)]
-observeNodeMap = concatMap step
-  where
-    step (DoExpr e) = case collectApp e of
-      Right ("observe", [ELit (LText obsName), _distExpr, ECol colName]) ->
-        [(obsName, colName)]
-      _ -> []
-    step _ = []
-
--- ===========================================================================
--- 結果整形 (param summary / posterior mean curves)
--- ===========================================================================
-
-data ParamSummary = ParamSummary
-  { psName :: !Text
-  , psMean :: !Double
-  , psSd   :: !Double
-  , psLow  :: !Double
-  , psHigh :: !Double
-  , psRhat :: !(Maybe Double)
-  , psEss  :: !Double
-  } deriving (Show)
-
--- | SC29: 複数 chain から事後統計を計算。R̂ は split-R̂ (hanalyze)、
--- ESS は Geyer initial monotone(全チェーン pool)。
-paramSummaryMulti :: [MC.Chain] -> Text -> ParamSummary
-paramSummaryMulti chains name =
-  let perChain = map (MC.chainVals name) chains
-      pooled = concat perChain
-      n = length pooled
-      m = if n == 0 then 0 else sum pooled / fromIntegral n
-      sd2 = if n < 2 then 0
-            else sum (map (\v -> (v - m) ** 2) pooled) / fromIntegral (n - 1)
-      sd = sqrt sd2
-      sorted = LA.toList (LA.sortVector (LA.fromList pooled))
-      pct :: Double -> Double
-      pct q = if n == 0 then 0
-              else let idx = max 0 (min (n - 1) (floor (q * fromIntegral (n - 1)) :: Int))
-                   in case drop idx sorted of (x:_) -> x; [] -> 0
-      rh = SMC.rhat perChain
-      e  = SMC.ess pooled
-  in ParamSummary name m sd (pct 0.025) (pct 0.975) rh e
-
-fmtSummary :: ParamSummary -> Text
-fmtSummary p = psName p <> "="
-  <> T.pack (show (round4 (psMean p)))
-  <> "±" <> T.pack (show (round4 (psSd p)))
-
-round4 :: Double -> Double
-round4 x = fromIntegral (round (x * 10000) :: Int) / 10000
-
--- | thinning: stride 飛ばしに要素を取る。
-takeEvery :: Int -> [a] -> [a]
-takeEvery _ []     = []
-takeEvery n (x:xs) = x : takeEvery n (drop (max 0 (n - 1)) xs)
-
-summaryToJson :: ParamSummary -> A.Value
-summaryToJson p = A.object
-  [ Key.fromText "name" A..= psName p
-  , Key.fromText "mean" A..= psMean p
-  , Key.fromText "sd"   A..= psSd p
-  , Key.fromText "ci2_5" A..= psLow p
-  , Key.fromText "ci97_5" A..= psHigh p
-  , Key.fromText "rhat"  A..= psRhat p
-  , Key.fromText "ess"   A..= psEss p
-  ]
-
--- ---------------------------------------------------------------------------
--- Phase NN §A (2026-05-27): HBM posterior predictive mean curves
--- ---------------------------------------------------------------------------
--- DSL の observe 文の mean expression (= 例: `alpha + beta * 'x'`) を
--- 直接評価して posterior predictive curve を計算する。 frontend の
--- buildHbmOverlay 内 heuristic (= "alpha" / "beta_<x>" 等の名前推定) を
--- 撤去するための backend AST driven 経路。
-
-data HbmMeanCurve = HbmMeanCurve
-  { hmcObsName   :: !Text          -- observe 文の名前 (top="y"、 per-group="y_1")
-  , hmcPredictor :: !Text          -- 説明変数列名 (= 例: "x")
-  , hmcX         :: ![Double]      -- 64 grid points
-  , hmcMedian    :: ![Double]      -- posterior median of mu(x)
-  , hmcLower     :: ![Double]      -- 2.5%
-  , hmcUpper     :: ![Double]      -- 97.5%
-  -- Phase 27 GLMM overlay: per-group curve の群ラベル (top-level は Nothing)。
-  -- frontend が群ごと色分け + 凡例に使う。
-  , hmcGroupCol  :: !(Maybe Text)   -- 群列名 (= forEachGroup "gcol")
-  , hmcGroupVal  :: !(Maybe Double) -- 群値
-  } deriving (Show)
-
-hbmMeanCurveToJson :: HbmMeanCurve -> A.Value
-hbmMeanCurveToJson c = A.object
-  [ Key.fromText "obsName"   A..= hmcObsName c
-  , Key.fromText "predictor" A..= hmcPredictor c
-  , Key.fromText "x"         A..= hmcX c
-  , Key.fromText "median"    A..= hmcMedian c
-  , Key.fromText "lower"     A..= hmcLower c
-  , Key.fromText "upper"     A..= hmcUpper c
-  , Key.fromText "groupCol"  A..= hmcGroupCol c
-  , Key.fromText "groupVal"  A..= hmcGroupVal c
-  ]
-
--- | stmts から各 observe の mean 式 (= Distribution の第 1 引数) を抽出。
-extractObserveMeans :: [DoStmt] -> [(Text, Expr)]
-extractObserveMeans = concatMap go
-  where
-    go (DoExpr e) = case collectApp e of
-      Right ("observe", [ELit (LText nm), distExpr, _]) ->
-        case collectApp distExpr of
-          Right (_, m : _) -> [(nm, m)]
-          _ -> []
-      _ -> []
-    go _ = []
-
--- | 式中の全 ECol 列名を集める (重複排除)。
-collectCols :: Expr -> [Text]
-collectCols = nub . go
-  where
-    go (ECol c)    = [c]
-    go (EOp _ a b) = go a <> go b
-    go (EApp f x)  = go f <> go x
-    go (ENeg e)    = go e
-    go (ELet bs body) = concatMap (\(Bind _ v) -> go v) bs <> go body
-    go _           = []
-
--- | percentile (= 0..1) を sorted リストから線形補間で取る。
-percentileOf :: Double -> [Double] -> Double
-percentileOf q xs0 =
-  let xs = sort xs0
-      n = length xs
-  in if n == 0 then 0
-     else
-       let idxF = q * fromIntegral (n - 1)
-           idx  = max 0 (min (n - 1) (floor idxF))
-       in case drop idx xs of
-            (v:_) -> v
-            []    -> 0
-
--- | observe 1 個分の mean-curve 計算文脈 (top-level または per-group)。
--- Phase 27 GLMM overlay: `forEachGroup` 内の observe も拾えるよう、 plate
--- 展開を replay しながら集める ('collectObsInstances')。
--- | Phase 43: WAIC/PPC 再評価用、 list 値 combinator 由来の latent vector を
--- posterior サンプルから再構築する仕様。 stream worker は @sampleNames@
--- (= sample された latent のみ) を samples に載せ、 combinator の deterministic
--- (cut_c_*/pi_*) は載せないため、 再評価 env で sampled latent (cut_d_*/pi_b*) から
--- VList を組み直す ('reconstructComb')。
-data ListCombSpec
-  = OCutsSpec Text Int Expr   -- ^ suffix 付き base 名, nCuts, cMin 式 (定数想定)
-  | DirSpec   Text Int        -- ^ suffix 付き base 名, K (= α ベクトル長)
-
-data ObsInstance = ObsInstance
-  { oiObsName  :: Text                  -- 表示名 (top="y"、 group="y_1")
-  , oiMeanExpr :: Expr                  -- distribution の第 1 引数 (mean 式)
-  , oiDistExpr :: Expr                  -- distribution 式全体 (例: `Normal mu sigma`)
-  , oiObsCol   :: Text                  -- 観測列名 (observe の第 3 引数 `col "y"`)
-  , oiNameKey  :: Map.Map Text Text     -- local latent 名 → samples のキー (theta→"theta_1")
-  , oiParamEnv :: Map.Map Text Double   -- 群変数 (forEachGroup の \g) → 群値
-  , oiRows     :: Maybe [Int]           -- 対象行 (Nothing=全行)
-  , oiGroup    :: Maybe (Text, Double)  -- 最内 forEachGroup の (群列, 群値)
-  , oiListCombs :: [(Text, ListCombSpec)]  -- Phase 43: list latent bind (cuts/probs) の再構築仕様
-  }
-
--- | list 値 combinator の latent vector を 1 posterior サンプルから再構築する。
--- @base@ は cMin 定数評価用の env (sample 非依存)、 @sm@ は sampled latent の Map。
--- sampled latent (cut_d_*/pi_b*) が欠けていれば 'Nothing'。
-reconstructComb
-  :: EnvA Double -> DataMap -> Map.Map Text Double -> ListCombSpec -> Maybe [Double]
-reconstructComb base dataMap sm spec = case spec of
-  -- orderedCuts: c_1 = cMin、 c_i = c_{i-1} + d_i (d_i = name_d_i, i=2..n)。
-  OCutsSpec nm n cMinE -> do
-    cMin <- either (const Nothing) Just (evalScalar base dataMap Nothing cMinE)
-    ds   <- mapM (\j -> Map.lookup (nm <> "_d_" <> T.pack (show j)) sm) [2 .. n]
-    pure (scanl (+) cMin ds)            -- 長さ n、 単調増加
-  -- dirichlet: stick-breaking。 betas = name_b0..name_b{K-2}、 π を復元。
-  DirSpec nm k -> do
-    betas <- mapM (\j -> Map.lookup (nm <> "_b" <> T.pack (show j)) sm) [0 .. k - 2]
-    let prods = scanl (\acc b -> acc * (1 - b)) 1 betas
-    pure [ if j < length betas then (betas !! j) * (prods !! j) else prods !! j
-         | j <- [0 .. k - 1] ]
-
--- | DoBind の RHS が list 値 combinator なら 'ListCombSpec' を返す (suffix 付き
--- base 名で。 'matchListComb' / 'listCombSuffix' と同じ命名規律)。
-matchListCombSpec :: Text -> Expr -> Maybe ListCombSpec
-matchListCombSpec suf rhs = case collectApp rhs of
-  Right ("orderedCuts", [ELit (LText bn), ELit (LNumber d), cMinE, _scaleE]) ->
-    Just (OCutsSpec (bn <> suf) (round d) cMinE)
-  Right ("dirichlet", [ELit (LText bn), EList alphas]) ->
-    Just (DirSpec (bn <> suf) (length alphas))
-  _ -> Nothing
-
--- | observe 式から (名前, mean 式, distribution 式全体, 観測列名) を抽出。
--- mean 式 = Distribution の第 1 引数、 dist 式全体は WAIC/PPC で logDensity /
--- sampleDist を回すために必要。 観測列名は第 3 引数 `col "y"` の列。
-observeFull :: Expr -> Maybe (Text, Expr, Expr, Text)
-observeFull e = case collectApp e of
-  Right ("observe", [ELit (LText nm), distExpr, ECol colName]) ->
-    case collectApp distExpr of
-      Right (_, m : _) -> Just (nm, m, distExpr, colName)
-      _                -> Nothing
-  _ -> Nothing
-
--- | stmts を plate 展開しながら observe instance を集める。 'interpStmts' と
--- 同じ suffix (groupSuffix) / 行 (rowsForGroup) / 群変数束縛の規律を replay し、
--- top-level observe は suffix=""・全行、 forEachGroup 内 observe は群ごとに
--- suffix 付き・群行で展開する。 latent 名は sample された scope の suffix で
--- samples のキーに対応づける (top latent="mu"、 群 latent="theta_1" 等)。
-collectObsInstances :: DataMap -> [DoStmt] -> [ObsInstance]
-collectObsInstances dm = go "" Nothing Map.empty Map.empty Nothing []
-  where
-    go :: Text -> Maybe [Int] -> Map.Map Text Text -> Map.Map Text Double
-       -> Maybe (Text, Double) -> [(Text, ListCombSpec)] -> [DoStmt] -> [ObsInstance]
-    go _ _ _ _ _ _ [] = []
-    go suf rows nameKey penv grp combs (st : rest) = case st of
-      -- latent: 現 suffix 付きで samples に載る (= キー対応を記録)。 Phase 43:
-      -- list 値 combinator なら再構築仕様も記録 (= WAIC/PPC 再評価で VList 復元)。
-      DoBind name rhs ->
-        let combs' = case matchListCombSpec suf rhs of
-                       Just spec -> (name, spec) : combs
-                       Nothing   -> combs
-        in go suf rows (Map.insert name (name <> suf) nameKey) penv grp combs' rest
-      DoLet _ -> go suf rows nameKey penv grp combs rest
-      DoExpr e
-        | Just (gcol, param, inner) <- matchForEachGroup e ->
-            let gvals = groupValsIn dm gcol rows
-                here = concatMap
-                  (\gv ->
-                     go (suf <> groupSuffixFor (Map.lookup gcol dm) gv)
-                        (Just (rowsForGroup dm gcol gv rows))
-                        nameKey
-                        (Map.insert param gv penv)
-                        (Just (gcol, gv))
-                        combs
-                        inner)
-                  gvals
-            in here ++ go suf rows nameKey penv grp combs rest
-        | Just (obsName, meanExpr, distExpr, obsCol) <- observeFull e ->
-            ObsInstance (obsName <> suf) meanExpr distExpr obsCol nameKey penv rows grp combs
-              : go suf rows nameKey penv grp combs rest
-        | otherwise -> go suf rows nameKey penv grp combs rest
-
--- ===========================================================================
--- model graph plate aggregation (Phase 27.5 後続 TODO3、 2026-06-02)
--- ===========================================================================
---
--- forEachGroup は群ごとに内部 do-block を展開するため、 'buildModelGraph' が
--- 見る realized ModelP には alpha_1 / alpha_2 / alpha_3 … と群数ぶんの latent /
--- observe ノードが並ぶ (3 群 × 数 latent で 40 ノード級に肥大)。 PyMC 流の
--- plate 表記では「群コピーを 1 つの代表ノードに畳み、 箱のラベルに群数」 を
--- 出すので、 ここでは AST (= forEachGroup 構造が残る層) から
---
---   * realized 名 → base 名 の rename map ('plateRenameMap')
---   * forEachGroup ごとの plate (ラベル "<群列> (<群数>)" + 直下 base 名)
---     ('collectGraphPlates')
---
--- を導き、 realized ModelGraph を base 名へ collapse する
--- ('collapsePlateGraph')。 rename は interpStmts と同じ groupSuffix 規律を
--- replay して作る total な対応なので、 文字列推測ではない。
-
--- | model graph 上の plate (= forEachGroup 1 サイト)。 frontend
---   hgg DAGPlate (label + member id 群) に対応。
-data GraphPlate = GraphPlate
-  { gpLabel   :: Text     -- ^ "<群列> (<群数>)"
-  , gpMembers :: [Text]   -- ^ この plate 直下の base 名 (latent + observe)
-  } deriving (Show, Eq)
-
--- | realized node 名 (alpha_1 等) → base 名 (alpha) の rename map。
---   'collectObsInstances' / 'interpStmts' と同じ suffix (groupSuffix) /
---   行 (rowsForGroup) 規律を replay し、 各 DoBind latent / observe を
---   その出現 suffix 付き名 → base 名 で登録する。 top-level は suffix="" なので
---   恒等 (alpha → alpha)。
-plateRenameMap :: DataMap -> [DoStmt] -> Map.Map Text Text
-plateRenameMap dm = go "" Nothing
-  where
-    go :: Text -> Maybe [Int] -> [DoStmt] -> Map.Map Text Text
-    go _ _ [] = Map.empty
-    go suf rows (st : rest) = case st of
-      DoBind name _ ->
-        Map.insert (name <> suf) name (go suf rows rest)
-      DoLet _ -> go suf rows rest
-      DoExpr e
-        | Just (gcol, _param, inner) <- matchForEachGroup e ->
-            let gvals  = groupValsIn dm gcol rows
-                inners = Map.unions
-                  [ go (suf <> groupSuffixFor (Map.lookup gcol dm) gv)
-                       (Just (rowsForGroup dm gcol gv rows)) inner
-                  | gv <- gvals ]
-            in inners `Map.union` go suf rows rest
-        | Just (obsName, _meanE, distExpr, obsCol) <- observeFull e ->
-            -- observe は col 参照を含むと 'observeColumns' で per-row 展開され、
-            -- 実ノードは "<obsName><suf>_<j>" (j = 当該 plate 対象行の 0 始まり
-            -- 連番、 = execObserve の規律) になる。 col 参照無しなら単一
-            -- "<obsName><suf>"。 どちらも base 名 obsName に畳む。
-            let colLen     = length (lookupDoubles obsCol dm)
-                baseRows   = fromMaybe [0 .. colLen - 1] rows
-                validCount = length [ i | i <- baseRows, i >= 0, i < colLen ]
-                names | hasColRef distExpr =
-                          [ obsName <> suf <> "_" <> T.pack (show j)
-                          | j <- [0 .. validCount - 1] ]
-                      | otherwise = [ obsName <> suf ]
-            in Map.union (Map.fromList [ (nm, obsName) | nm <- names ])
-                         (go suf rows rest)
-        | otherwise -> go suf rows rest
-
--- | forEachGroup ごとに 1 plate を集める (群値ぶんは展開しない)。 ラベルは
---   "<群列> (<群数>)"、 member は当該 forEachGroup 直下の base 名 (latent +
---   observe、 ネストした forEachGroup の中身は含めない = ネストは別 plate)。
---   ネスト plate は代表 1 群の行で再帰的に拾う。
-collectGraphPlates :: DataMap -> [DoStmt] -> [GraphPlate]
-collectGraphPlates dm = go Nothing
-  where
-    go :: Maybe [Int] -> [DoStmt] -> [GraphPlate]
-    go _ [] = []
-    go rows (st : rest) = case st of
-      DoExpr e
-        | Just (gcol, _param, inner) <- matchForEachGroup e ->
-            let gvals   = groupValsIn dm gcol rows
-                count   = length gvals
-                label   = gcol <> " (" <> T.pack (show count) <> ")"
-                members = directMembers inner
-                nested  = case gvals of
-                  (gv : _) -> go (Just (rowsForGroup dm gcol gv rows)) inner
-                  []       -> []
-            in GraphPlate label members : nested ++ go rows rest
-        | otherwise -> go rows rest
-      _ -> go rows rest
-    -- 直下の DoBind latent + observe 名 (ネスト forEachGroup は DoExpr なので除外)。
-    directMembers :: [DoStmt] -> [Text]
-    directMembers stmts =
-      [ name | DoBind name _ <- stmts ]
-      ++ [ obsName | DoExpr e <- stmts, Just (obsName, _, _, _) <- [observeFull e] ]
-
--- | realized 'HBM.ModelGraph' を plate 単位に collapse。 群展開ノードを base 名に
---   畳み (重複ノードは初出を残す)、 辺は両端を rename して自己ループ除去 + 重複
---   除去。 併せて plate 一覧を返す。
-collapsePlateGraph
-  :: DataMap -> [DoStmt] -> HBM.ModelGraph -> (HBM.ModelGraph, [GraphPlate])
-collapsePlateGraph dm stmts mg =
-  let rn      = plateRenameMap dm stmts
-      ren x   = Map.findWithDefault x x rn
-      nodes'  = dedupNodes [ renameNode ren n | n <- HBM.mgNodes mg ]
-      edges'  = nub [ (ren a, ren b)
-                    | (a, b) <- HBM.mgEdges mg, ren a /= ren b ]
-     -- Phase 40 merge: ModelGraph に mgPlates (plate→size) フィールドが追加された。
-     -- DSL forEachGroup の collapse は独自の GraphPlate 列 (collectGraphPlates) を
-     -- 別途返すので、 ここでは入力 graph の mgPlates をそのまま引き継ぐ。
-  in (HBM.ModelGraph nodes' edges' (HBM.mgPlates mg), collectGraphPlates dm stmts)
-  where
-    renameNode ren n = n
-      { HBM.nodeName = ren (HBM.nodeName n)
-      , HBM.nodeDeps = Set.map ren (HBM.nodeDeps n)
-      }
-    dedupNodes = goD Set.empty
-      where
-        goD _ [] = []
-        goD seen (n : ns)
-          | HBM.nodeName n `Set.member` seen = goD seen ns
-          | otherwise = n : goD (Set.insert (HBM.nodeName n) seen) ns
-
--- | observe ごと × 列ごとに 64 点 curve を計算。 Phase 27 GLMM overlay:
--- top-level observe に加え forEachGroup 内の per-group observe も対象
--- ('collectObsInstances' が plate 展開)。
--- |   * 主 predictor = 当該列、 grid は (per-group なら群の) data min..max
--- |   * 他 predictor は (per-group なら群の) data median で固定
--- |   * 群 latent は suffix 付きキー (theta_1 等)、 群変数は定数として env に注入
--- |   * 各 sample × 各 grid 点で mean 式を Double 評価
--- |   * 各 grid 点で全 sample から median + 2.5% / 97.5% percentile
-computeMeanCurves
-  :: [TopBind]                            -- top-level 値/関数束縛 (ユーザ定義リンク等)
-  -> [DoStmt]
-  -> DataMap                              -- data: col → values
-  -> [Map.Map Text Double]                -- posterior samples
-  -> [HbmMeanCurve]
-computeMeanCurves topBinds stmts dataMap samples =
-  [ HbmMeanCurve
-      { hmcObsName   = oiObsName inst
-      , hmcPredictor = col
-      , hmcX = xGrid
-      , hmcMedian = map (percentileOf 0.5)   valuesPerX
-      , hmcLower  = map (percentileOf 0.025) valuesPerX
-      , hmcUpper  = map (percentileOf 0.975) valuesPerX
-      , hmcGroupCol = fst <$> oiGroup inst
-      , hmcGroupVal = snd <$> oiGroup inst
-      }
-  | inst <- collectObsInstances dataMap stmts
-  , let meanExpr = oiMeanExpr inst
-        mrows    = oiRows inst
-        -- per-group なら群の行に限定して列値を取り出す。
-        colValsFor c =
-          let ca = lookupDoubles c dataMap
-          in case mrows of
-               Nothing -> ca
-               Just rs -> [ ca !! i | i <- rs, i >= 0, i < length ca ]
-  , col <- collectCols meanExpr
-  , let xs = colValsFor col
-  , not (null xs)
-  , let nGrid = 64 :: Int
-        xLo = minimum xs
-        xHi = maximum xs
-        step = if xHi == xLo then 1.0 else (xHi - xLo) / fromIntegral (nGrid - 1)
-        xGrid = [ xLo + step * fromIntegral i | i <- [0 .. nGrid - 1] ]
-        otherCols = filter (/= col) (collectCols meanExpr)
-        medianOf vs = case sort vs of
-          [] -> 0
-          ss -> ss !! (length ss `div` 2)
-        fixedOther =
-          Map.fromList [ (c, Numeric [medianOf (colValsFor c)]) | c <- otherCols ]
-        -- 群変数 (forEachGroup の \g) を定数として env に注入。
-        paramVNums = Map.map VNum (oiParamEnv inst)
-        -- sample を nameKey で remap: local latent 名 → samples の suffix 付きキー。
-        -- (Map.union は left-biased なので renamed が元キーより優先)
-        remapSample sample =
-          let renamed = Map.fromList
-                [ (localNm, v)
-                | (localNm, key) <- Map.toList (oiNameKey inst)
-                , Just v <- [Map.lookup key sample] ]
-          in Map.union renamed sample
-        evalAt sample x =
-          let synthetic = Map.insert col (Numeric [x]) fixedOther
-              sm = remapSample sample
-              -- posterior サンプル (alpha/beta 等) を env に、 top-level
-              -- 値/関数 (ユーザ定義リンク等) + 群変数も併せて見えるようにする。
-              senv = Map.union paramVNums
-                       (Map.union (Map.map VNum sm) (buildTopEnv dataMap topBinds))
-          in case evalScalar @Double senv synthetic (Just 0) meanExpr of
-               Right v -> v
-               Left _  -> 0 / 0  -- NaN
-        valuesPerX = [ [ evalAt sample x | sample <- samples ] | x <- xGrid ]
-  ]
-
--- ---------------------------------------------------------------------------
--- Phase 27.5 (2026-06-02): WAIC / LOO / posterior predictive 用の observe
--- distribution 評価。 computeMeanCurves と同じ plate 展開 (collectObsInstances)
--- を使い、 mean 式ではなく distribution 式全体を各 sample × 各対象行で評価する。
--- これにより pointwise log-likelihood (WAIC/LOO) と posterior predictive draw
--- (PPC、 worker 側で sampleDist) の共通基盤を 1 度で作る。
--- ---------------------------------------------------------------------------
-
--- | observe instance 1 個分の、 全 posterior sample × 対象行で評価した結果。
-data ObsDistSet = ObsDistSet
-  { odsName     :: !Text                          -- observe ノード名 (suffix 付き)
-  , odsObserved :: ![Double]                      -- 対象行の観測値 (= col の当該行)
-  , odsDists    :: ![[HBM.Distribution Double]]   -- [sample][row] の Distribution
-  }
-
--- | 各 observe instance について、 各 posterior sample × 各対象行で
--- distribution を評価する。 mean に列参照がある GLM 形 (例: Normal (a+b*'x') s)
--- も evalDist の per-row 評価 ((Just i)) で正しく行ごとに展開される。
-computeObsDists
-  :: [TopBind]
-  -> [DoStmt]
-  -> DataMap
-  -> [Map.Map Text Double]
-  -> [ObsDistSet]
-computeObsDists topBinds stmts dataMap samples =
-  [ ObsDistSet
-      { odsName     = oiObsName inst
-      , odsObserved = ys
-      , odsDists    = [ [ distAt sample i | i <- rows ] | sample <- samples ]
-      }
-  | inst <- collectObsInstances dataMap stmts
-  , let distExpr = oiDistExpr inst
-        yCol     = lookupDoubles (oiObsCol inst) dataMap
-        allRows  = [0 .. length yCol - 1]
-        rows     = filter (\i -> i >= 0 && i < length yCol)
-                     (fromMaybe allRows (oiRows inst))
-        ys       = [ yCol !! i | i <- rows ]
-        paramVNums = Map.map VNum (oiParamEnv inst)
-        remapSample sample =
-          let renamed = Map.fromList
-                [ (localNm, v)
-                | (localNm, key) <- Map.toList (oiNameKey inst)
-                , Just v <- [Map.lookup key sample] ]
-          in Map.union renamed sample
-        distAt sample i =
-          let sm   = remapSample sample
-              base = Map.union paramVNums
-                       (Map.union (Map.map VNum sm) (buildTopEnv dataMap topBinds))
-              -- Phase 43: combinator 由来 latent vector (cuts/probs) を sampled
-              -- latent から再構築して VList 束縛 (= 消費分布の list 引数を解決)。
-              combBinds = [ (bindNm, VList (map VNum vals))
-                          | (bindNm, spec) <- oiListCombs inst
-                          , Just vals <- [reconstructComb base dataMap sample spec] ]
-              senv = Map.union (Map.fromList combBinds) base
-          in case evalDist senv dataMap (Just i) distExpr :: Err (HBM.Distribution Double) of
-               Right d -> d
-               Left _  -> HBM.Normal (0 / 0) 1   -- 評価不能は NaN 化 (logDensity→NaN)
-  , not (null rows)
-  ]
-
--- | 'ObsDistSet' 群から WAIC/LOO 用の log-likelihood 行列 (S × N) を作る。
--- 行 = posterior sample、 列 = 全 observe instance の全対象行を連結。
--- @Hanalyze.Stat.ModelSelect.waic@ / @loo@ がこの shape を期待する。
-pointwiseLogLik :: [ObsDistSet] -> [[Double]]
-pointwiseLogLik sets =
-  [ concatMap (\set -> zipWith HBM.logDensity (sampleRow set s) (odsObserved set)) sets
-  | s <- [0 .. nSamples - 1] ]
-  where
-    nSamples = case sets of
-      (set : _) -> length (odsDists set)
-      []        -> 0
-    sampleRow set s = case drop s (odsDists set) of
-      (row : _) -> row
-      []        -> []
-
--- | log-lik 行列 (S×N) から非有限 (NaN / ±Inf) を含む観測列を除外する
--- (Phase 27.5 後続 TODO4、 2026-06-02)。 'distAt' の eval 失敗 (= Normal NaN) や
--- 退化パラメータで 'logDensity' が NaN / Inf になった観測点は、 そのまま waic/loo に
--- 渡すと per-observation 集計 (lppd / pwaic) を汚染して全体が NaN → JSON null に
--- なる。 該当する観測列 (= 全 sample で同一観測点) を丸ごと落として残りで waic/loo を
--- 計算できるようにする。 返り値は (除外後行列, 落とした列数)。 全列が非有限なら
--- ([], N) を返し、 呼び出し側は waic/loo を Nothing にできる。
-finitePointwiseLogLik :: [[Double]] -> ([[Double]], Int)
-finitePointwiseLogLik mat =
-  let cols     = transpose mat              -- [obs点][sample]
-      keptCols = filter (all isFiniteD) cols
-      dropped  = length cols - length keptCols
-  in (transpose keptCols, dropped)
-  where
-    isFiniteD x = not (isNaN x || isInfinite x)
-
--- ===========================================================================
--- Phase 44: multi-column observe (observeMV) の WAIC / PPC 経路
--- ===========================================================================
---
--- scalar observe (1 列) の WAIC/PPC ('collectObsInstances' / 'computeObsDists' /
--- 'pointwiseLogLik') は per-row スカラ logDensity 前提で、 multi-column の
--- k-vector joint density (= 'HBM.obsLogSum') を扱えない。 そこで Phase 44 の
--- 設計方針 (observeMV は scalar observe と別 builtin の並行経路) を WAIC/PPC まで
--- 貫き、 **MV 専用の並行経路** を足す (scalar 経路は無傷)。
---
--- latent Σ (lkjCorrCholesky 由来の相関 Cholesky L) は posterior sample に
--- 内部 latent (@L_u<i>_<j>@ = partial-correlation の Beta latent) として載るので、
--- bind 名 @L@ を 'reconstructMatrixComb' で再構築して MvNormalChol に渡す
--- (Phase 43 'reconstructComb' の行列版)。
-
--- | 行列値 combinator の再構築仕様 (suffix 付き base 名 + 次元 k)。
-data MatrixCombSpec
-  = LkjCholSpec Text Int   -- ^ suffix 付き base 名, k (= 行列次元)
-  deriving (Show)
-
--- | DoBind の RHS が行列値 combinator (lkjCorrCholesky) なら 'MatrixCombSpec' を
--- 返す ('matchMatrixComb' / 'matrixCombSuffix' と同じ命名規律)。
-matchMatrixCombSpec :: Text -> Expr -> Maybe MatrixCombSpec
-matchMatrixCombSpec suf rhs = case collectApp rhs of
-  Right ("lkjCorrCholesky", [ELit (LText bn), ELit (LNumber d), _etaE]) ->
-    Just (LkjCholSpec (bn <> suf) (round d))
-  _ -> Nothing
-
--- | lkjCorrCholesky の相関 Cholesky L を 1 posterior サンプルから再構築する。
--- sampled latent は @<nm>_u<i>_<j>@ (Beta in (0,1))、 partial correlation は
--- @z_ij = 2u - 1@。 L は 'HBM.lkjCorrCholesky' の deterministic 構築を replay:
---   L_00 = 1、 対角 L_ii = √(1 - Σ_{k<i} z_{i,k}²)、
---   対角下 L_ij = z_ij · √(Π_{k<j}(1 - z_{i,k}²))  (j < i)。
--- sampled latent が欠ければ 'Nothing'。
-reconstructMatrixComb :: Map.Map Text Double -> MatrixCombSpec -> Maybe [[Double]]
-reconstructMatrixComb sm (LkjCholSpec nm k) = do
-  let uKey i j = nm <> "_u" <> T.pack (show i) <> "_" <> T.pack (show j)
-  pcPairs <- mapM
-    (\(i, j) -> do u <- Map.lookup (uKey i j) sm; pure ((i, j), 2 * u - 1))
-    [(i, j) | i <- [1 .. k - 1], j <- [0 .. i - 1]]
-  let pcMap = Map.fromList pcPairs
-      pc i j = Map.findWithDefault 0 (i, j) pcMap
-      sq z = z * z
-      lRow i =
-        [ if j > i then 0
-          else if i == 0 && j == 0 then 1
-          else if j == i
-               then sqrt (max 0 (1 - sum [ sq (pc i kk) | kk <- [0 .. i - 1] ]))
-          else pc i j * sqrt (max 0 (product [ 1 - sq (pc i kk) | kk <- [0 .. j - 1] ]))
-        | j <- [0 .. k - 1] ]
-  pure [ lRow i | i <- [0 .. k - 1] ]
-
--- | observeMV 式から (名前, distribution 式全体, 観測列名リスト) を抽出。
-observeMVFull :: Expr -> Maybe (Text, Expr, [Text])
-observeMVFull e = case collectApp e of
-  Right ("observeMV", [ELit (LText nm), distExpr, EList colRefs]) ->
-    let cols = [ c | ECol c <- colRefs ]
-    in if length cols == length colRefs && length cols >= 2
-         then Just (nm, distExpr, cols) else Nothing
-  _ -> Nothing
-
--- | observeMV instance (plate 展開済)。 'collectObsInstances' の MV 版で、
--- 単一 'oiObsCol' でなく **列リスト** を持ち、 list/matrix combinator の
--- 再構築仕様も保持する。
-data MvObsInstance = MvObsInstance
-  { mviObsName     :: Text
-  , mviDistExpr    :: Expr
-  , mviObsCols     :: [Text]
-  , mviNameKey     :: Map.Map Text Text
-  , mviParamEnv    :: Map.Map Text Double
-  , mviRows        :: Maybe [Int]
-  , mviListCombs   :: [(Text, ListCombSpec)]
-  , mviMatrixCombs :: [(Text, MatrixCombSpec)]
-  }
-
--- | stmts を plate 展開しながら observeMV instance を集める
--- ('collectObsInstances' と同じ suffix / 行 / 群変数 / latent 名規律を replay)。
-collectMvObsInstances :: DataMap -> [DoStmt] -> [MvObsInstance]
-collectMvObsInstances dm = go "" Nothing Map.empty Map.empty [] []
-  where
-    go :: Text -> Maybe [Int] -> Map.Map Text Text -> Map.Map Text Double
-       -> [(Text, ListCombSpec)] -> [(Text, MatrixCombSpec)] -> [DoStmt]
-       -> [MvObsInstance]
-    go _ _ _ _ _ _ [] = []
-    go suf rows nameKey penv lcombs mcombs (st : rest) = case st of
-      DoBind name rhs ->
-        let lcombs' = case matchListCombSpec suf rhs of
-                        Just spec -> (name, spec) : lcombs
-                        Nothing   -> lcombs
-            mcombs' = case matchMatrixCombSpec suf rhs of
-                        Just spec -> (name, spec) : mcombs
-                        Nothing   -> mcombs
-        in go suf rows (Map.insert name (name <> suf) nameKey) penv lcombs' mcombs' rest
-      DoLet _ -> go suf rows nameKey penv lcombs mcombs rest
-      DoExpr e
-        | Just (gcol, param, inner) <- matchForEachGroup e ->
-            let gvals = groupValsIn dm gcol rows
-                here = concatMap
-                  (\gv ->
-                     go (suf <> groupSuffixFor (Map.lookup gcol dm) gv)
-                        (Just (rowsForGroup dm gcol gv rows))
-                        nameKey (Map.insert param gv penv) lcombs mcombs inner)
-                  gvals
-            in here ++ go suf rows nameKey penv lcombs mcombs rest
-        | Just (obsName, distExpr, cols) <- observeMVFull e ->
-            MvObsInstance (obsName <> suf) distExpr cols nameKey penv rows lcombs mcombs
-              : go suf rows nameKey penv lcombs mcombs rest
-        | otherwise -> go suf rows nameKey penv lcombs mcombs rest
-
--- | observeMV 1 個分の WAIC/PPC 評価結果。 'ObsDistSet' の MV 版。
-data MvObsDistSet = MvObsDistSet
-  { mvodsName     :: !Text                          -- ^ 表示名 (top="y"、 group="y_1")
-  , mvodsCols     :: ![Text]                        -- ^ 観測列名リスト (長さ k)
-  , mvodsObserved :: ![[Double]]                    -- ^ [row][component] = 各行の k-vector
-  , mvodsDists    :: ![[HBM.Distribution Double]]   -- ^ [sample][row] の Distribution
-  }
-
--- | observeMV instance × sample × 行で多変量 Distribution を評価する
--- ('computeObsDists' の MV 版)。 dist は行不変 (μ/Σ は latent) なので各行で
--- 同一だが、 既存経路に合わせ row 評価する。 latent Σ は 'reconstructMatrixComb'
--- で L を、 list 引数は 'reconstructComb' で復元して env に束縛する。
-computeMvObsDists
-  :: [TopBind]
-  -> [DoStmt]
-  -> DataMap
-  -> [Map.Map Text Double]
-  -> [MvObsDistSet]
-computeMvObsDists topBinds stmts dataMap samples =
-  [ MvObsDistSet
-      { mvodsName     = mviObsName inst
-      , mvodsCols     = cols
-      , mvodsObserved = [ [ lookupDoubles c dataMap !! i | c <- cols ] | i <- rows ]
-      , mvodsDists    = [ [ distAt sample i | i <- rows ] | sample <- samples ]
-      }
-  | inst <- collectMvObsInstances dataMap stmts
-  , let distExpr = mviDistExpr inst
-        cols     = mviObsCols inst
-        colLens  = map (length . (`lookupDoubles` dataMap)) cols
-        colLen   = if null colLens then 0 else minimum colLens
-        allRows  = [0 .. colLen - 1]
-        rows     = filter (\i -> i >= 0 && i < colLen)
-                     (fromMaybe allRows (mviRows inst))
-        paramVNums = Map.map VNum (mviParamEnv inst)
-        remapSample sample =
-          let renamed = Map.fromList
-                [ (localNm, v)
-                | (localNm, key) <- Map.toList (mviNameKey inst)
-                , Just v <- [Map.lookup key sample] ]
-          in Map.union renamed sample
-        distAt sample i =
-          let sm   = remapSample sample
-              base = Map.union paramVNums
-                       (Map.union (Map.map VNum sm) (buildTopEnv dataMap topBinds))
-              listBinds = [ (bn, VList (map VNum vals))
-                          | (bn, spec) <- mviListCombs inst
-                          , Just vals <- [reconstructComb base dataMap sample spec] ]
-              matBinds  = [ (bn, VList (map (VList . map VNum) m))
-                          | (bn, spec) <- mviMatrixCombs inst
-                          , Just m <- [reconstructMatrixComb sample spec] ]
-              senv = Map.union (Map.fromList (listBinds ++ matBinds)) base
-          in case evalDist senv dataMap (Just i) distExpr :: Err (HBM.Distribution Double) of
-               Right d -> d
-               Left _  -> HBM.MvNormal [0 / 0] [[1]]   -- eval 不能は NaN 化
-  , not (null rows)
-  ]
-
--- | MV observe の pointwise log-lik 行列 (S×N)。 各行 (= 1 観測点) の寄与は
--- k-vector joint density 'HBM.obsLogSum'。 scalar 経路の 'pointwiseLogLik' と
--- 列方向に連結して使う (worker 側)。
-pointwiseLogLikMv :: [MvObsDistSet] -> [[Double]]
-pointwiseLogLikMv sets =
-  [ concatMap (\set -> [ HBM.obsLogSum (distRow set s !! r) (mvodsObserved set !! r)
-                       | r <- [0 .. nRows set - 1] ]) sets
-  | s <- [0 .. nSamples - 1] ]
-  where
-    nSamples = case sets of
-      (set : _) -> length (mvodsDists set)
-      []        -> 0
-    nRows set = length (mvodsObserved set)
-    distRow set s = case drop s (mvodsDists set) of
-      (row : _) -> row
-      []        -> []
diff --git a/src/Hanalyze/Model/HBM/Model.hs b/src/Hanalyze/Model/HBM/Model.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/HBM/Model.hs
+++ /dev/null
@@ -1,1132 +0,0 @@
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE ImpredicativeTypes #-}
-
--- |
--- Module      : Hanalyze.Model.HBM.Model
--- Description : HBM の多相モデル DSL (Free monad) 記述層
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Phase 58.5: 多相モデル DSL (Free monad) を 'Hanalyze.Model.HBM' から分離。
---
--- 本モジュールは PPL の **記述層** を担う:
---
---   * @Free@ monad 再実装 (型は 'Hanalyze.Model.HBM' 公開のものと別個)
---   * 'ModelF' プリミティブ (sample / observe / observeLM / deterministic /
---     plate / Data / Potential) と 'Model' / 'ModelP' 型エイリアス
---   * 第一級ランダム効果値 'REffect' / 'REff' と階層モデル helper 群
---     (reNormal / mvNormalLatent / lkjCorrCholesky / ar1Latent / dirichlet /
---      orderedCuts / dpStickBreaking / hmmLatent / glmmRandomIntercept 等)
---   * Plate notation (Phase 40) と構造検査 ('collectNodes' / 'sampleNames')
---
--- 評価 (logJoint 等)・AD 勾配・IR は **上層** に置かれ、 本モジュールは
--- それらに依存しない (leaf-first・facade 非 import の規律。 Phase 58 計画参照)。
--- 依存は下層 'Hanalyze.Model.HBM.Util' / '...Distribution' のみ。
-module Hanalyze.Model.HBM.Model
-  ( -- * Free monad
-    Free (..)
-  , liftF
-    -- * Polymorphic model DSL
-  , ModelF (..)
-  , Model
-  , ModelP
-  , sample
-  , observe
-  , observeMV
-  , observeColumns
-  , observeLM
-  , observeLMR
-  , observeNormalLM
-  , LMFamily (..)
-  , lmFamilyName
-  , lmParents
-  , REff (..)
-  , REffect (..)
-  , reffNames
-  , reNormal
-  , at
-  , indexed
-  , (.#)
-  , potential
-  , deterministic
-  , nonCenteredNormal
-  , dirichlet
-  , orderedCuts
-  , dpStickBreaking
-  , hmmLatent
-  , hmmForwardLogLik
-  , GlmmFamily (..)
-  , glmmRandomIntercept
-  , dataNamed
-  , dataNamedX
-  , dataNamedIx
-  , dataNamedObs
-  , Ix (..)
-  , TrackTag (..)
-  , (!!!)
-  , atIx
-  , withData
-  , withDataIx
-  , mvNormalLatent
-  , lkjCorrCholesky
-  , gpExpQuadCov
-  , gpLatent
-  , ar1Latent
-    -- ** Phase 40 plate notation
-  , plate
-  , plateI
-  , plateI_
-  , plateForM
-  , plateForM_
-  , withPlate
-    -- * Structural inspection
-  , Node (..)
-  , NodeKind (..)
-  , collectNodes
-  , sampleNames
-  , dataSlots
-  , dataIxSlots
-  ) where
-
-import Control.DeepSeq (NFData (..))
-import Control.Monad (forM, forM_)
-import Data.List (foldl', nub)
-import qualified Data.Set as Set
-import Data.Set (Set)
-import Data.Text (Text)
-import qualified Data.Text as T
-
-import Hanalyze.Model.HBM.Util (negInf, logSumExpA, choleskyL, hmmForwardLogLik)
-import Hanalyze.Model.HBM.Distribution
-
--- ---------------------------------------------------------------------------
--- @Free@ monad (再実装。Hanalyze.Model.HBM のものとは型が違うので別途定義)
--- ---------------------------------------------------------------------------
-
-data Free f a = Pure a | Free (f (Free f a))
-
-instance Functor f => Functor (Free f) where
-  fmap g (Pure a) = Pure (g a)
-  fmap g (Free x) = Free (fmap (fmap g) x)
-
-instance Functor f => Applicative (Free f) where
-  pure = Pure
-  Pure g <*> x  = fmap g x
-  Free fg <*> x = Free (fmap (<*> x) fg)
-
-instance Functor f => Monad (Free f) where
-  return = pure
-  Pure a >>= g = g a
-  Free x >>= g = Free (fmap (>>= g) x)
-
-liftF :: Functor f => f a -> Free f a
-liftF fa = Free (fmap Pure fa)
-
--- ---------------------------------------------------------------------------
--- 多相モデル (@Free@ monad)
--- ---------------------------------------------------------------------------
-
--- | DSL のプリミティブ。継続が @a -> next@ なので任意の @a@ を流せる。
---
--- 'Potential' は PyMC の @pm.Potential@ 相当で、任意の log-prob 項を
--- log-joint に加える。ソフト制約・カスタム尤度・正則化項などに使える。
--- | 構造化線形予測子 observe (Phase 54.1) の family / link。
---
--- 通常の 'Observe' は平均が不透明な AD 値ゆえ「β に線形」 という構造を
--- 保持できない。 'ObserveLM' は設計行列 X (Double) と β パラメタ名を **分離**
--- して持つことで線形構造をライブラリが知り、 54.2 で Gaussian-恒等リンクの
--- 十分統計量 collapse (観測和を tape O(p²) に畳む) を可能にする。
-data LMFamily
-  = LMGaussian Text   -- ^ identity link。 引数 = σ (誤差 SD) パラメタ名。
-  | LMPoisson         -- ^ log link (μ = exp η)。
-  | LMBernoulli       -- ^ logit link (p = 1/(1+e^{-η}))。
-  deriving (Show, Eq)
-
--- | 'ObserveLM' のランダム効果項 (Phase 54.4a)。 線形予測子に
--- @η_i += u^{re}[gid_i]@ を gather で加える。 設計行列の one-hot 指示列として
--- 密に展開する代わりに、 群 id ベクトルで疎に保持することで vec-tape の
--- 観測尤度勾配が群効果に対しても O(n) で済む (密展開は O(nG·n) で階層モデルで
--- 逆効果になる・54.4a 計測で確認)。
---
--- フィールド: u パラメタ名 (長さ nG・既に 'sample' 済の latent を参照) /
--- 各観測の群 id (長さ n・0..nG-1) /
--- prior スケール名 (Phase 54.4c): @Just τName@ なら各 u_j が
--- @u_j ~ Normal(0, τ)@ という標準的な階層 prior を持つことを宣言する。
--- これがあると 'compileGradU' は u-prior 勾配を **解析的に** (ベクトル化して)
--- 計算し、 対応する @u_j@ 'Sample' ノードを `ad` walk から除外できる
--- (per-grad の支配項だった O(nG) スカラ `ad` を排除)。 @Nothing@ なら
--- prior は従来通り `ad` 経路で扱う (後方互換)。 通常は 'reNormal'/'at' で
--- 自動的に @Just@ が載るので、 ユーザがこの構築子を直接書く必要はない。
---
--- per-row 重み (Phase 54.10): @Just ws@ (長さ n) なら @η_i += w_i·u^{re}[gid_i]@
--- (random slope = 群別係数 × 共変量)。 @Nothing@ = 全 1 (random intercept・
--- 後方互換)。 prior 解析勾配 (@u_j ~ Normal(0,τ)@) は重みと無関係に同形。
--- 由来 slot 名 (Phase 62): 5 番目 field は gids がどのデータ slot
--- ('dataNamedIx') 由来かの静的属性。 @Just slot@ なら 'lmParents' が slot 名を
--- 親集合に加え、 DAG に slot (DataN)→観測ノードのエッジが出る (PyMC
--- @b0[gid]@ 同型)。 'atIx' が自動で載せる。 'at' / IR 合成経路は @Nothing@
--- (従来挙動)。 hot closure ('CompiledLMBlock') には乗らない = per-draw 無影響。
-data REff = REff [Text] [Int] (Maybe Text) (Maybe [Double]) !(Maybe Text)
-  deriving (Show, Eq)
-
--- Phase 54.8: synthGaussLMBlocks の安全網 (force で全評価し poison を捕捉) 用。
-instance NFData REff where
-  rnf (REff us gids sc mw ms) =
-    rnf us `seq` rnf gids `seq` rnf sc `seq` rnf mw `seq` rnf ms
-
-data ModelF a next
-  = Sample  Text (Distribution a) (a -> next)
-  | Observe Text (Distribution a) [Double] next
-  | ObserveLM Text [Text] [[Double]] [REff] LMFamily [Double] next
-    -- ^ 構造化線形予測子 observe (Phase 54.1、 54.4a で REff 追加)。
-    --   フィールド: ブロック名 / β パラメタ名 (順序 = X の列) /
-    --   設計行列 X (n 行 × p 列、 Double) / ランダム効果項 (gather) /
-    --   family-link / 観測 ys (長さ n)。
-    --   各 i について η_i = Σ_j β_j·X_ij + Σ_re u^{re}[gid^{re}_i]、
-    --   μ_i = link⁻¹(η_i)、 log-lik = Σ_i logDensityObs(family μ_i) y_i。
-    --   β / u / 分散パラメタは別途 'sample' で宣言された latent を
-    --   **名前参照**する (prior は持たない)。
-    --   DAG 上は 1 観測ノード (親 = β + u + 分散パラメタ名)。
-  | Potential Text a next
-    -- ^ 名前付きの ad-hoc な log-prob 項。値 @a@ がそのまま log-joint に加算される。
-  | Deterministic Text a (a -> next)
-    -- ^ 名前付きの派生量 (PyMC `pm.Deterministic`)。log-joint には寄与せず、
-    --   サンプルごとに値を保存する。継続には値そのものを通すので、その後の
-    --   モデル中でも参照可能。
-  | Data Text [Double] (([a], [Double]) -> next)
-    -- ^ 名前付き観測データプレースホルダ (PyMC `pm.Data`)。
-    --   モデル内でデータを保持し、`withData` で外部から差し替え可能。
-    --   観測値を直接 `observe` に渡す代わりに、`dataNamed` で受け取って
-    --   `observe` に渡すと、後でデータ差し替えができる。
-    --   ★Phase 60.2 破壊的変更: 継続は ([a], [Double]) の 2 view を受ける
-    --   (格納は [Double] のまま・各 interpreter が lift)。 fst = モデル数値型
-    --   ('dataNamed'、 covariate 用・realToFrac 不要)、 snd = 生 [Double]
-    --   ('dataNamedObs'、 'observe' の観測値用)。 tuple は lazy なので
-    --   未使用側の lift コストは掛からない。
-  | DataIx Text [Int] ([Int] -> next)
-    -- ^ 離散 index 専用のデータプレースホルダ (Phase 60.2)。 群 index 等の
-    --   名義尺度を [Int] のまま運ぶ (= AD 型に持ち上げない・round 罠の根治)。
-    --   継続型は @a@ に依らず [Int] なので interpreter の lift も不要。
-  | PlateBegin Text Int next
-    -- ^ Plate 開始マーカー (Phase 40-A1、 Pyro/NumPyro 流の plate-block 糖衣)。
-    --   名前 + サイズ N を持つ plate スコープの開始。 直後から 'PlateEnd'
-    --   までに登録される 'Sample' / 'Observe' / 'Deterministic' は
-    --   buildModelGraph で「plate メンバ」 として描画される。
-    --   nested plate は LIFO スタックで対応。 log eval interpreter (logJoint
-    --   等) は **透過** に処理する (何もしない)。
-  | PlateEnd next
-    -- ^ Plate 終了マーカー (Phase 40-A1)。 最新の PlateBegin スコープを閉じる。
-  deriving Functor
-
-type Model a = Free (ModelF a)
-
--- | Type alias for the polymorphic model DSL.
--- @ModelP r = forall a. (Floating a, Ord a, TrackTag a) => Model a r@
--- ('TrackTag' は Phase 60.7 '!!!' の依存タグ注入用。 数値解釈は既定 id)。
-type ModelP r = forall a. (Floating a, Ord a, TrackTag a) => Model a r
-
-sample :: Text -> Distribution a -> Model a a
-sample n d = liftF (Sample n d id)
-
-observe :: Text -> Distribution a -> [Double] -> Model a ()
-observe n d ys = liftF (Observe n d ys ())
-
--- | 構造化線形予測子 observe (Phase 54.1)。
---
--- @observeLM name betaNames designX family ys@ は、 設計行列 @designX@
--- (n 行 × p 列) と β パラメタ名 @betaNames@ (長さ p・既に 'sample' で宣言済の
--- latent を参照) を **分離して**保持する観測ブロック。 各観測 i について
--- η_i = Σ_j β_j·X_ij を作り、 @family@ のリンク逆関数で μ_i に写して
--- 観測 @ys !! i@ の log-density を加算する。
---
--- 通常の per-obs @observe@ を N 回呼ぶのと数値的に等価だが、 線形構造を
--- 保持するので 54.2 で Gaussian-恒等リンクの十分統計量 collapse に乗せられる。
-observeLM :: Text -> [Text] -> [[Double]] -> LMFamily -> [Double] -> Model a ()
-observeLM n betas designX fam ys = liftF (ObserveLM n betas designX [] fam ys ())
-
--- | ランダム効果付き 'observeLM' (Phase 54.4a)。
---
--- @observeLMR name betaNames designX reffs family ys@ は 'observeLM' に
--- ランダム効果項 @reffs@ を加えたもの。 各 'REff' は (u パラメタ名, 群 id) で
--- @η_i += u^{re}[gid_i]@ を **gather** で寄与する。 群効果を設計行列の one-hot
--- 指示列に密展開すると vec-tape 勾配が O(nG·n) になり階層モデルで逆効果になる
--- (54.4a 計測) ため、 群構造は疎に保持して gather で O(n) に保つ。
-observeLMR :: Text -> [Text] -> [[Double]] -> [REff] -> LMFamily -> [Double]
-           -> Model a ()
-observeLMR n betas designX reffs fam ys =
-  liftF (ObserveLM n betas designX reffs fam ys ())
-
--- ---------------------------------------------------------------------------
--- 第一級ランダム効果値 (Phase 54.4c)
--- ---------------------------------------------------------------------------
-
--- | 第一級ランダム効果値 (Phase 54.4c)。 'reNormal' で宣言した nG 個の
--- iid @Normal(0, τ)@ latent を、 構造 (基底名・群数・スケール名・値) ごと
--- ひとつの値に載せて持ち運ぶ。 これにより観測の線形予測子に効果を載せるとき
--- 文字列添字 (@"u_" <> show j@) も @us !! g@ も書かずに 'at' で gather でき
--- (Haskell 王道の「構造を値に載せて流す」)、 さらにスケール名が構造として
--- 保持されるので 'compileGradU' が u-prior 勾配を解析的にベクトル化できる。
-data REffect a = REffect
-  { reffBase   :: !Text   -- ^ 基底名 (例 @"u"@)。 latent 名は @base_<j>@。
-  , reffNG     :: !Int    -- ^ 群数 nG
-  , reffScale  :: !Text   -- ^ スケール latent の名前 (@u_j ~ Normal(0, scale)@)
-  , reffValues :: [a]     -- ^ サンプル済 nG 個の値 (forward 評価・deterministic 用)
-  }
-
--- | 'REffect' の latent 名 (@base_0 .. base_{nG-1}@)。
-reffNames :: REffect a -> [Text]
-reffNames re = [ indexed (reffBase re) j | j <- [0 .. reffNG re - 1] ]
-
--- | 群別ランダム効果を第一級値として宣言する (Phase 54.4c)。
---
--- @reNormal base nG scaleName scaleVal@ は @base_0 .. base_{nG-1}@ という
--- nG 個の latent を各々 @Normal(0, scaleVal)@ として 'sample' し、 その構造
--- (基底名 / nG / スケール名 / 値) を 'REffect' にまとめて返す。 @scaleName@ は
--- @scaleVal@ を生んだスケール latent の名前 (例 @"tau_u"@) で、 解析 prior 勾配
--- 経路 ('compileGradU') がスケール変数を引くために構造として保持する
--- (値は名前を覚えていないため明示的に渡す)。
---
--- @
--- tau <- sample "tau_u" (HalfNormal 5)
--- u   <- reNormal "u" nG "tau_u" tau
--- observeNormalLM "y" xRows betaNames [u \`at\` gids] "sigma" ys
--- @
-reNormal :: Num a => Text -> Int -> Text -> a -> Model a (REffect a)
-reNormal base nG scaleName scaleVal = do
-  vals <- forM [0 .. nG - 1] $ \j ->
-            sample (indexed base j) (Normal 0 scaleVal)
-  pure (REffect base nG scaleName vals)
-
--- | 'REffect' を観測の群 id 列に対して gather し 'REff' (観測ブロック用) に変換する。
--- @η_i += u^{re}[gid_i]@。 スケール名を 'REff' に載せるので、 これ経由で観測に
--- 入った効果は 'compileGradU' の解析 prior 勾配経路に乗る。
-at :: REffect a -> [Int] -> REff
-at re gids = REff (reffNames re) gids (Just (reffScale re)) Nothing Nothing
-
--- | Gaussian-恒等リンク版の構造化 observe (Phase 54.4c)。 'observeLMR' の
--- @LMGaussian@ 特化で、 'at' で作った 'REff' をそのまま渡せる薄いラッパ。
---
--- @observeNormalLM name designX betaNames reffs sigmaName ys@。
-observeNormalLM :: Text -> [[Double]] -> [Text] -> [REff] -> Text -> [Double]
-                -> Model a ()
-observeNormalLM name designX betaNames reffs sName ys =
-  observeLMR name betaNames designX reffs (LMGaussian sName) ys
-
--- | Multivariate observation (for 'MvNormal'). Each observation is a
--- length-@k@ vector; pass them as a list @[[Double]]@.
--- 内部的には @concat@ で flatten され、評価時に Distribution の次元 k で chunk される。
-observeMV :: Text -> Distribution a -> [[Double]] -> Model a ()
-observeMV n d obss = liftF (Observe n d (concat obss) ())
-
--- | Multi-output observation helper. Takes @q@ pairs of
--- @observe (prefix <> \"_\" <> j) dist_j ys_j@ を順に発行する。
---
--- 多出力回帰の尤度を 1 行で書きたいときに使う:
---
--- @
--- observeColumns \"y\" [(Normal mu_j sigma_j, ysCol j) | j <- [0 .. q - 1]]
--- @
-observeColumns :: Text -> [(Distribution a, [Double])] -> Model a ()
-observeColumns prefix pairs =
-  mapM_ (\(j, (d, ys)) ->
-           observe (prefix <> "_" <> T.pack (show (j :: Int))) d ys)
-        (zip [0..] pairs)
-
--- | インデックス付きノード名を作る: @indexed "theta" 1 == "theta_1"@。
---
--- 階層モデルで群ごとの 'sample' / 'observe' 名を作るときに頻出する
--- @T.pack ("theta_" ++ show j)@ ボイラープレートを畳む。 アンダースコアは
--- 自動付与 (= 'observeColumns' / 'nonCenteredNormal' 等の命名規約に一致)。
---
--- > forM_ (zip [1..] groupData) $ \(j, ys) -> do
--- >   theta <- sample (indexed "theta" j) (Normal mu tau)   -- "theta_1" …
--- >   observe (indexed "y" j) (Normal theta 1) ys
-indexed :: Text -> Int -> Text
-indexed pre i = pre <> "_" <> T.pack (show i)
-
--- | 'indexed' の中置演算子版: @"theta" .# j == "theta_1"@。
---
--- (Haskell の演算子記号に @_@ は使えないため @.#@ を採用。)
-infixl 9 .#
-(.#) :: Text -> Int -> Text
-(.#) = indexed
-
--- | Add an arbitrary log-probability term to the model (analogous to
--- PyMC's @pm.Potential@).
---
--- 通常のサンプリング/観測では表せない log-density 寄与を入れるのに使う。
--- 典型用途:
---
---   * **ソフト制約**: @potential \"order\" (if mu1 < mu2 then 0 else (-1e10))@
---   * **カスタム尤度**: 既存 'Distribution' で表せない尤度項
---   * **正則化**: ベイズ的な正則化 (e.g. ridge: @-0.5 * lambda * sum (map (^2) betas)@)
---
--- @Potential@ の値は 'logJoint' と 'logPrior' に加算される
--- ('logLikelihood' には含まれない — これらは @observe@ 専用)。
-potential :: Text -> a -> Model a ()
-potential nm v = liftF (Potential nm v ())
-
--- | 派生量を名前付きで保存する (PyMC `pm.Deterministic` 相当)。
---
--- log-joint には寄与しないが、各 posterior サンプルごとに値が記録され
--- 'augmentChainWithDeterministic' で Chain に注入できる。
---
--- 例:
---
--- > tau <- deterministic "tau" (1 / (sigma * sigma))
-deterministic :: Text -> a -> Model a a
-deterministic nm v = liftF (Deterministic nm v id)
-
--- | DAG / Node 表示用の分布名 (リンク逆関数を適用した観測分布の名前)。
-lmFamilyName :: LMFamily -> Text
-lmFamilyName (LMGaussian _) = "Normal"
-lmFamilyName LMPoisson      = "Poisson"
-lmFamilyName LMBernoulli    = "Bernoulli"
-
--- | 'ObserveLM' が参照する latent パラメタ名の集合 (DAG の親)。
--- β + ランダム効果 u + (Gaussian の) σ。
-lmParents :: [Text] -> [REff] -> LMFamily -> Set Text
-lmParents betaNames reffs fam =
-  Set.fromList betaNames
-  <> Set.fromList (concat [ uNames | REff uNames _ _ _ _ <- reffs ])
-  -- Phase 62: gids の由来 slot 名 ('atIx' 経由) も親に = slot→観測ノードのエッジ
-  <> Set.fromList [ s | REff _ _ _ _ (Just s) <- reffs ]
-  <> case fam of
-       LMGaussian sName -> Set.singleton sName
-       LMPoisson        -> Set.empty
-       LMBernoulli      -> Set.empty
-
--- ---------------------------------------------------------------------------
--- Phase 40-A1: Plate notation
--- ---------------------------------------------------------------------------
-
--- | Pyro / NumPyro 流の plate-block (Phase 40)。
---
--- @plate name n body@ は、 do-block 内で繰り返し作られる indexed RV 群
--- (e.g. @eta_0, eta_1, …, eta_{n-1}@) を **同じ plate に属する** と
--- マークする bracket。 'buildModelGraph' で plate 集約描画される。
---
--- 例 (8-schools):
---
--- > mu  <- sample "mu" (Normal 0 5)
--- > tau <- sample "tau" (HalfCauchy 5)
--- > etas <- plate "school" 8 $ forM [0..7] $ \j ->
--- >           sample ("eta_" <> T.pack (show j)) (Normal 0 1)
--- > _ <- plate "school" 8 $ forM_ [0..7] $ \j ->
--- >        observe ("y_" <> T.pack (show j))
--- >                (Normal (mu + tau * (etas !! j)) 1) [ys !! j]
---
--- 内部: 'PlateBegin' / 'PlateEnd' マーカーで囲む。 log eval (logJoint
--- / logPrior 等) は **透過** に動作し、 plate は描画レイヤーでのみ
--- 意味を持つ。 NUTS / Gibbs / VI への影響なし。
-plate :: Text -> Int -> Model a r -> Model a r
-plate name n body = do
-  liftF (PlateBegin name n ())
-  r <- body
-  liftF (PlateEnd ())
-  return r
-
--- | 'plate' の利便 helper: @plateI name n f@ = @plate name n (forM [0..n-1] f)@。
--- 「N 個の indexed RV を作る」 という最頻パターン向け糖衣。
---
--- 例:
---
--- > etas <- plateI "school" 8 $ \j ->
--- >           sample ("eta_" <> T.pack (show j)) (Normal 0 1)
-plateI :: Text -> Int -> (Int -> Model a r) -> Model a [r]
-plateI name n action = plate name n (forM [0 .. n - 1] action)
-
--- | 'plateI' の返り値を捨てる版 (@forM_@ の plate 版・index 反復)。
--- @plateI_ name n f = plate name n (forM_ [0..n-1] f)@。 観測のみの index
--- ループ向け (@plateForM_ name [0..n-1] f@ と同義だが index 反復の意図が明示的・
--- 'plateForM' / 'plateForM_' の対称に合わせ index 版にも破棄形を用意)。
---
--- 例 (8-schools の観測):
---
--- > plateI_ "school" 8 $ \j ->
--- >   observe ("y" .# j) (Normal (mu + tau * etas !! j) 1) [ys !! j]
-plateI_ :: Text -> Int -> (Int -> Model a r) -> Model a ()
-plateI_ name n action = plate name n (forM_ [0 .. n - 1] action)
-
--- | データ行リストを plate で囲んで反復する糖衣 (@forM@ の plate 版・引数順も @forM@ 形)。
--- @plateForM name rows f = plate name (length rows) (forM rows f)@。 plate サイズは
--- 行数から自動。 観測ループの定番 @plate name (length rows) $ forM_ … rows@ を畳む。
---
--- 例 (ベイズ線形回帰の観測):
---
--- > plateForM_ "obs" (zip x y) $ \(xi, yi) -> do
--- >   mu <- deterministic "mu" (a + b * realToFrac xi)
--- >   observe "obs" (Normal mu s) [yi]
-plateForM :: Text -> [b] -> (b -> Model a r) -> Model a [r]
-plateForM name rows f = plate name (length rows) (forM rows f)
-
--- | 返り値を捨てる版 (@forM_@ の plate 版)。 観測のみのループに。
-plateForM_ :: Text -> [b] -> (b -> Model a r) -> Model a ()
-plateForM_ name rows f = plate name (length rows) (forM_ rows f)
-
--- | 低レベル plate API: 任意の Model action を plate スコープで包む。
--- 'plate' は @withPlate name n@ + body の組合せに分解される。 nested
--- plate を独自構築する際の primitive。
-withPlate :: Text -> Int -> Model a r -> Model a r
-withPlate = plate
-
--- | 名前付きデータプレースホルダを宣言する (PyMC `pm.Data` 相当)。
--- 既定値 @ys@ を持ち、後で 'withData' により差し替え可能。
---
--- 典型的な使い方:
---
--- > model = do
--- >   y <- dataNamed "y" trainData
--- >   mu <- sample "mu" (Normal 0 5)
--- >   observe "y" (Normal mu 1) y
---
--- そして @withData \"y\" testData model@ で同じ構造で別データを使う。
---
--- ★Phase 60.2 破壊的変更: 戻り値は @[a]@ (モデルの数値型)。 受け取った値は
--- そのまま式に入る (@realToFrac@ 不要)。 @a@ には @Real@ 制約が無いので、
--- 旧コードの @realToFrac xi@ は型エラーになる (= 無言の挙動変化が起きない
--- 壊れ方)。 機械的に @realToFrac@ を消せば移行完了。
--- 観測値として 'observe' に渡す側 (@[Double]@ が要る) は 'dataNamedObs' を使う。
-dataNamed :: Text -> [Double] -> Model a [a]
-dataNamed n ys = liftF (Data n ys fst)
-
--- | 'dataNamed' の同義 (Phase 60.6)。 役割 suffix 三点セットの正書き:
---
--- > x  <- dataNamedX   "x" []   -- 説明変数: モデル数値型 [a]
--- > ys <- dataNamedObs "y" []   -- 目的変数: 生 [Double] ('observe' へ)
--- > gs <- dataNamedIx  "g" []   -- 群 index: [Int]
---
--- 既存コードの 'dataNamed' もそのまま使える (削除予定なし)。
-dataNamedX :: Text -> [Double] -> Model a [a]
-dataNamedX = dataNamed
-
--- | 'dataNamed' と同じ slot の **観測値 view** (生 @[Double]@)。
--- 'observe' / 'observeLM' の観測値引数は AD に持ち上げない @[Double]@ 固定
--- なので、 y 側のデータ slot はこちらで受ける (Phase 60.2):
---
--- > x  <- dataNamed    "x" []   -- covariate: モデル数値型 [a]
--- > ys <- dataNamedObs "y" []   -- 観測値:    生 [Double]
--- > ...
--- > observe "y" (Normal mu s) ys
---
--- 同名 slot を 'dataNamed' と 'dataNamedObs' の両 view で読んでもよい
--- (差し替えは 'withData' / 列 bind が slot 名単位で行うため一貫する)。
-dataNamedObs :: Text -> [Double] -> Model a [Double]
-dataNamedObs n ys = liftF (Data n ys snd)
-
--- | 離散 index 専用のデータプレースホルダ (Phase 60.2、 60.7 で 'Ix' 戻りに刷新)。
--- 群 index 等を slot 名タグ付き index 'Ix' で運ぶ。 @bs '!!!' g@ で引くと
--- DAG に slot→利用先のエッジが自動で出る (PyMC の @b0[gid]@ 同型)。
--- 'Ix' は Num でないので誤って算術に混ぜると型エラーで止まる
--- (= 連続値経路の round 罠根治、 60.2 から継続)。
---
--- > gs <- dataNamedIx "g" [0,0,1,1,2]
--- > let mu_i = b0s !!! g   -- round 不要・DAG に g→mu エッジ
-dataNamedIx :: Text -> [Int] -> Model a [Ix]
-dataNamedIx n is = liftF (DataIx n is (map (\i -> Ix i (Just n))))
-
--- | slot 名タグ付き離散 index (Phase 60.7)。 'dataNamedIx' が返し、 '!!!' で
--- 使う。 由来 slot 名 ('ixSlot') は DAG 抽出 (Track 解釈) のエッジ生成にだけ
--- 使われ、 数値評価では 'ixVal' のみが意味を持つ。
-data Ix = Ix
-  { ixVal  :: !Int          -- ^ index 本体 (0..nG-1)
-  , ixSlot :: !(Maybe Text) -- ^ 由来 slot 名 ('dataNamedIx' なら Just)
-  } deriving (Show, Eq)
-
--- | 解釈ごとの依存タグ注入 (Phase 60.7)。 既定 = 何もしない (数値解釈は
--- ゼロコスト・サンプリングはビット不変)。 'Track' 解釈だけが override して
--- 依存集合に slot 名を足し、 DAG にエッジを出す。
-class TrackTag a where
-  tagDep :: Text -> a -> a
-  tagDep _ = id
-  {-# INLINE tagDep #-}
-
-instance TrackTag Double
-
--- dogfood 典型 (群別係数のタプル) 用: 成分ごとに伝播
-instance (TrackTag a, TrackTag b) => TrackTag (a, b) where
-  tagDep nm (a, b) = (tagDep nm a, tagDep nm b)
-instance (TrackTag a, TrackTag b, TrackTag c) => TrackTag (a, b, c) where
-  tagDep nm (a, b, c) = (tagDep nm a, tagDep nm b, tagDep nm c)
-instance (TrackTag a, TrackTag b, TrackTag c, TrackTag d)
-      => TrackTag (a, b, c, d) where
-  tagDep nm (a, b, c, d) = (tagDep nm a, tagDep nm b, tagDep nm c, tagDep nm d)
-
--- | slot 名タグ付き索引 (Phase 60.7)。 @bs '!!!' g@ = @bs !! ixVal g@ に、
--- Track 解釈でのみ g の由来 slot 名を依存タグとして注入する
--- (= DAG に slot→利用先エッジ。 数値解釈は '!!' と同コスト)。
-(!!!) :: TrackTag b => [b] -> Ix -> b
-xs !!! Ix i ms = maybe id tagDep ms (xs !! i)
-infixl 9 !!!
-{-# INLINE (!!!) #-}
-
--- | 'at' の 'Ix' 版 (Phase 60.7)。 'dataNamedIx' の gids を random effect の
--- gather に渡す。 Phase 62: 先頭 'Ix' の由来 slot 名 ('ixSlot') を 'REff' に
--- 載せるので、 DAG に slot→観測ノードのエッジが出る (gather の gids は単一
--- slot 由来が通常形ゆえ先頭で代表)。 '!!!' (deterministic μ 経路) と並ぶ
--- PyMC @b0[gid]@ 同型の両経路対応。
-atIx :: REffect a -> [Ix] -> REff
-atIx re gids =
-  REff (reffNames re) (map ixVal gids) (Just (reffScale re)) Nothing
-       (case gids of { Ix _ ms : _ -> ms; [] -> Nothing })
-
--- | Replace a named data block in the model. If no match exists the
--- model is returned unchanged.
--- 同じ名前が複数回出現する場合は全箇所で差し替わる。
---
--- 型シグネチャは @Model a r@ なので、ユーザーが @ModelP r@ から呼ぶ場合
--- そのまま多相的に使える (各 @a@ で個別に適用される)。
-withData :: forall r. Text -> [Double] -> ModelP r -> ModelP r
-withData n new m = mPoly
-  where
-    -- 戻り値を多相モデルとして再構築。各 @a@ 個別に元の m を走査する。
-    mPoly :: forall a. (Floating a, Ord a, TrackTag a) => Model a r
-    mPoly = go m
-      where
-        go :: Model a r -> Model a r
-        go (Pure r) = Pure r
-        go (Free f) = Free (case f of
-          Data n' ys k
-            | n == n'   -> Data n' new (\d -> go (k d))
-            | otherwise -> Data n' ys  (\d -> go (k d))
-          DataIx n' is k       -> DataIx n' is (\d -> go (k d))
-          Sample nm d k        -> Sample nm d (\v -> go (k v))
-          Observe nm d ys nx   -> Observe nm d ys (go nx)
-          ObserveLM nm bs xs re fam ys nx -> ObserveLM nm bs xs re fam ys (go nx)
-          Potential nm v nx    -> Potential nm v (go nx)
-          Deterministic nm v k -> Deterministic nm v (\v' -> go (k v'))
-          PlateBegin nm sz nx  -> PlateBegin nm sz (go nx)
-          PlateEnd nx          -> PlateEnd (go nx))
-
--- | 'withData' の離散 index 版 (Phase 60.2): 名前付き 'DataIx' ブロックを
--- 外部から差し替える。 一致しなければモデルは不変。
-withDataIx :: forall r. Text -> [Int] -> ModelP r -> ModelP r
-withDataIx n new m = mPoly
-  where
-    mPoly :: forall a. (Floating a, Ord a, TrackTag a) => Model a r
-    mPoly = go m
-      where
-        go :: Model a r -> Model a r
-        go (Pure r) = Pure r
-        go (Free f) = Free (case f of
-          DataIx n' is k
-            | n == n'   -> DataIx n' new (\d -> go (k d))
-            | otherwise -> DataIx n' is  (\d -> go (k d))
-          Data n' ys k         -> Data n' ys (\d -> go (k d))
-          Sample nm d k        -> Sample nm d (\v -> go (k v))
-          Observe nm d ys nx   -> Observe nm d ys (go nx)
-          ObserveLM nm bs xs re fam ys nx -> ObserveLM nm bs xs re fam ys (go nx)
-          Potential nm v nx    -> Potential nm v (go nx)
-          Deterministic nm v k -> Deterministic nm v (\v' -> go (k v'))
-          PlateBegin nm sz nx  -> PlateBegin nm sz (go nx)
-          PlateEnd nx          -> PlateEnd (go nx))
-
--- | Latent multivariate-normal vector (analogous to PyMC's
--- @pm.MvNormal@ used as a latent).
---
--- 非中心化パラメタ化 + Cholesky 分解で実装:
---
---   z_i ~ Normal(0, 1)  (i = 0..K-1, 独立な latent)
---   x   = μ + L z       (L = Cholesky(Σ))
---
--- 各 z_i は通常の latent として NUTS が探索し、x は派生量として
--- Chain に記録される。共分散行列が他の latent に依存する形でも
--- 動作する (choleskyL は @(Floating a, Ord a)@ 多相)。
---
--- 共分散が非正定値のときは μ をそのまま返す (NUTS 探索中の不正領域
--- に対する graceful fallback)。
---
--- 戻り値: K 次元 latent ベクトル @[a]@ (μ + L z)。
--- Chain には @<name>_z<i>@ (raw latent) と @<name>_<i>@ (派生量) を保存。
-mvNormalLatent :: forall a. (Floating a, Ord a)
-               => Text -> [a] -> [[a]] -> Model a [a]
-mvNormalLatent name muVec covMatrix = do
-  let k = length muVec
-  zs <- mapM (\i -> sample (name <> "_z" <> T.pack (show i)) (Normal 0 1))
-             [0 .. k - 1]
-  let xs = case choleskyL covMatrix of
-        Just l  -> [ (muVec !! i) +
-                       sum [ ((l !! i) !! j) * (zs !! j)
-                           | j <- [0 .. i] ]
-                   | i <- [0 .. k - 1] ]
-        Nothing -> muVec      -- non-PD のフォールバック
-  mapM
-    (\(i, x) -> deterministic (name <> "_" <> T.pack (show i)) x)
-    (zip [0 :: Int ..] xs)
-
--- | LKJ 相関行列の Cholesky factor (PyMC @LKJCholeskyCov@ 相当)。
---
--- LKJ(η) 事前: p(R) ∝ |R|^(η-1)。η = 1 で uniform、η > 1 で I に集中。
---
--- 実装は canonical partial correlations (CPC) 法:
---   z_ij ~ scaled Beta(α_i, α_i) on (-1, 1),  α_i = η + (K - i - 1) / 2
---     (i = 1..K-1, j = 0..i-1)
---
--- 各 z_ij は @<name>_pc<i>_<j>@ (Beta latent in (0,1)、内部で 2u-1 に変換)
--- として保存。Cholesky factor の各要素は派生量 @<name>_L<i>_<j>@。
---
--- 戻り値: K×K 下三角行列 L (R = L Lᵀ となる相関の Cholesky)。
--- 対角は √(1 - Σ z_{i,k}²)、対角下は z_ij × √(Π_{k<j}(1-z_{i,k}²))。
-lkjCorrCholesky :: forall a. (Floating a, Ord a)
-                => Text -> Int -> a -> Model a [[a]]
-lkjCorrCholesky name k eta
-  | k < 2     = error "lkjCorrCholesky: dimension must be >= 2"
-  | otherwise = do
-      -- 各 (i, j) で 1 <= j < i <= K-1 の partial correlation を sample
-      let pcIndices = [(i, j) | i <- [1 .. k - 1], j <- [0 .. i - 1]]
-      pcs <- mapM
-        (\(i, j) -> do
-            let alpha = eta + fromIntegral (k - i - 1) / 2
-                tag   = T.pack (show i) <> "_" <> T.pack (show j)
-            u <- sample (name <> "_u" <> tag) (Beta alpha alpha)
-            deterministic (name <> "_pc" <> tag) (2 * u - 1))
-        pcIndices
-      -- (i,j) → z_ij マップ
-      let pcMap = zip pcIndices pcs
-          lookupPC i j = head [v | ((ii, jj), v) <- pcMap, ii == i, jj == j]
-      -- Cholesky factor を構築 (下三角)
-      let lRow i =
-            [ if j > i then 0
-              else if i == 0 && j == 0 then 1
-              else if j == i  -- 対角
-                   then sqrt (1 - sum [ let z = lookupPC i kk
-                                        in z * z | kk <- [0 .. i - 1] ])
-              else            -- 対角下 j < i
-                let z       = lookupPC i j
-                    factor2 = product [ let z' = lookupPC i kk
-                                        in 1 - z' * z' | kk <- [0 .. j - 1] ]
-                in z * sqrt factor2
-            | j <- [0 .. k - 1] ]
-          lMat = [lRow i | i <- [0 .. k - 1]]
-      -- L 各要素を deterministic として保存
-      _ <- mapM
-        (\(i, j) ->
-          deterministic (name <> "_L" <> T.pack (show i) <> "_" <> T.pack (show j))
-                        ((lMat !! i) !! j))
-        [(i, j) | i <- [0 .. k - 1], j <- [0 .. i]]
-      return lMat
-
--- | RBF (exponentiated quadratic) カーネルによる GP 共分散行列
--- (Stan @gp_exp_quad_cov(x, alpha, rho)@ 相当)。
---
--- @K[i][j] = alpha^2 * exp(-0.5 * (x_i - x_j)^2 / rho^2)@、対角には数値安定化の
--- jitter (1e-10) を加える (Stan 原典の @+ diag_matrix(rep_vector(1e-10, N))@ に
--- 対応)。@x@ は 'dataNamedX' で束縛した @[a]@ をそのまま渡す (data と
--- ハイパーパラメータ alpha/rho は共に @a@ 型なので realToFrac 不要)。
---
--- Phase 90 A2: vecIR (per-row 独立項の和が前提) には密行列が構造的に載らない
--- ため、legacy walk+ad 経路 (`grad fFull`) で使う想定の孤立関数。
-gpExpQuadCov :: forall a. Floating a => [a] -> a -> a -> [[a]]
-gpExpQuadCov xs alpha rho =
-  [ [ let d = xi - xj
-      in alpha * alpha * exp (negate 0.5 * d * d / (rho * rho))
-           + (if i == j then 1e-10 else 0)
-    | (j, xj) <- zip [0 :: Int ..] xs ]
-  | (i, xi) <- zip [0 :: Int ..] xs ]
-
--- | Gaussian Process 潜在関数 (Stan の non-centered GP パラメタ化相当):
---
--- > f_tilde ~ Normal(0, 1)     (各点独立)
--- > L_cov = cholesky_decompose(gp_exp_quad_cov(x, alpha, rho))
--- > f = L_cov * f_tilde
---
--- 既存の 'choleskyL' ('mvNormalLatent' と同じ AD 対応 Cholesky 分解) をそのまま
--- 流用する。共分散が非正定値のときは全ゼロにフォールバックする
--- ('mvNormalLatent' と同型の graceful fallback)。
---
--- 戻り値: N 次元 latent ベクトル @[a]@ (GP 事後関数値 f)。各要素は
--- @<name>_f<i>@ として deterministic 保存される。
-gpLatent :: forall a. (Floating a, Ord a)
-         => Text -> [a] -> a -> a -> Model a [a]
-gpLatent name xs alpha rho = do
-  let n = length xs
-  ftilde <- mapM (\i -> sample (name <> "_ftilde" <> T.pack (show i)) (Normal 0 1))
-                 [0 .. n - 1]
-  let cov = gpExpQuadCov xs alpha rho
-      fs = case choleskyL cov of
-        Just l  -> [ sum [ (l !! i !! j) * (ftilde !! j) | j <- [0 .. i] ]
-                   | i <- [0 .. n - 1] ]
-        Nothing -> replicate n 0    -- non-PD のフォールバック
-  mapM
-    (\(i, f) -> deterministic (name <> "_f" <> T.pack (show i)) f)
-    (zip [0 :: Int ..] fs)
-
--- | AR(1) latent 時系列 (PyMC `pm.AR1` 相当)。
---
--- 状態方程式:  x_t = ϕ x_{t−1} + ε_t,   ε_t ~ Normal(0, σ)
--- 初期分布:    x_0 ~ Normal(0, σ / √(1 − ϕ²))   (定常分布、|ϕ| < 1 なら有限)
---
--- 引数 @phi@ は AR 係数、@sigma@ は innovation の sd。N 個の latent
--- 状態 x_0 .. x_{N-1} を非中心化パラメタ化で sample する:
---
---   raw_t ~ Normal(0, 1)
---   x_t = phi * x_{t-1} + sigma * raw_t       (t > 0)
---   x_0 = (sigma / √(1 - ϕ²)) * raw_0
---
--- 戻り値: x_0 .. x_{N-1} の latent 値リスト ([a])。各 raw_t は
--- @<name>_raw<t>@、x_t 自体は派生量 @<name>_<t>@ として保存。
---
--- |ϕ| ≥ 1 のフォールバック: 初期 sd を sigma に置き換える。
-ar1Latent :: forall a. (Floating a, Ord a)
-          => Text -> Int -> a -> a -> Model a [a]
-ar1Latent name nT phi sigma
-  | nT < 1 = error "ar1Latent: length must be >= 1"
-  | otherwise = do
-      raws <- mapM
-        (\t -> sample (name <> "_raw" <> T.pack (show t)) (Normal 0 1))
-        [0 .. nT - 1]
-      let phi2     = phi * phi
-          stat     = if phi2 < 1
-                       then sigma / sqrt (1 - phi2)
-                       else sigma   -- フォールバック
-      -- Phase 38: scanl で xs を先に組み立てると、 各 x_t の Track が
-      -- {x_raw0, …, x_raw_t} という遠い親集合を保持してしまい、 後で
-      -- deterministic 登録しても下流の親が plate-style にならない。
-      -- 各 step で deterministic の戻り値 (det 名で再ラベルされた Track)
-      -- を次の step に渡す monadic recursion で組む。
-      x0 <- deterministic (name <> "_0") (stat * head raws)
-      let chain _    []           = return []
-          chain xPrev ((t, rt):rest) = do
-            xt <- deterministic
-                    (name <> "_" <> T.pack (show t))
-                    (phi * xPrev + sigma * rt)
-            xs' <- chain xt rest
-            return (xt : xs')
-      xs' <- chain x0 (zip [(1 :: Int) .. ] (tail raws))
-      return (x0 : xs')
-
--- | 非中心化 (non-centered) 正規分布。
---
--- @x ~ Normal(loc, scale)@ を直接サンプリングする代わりに、
---
--- > raw <- sample (name <> "_raw") (Normal 0 1)
--- > deterministic name (loc + scale * raw)
---
--- に展開する。loc / scale が他の latent に依存するとき、centered
--- パラメタ化は HMC の posterior が病的になりやすいので、それを
--- 緩和するヘルパ。Neal's funnel が代表例。
---
--- 戻り値は constrained な値 @loc + scale * raw@。Chain には
--- @<name>_raw@ (latent) と @<name>@ (derived) の両方が保存される。
-nonCenteredNormal :: Num a => Text -> a -> a -> Model a a
-nonCenteredNormal name loc scale = do
-  raw <- sample (name <> "_raw") (Normal 0 1)
-  deterministic name (loc + scale * raw)
-
--- | GLMM family for 'glmmRandomIntercept' (Phase 37-A6)。
-data GlmmFamily
-  = GlmmGaussian   -- ^ 連続 y、 残差 SD `sigma` も sample される
-  | GlmmBinomial   -- ^ 0/1 y、 Bernoulli(σ(η))
-  | GlmmPoisson    -- ^ 非負整数 y、 Poisson(exp η)
-  deriving (Show, Eq)
-
--- | Random intercept GLMM helper (Phase 37-A6)。
---
--- `y ~ X β + u_{group(i)} + (error)` を 1 関数で組み立てる:
---
--- * 固定効果 @β_k ~ Normal(0, 5)@ (p 個)
--- * 群レベル SD @τ_u ~ HalfNormal(5)@
--- * 群効果 @u_j ~ Normal(0, τ_u)@ (nG 個、 centered パラメタ化。
---   群数大 / 群内 N 小なら別途 'nonCenteredNormal' を直接使う)
--- * family に応じた観測:
---     * Gaussian: 残差 @σ ~ Exp(1)@ を sample → @y ~ Normal(X β + u_j, σ)@
---     * Binomial: @y ~ Bernoulli(σ(X β + u_j))@、 y は 0/1
---     * Poisson:  @y ~ Poisson(exp(X β + u_j))@、 y は非負整数
---
--- 観測は単一の構造化ブロック @observeLMR \"y\"@ として発行される (Phase 54.4a・
--- PyMC/Stan と同じく 1 ベクトル化観測ノード。 旧実装は per-obs @y_i@ を n 個展開)。
--- 固定効果は密設計行列・群効果は gather で表現するので vec-tape ハイブリッド
--- gradADU の高速経路に乗る。 chain 上の latent 名:
--- @beta_0, …, beta_{p-1}, tau_u, u_0, …, u_{nG-1}, sigma?@.
---
--- 個別 (random slope や non-centered) が必要ならパターン 5 (random slope) /
--- 形式 C (non-centered) を直接書く方が柔軟。 本 helper は最頻ユースケース
--- 「固定効果 + 群別切片」 専用の shorthand。
-glmmRandomIntercept
-  :: forall a. (Floating a, Ord a)
-  => GlmmFamily   -- ^ 尤度の family
-  -> [[Double]]   -- ^ 固定効果 design X (n × p)、 切片は手で 1 列追加すること
-  -> [Int]        -- ^ 各観測の group id (0..nG-1)
-  -> [Double]     -- ^ 観測 y (length n)
-  -> Model a ()
-glmmRandomIntercept fam xRows gids ys = do
-  let n  = length ys
-      p  = if null xRows then 0 else length (head xRows)
-      nG = if null gids then 0 else maximum gids + 1
-  -- 固定効果
-  betas <- forM [0 .. p - 1] $ \k ->
-    sample (T.pack ("beta_" ++ show k)) (Normal 0 5)
-  -- 群レベル SD
-  tauU <- sample "tau_u" (HalfNormal 5)
-  -- 群別切片を第一級ランダム効果値として宣言 (Phase 54.4c)。 reNormal が
-  -- u_0..u_{nG-1} ~ Normal(0, tauU) を sample しつつスケール名 "tau_u" を構造に
-  -- 載せるので、 観測に `at` で gather すると compileGradU の **解析 prior 勾配**
-  -- 経路に乗り、 prior の O(nG) スカラ ad が排除される。
-  u <- reNormal "u" nG "tau_u" tauU
-  -- Gaussian のみ残差 SD
-  _mSig <- case fam of
-    GlmmGaussian -> Just <$> sample "sigma" (Exponential 1)
-    _            -> return Nothing
-  -- 観測は単一の構造化ブロック (observeLMR) として発行する (Phase 54.4a)。
-  -- η_i = Σ_k β_k X_ik + u_{g(i)} を固定効果 (密設計行列) + 群効果 (gather) で
-  -- 表現するので、 vec-tape ハイブリッド gradADU の高速経路に乗る。 PyMC/Stan と
-  -- 同じく観測は 1 ベクトル化ノード "y" (旧: per-obs y_i を n 個展開)。
-  let betaNames = [ T.pack ("beta_" ++ show k) | k <- [0 .. p - 1] ]
-      reffs     = [ u `at` gids ]
-      lmFam     = case fam of
-        GlmmGaussian -> LMGaussian "sigma"
-        GlmmBinomial -> LMBernoulli
-        GlmmPoisson  -> LMPoisson
-  -- betas/n は名前参照ゆえ値は使わないが、 latent 宣言として必要。
-  _ <- pure (betas, n)
-  observeLMR "y" betaNames xRows reffs lmFam ys
-
--- | Dirichlet distribution (analogous to PyMC's @pm.Dirichlet@), expanded
--- via stick-breaking
--- latent ベクトル。
---
--- 引数:
---   * @name@   : ベース名。展開後は @<name>_b<i>@ (i=0..K-2) が Beta 由来の
---                棒折り変数、@<name>_<i>@ (i=0..K-1) が deterministic で
---                記録された π 成分。
---   * @alphas@ : 集中度ベクトル α = (α_1,...,α_K)。長さ K ≥ 2。
---
--- アルゴリズム:
---   k = 1..K-1 で β_k ~ Beta(α_k, Σ_{j>k} α_j) を sample する。
---   π_1 = β_1,  π_k = β_k Π_{j<k} (1 − β_j),  π_K = Π_{j<K} (1 − β_j)
---
--- これは π ~ Dirichlet(α) と厳密に等価なので、追加の Jacobian 補正は不要。
--- HMC/NUTS では β_k が UnitIntervalT (logit) で自動的に
--- (0,1) ↔ ℝ 変換されるので、シンプレックス制約は満たされる。
-dirichlet :: forall a. (Floating a, Ord a) => Text -> [a] -> Model a [a]
-dirichlet name alphas = do
-  let k = length alphas
-  if k < 2
-    then error "dirichlet: 長さ 2 未満のベクトルは未対応"
-    else do
-      let -- α_k+1..K の累積和 (右から)。長さ K (最後の要素は 0)
-          tailSums = scanr (+) 0 alphas
-      -- β_0..β_{K-2} を sample
-      betas <- mapM
-        (\i -> sample (name <> "_b" <> T.pack (show i))
-                      (Beta (alphas !! i) (tailSums !! (i + 1))))
-        [0 .. k - 2]
-      -- 残り棒の累積積 prods[i] = Π_{j<i} (1 - β_j),  prods[0] = 1
-      let prods = scanl (\acc b -> acc * (1 - b)) (1 :: a) betas
-          -- π_i = β_i * prods[i] for i < K-1, π_{K-1} = prods[K-1]
-          pis = [ if i < length betas
-                    then (betas !! i) * (prods !! i)
-                    else prods !! i
-                | i <- [0 .. k - 1] ]
-      -- 各 π_i を deterministic として保存し戻り値にも返す
-      mapM (\(i, p) ->
-              deterministic (name <> "_" <> T.pack (show i)) p)
-           (zip [0 :: Int ..] pis)
-
--- | Increasing cuts helper for 'OrderedLogistic' / 'OrderedProbit'
--- (Phase 39-A6)。 @c_1 = c_min@、 @c_k = c_{k-1} + d_k@ with
--- @d_k ~ HalfNormal(scale)@ により自動的に increasing 列を保証する。
---
--- 戻り値は長さ @nCuts@ の Track が通る deterministic 値の列
--- (@name_c_0@, …, @name_c_{nCuts-1}@)。 各 @d_k@ は @name_d_k@ で
--- latent として登録される。 cuts は OrderedLogistic / OrderedProbit に
--- そのまま渡せる。
---
--- DAG-safe pattern (Phase 38 で確立): monadic recursion で
--- @deterministic@ の戻り値 (det 名で relabel された Track) を次 step に
--- 渡すことで plate-style の親集合を保つ。
-orderedCuts :: forall a. (Floating a, Ord a)
-            => Text   -- ^ ベース名
-            -> Int    -- ^ カット数 K-1 (≥ 1)
-            -> a      -- ^ 最小値 c_min
-            -> a      -- ^ 増分の HalfNormal スケール
-            -> Model a [a]
-orderedCuts name nCuts cMin scale
-  | nCuts < 1 = error "orderedCuts: nCuts < 1 は未対応"
-  | otherwise = do
-      -- c_1 = c_min (定数を deterministic で登録、 Track 透過のため)
-      c1 <- deterministic (name <> "_c_1") cMin
-      -- c_2, ..., c_nCuts を monadic recursion で順に作る
-      -- chain prev i: 現在の前 cut Track が prev、 次に作るのは index i (1-based)
-      let chain prev i acc
-            | i > nCuts = return (reverse acc)
-            | otherwise = do
-                d  <- sample (name <> "_d_" <> T.pack (show i))
-                             (HalfNormal scale)
-                ci <- deterministic (name <> "_c_" <> T.pack (show i))
-                                    (prev + d)
-                chain ci (i + 1) (ci : acc)
-      rest <- chain c1 2 []
-      return (c1 : rest)
-
--- | Dirichlet Process の有限近似 stick-breaking (Phase 39-A5)。
--- @β_k ~ Beta(1, α)@ for @k = 1, …, T-1@、 重み
--- @π_k = β_k Π_{j<k}(1 - β_j)@、 @π_T = Π_{j<T}(1 - β_j)@ (残差) で
--- @Σ_k π_k = 1@ を保証。 truncation level @T@ で打ち切る (実用 T = 20-50)。
---
--- 戻り値は長さ @T@ の deterministic Track 列
--- (@name_pi_1@, …, @name_pi_T@)。 @β_k@ は @name_b_k@ で latent 登録。
---
--- DAG-safe: 各 β を sample 後、 累積積を deterministic で chain して
--- π を計算 (Phase 38 確立の規律)。
-dpStickBreaking :: forall a. (Floating a, Ord a)
-                => Text   -- ^ ベース名
-                -> Int    -- ^ truncation level T (≥ 2)
-                -> a      -- ^ concentration α (> 0)
-                -> Model a [a]
-dpStickBreaking name truncT alpha
-  | truncT < 2 = error "dpStickBreaking: truncation level < 2 は未対応"
-  | otherwise = do
-      -- β_1, …, β_{T-1} を sample
-      betas <- mapM
-        (\i -> sample (name <> "_b_" <> T.pack (show i))
-                      (Beta 1 alpha))
-        [1 .. truncT - 1]
-      -- 累積積 stick_k = Π_{j<k} (1 - β_j) を deterministic で chain
-      -- stick_1 = 1、 stick_{k+1} = stick_k * (1 - β_k)
-      stick1 <- deterministic (name <> "_stick_1") (1 :: a)
-      let stickChain prev i acc
-            | i > truncT = return (reverse acc)
-            | otherwise = do
-                let bIdx  = i - 1
-                    beta  = betas !! (bIdx - 1)  -- 1-based β_{i-1}
-                sNext <- deterministic
-                           (name <> "_stick_" <> T.pack (show i))
-                           (prev * (1 - beta))
-                stickChain sNext (i + 1) (sNext : acc)
-      restSticks <- stickChain stick1 2 []
-      let sticks = stick1 : restSticks  -- 長さ T
-      -- π_k = β_k * stick_k for k < T、 π_T = stick_T
-      pis <- mapM
-        (\i ->
-          let stickI = sticks !! (i - 1)
-              piVal  = if i < truncT
-                         then (betas !! (i - 1)) * stickI
-                         else stickI
-          in deterministic (name <> "_pi_" <> T.pack (show i)) piVal)
-        [1 .. truncT]
-      return pis
-
--- | Hidden Markov Model 用の遷移行列 + 初期分布 prior helper
--- (Phase 39-A4)。 K 状態の HMM について、 初期分布 π_0 と
--- K×K 遷移行列の各行に Dirichlet(α, …, α) prior を置く。
---
--- 戻り値は @(π_0, transitions)@:
--- * @π_0@: 長さ K の確率列 (Σ = 1)、 @name_pi0_<i>@ で deterministic 登録
--- * @transitions@: 長さ K のリスト、 i 番目は遷移行列 i 行目
---   (@name_trans_i_<j>@ で deterministic)
---
--- 離散状態列は **直接 latent としない** (NUTS は離散変数を扱えない)。
--- 代わりに、 ユーザは観測列 @y@ の emission log-prob 行列を計算し、
--- 'hmmForwardLogLik' で状態列をマージナル化した周辺対数尤度を求め、
--- 'potential' で組み込む形を取る。
---
--- 内部実装は既存 'dirichlet' helper を K+1 回呼ぶだけ。 すべて
--- deterministic chain で DAG-safe (Phase 38 規律)。
-hmmLatent :: forall a. (Floating a, Ord a)
-          => Text   -- ^ ベース名
-          -> Int    -- ^ 状態数 K (≥ 2)
-          -> a      -- ^ Dirichlet concentration α (> 0、 1 で uniform prior)
-          -> Model a ([a], [[a]])
-hmmLatent name k alpha
-  | k < 2 = error "hmmLatent: K < 2 は未対応"
-  | otherwise = do
-      pi0 <- dirichlet (name <> "_pi0") (replicate k alpha)
-      trans <- mapM
-        (\i -> dirichlet (name <> "_trans_" <> T.pack (show i))
-                         (replicate k alpha))
-        [0 .. k - 1]
-      return (pi0, trans)
-
--- | HMM forward algorithm marginal log-likelihood (Phase 39-A4)。
--- Phase 92 A2 で 'Hanalyze.Model.HBM.Util' へ純粋移設 (ここは re-export
--- のみ・API 不変)。 用法は従来の @'potential' nm (hmmForwardLogLik ...)@ に加え、
--- Normal emission の場合は 'HmmForwardNormal' + 'observeMV' が推奨
--- (勾配コンパイラが forward-backward の閉形式随伴を使えるため大幅に速い)。
-
--- ---------------------------------------------------------------------------
--- 構造検査
--- ---------------------------------------------------------------------------
-
-data NodeKind = LatentN | ObservedN Int | DeterministicN
-              | DataN Int   -- ^ Phase 60.4: データ slot ('dataNamed' / 'dataNamedIx')。
-                            --   Int = 長さ。 PyMC の pm.Data (ConstantData) 相当。
-  deriving (Show, Eq)
-
-data Node = Node
-  { nodeName   :: Text
-  , nodeKind   :: NodeKind
-  , nodeDist   :: Text         -- 分布名 (e.g. "Normal")
-  , nodeDeps   :: Set Text     -- 直接の親 (依存変数)
-  , nodePlates :: [Text]       -- Phase 40: plate スタック (外側から内側、 空 = 任意の plate に属さない)
-  } deriving (Show)
-
--- | Walk the model with placeholder zeros and collect 'Node' metadata.
--- 依存関係 ('nodeDeps') は 'extractDeps' を使うこと (placeholder 走査では取れない)。
-collectNodes :: forall r. ModelP r -> [Node]
-collectNodes m = go m []
-  where
-    go :: Model Double r -> [Node] -> [Node]
-    go (Pure _) acc = reverse acc
-    go (Free (Sample n d k)) acc =
-      go (k 0) (Node n LatentN (distName d) Set.empty [] : acc)
-    go (Free (Observe n d ys next)) acc =
-      go next (Node n (ObservedN (length ys)) (distName d) Set.empty [] : acc)
-    go (Free (ObserveLM n _ _ _ fam ys next)) acc =
-      go next (Node n (ObservedN (length ys)) (lmFamilyName fam) Set.empty [] : acc)
-    go (Free (Potential _ _ next)) acc = go next acc   -- Node 表示には含めない
-    go (Free (Deterministic _ v k)) acc = go (k v) acc
-    go (Free (Data n ys k)) acc =
-      go (k (ys, ys)) (Node n (DataN (length ys)) "Data" Set.empty [] : acc)
-    go (Free (DataIx n is k)) acc =
-      go (k is) (Node n (DataN (length is)) "DataIx" Set.empty [] : acc)
-    go (Free (PlateBegin _ _ next)) acc = go next acc  -- Phase 40: 透過
-    go (Free (PlateEnd next))       acc = go next acc
-
-sampleNames :: ModelP r -> [Text]
-sampleNames m = [nodeName n | n <- collectNodes m, nodeKind n == LatentN]
-
--- | モデル中の 'Data' slot を (名前, placeholder が空か) で列挙する (Phase 60.3)。
--- 同名 slot が複数回現れる場合は 1 entry に集約し、 **いずれかが空なら空扱い**
--- (束縛層の loud error 判定は保守側に倒す)。 'DataIx' slot は 'dataIxSlots'。
-dataSlots :: forall r. ModelP r -> [(Text, Bool)]
-dataSlots m = dedupSlots (go m [])
-  where
-    go :: Model Double r -> [(Text, Bool)] -> [(Text, Bool)]
-    go (Pure _) acc = reverse acc
-    go (Free (Sample _ _ k)) acc = go (k 0) acc
-    go (Free (Observe _ _ _ next)) acc = go next acc
-    go (Free (ObserveLM _ _ _ _ _ _ next)) acc = go next acc
-    go (Free (Potential _ _ next)) acc = go next acc
-    go (Free (Deterministic _ v k)) acc = go (k v) acc
-    go (Free (Data n ys k)) acc = go (k (ys, ys)) ((n, null ys) : acc)
-    go (Free (DataIx _ is k)) acc = go (k is) acc
-    go (Free (PlateBegin _ _ next)) acc = go next acc
-    go (Free (PlateEnd next))       acc = go next acc
-
--- | モデル中の 'DataIx' slot を (名前, placeholder が空か) で列挙する (Phase 60.3)。
-dataIxSlots :: forall r. ModelP r -> [(Text, Bool)]
-dataIxSlots m = dedupSlots (go m [])
-  where
-    go :: Model Double r -> [(Text, Bool)] -> [(Text, Bool)]
-    go (Pure _) acc = reverse acc
-    go (Free (Sample _ _ k)) acc = go (k 0) acc
-    go (Free (Observe _ _ _ next)) acc = go next acc
-    go (Free (ObserveLM _ _ _ _ _ _ next)) acc = go next acc
-    go (Free (Potential _ _ next)) acc = go next acc
-    go (Free (Deterministic _ v k)) acc = go (k v) acc
-    go (Free (Data _ ys k)) acc = go (k (ys, ys)) acc
-    go (Free (DataIx n is k)) acc = go (k is) ((n, null is) : acc)
-    go (Free (PlateBegin _ _ next)) acc = go next acc
-    go (Free (PlateEnd next))       acc = go next acc
-
--- | slot 列挙の重複集約 (先頭出現順を保ち、 空 flag は OR)。
-dedupSlots :: [(Text, Bool)] -> [(Text, Bool)]
-dedupSlots xs =
-  [ (n, or [ e | (n', e) <- xs, n' == n ])
-  | n <- nub (map fst xs) ]
-
diff --git a/src/Hanalyze/Model/HBM/Sampling.hs b/src/Hanalyze/Model/HBM/Sampling.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/HBM/Sampling.hs
+++ /dev/null
@@ -1,405 +0,0 @@
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- |
--- Module      : Hanalyze.Model.HBM.Sampling
--- Description : HBM の分布サンプリング (事前/事後予測用)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Phase 58.4: 分布からのサンプリング (事前/事後予測用) を分離。
---
--- 'Distribution' / 'HBM.Util' の上層。 PrimMonad + mwc-random に依存し、
--- mwc-random が直接提供しない分布 (Cauchy, HalfCauchy, Weibull, …) は
--- 逆 CDF 法 / rejection でここに実装する。 NUTS の per-draw 経路には乗らず
--- (事前/事後予測のみ)、 性能ホットではない。
-module Hanalyze.Model.HBM.Sampling
-  ( sampleDist
-  , sampleMvDist
-  , sampleObsRep
-  ) where
-
-import Control.Monad (replicateM)
-import Data.List (zip4)
-import Control.Monad.Primitive (PrimMonad, PrimState)
-import qualified System.Random.MWC as MWCBase
-import qualified System.Random.MWC.Distributions as MWC
-import System.Random.MWC (Gen)
-
-import Hanalyze.Model.HBM.Util (choleskyL, chunksOf, gpRBFCovList)
-import Hanalyze.Model.HBM.Distribution (Distribution (..), phiCdfA)
-
--- ---------------------------------------------------------------------------
--- 分布からのサンプリング (事前/事後予測用)
--- ---------------------------------------------------------------------------
-
--- | Draw a single sample from a 'Distribution Double'.
--- 事前予測サンプリング、事後予測サンプリング、観測値の生成に使う。
---
--- mwc-random が直接提供しない分布はここで実装する (Cauchy, HalfCauchy, etc.)。
-sampleDist :: forall m. PrimMonad m => Distribution Double -> Gen (PrimState m) -> m Double
-sampleDist (Normal mu sig) gen = MWC.normal mu sig gen
-sampleDist (Exponential rate) gen = do
-  u <- MWCBase.uniform gen :: m Double
-  return (-log u / rate)
-sampleDist (Gamma shape rate) gen =
-  -- mwc-random の gamma は scale パラメタ化なので 1/rate を渡す
-  MWC.gamma shape (1 / rate) gen
-sampleDist (Beta a b) gen = do
-  x <- MWC.gamma a 1 gen
-  y <- MWC.gamma b 1 gen
-  return (x / (x + y))
-sampleDist (Poisson lam) gen = samplePoissonKnuth lam gen
-sampleDist (Binomial n p) gen = do
-  -- n 回のベルヌーイ試行
-  let go 0 acc = return acc
-      go k acc = do
-        u <- MWCBase.uniform gen :: m Double
-        go (k - 1) (if u < p then acc + 1 else acc)
-  fmap fromIntegral (go n (0 :: Int))
-sampleDist (Uniform lo hi) gen = do
-  u <- MWCBase.uniform gen :: m Double
-  return (lo + u * (hi - lo))
-sampleDist (StudentT df mu sig) gen = do
-  -- t = mu + sig * Normal(0,1) / sqrt(Chi2(df) / df)
-  z    <- MWC.standard gen
-  chi2 <- MWC.gamma (df / 2) 2 gen   -- Chi2(df) = Gamma(df/2, scale=2)
-  return (mu + sig * z / sqrt (chi2 / df))
-sampleDist (Cauchy loc sc) gen = do
-  u <- MWCBase.uniform gen :: m Double
-  return (loc + sc * tan (pi * (u - 0.5)))
-sampleDist (HalfNormal sig) gen = do
-  z <- MWC.standard gen
-  return (abs (sig * z))
-sampleDist (HalfCauchy sc) gen = do
-  u <- MWCBase.uniform gen :: m Double
-  return (sc * abs (tan (pi * (u - 0.5))))
-sampleDist (LogNormal mu sig) gen = do
-  z <- MWC.standard gen
-  return (exp (mu + sig * z))
-sampleDist (Bernoulli p) gen = do
-  u <- MWCBase.uniform gen :: m Double
-  return (if u < p then 1.0 else 0.0)
-sampleDist (Categorical probs) gen = do
-  u <- MWCBase.uniform gen :: m Double
-  let total = sum probs
-      go _   []     = fromIntegral (length probs - 1)
-      go acc (p:ps) =
-        let acc' = acc + p / total
-        in if u < acc' then 0 else 1 + go acc' ps
-  return (go 0 probs)
-sampleDist (Mixture ws comps) gen
-  | null ws || length ws /= length comps = return (0/0)  -- NaN: 不正
-  | otherwise = do
-      -- 1) 重みに比例して成分 k を選ぶ
-      u <- MWCBase.uniform gen :: m Double
-      let total = sum ws
-          pickIdx _ [] = length ws - 1
-          pickIdx acc (w:rest) =
-            let acc' = acc + w / total
-            in if u < acc' then 0 else 1 + pickIdx acc' rest
-          k = pickIdx 0 ws
-      -- 2) 選んだ成分からサンプリング
-      sampleDist (comps !! k) gen
-sampleDist (Truncated d mLo mHi) gen =
-  -- 単純なリジェクション・サンプリング (範囲が極めて狭いと収束遅い)
-  let inRange y = case (mLo, mHi) of
-        (Just lo, _      ) | y < lo  -> False
-        (_,       Just hi) | y > hi  -> False
-        _                            -> True
-      tryOnce maxAttempts
-        | maxAttempts <= 0 = return (0/0)  -- 諦め
-        | otherwise = do
-            y <- sampleDist d gen
-            if inRange y then return y else tryOnce (maxAttempts - 1)
-  in tryOnce (10000 :: Int)
-sampleDist MvNormal{} _ =
-  error "MvNormal: observation-only — 'sample' 経由でのドローは未対応"
-sampleDist MvNormalChol{} _ =
-  error "MvNormalChol: observation-only — 'sample' 経由でのドローは未対応"
-sampleDist MvNormalGpRBF{} _ =
-  error "MvNormalGpRBF: observation-only — 'sample' 経由でのドローは未対応"
-sampleDist HmmForwardNormal{} _ =
-  error "HmmForwardNormal: observation-only — 'sample' 経由でのドローは未対応"
-sampleDist ArmaNormal{} _ =
-  error "ArmaNormal: observation-only — 'sample' 経由でのドローは未対応"
-sampleDist GradedResponseIrt{} _ =
-  error "GradedResponseIrt: observation-only — 'sample' 経由でのドローは未対応"
-sampleDist Multinomial{} _ =
-  error "Multinomial: observation-only — 'sample' 経由でのドローは未対応"
-sampleDist (InverseGamma alpha beta) gen = do
-  -- 1 / Gamma(α, rate=β) = 1 / Gamma(α, scale=1/β)
-  y <- MWC.gamma alpha (1 / beta) gen
-  return (1 / y)
-sampleDist (Weibull kShape lam) gen = do
-  -- 逆 CDF 法: x = λ (-log(1-u))^(1/k)
-  u <- MWCBase.uniform gen :: m Double
-  return (lam * ((-log (1 - u)) ** (1 / kShape)))
-sampleDist (Pareto alpha xm) gen = do
-  -- 逆 CDF 法: x = x_m / u^(1/α)
-  u <- MWCBase.uniform gen :: m Double
-  return (xm / (u ** (1 / alpha)))
-sampleDist (BetaBinomial n alpha beta) gen = do
-  -- p ~ Beta(α, β); k ~ Binomial(n, p)
-  p <- sampleDist (Beta alpha beta) gen
-  sampleDist (Binomial n p) gen
-sampleDist (VonMises mu kappa) gen = do
-  -- Best-Fisher の rejection sampler
-  let a = 1 + sqrt (1 + 4 * kappa * kappa)
-      b = (a - sqrt (2 * a)) / (2 * kappa)
-      r = (1 + b * b) / (2 * b)
-      tryOnce = do
-        u1 <- MWCBase.uniform gen :: m Double
-        let z = cos (pi * u1)
-            f = (1 + r * z) / (r + z)
-            c = kappa * (r - f)
-        u2 <- MWCBase.uniform gen :: m Double
-        if c * (2 - c) - u2 > 0 || log (c / u2) + 1 - c >= 0
-          then do
-            u3 <- MWCBase.uniform gen :: m Double
-            let sign = if u3 - 0.5 < 0 then (-1.0) else 1.0
-            return (mu + sign * acos f)
-          else tryOnce
-  tryOnce
-sampleDist (ZeroInflatedPoisson psi lam) gen = do
-  u <- MWCBase.uniform gen :: m Double
-  if u < psi
-    then return 0
-    else samplePoissonKnuth lam gen
-sampleDist (ZeroInflatedBinomial n psi p) gen = do
-  u <- MWCBase.uniform gen :: m Double
-  if u < psi
-    then return 0
-    else sampleDist (Binomial n p) gen
-sampleDist (SkewNormal mu sig alpha) gen = do
-  -- Henze 1986: δ = α/√(1+α²), X = μ + σ(δ|U₀| + √(1-δ²)U₁)
-  let delta = alpha / sqrt (1 + alpha * alpha)
-  u0 <- MWC.standard gen
-  u1 <- MWC.standard gen
-  return (mu + sig * (delta * abs u0 + sqrt (1 - delta * delta) * u1))
-sampleDist (Logistic mu s) gen = do
-  -- 逆 CDF: X = μ + s · log(u/(1-u))
-  u <- MWCBase.uniform gen :: m Double
-  return (mu + s * log (u / (1 - u)))
-sampleDist (Gumbel mu beta) gen = do
-  -- 逆 CDF: X = μ - β · log(-log u)
-  u <- MWCBase.uniform gen :: m Double
-  return (mu - beta * log (- log u))
-sampleDist (AsymmetricLaplace b kappa mu) gen = do
-  -- 逆 CDF。 pc = κ²/(1+κ²)、 U < pc なら左尾、 そうでなければ右尾
-  u <- MWCBase.uniform gen :: m Double
-  let k2 = kappa * kappa
-      pc = k2 / (1 + k2)
-  if u < pc
-    then return (mu + (kappa / b) * log (u / pc))
-    else return (mu - (1 / (b * kappa)) * log ((1 - u) / (1 - pc)))
-sampleDist (OrderedLogistic eta cuts) gen = do
-  -- η と cuts から各カテゴリの確率を計算して Categorical で sample
-  let sigm x = 1 / (1 + exp (-x))
-      probs  = catProbs cuts
-      catProbs []           = [1]
-      catProbs (c:rest)     = sigm (c - eta) : restProbs (sigm (c - eta)) rest
-      restProbs _    []         = [1 - sigm (last cuts - eta)]
-      restProbs prev (c:rest)   =
-        let cur = sigm (c - eta)
-        in (cur - prev) : restProbs cur rest
-  u <- MWCBase.uniform gen :: m Double
-  let go acc k []     = realToFrac (k - 1 :: Int)
-      go acc k (p:ps) =
-        let acc' = acc + p
-        in if u < acc' then realToFrac k else go acc' (k + 1) ps
-  return (go 0 (0 :: Int) probs)
-sampleDist (DiscreteUniform lo hi) gen = do
-  u <- MWCBase.uniform gen :: m Double
-  let span_ = hi - lo + 1
-      k     = lo + floor (u * realToFrac span_)
-      kClip = min hi k  -- u が 1 のとき span_ になるのを防ぐ
-  return (realToFrac kClip)
-sampleDist (Geometric p) gen = do
-  -- 逆 CDF: X = ceil(log U / log(1-p))、 PyMC convention で support から 1
-  u <- MWCBase.uniform gen :: m Double
-  let lq = log (1 - p)
-      x  = ceiling (log u / lq) :: Int
-  return (realToFrac (max 1 x))
-sampleDist (HyperGeometric nN kK nDraw) gen = do
-  -- 単純な urn sampling: 各引きで残り成功/失敗の割合から二項
-  let loop remN remK remDraw acc
-        | remDraw <= 0 = return (realToFrac (acc :: Int))
-        | otherwise = do
-            u <- MWCBase.uniform gen :: m Double
-            let pSucc = realToFrac remK / realToFrac remN :: Double
-                pick  = if u < pSucc then 1 else 0
-            loop (remN - 1) (remK - pick) (remDraw - 1) (acc + pick)
-  loop nN kK nDraw 0
-sampleDist (ZeroInflatedNegativeBinomial psi mu alpha) gen = do
-  u <- MWCBase.uniform gen :: m Double
-  if u < psi
-    then return 0
-    else sampleDist (NegativeBinomial mu alpha) gen
-sampleDist MvStudentT{} _ =
-  error "MvStudentT: observation-only — 'sample' 経由でのドローは未対応 (latent helper を別途用意予定)"
-sampleDist DirichletMultinomial{} _ =
-  error "DirichletMultinomial: observation-only — 'sample' 経由でのドローは未対応"
-sampleDist (NegativeBinomial mu alpha) gen = do
-  -- Gamma-Poisson mixture: λ ~ Gamma(α, β=α/μ); X ~ Poisson(λ)
-  lam <- MWC.gamma alpha (mu / alpha) gen
-  samplePoissonKnuth lam gen
-sampleDist (Censored d _ _) gen =
-  -- 元分布から普通にサンプリング (打ち切りは「観測過程」の話で生成側ではない)
-  sampleDist d gen
-sampleDist (Triangular lo c hi) gen = do
-  -- 逆 CDF。 Fc = (c-lo)/(hi-lo) 未満なら左側、 以上なら右側
-  u <- MWCBase.uniform gen :: m Double
-  let fc = (c - lo) / (hi - lo)
-  if u < fc
-    then return (lo + sqrt (u * (hi - lo) * (c - lo)))
-    else return (hi - sqrt ((1 - u) * (hi - lo) * (hi - c)))
-sampleDist (Kumaraswamy a b) gen = do
-  -- 逆 CDF: x = (1 - (1-u)^{1/b})^{1/a}
-  u <- MWCBase.uniform gen :: m Double
-  return ((1 - (1 - u) ** (1 / b)) ** (1 / a))
-sampleDist (Rice nu sig) gen = do
-  -- Y1 ~ N(ν, σ), Y2 ~ N(0, σ); X = sqrt(Y1²+Y2²)
-  y1 <- MWC.normal nu sig gen
-  y2 <- MWC.normal 0  sig gen
-  return (sqrt (y1 * y1 + y2 * y2))
-sampleDist Wishart{} _ =
-  error "Wishart: observation-only — 'sample' 経由でのドローは未対応 (Bartlett decomp の latent helper を別途用意予定)"
-sampleDist (Bound d mLo mHi) gen =
-  -- Bound は Truncated とほぼ同じ。 sample は rejection。
-  sampleDist (Truncated d mLo mHi) gen
-sampleDist (OrderedProbit eta cuts) gen = do
-  -- η と cuts から各カテゴリの確率を計算して Categorical で sample
-  let probs  = catProbs cuts
-      catProbs []        = [1]
-      catProbs (c:rest)  = phiCdfA (c - eta) : restProbs (phiCdfA (c - eta)) rest
-      restProbs _    []         = [1 - phiCdfA (last cuts - eta)]
-      restProbs prev (c:rest)   =
-        let cur = phiCdfA (c - eta)
-        in (cur - prev) : restProbs cur rest
-  u <- MWCBase.uniform gen :: m Double
-  let go acc k []     = realToFrac (k - 1 :: Int)
-      go acc k (p:ps) =
-        let acc' = acc + p
-        in if u < acc' then realToFrac k else go acc' (k + 1) ps
-  return (go 0 (0 :: Int) probs)
-sampleDist (DiscreteWeibull q beta) gen = do
-  -- 逆 CDF: k = ceil((log(1-u)/log q)^{1/β}) - 1
-  u <- MWCBase.uniform gen :: m Double
-  let r  = log (1 - u) / log q
-      k0 = ceiling (r ** (1 / beta)) - 1 :: Int
-      k  = max 0 k0
-  return (fromIntegral k)
-
--- | 多変量分布から 1 観測 (k-vector) を draw する (Phase 44、 PPC 用)。
--- 'sampleDist' (スカラ観測専用 'error') と別経路。 @y = μ + C z@ で z ~ N(0,1)、
--- C は MvNormal なら @choleskyL Σ@、 MvNormalChol なら scaled Cholesky
--- @M = diag σ · L@ (再分解不要)。 対応外の多変量分布は空リスト (= worker 側で
--- graceful にスキップ。 本 Phase は MvNormal/MvNormalChol に集中)。
-sampleMvDist :: forall m. PrimMonad m => Distribution Double -> Gen (PrimState m) -> m [Double]
-sampleMvDist (MvNormal mu cov) gen = do
-  let k = length mu
-  zs <- replicateM k (MWC.standard gen)
-  pure $ case choleskyL cov of
-    Just c  -> [ (mu !! i) + sum [ ((c !! i) !! j) * (zs !! j) | j <- [0 .. i] ]
-               | i <- [0 .. k - 1] ]
-    Nothing -> mu
-sampleMvDist (MvNormalChol mu sigma l) gen = do
-  let k = length mu
-      m = [ [ (sigma !! i) * ((l !! i) !! j) | j <- [0 .. k - 1] ] | i <- [0 .. k - 1] ]
-  zs <- replicateM k (MWC.standard gen)
-  pure [ (mu !! i) + sum [ ((m !! i) !! j) * (zs !! j) | j <- [0 .. i] ]
-       | i <- [0 .. k - 1] ]
-sampleMvDist (MvNormalGpRBF xs alpha rho sigma) gen =    -- Phase 95 B-dsl: cov 展開して MvNormal と同じ
-  sampleMvDist (MvNormal (replicate (length xs) 0) (gpRBFCovList xs alpha rho sigma)) gen
-sampleMvDist _ _ = pure []
-
--- | 1 observe ノード分の複製データ (y_rep) をまとめて draw する (Phase 90 A2)。
--- PPC ('sampleYRep'/'epredPIAtHeld' 等) はこれまで観測分布を問わず ys の要素
--- ごとに 'sampleDist' (スカラ専用) を呼んでいたため、 多変量分布 (MvNormal/
--- MvNormalChol、 ys = 1 つの k-vector 観測がフラット化された形) では即座に
--- @error@ していた (07-gp-regr で実測発覚)。 ここで次元 k ごとに ys をチャンク
--- し 'sampleMvDist' に委譲する。 Multinomial は 'sampleMvDist' が未対応の
--- ままなので (本 Phase の対象外)、 従来どおり 'sampleDist' に委ねる。
-sampleObsRep :: forall m. PrimMonad m
-             => Gen (PrimState m) -> Distribution Double -> [Double] -> m [Double]
-sampleObsRep gen d@(MvNormal mu _) ys =
-  concat <$> mapM (const (sampleMvDist d gen)) (chunksOf (length mu) ys)
-sampleObsRep gen d@(MvNormalChol mu _ _) ys =
-  concat <$> mapM (const (sampleMvDist d gen)) (chunksOf (length mu) ys)
-sampleObsRep gen d@(MvNormalGpRBF xs _ _ _) ys =   -- Phase 95 B-dsl
-  concat <$> mapM (const (sampleMvDist d gen)) (chunksOf (length xs) ys)
-sampleObsRep gen (HmmForwardNormal pi0 trans mus sg) ys = do
-  -- Phase 92 A2 (PPC): 状態列を π_0/遷移行列から draw → Normal(μ_s, σ) で emission。
-  -- 観測列全体 = 1 観測なので T = length ys の系列を 1 本生成する。
-  let kk = length pi0
-      pick ws = do                       -- 重み ws (非正規化可) からカテゴリを 1 つ draw
-        let s = sum ws
-        u <- (* s) <$> MWCBase.uniform gen
-        let go i acc (w:rest) | null rest || u <= acc + w = pure i
-                              | otherwise                 = go (i + 1) (acc + w) rest
-            go i _ []                                     = pure (max 0 (i - 1))
-        go 0 0 ws
-      stepState s = pick (if s < length trans then trans !! s else replicate kk 1)
-      emitAt s = do
-        z <- MWC.standard gen
-        pure ((if s < length mus then mus !! s else 0) + sg * z)
-      go' _ 0 acc = pure (reverse acc)
-      go' s n acc = do
-        y <- emitAt s
-        s' <- stepState s
-        go' s' (n - 1 :: Int) (y : acc)
-  s0 <- pick pi0
-  go' s0 (length ys) []
-sampleObsRep gen (ArmaNormal mu phi theta sg) ys = do
-  -- Phase 101 A2 (PPC): err_t ~ Normal(0, σ) を draw し、y を前向き再帰で生成
-  -- (y_1 = μ+φμ+e_1・y_t = μ + φ·y_{t−1} + θ·e_{t−1} + e_t)。
-  -- 観測列全体 = 1 観測なので T = length ys の系列を 1 本生成する。
-  let drawE = (sg *) <$> MWC.standard gen
-      go' _ _ 0 acc = pure (reverse acc)
-      go' prevY prevE n acc = do
-        e <- drawE
-        let y = mu + phi * prevY + theta * prevE + e
-        go' y e (n - 1 :: Int) (y : acc)
-  case length ys of
-    0 -> pure []
-    t -> do
-      e1 <- drawE
-      let y1 = mu + phi * mu + e1
-      go' y1 e1 (t - 1) [y1]
-sampleObsRep gen (GradedResponseIrt thetas ncats deltas gammas) ys = do
-  -- Phase 101 A3 (PPC): 各 (child, item) の p ベクトルからカテゴリを draw。
-  -- 欠測 (−1) 位置は −1 のまま返す (観測の欠測パターンを保存)。
-  let nItem = length ncats
-      rows  = chunksOf nItem ys
-      catPs th nc dl gm =
-        let kMax = nc - 1
-            qs = [ 1 / (1 + exp (negate (dl * (th - gm !! (kk - 1)))))
-                 | kk <- [1 .. kMax] ]
-        in [ if k == 1 then 1 - head qs
-             else if k == nc then qs !! (kMax - 1)
-             else (qs !! (k - 2)) - (qs !! (k - 1))
-           | k <- [1 .. nc] ]
-      pickCat ps = do
-        u <- MWCBase.uniform gen
-        let go k acc (w:rest) | null rest || u <= acc + w = pure k
-                              | otherwise                 = go (k + 1) (acc + w) rest
-            go k _ []                                     = pure (max 1 (k - 1))
-        go 1 0 ps
-      drawRow th row =
-        sequence [ if gr == -1 then pure (-1)
-                   else fromIntegral <$> pickCat (catPs th nc dl gm)
-                 | (nc, dl, gm, gr) <- zip4 ncats deltas gammas row ]
-  concat <$> sequence [ drawRow th row | (th, row) <- zip thetas rows ]
-sampleObsRep gen d ys = mapM (const (sampleDist d gen)) ys
-
--- | Knuth のアルゴリズムで Poisson(λ) サンプル。λ < 30 程度なら十分高速。
-samplePoissonKnuth :: forall m. PrimMonad m => Double -> Gen (PrimState m) -> m Double
-samplePoissonKnuth lam gen = do
-  let l = exp (-lam)
-      go k p = do
-        u <- MWCBase.uniform gen :: m Double
-        let p' = p * u
-        if p' < l
-          then return (fromIntegral k)
-          else go (k + 1) p'
-  go 0 (1.0 :: Double)
diff --git a/src/Hanalyze/Model/HBM/Track.hs b/src/Hanalyze/Model/HBM/Track.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/HBM/Track.hs
+++ /dev/null
@@ -1,297 +0,0 @@
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- |
--- Module      : Hanalyze.Model.HBM.Track
--- Description : HBM の依存追跡型 Track (latent 変数への依存伝播)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Phase 58.6b: 依存追跡型 'Track' を 'Hanalyze.Model.HBM' から分離。
---
--- 'Track' は @Floating@ 演算を通して「この値はどの latent 変数に依存するか」を
--- 伝播する型。 'ModelP' をこの型で特殊化することで各 Observe / Deterministic
--- ノードの親集合を自動抽出する ('extractDeps')。 DAG 可視化 (buildModelGraph)
--- の基盤。
---
--- 依存は下層 'Hanalyze.Model.HBM.Model' (Node / ModelF / lmParents 等) と
--- '...Distribution' (Distribution / distName) のみ。 評価層 (logJoint 等) には
--- 依存しない (runTrack = logJoint の Track 特殊化は Eval 層に置く)。
-module Hanalyze.Model.HBM.Track
-  ( Track (..)
-  , trackVar
-  , trackConst
-  , extractDeps
-  ) where
-
-import Data.Set (Set)
-import qualified Data.Set as Set
-import Data.Map.Strict (Map)
-import qualified Data.Map.Strict as Map
-import Data.Text (Text)
-
-import Hanalyze.Model.HBM.Model
-import Hanalyze.Model.HBM.Distribution
-
--- ---------------------------------------------------------------------------
--- 依存追跡型 Track
--- ---------------------------------------------------------------------------
-
--- | Floating 演算を通して「この値はどの変数に依存するか」を伝播する型。
---
--- @ModelP@ をこの型で特殊化することで、各 Observe ノードが
--- どの latent 変数に依存しているか自動抽出できる。
-data Track = Track
-  { trackVal  :: !Double
-  , trackDeps :: !(Set Text)
-  } deriving (Show, Eq)
-
--- | 変数として登場する Track (deps に自分の名前を入れる)。
-trackVar :: Text -> Double -> Track
-trackVar n v = Track v (Set.singleton n)
-
--- | 定数として扱う Track (deps なし)。
-trackConst :: Double -> Track
-trackConst v = Track v Set.empty
-
--- Phase 60.7: '!!!' の依存タグ注入。 Track 解釈だけが slot 名を依存集合に
--- 足し、 DAG に slot→利用先のエッジを出す (数値解釈は既定 id)。
-instance TrackTag Track where
-  tagDep nm (Track v ds) = Track v (Set.insert nm ds)
-
--- 自然な順序関係 (Double の比較を使う)
-instance Ord Track where
-  compare a b = compare (trackVal a) (trackVal b)
-
--- Floating の階段
-instance Num Track where
-  fromInteger n = trackConst (fromInteger n)
-  Track a sa + Track b sb = Track (a + b) (sa <> sb)
-  Track a sa - Track b sb = Track (a - b) (sa <> sb)
-  Track a sa * Track b sb = Track (a * b) (sa <> sb)
-  abs    (Track a sa) = Track (abs a) sa
-  signum (Track a sa) = Track (signum a) sa
-  negate (Track a sa) = Track (negate a) sa
-
-instance Fractional Track where
-  fromRational r = trackConst (fromRational r)
-  Track a sa / Track b sb = Track (a / b) (sa <> sb)
-
-instance Floating Track where
-  pi             = trackConst pi
-  exp   (Track a sa) = Track (exp   a) sa
-  log   (Track a sa) = Track (log   a) sa
-  sin   (Track a sa) = Track (sin   a) sa
-  cos   (Track a sa) = Track (cos   a) sa
-  tan   (Track a sa) = Track (tan   a) sa
-  asin  (Track a sa) = Track (asin  a) sa
-  acos  (Track a sa) = Track (acos  a) sa
-  atan  (Track a sa) = Track (atan  a) sa
-  sinh  (Track a sa) = Track (sinh  a) sa
-  cosh  (Track a sa) = Track (cosh  a) sa
-  tanh  (Track a sa) = Track (tanh  a) sa
-  asinh (Track a sa) = Track (asinh a) sa
-  acosh (Track a sa) = Track (acosh a) sa
-  atanh (Track a sa) = Track (atanh a) sa
-  sqrt  (Track a sa) = Track (sqrt  a) sa
-  Track a sa ** Track b sb = Track (a ** b) (sa <> sb)
-  logBase (Track a sa) (Track b sb) = Track (logBase a b) (sa <> sb)
-
-instance Real Track where
-  toRational = toRational . trackVal
-
-instance RealFrac Track where
-  properFraction (Track a sa) = let (i, f) = properFraction a in (i, Track f sa)
-
--- | モデルを Track 型で実行し、各ノードの依存関係を抽出する。
---
--- Sample n: その変数自体は @{n}@ に依存する (自己依存)。
--- Observe n: 分布のパラメータに含まれる latent 変数の集合を deps とする。
---
--- Phase 40: plate スタックを保持し、 各 Node に 'nodePlates' を埋める。
--- 同時に出現した plate (name, size) を 'Map Text Int' で返す。
-extractDeps :: forall r. ModelP r -> ([Node], Map Text Int)
-extractDeps m =
-  let (ns, plates) = go m [] [] Map.empty Map.empty Map.empty in (ns, plates)
-  where
-    -- 引数 stack は **inner-most が head** の plate 名スタック。
-    -- slots / obsAcc は Phase 63.1 の side map: slots = データ slot の生値
-    -- (slot 名 → ys)、 obsAcc = observe の生 ys を obs 名ごとに chunk 蓄積
-    -- (per-point loop の observe \"y\" … [y] も連結すれば slot 全列と一致する)。
-    -- walk 終端 (Pure) で値一致逆引きし obs→slot エッジを張る ('linkObsSlots')。
-    go :: Model Track r -> [Text] -> [Node] -> Map Text Int
-       -> Map Text [Double] -> Map Text [[Double]] -> ([Node], Map Text Int)
-    go (Pure _) _ acc plates slots obsAcc =
-      (reverse (linkObsSlots slots obsAcc acc), plates)
-    go (Free (Sample n d k)) stack acc plates slots obsAcc =
-      let parentDeps = distDepsT d
-          node = Node n LatentN (distName d) parentDeps (reverse stack)
-          v    = trackVar n 1.0  -- 1 にすると log/exp が安全
-      in go (k v) stack (node : acc) plates slots obsAcc
-    go (Free (Observe n d ys next)) stack acc plates slots obsAcc =
-      let parentDeps = distDepsT d
-          node = Node n (ObservedN (length ys)) (distName d) parentDeps (reverse stack)
-      in go next stack (node : acc) plates slots (obsChunk n ys obsAcc)
-    go (Free (ObserveLM n bs _ re fam ys next)) stack acc plates slots obsAcc =
-      -- 親 = β + u + 分散パラメタ名 (lmParents)。 観測ブロックは 1 ノード。
-      let parentDeps = lmParents bs re fam
-          node = Node n (ObservedN (length ys)) (lmFamilyName fam) parentDeps (reverse stack)
-      in go next stack (node : acc) plates slots (obsChunk n ys obsAcc)
-    go (Free (Potential nm v next)) stack acc plates slots obsAcc =
-      -- Potential も DAG 上は「依存を持つ無形ノード」として可視化
-      let parentDeps = trackDeps v
-          node = Node nm LatentN "Potential" parentDeps (reverse stack)
-      in go next stack (node : acc) plates slots obsAcc
-    go (Free (Deterministic nm v k)) stack acc plates slots obsAcc =
-      -- Deterministic ノードの親は @v@ が触れた latent 集合。
-      -- 継続には deps を @{nm}@ に「再ラベル」 した Track を渡し、 下流が
-      -- @v@ の遠い親 (mu, tau 等) ではなく **det 名 nm そのもの** を
-      -- 親として認識するようにする (Phase 38 で plate-style DAG に修正)。
-      -- 数値値は元の @trackVal v@ を保持 (下流の log/exp 等が安全)。
-      let parentDeps = trackDeps v
-          node = Node nm DeterministicN "Deterministic" parentDeps (reverse stack)
-          v'   = Track (trackVal v) (Set.singleton nm)
-      in go (k v') stack (node : acc) plates slots obsAcc
-    go (Free (Data n ys k)) stack acc plates slots obsAcc =
-      -- Phase 60.4: pm.Data 相当のデータノード。 値 (fst view) には slot 名の
-      -- dep タグを載せ、 下流 (deterministic / observe の dist パラメタ) が
-      -- x→mu のエッジを自動で張れるようにする (Phase 38 deterministic
-      -- re-label と同手法)。 snd view (dataNamedObs の生 [Double]) には
-      -- deps を載せられないため、 slots に生値を控えて walk 終端で
-      -- 値一致逆引きの obs→slot エッジを張る (Phase 63.1)。
-      let node = Node n (DataN (length ys)) "Data" Set.empty (reverse stack)
-          vals = map (\v -> Track v (Set.singleton n)) ys
-      in go (k (vals, ys)) stack (node : acc) plates (Map.insert n ys slots) obsAcc
-    go (Free (DataIx n is k)) stack acc plates slots obsAcc =
-      -- DataIx は [Int] のまま継続に渡すため dep タグは載らない (ノードのみ)。
-      -- observe の ys ([Double]) と一致し得ないので slots にも入れない。
-      let node = Node n (DataN (length is)) "DataIx" Set.empty (reverse stack)
-      in go (k is) stack (node : acc) plates slots obsAcc
-    go (Free (PlateBegin nm sz next)) stack acc plates slots obsAcc =
-      -- plate を開始 = stack に push、 サイズも記録 (重複時は新値で上書き
-      -- = 同名 plate は同サイズ前提)
-      go next (nm : stack) acc (Map.insert nm sz plates) slots obsAcc
-    go (Free (PlateEnd next)) stack acc plates slots obsAcc =
-      -- plate を終了 = stack から pop。 空 stack は誤用 (PlateBegin 抜き
-      -- で PlateEnd が来た等) — 黙って無視する
-      let stack' = case stack of { _ : t -> t; [] -> [] }
-      in go next stack' acc plates slots obsAcc
-
-    -- obs 名ごとの ys chunk 蓄積 (新 chunk を先頭 prepend = 逆順保持。
-    -- per-observe の list append による O(n²) を避ける)。
-    obsChunk :: Text -> [Double] -> Map Text [[Double]] -> Map Text [[Double]]
-    obsChunk n ys = Map.insertWith (++) n [ys]
-
-    -- Phase 63.1: observe の連結 ys と値一致するデータ slot へ obs→slot エッジ
-    -- (= 該当 DataN Node の nodeDeps に obs 名を追加。 nodeDeps は「直接の親」
-    -- ゆえ slot は obs の子 = PyMC `make_compute_graph` の obs→y と同型)。
-    --
-    -- - 値一致は plate 長さ match (60.6) と同種の表示専用ヒューリスティック:
-    --   偶然同値の slot にも張られる (既知 caveat・doc 明記)、 同値 slot 複数は
-    --   全部に張る。 空 slot (未 bind placeholder) は対象外。
-    -- - 同名 (dataNamedObs \"y\" + observe \"y\" の docs 慣例) は対象外:
-    --   mergeByName で 1 ノードに統合されるため自己ループになる。
-    -- - 引数 acc は逆順のまま受けて逆順のまま返す (呼び元 Pure 節で reverse)。
-    linkObsSlots :: Map Text [Double] -> Map Text [[Double]] -> [Node] -> [Node]
-    linkObsSlots slots obsAcc acc
-      | Map.null links = acc
-      | otherwise      = map upd acc
-      where
-        -- obs 名 → 連結 ys (chunk は新しい順 prepend 蓄積ゆえ reverse)
-        obsYs = Map.map (concat . reverse) obsAcc
-        -- slot 名 → 親として足す obs 名集合
-        links = Map.fromListWith Set.union
-          [ (slotName, Set.singleton obsName)
-          | (slotName, sv) <- Map.toList slots
-          , not (null sv)
-          , (obsName, ys) <- Map.toList obsYs
-          , obsName /= slotName
-          , sv == ys ]
-        upd nd = case nodeKind nd of
-          DataN _ | Just parents <- Map.lookup (nodeName nd) links ->
-            nd { nodeDeps = nodeDeps nd <> parents }
-          _ -> nd
-
--- | Distribution Track に含まれる依存変数集合を取り出す。
-distDepsT :: Distribution Track -> Set Text
-distDepsT (Normal mu sig)    = trackDeps mu <> trackDeps sig
-distDepsT (Exponential r)    = trackDeps r
-distDepsT (Gamma s r)        = trackDeps s <> trackDeps r
-distDepsT (Beta a b)         = trackDeps a <> trackDeps b
-distDepsT (Poisson lam)      = trackDeps lam
-distDepsT (Binomial _ p)     = trackDeps p
-distDepsT (Uniform lo hi)    = trackDeps lo <> trackDeps hi
-distDepsT (StudentT df mu s) = trackDeps df <> trackDeps mu <> trackDeps s
-distDepsT (Cauchy loc s)     = trackDeps loc <> trackDeps s
-distDepsT (HalfNormal s)     = trackDeps s
-distDepsT (HalfCauchy s)     = trackDeps s
-distDepsT (LogNormal mu s)   = trackDeps mu <> trackDeps s
-distDepsT (Bernoulli p)      = trackDeps p
-distDepsT (Categorical ps)   = mconcat (map trackDeps ps)
-distDepsT (Mixture ws ds)    = mconcat (map trackDeps ws) <> mconcat (map distDepsT ds)
-distDepsT (Truncated d mLo mHi) =
-  distDepsT d <> maybe mempty trackDeps mLo <> maybe mempty trackDeps mHi
-distDepsT (Censored  d mLo mHi) =
-  distDepsT d <> maybe mempty trackDeps mLo <> maybe mempty trackDeps mHi
-distDepsT (MvNormal mus covRows) =
-  mconcat (map trackDeps mus)
-    <> mconcat (concatMap (map trackDeps) covRows)
-distDepsT (MvNormalChol mus sigmas lRows) =
-  mconcat (map trackDeps mus)
-    <> mconcat (map trackDeps sigmas)
-    <> mconcat (concatMap (map trackDeps) lRows)
-distDepsT (MvNormalGpRBF xs alpha rho sigma) =   -- Phase 95 B-dsl: x は data・α/ρ/σ が param
-  mconcat (map trackDeps xs)
-    <> trackDeps alpha <> trackDeps rho <> trackDeps sigma
-distDepsT (HmmForwardNormal pi0 trans mus sg) =   -- Phase 92 A2: 全て param 側 (data は Observe に載る)
-  mconcat (map trackDeps pi0)
-    <> mconcat (concatMap (map trackDeps) trans)
-    <> mconcat (map trackDeps mus) <> trackDeps sg
-distDepsT (ArmaNormal mu phi theta sg) =   -- Phase 101 A2: 全て param 側 (data は Observe に載る)
-  trackDeps mu <> trackDeps phi <> trackDeps theta <> trackDeps sg
-distDepsT (GradedResponseIrt thetas _ _ _) =   -- Phase 101 A3: θs のみ param 側 (他は定数 data)
-  mconcat (map trackDeps thetas)
-distDepsT (NegativeBinomial mu alpha) = trackDeps mu <> trackDeps alpha
-distDepsT (Multinomial _ ps) = mconcat (map trackDeps ps)
-distDepsT (ZeroInflatedPoisson psi lam) = trackDeps psi <> trackDeps lam
-distDepsT (ZeroInflatedBinomial _ psi p) = trackDeps psi <> trackDeps p
-distDepsT (InverseGamma a b) = trackDeps a <> trackDeps b
-distDepsT (Weibull k l)      = trackDeps k <> trackDeps l
-distDepsT (Pareto a xm)      = trackDeps a <> trackDeps xm
-distDepsT (BetaBinomial _ a b) = trackDeps a <> trackDeps b
-distDepsT (VonMises mu k)    = trackDeps mu <> trackDeps k
--- Phase 37 で追加した分布 (Phase 38 補修で網羅追加)
-distDepsT (SkewNormal mu sig alpha) =
-  trackDeps mu <> trackDeps sig <> trackDeps alpha
-distDepsT (Logistic mu s)    = trackDeps mu <> trackDeps s
-distDepsT (Gumbel mu beta)   = trackDeps mu <> trackDeps beta
-distDepsT (AsymmetricLaplace b kappa mu) =
-  trackDeps b <> trackDeps kappa <> trackDeps mu
-distDepsT (OrderedLogistic eta cuts) =
-  trackDeps eta <> mconcat (map trackDeps cuts)
-distDepsT DiscreteUniform{}  = mempty   -- Int 引数のみ
-distDepsT (Geometric p)      = trackDeps p
-distDepsT HyperGeometric{}   = mempty   -- Int 引数のみ
-distDepsT (ZeroInflatedNegativeBinomial psi mu alpha) =
-  trackDeps psi <> trackDeps mu <> trackDeps alpha
-distDepsT (MvStudentT nu mus covRows) =
-  trackDeps nu
-    <> mconcat (map trackDeps mus)
-    <> mconcat (concatMap (map trackDeps) covRows)
-distDepsT (DirichletMultinomial _ alphas) =
-  mconcat (map trackDeps alphas)
-distDepsT (Triangular lo c hi) =
-  trackDeps lo <> trackDeps c <> trackDeps hi
-distDepsT (Kumaraswamy a b)    = trackDeps a <> trackDeps b
-distDepsT (Rice nu sig)        = trackDeps nu <> trackDeps sig
-distDepsT (DiscreteWeibull q beta) = trackDeps q <> trackDeps beta
-distDepsT (Wishart nu vRows) =
-  trackDeps nu <> mconcat (concatMap (map trackDeps) vRows)
-distDepsT (Bound d mLo mHi) =
-  distDepsT d
-    <> maybe mempty trackDeps mLo
-    <> maybe mempty trackDeps mHi
-distDepsT (OrderedProbit eta cuts) =
-  trackDeps eta <> mconcat (map trackDeps cuts)
-
diff --git a/src/Hanalyze/Model/HBM/Util.hs b/src/Hanalyze/Model/HBM/Util.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/HBM/Util.hs
+++ /dev/null
@@ -1,403 +0,0 @@
-{-# LANGUAGE ScopedTypeVariables #-}
--- |
--- Module      : Hanalyze.Model.HBM.Util
--- Description : HBM の純粋な数値・線形代数 leaf ユーティリティ
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- HBM の純粋な数値・線形代数 leaf ユーティリティ。
---
--- ここに集めた定義は HBM のいずれの型 (Distribution / Model / Track 等) にも
--- 依存しない葉 (leaf) であり、 Floating / Ord のみで多相に書かれている。
--- AD (Reverse.Double) でも Track でも評価できるよう型クラス制約を最小に保つ。
--- 'Hanalyze.Model.HBM' は本モジュールを import して内部利用 + 一部を re-export する
--- (公開シンボル: 'lgammaApprox' / 'digamma')。
---
--- Phase 58.2 で 'Hanalyze.Model.HBM' (5,519 行) から責務分離して抽出。
--- 数値は 1 bit も変えていない (純粋な移設)。
-module Hanalyze.Model.HBM.Util
-  ( -- * 線形代数 (下三角ソルバ / Cholesky / リスト整形)
-    backSubLT
-  , chunksOf
-  , choleskyL
-  , forwardSub
-  , gpRBFCovList
-    -- * log-sum-exp / HMM forward
-  , negInf
-  , logSumExpA
-  , hmmForwardLogLik
-    -- * 不完全ガンマ関数 P(a, x)
-  , incGammaPA
-  , igammSer
-  , igammCF
-    -- * 正則化不完全ベータ関数 I_x(a, b)
-  , incBetaA
-  , betaCFA
-    -- * 数値ユーティリティ (Γ / digamma / 階乗 / Bessel)
-  , lgammaApprox
-  , digamma
-  , lgammaApproxDeriv
-  , logFactorial
-  , logBinomCoeff
-  , logBesselI0
-  ) where
-
-import Data.List (foldl')
-import qualified Data.Vector as V
-
--- ===========================================================================
--- 線形代数 (下三角ソルバ / Cholesky / リスト整形)
--- ===========================================================================
--- Phase 95 A2 (2026-07-13): choleskyL/forwardSub/backSubLT の内部を nested-list
---   ([[a]] + !! O(n)索引 + ++ O(n)追記) から Data.Vector (O(1) 索引 + snoc) へ
---   脱リスト化。公開シグネチャ ([[a]]) は不変 = 呼び出し元は無改修。数値は回帰
---   テスト内で一致 (posterior bit 一致を実測)。★N=11 の gp-regr では効果ゼロ
---   (真因は AD tape ノード alloc・§A2 参照) だが、大 N の密行列では list !!/++ が
---   O(N⁴) 化して支配的になるため user 判断で先行 infra として採用 (2026-07-13)。
---   ※さらなる高速化には interface 自体の Vector 化 (呼出側の per-call 変換除去) が
---   要・大 N 密行列モデル出現時の TODO。
-
--- | Phase 95 B-dsl: RBF (exponentiated-quadratic) GP カーネルの共分散行列を
---   nested list で構築する。 @Σ_ij = α² exp(-0.5 (x_i-x_j)²/ρ²) + [i=j](1e-10 + σ)@。
---   'Hanalyze.Model.HBM.gpExpQuadCov' (jitter 1e-10 込) + 対角 σ と一致する
---   = 'MvNormalGpRBF' 密度が呼ぶ (値は既存 gp-regr モデルと bit 一致)。 下層 (Util)
---   に置くことで 'Distribution' の @obsLogSum@ から参照できる (Model 層の
---   'gpExpQuadCov' は上層ゆえ密度からは呼べない)。 ★ホット経路 (Gradient の
---   'gpRBFAnalyticVG') は本 list 版を使わず hmatrix Matrix で直接組む (脱リスト)。
-{-# INLINABLE gpRBFCovList #-}
-gpRBFCovList :: forall a. Floating a => [a] -> a -> a -> a -> [[a]]
-gpRBFCovList xs alpha rho sigma =
-  [ [ let d = xi - xj
-          k = alpha * alpha * exp (negate 0.5 * d * d / (rho * rho))
-      in k + (if i == j then 1e-10 + sigma else 0)
-    | (j, xj) <- zip [0 :: Int ..] xs ]
-  | (i, xi) <- zip [0 :: Int ..] xs ]
-
--- | 下三角 L から Lᵀ x = b を後退代入で解く (L は @choleskyL@ 形式)。
-{-# INLINABLE backSubLT #-}
-backSubLT :: forall a. Floating a => [[a]] -> [a] -> [a]
-backSubLT l b =
-  let n   = length b
-      lV  = V.fromList [ V.fromList r | r <- l ]
-      arr = V.fromListN n (b ++ repeat 0)
-      go :: Int -> V.Vector a -> V.Vector a
-      go i acc                       -- acc = x[i+1..n-1]
-        | i < 0 = acc
-        | otherwise =
-            -- (acc は index i+1..n-1 の解、 i 番目を解く)
-            -- Lᵀ x = b → 行 i: Σ_{j>=i} L[j][i] x_j = b_i
-            -- → x_i = (b_i - Σ_{j>i} L[j][i] x_j) / L[i][i]
-            let lii  = (lV V.! i) V.! i
-                bi   = arr V.! i
-                s    = V.sum (V.imap (\t xj -> (lV V.! (i + 1 + t)) V.! i * xj) acc)
-                xi   = (bi - s) / lii
-            in go (i - 1) (V.cons xi acc)
-  in V.toList (go (n - 1) V.empty)
-
--- | リストを長さ @n@ ごとに分割。最後が短ければそのまま (本実装では使わない想定)。
-chunksOf :: Int -> [a] -> [[a]]
-chunksOf _ [] = []
-chunksOf n xs = let (h, t) = splitAt n xs in h : chunksOf n t
-
--- | 対称正定値行列 Σ の Cholesky 下三角分解 L (Σ = L Lᵀ)。
--- 行列は行リスト @[[a]]@ で、l[i] は長さ @i+1@ の下三角行 ([L[i][0]..L[i][i]])。
--- 対角が非正になれば @Nothing@。
-{-# INLINABLE choleskyL #-}
-choleskyL :: forall a. (Floating a, Ord a) => [[a]] -> Maybe [[a]]
-choleskyL a0 =
-  let n  = length a0
-      aV = V.fromList [ V.fromList r | r <- a0 ]   -- 入力行 (各行長 >= i+1)
-      step :: Int -> V.Vector (V.Vector a) -> Maybe (V.Vector (V.Vector a))
-      step i prev                                   -- prev = 確定済 L[0..i-1]
-        | i == n = Just prev
-        | otherwise =
-            let row = aV V.! i
-                buildCol :: Int -> V.Vector a -> Maybe (V.Vector a)
-                buildCol j cur                        -- cur = L[i][0..j-1]
-                  | j > i  = Just cur
-                  | j == i =
-                      let s  = V.sum (V.map (\v -> v * v) cur)
-                          d2 = (row V.! i) - s
-                      in if d2 <= 0
-                           then Nothing
-                           else buildCol (j + 1) (V.snoc cur (sqrt d2))
-                  | otherwise =
-                      let lj  = prev V.! j           -- 長さ j+1
-                          s   = V.sum (V.zipWith (*) cur lj)
-                          ljj = lj V.! j
-                      in if ljj == 0
-                           then Nothing
-                           else buildCol (j + 1) (V.snoc cur ((row V.! j - s) / ljj))
-            in case buildCol 0 V.empty of
-                 Nothing -> Nothing
-                 Just nr -> step (i + 1) (V.snoc prev nr)
-  in fmap (\v -> [ V.toList r | r <- V.toList v ]) (step 0 V.empty)
-
--- | 下三角系 L z = b の前進代入 (L は @choleskyL@ 形式、長さ各 i+1)。
-{-# INLINABLE forwardSub #-}
-forwardSub :: forall a. Floating a => [[a]] -> [a] -> [a]
-forwardSub l b =
-  let n   = length b
-      lV  = V.fromList [ V.fromList r | r <- l ]
-      bV  = V.fromList b
-      go :: Int -> V.Vector a -> V.Vector a
-      go i acc                          -- acc = z[0..i-1]
-        | i == n = acc
-        | otherwise =
-            let lrow = lV V.! i           -- 長さ i+1
-                lii  = lrow V.! i
-                lpre = V.take i lrow      -- L[i][0..i-1]
-                bi   = bV V.! i
-                s    = V.sum (V.zipWith (*) lpre acc)
-                zi   = (bi - s) / lii
-            in go (i + 1) (V.snoc acc zi)
-  in V.toList (go 0 V.empty)
-
--- ===========================================================================
--- log-sum-exp
--- ===========================================================================
-
-negInf :: Floating a => a
-negInf = -1/0
-
--- | 多相 log-sum-exp。AD でも Track でも使えるよう Floating + Ord で書く。
--- @logSumExpA xs = log (Σ exp x)@ を最大値シフトで安定化。
-{-# INLINABLE logSumExpA #-}
-logSumExpA :: (Floating a, Ord a) => [a] -> a
-logSumExpA []  = negInf
-logSumExpA [x] = x
-logSumExpA xs  =
-  let m = maximum xs
-  -- 全要素が -∞ なら m - m = NaN になるので早期 return
-  in if m == negInf
-       then negInf
-       else m + log (sum (map (\x -> exp (x - m)) xs))
-
--- ===========================================================================
--- HMM forward algorithm (状態列の周辺化)
--- ===========================================================================
--- Phase 92 A2 (2026-07-17): Model.hs:1071 から純粋移設 (数値は 1 bit も不変)。
--- 'Distribution' の 'HmmForwardNormal' 密度 ('obsLogSum') が呼ぶため、
--- Model 非依存の leaf である本モジュールへ降ろした。
--- 'Hanalyze.Model.HBM.Model' が従来どおり re-export する。
-
--- | 隠れマルコフモデルの周辺対数尤度 (forward algorithm)。
---
--- Recursion in log-space (underflow 防止):
--- * @α_1[k] = log π_0[k] + emit[0][k]@
--- * @α_{t+1}[k'] = logSumExp_j (α_t[j] + log T[j][k']) + emit[t+1][k']@
--- * @log P(y_{1..T}) = logSumExp_k α_T[k]@
---
--- 多相 (@Floating a, Ord a@) のため Track / AD 経由でも動く。
--- 計算量 @O(T K²)@。 大 T では list-based なので O(K²) の内部ループは
--- そのまま、 step は foldl' で過去 α を破棄しメモリは @O(K)@。
-hmmForwardLogLik :: forall a. (Floating a, Ord a)
-                 => [a]     -- ^ 初期分布 π_0 (length K)
-                 -> [[a]]   -- ^ 遷移行列 (K×K rows of length K)
-                 -> [[a]]   -- ^ log emission [T][K]
-                 -> a
-hmmForwardLogLik pi0 trans emit
-  | null emit       = 0  -- T=0: 観測なし
-  | null pi0        = negInf
-  | length pi0 /= length trans = negInf
-  | any ((/= k) . length) trans = negInf
-  | otherwise =
-      let -- α_1[s] = log π_0[s] + emit[0][s]
-          alpha0 = zipWith (\p e -> log p + e) pi0 (head emit)
-          -- 1 step: α_{t+1}[s'] = logSumExp_s (α_t[s] + log T[s][s']) + emit_{t+1}[s']
-          step :: [a] -> [a] -> [a]
-          step alphaT emT =
-            [ logSumExpA
-                [ (alphaT !! s) + log ((trans !! s) !! s')
-                | s <- [0 .. k - 1] ]
-              + (emT !! s')
-            | s' <- [0 .. k - 1] ]
-          alphaFinal = foldl' step alpha0 (tail emit)
-      in logSumExpA alphaFinal
-  where
-    k = length pi0
-
--- ===========================================================================
--- 不完全ガンマ関数 P(a, x) = γ(a, x) / Γ(a)  (Numerical Recipes 6.2)
--- ===========================================================================
-
--- | 正則化された下側不完全ガンマ関数 P(a, x) = γ(a, x) / Γ(a) ∈ [0, 1]。
--- これは Gamma(shape=a, rate=1) の CDF F(x)。
-{-# INLINABLE incGammaPA #-}
-incGammaPA :: (Floating a, Ord a) => a -> a -> a
-incGammaPA a x
-  | x <= 0 || a <= 0 = 0
-  | x < a + 1        = igammSer a x          -- 級数展開で P(a,x)
-  | otherwise        = 1 - igammCF a x        -- 連分数で Q(a,x)、P = 1 - Q
-
--- 級数展開: P(a, x) = e^{-x} x^a / Γ(a) * Σ x^n / (a(a+1)...(a+n))
-{-# INLINABLE igammSer #-}
-igammSer :: forall a. (Floating a, Ord a) => a -> a -> a
-igammSer a x = sumSer * exp (-x + a * log x - lgammaApprox a)
-  where
-    -- 反復: term_{n+1} = term_n * x / (a + n + 1)
-    sumSer = go (0 :: Int) (1 / a) (1 / a)
-    eps :: a
-    eps    = 1e-13
-    maxIt  = 200 :: Int
-    go n term acc
-      | n >= maxIt           = acc
-      | abs term < abs acc * eps = acc
-      | otherwise =
-          let n'    = n + 1
-              term' = term * x / (a + fromIntegral n')
-              acc'  = acc + term'
-          in go n' term' acc'
-
--- 連分数 (Lentz 法): Q(a, x) = e^{-x} x^a / Γ(a) * CF
--- CF = 1/(x+1-a - 1·(1-a)/(x+3-a - 2·(2-a)/(...))
-{-# INLINABLE igammCF #-}
-igammCF :: forall a. (Floating a, Ord a) => a -> a -> a
-igammCF a x = exp (-x + a * log x - lgammaApprox a) * h
-  where
-    fpmin, eps :: a
-    fpmin = 1e-300
-    eps   = 1e-13
-    maxIt = 200 :: Int
-    -- modified Lentz's method
-    b0    = x + 1 - a
-    c0    = 1 / fpmin
-    d0    = 1 / b0
-    h     = goCF (1 :: Int) b0 c0 d0 d0
-    goCF i b c d hh
-      | i > maxIt              = hh
-      | abs (del - 1) < eps    = hh'
-      | otherwise              = goCF (i + 1) b' c'' d''' hh'
-      where
-        an   = -fromIntegral i * (fromIntegral i - a)
-        b'   = b + 2
-        d'   = b' + an * d
-        d''  = if abs d' < fpmin then fpmin else d'
-        c'   = b' + an / c
-        c''  = if abs c' < fpmin then fpmin else c'
-        d''' = 1 / d''
-        del  = d''' * c''
-        hh'  = hh * del
-    _ = c0  -- 未使用ダミー (修正された Lentz 法の起動値: 別経路)
-
--- ===========================================================================
--- 正則化された不完全ベータ関数 I_x(a, b) = B(x; a, b) / B(a, b)
--- ===========================================================================
-
--- | 正則化された不完全ベータ関数 I_x(a, b) ∈ [0, 1]。
--- これは Beta(a, b) の CDF F(x)。
--- StudentT の CDF にも内部で使用。
-{-# INLINABLE incBetaA #-}
-incBetaA :: (Floating a, Ord a) => a -> a -> a -> a
-incBetaA x a b
-  | x <= 0    = 0
-  | x >= 1    = 1
-  | otherwise =
-      -- 対数ベータ正規化定数
-      let bt = exp ( lgammaApprox (a + b)
-                   - lgammaApprox a
-                   - lgammaApprox b
-                   + a * log x
-                   + b * log (1 - x))
-      in if x < (a + 1) / (a + b + 2)
-           then bt * betaCFA x a b / a
-           else 1 - bt * betaCFA (1 - x) b a / b
-
--- 連分数 (modified Lentz, Numerical Recipes §6.4)
-{-# INLINABLE betaCFA #-}
-betaCFA :: forall a. (Floating a, Ord a) => a -> a -> a -> a
-betaCFA x a b = iterate' (1 :: Int) 1 d0 h0
-  where
-    fpmin, eps :: a
-    fpmin = 1e-300
-    eps   = 1e-13
-    maxIt = 200 :: Int
-    qab = a + b
-    qap = a + 1
-    qam = a - 1
-    capLent v = if abs v < fpmin then fpmin else v
-    d0 = 1 / capLent (1 - qab * x / qap)
-    h0 = d0
-
-    iterate' m c d h
-      | m > maxIt          = h
-      | abs (del - 1) < eps = hO
-      | otherwise          = iterate' (m + 1) cO dO hO
-      where
-        mD  = fromIntegral m :: a
-        -- 偶数項: aa_2m = m(b-m)x / ((qam+2m)(a+2m))
-        aaE = mD * (b - mD) * x / ((qam + 2 * mD) * (a + 2 * mD))
-        dE  = 1 / capLent (1 + aaE * d)
-        cE  = capLent (1 + aaE / c)
-        hE  = h * dE * cE
-        -- 奇数項: aa_2m+1 = -(a+m)(qab+m)x / ((a+2m)(qap+2m))
-        aaO = -(a + mD) * (qab + mD) * x / ((a + 2 * mD) * (qap + 2 * mD))
-        dO  = 1 / capLent (1 + aaO * dE)
-        cO  = capLent (1 + aaO / cE)
-        del = dO * cO
-        hO  = hE * del
-
--- ===========================================================================
--- 数値ユーティリティ (Γ / digamma / 階乗 / Bessel)
--- ===========================================================================
-
--- | log Γ(z) の Stirling 近似 (z > 0)。AD でも Track でも使える多相版。
-{-# INLINABLE lgammaApprox #-}
-lgammaApprox :: (Floating a, Ord a) => a -> a
-lgammaApprox z
-  | z < 12    = lgammaApprox (z + 1) - log z
-  | otherwise = (z - 0.5) * log z - z + 0.5 * log (2 * pi)
-              + 1 / (12 * z) - 1 / (360 * z ^ (3::Int))
-
--- | ψ(z) = d/dz log Γ(z) (z > 0・Phase 56.1)。 記号微分 IR の lgamma 単項 op
--- ('SLgammaO' 予定) の導関数用。 'lgammaApprox' と同一の recurrence
--- (z < 12 を押し上げ) + 漸近級数を lgammaApprox の Stirling 微分より 1 項深く
--- (-1/(252 z⁶) まで) 打切り: 真の ψ との差は z=12 で ~1e-11、
--- lgammaApprox の数値微分との差は lgammaApprox 側の打切り由来 ~1.3e-9
--- (試験許容 1e-8 内)。 z ≤ 0 は未対応 (利用箇所は正値前提)。
-digamma :: Double -> Double
-digamma z
-  | z < 12    = digamma (z + 1) - 1 / z
-  | otherwise = log z - 1 / (2 * z) - 1 / (12 * z * z)
-              + 1 / (120 * z ^ (4 :: Int)) - 1 / (252 * z ^ (6 :: Int))
-
--- | 'lgammaApprox' の**厳密な項別導関数** (Phase 56.4)。 'digamma' とは最終項
--- 1/(252z⁶) の有無だけ違う (digamma は真の ψ に 1 項深い分この差 ~1.3e-9 が
--- z=12 境界で出る・実測)。 記号微分 IR ('SLgammaO') の導関数は、 評価関数
--- (lgammaApprox) の AD 微分 = walk+ad fallback / 参照勾配と一致させる必要が
--- あるためこちらを使う。
-lgammaApproxDeriv :: Double -> Double
-lgammaApproxDeriv z
-  | z < 12    = lgammaApproxDeriv (z + 1) - 1 / z
-  | otherwise = log z - 1 / (2 * z) - 1 / (12 * z * z)
-              + 1 / (120 * z ^ (4 :: Int))
-
-logFactorial :: Int -> Double
-logFactorial n
-  | n <= 1    = 0
-  | otherwise = sum (map log [2 .. fromIntegral n])
-
-logBinomCoeff :: Int -> Int -> Double
-logBinomCoeff n k = logFactorial n - logFactorial k - logFactorial (n - k)
-
--- | log I_0(x) — 修正 Bessel 関数 (第一種・order 0) の対数。VonMises 用。
--- 小 x: 級数 I_0(x) = Σ (x/2)^(2k) / (k!)² (k = 0..)
--- 大 x: 漸近展開 I_0(x) ≈ exp(x) / √(2πx) × [1 + 1/(8x) + 9/(128x²) + …]
--- AD/Track 互換のため (Floating a, Ord a) 多相。
-{-# INLINABLE logBesselI0 #-}
-logBesselI0 :: (Floating a, Ord a) => a -> a
-logBesselI0 x
-  | x < 0     = logBesselI0 (-x)  -- 偶関数
-  | x < 3.75  =
-      -- Abramowitz & Stegun 9.8.1: 多項式近似 (誤差 < 1.6e-7)
-      let t = (x / 3.75) ^ (2::Int)
-          i0 = 1 + t * (3.5156229 + t * (3.0899424 + t * (1.2067492
-             + t * (0.2659732 + t * (0.0360768 + t * 0.0045813)))))
-      in log i0
-  | otherwise =
-      -- Abramowitz & Stegun 9.8.2: 漸近 (誤差 < 1.9e-7)
-      let t = 3.75 / x
-          poly = 0.39894228 + t * (0.01328592 + t * (0.00225319
-               + t * (-0.00157565 + t * (0.00916281 + t * (-0.02057706
-               + t * (0.02635537 + t * (-0.01647633 + t * 0.00392377)))))))
-      in x - 0.5 * log x + log poly
diff --git a/src/Hanalyze/Model/HBM/VecAD.hs b/src/Hanalyze/Model/HBM/VecAD.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/HBM/VecAD.hs
+++ /dev/null
@@ -1,337 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-
--- |
--- Module      : Hanalyze.Model.HBM.VecAD
--- Description : 自作の最小 reverse-mode AD (vector-op tape)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- 自作・最小 reverse-mode AD (vector-op tape)。 Phase 54.3 第2 spike で
--- 「採用 = 案B (自前 vector-op tape)」 と判断したエンジンを本実装用に移植した
--- もの (`bench/haskell/BenchHBMVecADSpike.hs` の gradHandroll 系)。
---
--- 設計: forward で「ベクトル演算ごとにノードを発番」 し、 各ノードの随伴更新
--- クロージャを逆順リストに積む (= 自前 Wengert tape)。 backward で出力に 1 を
--- seed し、 逆位相順 (= 発番の逆順 = prepend したリストの先頭) にクロージャを
--- replay して入力 (leaf) の随伴を得る。 tape は「ベクトル演算 1 個 = 1 ノード」
--- ゆえ `ad` のスカラ tape (per-scalar-op で O(n) ノード) より桁で小さい。
---
--- スカラは長さ 1 の Storable Vector として随伴を持ち、 ノード随伴は単一の
--- mutable 配列に統一格納する。
---
--- ⚠ 値依存制御フロー (分布の台チェック等) は tape に乗らない。 本エンジンは
--- 構造が値に依らず静的な部分 (Gaussian-恒等リンクの線形予測子 + 二乗和) 専用。
--- 非対応の構造は呼出側で scalar (`ad`) 経路に fallback する。
-module Hanalyze.Model.HBM.VecAD
-  ( -- * 値ハンドルと文脈
-    Rval (..)
-  , Ctx
-  , ridOf
-    -- * tape の実行
-  , runTape
-    -- * leaf
-  , inputVec
-  , inputScal
-  , constVec
-    -- * ベクトル演算 (随伴付き)
-  , idxHR
-  , sliceHR
-  , scaleHR
-  , vaddHR
-  , vsubHR
-  , dotHR
-  , gatherHR
-  , vexpHR
-  , bcastAddHR
-  , hadamardHR
-  , vmap1HR
-    -- * スカラ演算 (随伴付き)
-  , map1S
-  , cstS
-  , addS
-  , subS
-  , mulS
-  , divByS
-  , expS
-  , logS
-  , mulConstS
-  , addConstS
-  , foldVadd
-  ) where
-
-import           Control.Monad (when)
-import           Control.Monad.ST
-import           Data.Array.ST (STArray, newArray, readArray, writeArray)
-import           Data.STRef
-import qualified Data.Vector.Storable as VS
-import qualified Data.Vector.Unboxed  as VU
-
--- ===========================================================================
--- 値ハンドルと tape 文脈
--- ===========================================================================
-
--- | reverse-mode の値ハンドル: ノード id + primal (scalar / vector)。
-data Rval = RScal !Int !Double | RVec !Int !(VS.Vector Double)
-
-ridOf :: Rval -> Int
-ridOf (RScal i _) = i
-ridOf (RVec  i _) = i
-
-type Adj s = STArray s Int (VS.Vector Double)
-
--- | 発番カウンタ + backward クロージャ列 (prepend = 発番の逆順)。
-data Ctx s = Ctx !(STRef s Int) !(STRef s [Adj s -> ST s ()])
-
-fresh :: Ctx s -> ST s Int
-fresh (Ctx cnt _) = do
-  n <- readSTRef cnt
-  writeSTRef cnt (n + 1)
-  pure n
-
-record :: Ctx s -> (Adj s -> ST s ()) -> ST s ()
-record (Ctx _ bw) f = modifySTRef' bw (f :)
-
--- | 随伴の加算 (空 = ゼロ扱い)。
-bumpA :: Adj s -> Int -> VS.Vector Double -> ST s ()
-bumpA adj i contrib = do
-  cur <- readArray adj i
-  writeArray adj i (if VS.null cur then contrib else VS.zipWith (+) cur contrib)
-
-readAdjS :: Adj s -> Int -> ST s Double
-readAdjS adj i = do
-  v <- readArray adj i
-  pure (if VS.null v then 0 else v VS.! 0)
-
--- ===========================================================================
--- tape の実行 (forward build → seed → backward replay)
--- ===========================================================================
-
--- | tape を構築するアクション (出力ノード + 勾配を読みたい leaf 群を返す) を
--- 受け取り、 forward 評価 → 出力に 1 を seed → backward replay の上で、
--- 各 leaf の随伴 (= 出力の各 leaf に対する勾配ベクトル) を返す。
---
--- @build@ は @(出力 Rval, [leaf Rval])@ を返す。 結果は leaf ごとの随伴
--- ベクトル (RScal leaf は長さ1、 RVec leaf は元の長さ)。
-runTape :: (forall s. Ctx s -> ST s (Rval, [Rval])) -> [VS.Vector Double]
-runTape build = runST $ do
-  cnt <- newSTRef 0
-  bw  <- newSTRef []
-  let ctx = Ctx cnt bw
-  (out, leaves) <- build ctx
-  total <- readSTRef cnt
-  adj <- newArray (0, max 0 (total - 1)) VS.empty
-  writeArray adj (ridOf out) (VS.singleton 1)
-  closures <- readSTRef bw
-  mapM_ ($ adj) closures
-  mapM (\lf -> readArray adj (ridOf lf)) leaves
-
--- ===========================================================================
--- leaf
--- ===========================================================================
-
--- | ベクトル leaf (勾配を読む入力)。
-inputVec :: Ctx s -> VS.Vector Double -> ST s Rval
-inputVec ctx v = do
-  i <- fresh ctx
-  pure (RVec i v)
-
--- | スカラ leaf (勾配を読む入力)。
-inputScal :: Ctx s -> Double -> ST s Rval
-inputScal ctx x = do
-  i <- fresh ctx
-  pure (RScal i x)
-
--- | 定数ベクトルノード (backward 無し)。
-constVec :: Ctx s -> VS.Vector Double -> ST s Rval
-constVec ctx v = do { i <- fresh ctx; pure (RVec i v) }
-
--- ===========================================================================
--- ベクトル演算 (随伴付き)
--- ===========================================================================
-
--- | 全長 @l@ の vec から要素 @i@ を取り出す (scalar 化)。 随伴 = e_i·dy。
-idxHR :: Ctx s -> Int -> Int -> Rval -> ST s Rval
-idxHR ctx l i (RVec vid v) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    g <- readAdjS adj o
-    when (g /= 0) $ bumpA adj vid (VS.generate l (\j -> if j == i then g else 0))
-  pure (RScal o (v VS.! i))
-idxHR _ _ _ _ = error "idxHR: scalar input"
-
--- | 全長 @l@ の vec から @[off, off+len)@ を切り出す。 随伴は zeros l に散布。
-sliceHR :: Ctx s -> Int -> Int -> Int -> Rval -> ST s Rval
-sliceHR ctx l off len (RVec vid v) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    dy <- readArray adj o
-    when (not (VS.null dy)) $
-      bumpA adj vid (VS.generate l (\j -> if j >= off && j < off + len then dy VS.! (j - off) else 0))
-  pure (RVec o (VS.slice off len v))
-sliceHR _ _ _ _ _ = error "sliceHR: scalar input"
-
--- | scalar * vector。 ∂scalar = dy·v、 ∂v = scalar·dy。
-scaleHR :: Ctx s -> Rval -> Rval -> ST s Rval
-scaleHR ctx (RScal kid k) (RVec vid v) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    dy <- readArray adj o
-    when (not (VS.null dy)) $ do
-      bumpA adj kid (VS.singleton (VS.sum (VS.zipWith (*) dy v)))
-      bumpA adj vid (VS.map (* k) dy)
-  pure (RVec o (VS.map (* k) v))
-scaleHR _ _ _ = error "scaleHR: shape"
-
--- | vector + vector。
-vaddHR :: Ctx s -> Rval -> Rval -> ST s Rval
-vaddHR ctx (RVec aid a) (RVec bid b) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    dy <- readArray adj o
-    when (not (VS.null dy)) $ do
-      bumpA adj aid dy
-      bumpA adj bid dy
-  pure (RVec o (VS.zipWith (+) a b))
-vaddHR _ _ _ = error "vaddHR: shape"
-
--- | vector - vector。
-vsubHR :: Ctx s -> Rval -> Rval -> ST s Rval
-vsubHR ctx (RVec aid a) (RVec bid b) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    dy <- readArray adj o
-    when (not (VS.null dy)) $ do
-      bumpA adj aid dy
-      bumpA adj bid (VS.map negate dy)
-  pure (RVec o (VS.zipWith (-) a b))
-vsubHR _ _ _ = error "vsubHR: shape"
-
--- | 内積。 ∂a = dy·b、 ∂b = dy·a。
-dotHR :: Ctx s -> Rval -> Rval -> ST s Rval
-dotHR ctx (RVec aid a) (RVec bid b) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    g <- readAdjS adj o
-    when (g /= 0) $ do
-      bumpA adj aid (VS.map (* g) b)
-      bumpA adj bid (VS.map (* g) a)
-  pure (RScal o (VS.sum (VS.zipWith (*) a b)))
-dotHR _ _ _ = error "dotHR: shape"
-
--- | @u[gids]@ gather (gids/nG は定数)。 随伴は scatter-add で O(n)。
-gatherHR :: Ctx s -> VU.Vector Int -> Int -> Rval -> ST s Rval
-gatherHR ctx gids nG (RVec uid u) = do
-  let n = VU.length gids
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    dy <- readArray adj o
-    when (not (VS.null dy)) $
-      bumpA adj uid (VS.convert $
-        VU.accumulate (+) (VU.replicate nG 0) (VU.zip gids (VU.convert dy :: VU.Vector Double)))
-  pure (RVec o (VS.generate n (\i -> u VS.! (gids VU.! i))))
-gatherHR _ _ _ _ = error "gatherHR: shape"
-
--- | elementwise exp (Phase 54.11 spike: 非線形 μ 用)。 ∂v = dy ⊙ exp(v)。
-vexpHR :: Ctx s -> Rval -> ST s Rval
-vexpHR ctx (RVec vid v) = do
-  let ev = VS.map exp v
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    dy <- readArray adj o
-    when (not (VS.null dy)) $
-      bumpA adj vid (VS.zipWith (*) dy ev)
-  pure (RVec o ev)
-vexpHR _ _ = error "vexpHR: scalar input"
-
--- | scalar + vector の broadcast 加算 (Phase 54.11 spike)。 ∂scalar = Σ dy。
-bcastAddHR :: Ctx s -> Rval -> Rval -> ST s Rval
-bcastAddHR ctx (RScal kid k) (RVec vid v) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    dy <- readArray adj o
-    when (not (VS.null dy)) $ do
-      bumpA adj kid (VS.singleton (VS.sum dy))
-      bumpA adj vid dy
-  pure (RVec o (VS.map (+ k) v))
-bcastAddHR _ _ _ = error "bcastAddHR: shape"
-
--- | elementwise 積 v ⊙ w (Phase 54.11 spike: gather(a)[i]·exp(-b·x_i) 用)。
--- ∂v = dy ⊙ w、 ∂w = dy ⊙ v。
-hadamardHR :: Ctx s -> Rval -> Rval -> ST s Rval
-hadamardHR ctx (RVec aid a) (RVec bid b) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    dy <- readArray adj o
-    when (not (VS.null dy)) $ do
-      bumpA adj aid (VS.zipWith (*) dy b)
-      bumpA adj bid (VS.zipWith (*) dy a)
-  pure (RVec o (VS.zipWith (*) a b))
-hadamardHR _ _ _ = error "hadamardHR: shape"
-
--- | 汎用 elementwise 単項 (Phase 54.11: ベクトル式 IR の log/recip/sqrt/tanh 等)。
--- @f@ とその導関数 @f'@ を受け、 ∂v = dy ⊙ f'(v) (v は入力 primal)。
-vmap1HR :: Ctx s -> (Double -> Double) -> (Double -> Double) -> Rval -> ST s Rval
-vmap1HR ctx f df (RVec vid v) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    dy <- readArray adj o
-    when (not (VS.null dy)) $
-      bumpA adj vid (VS.zipWith (\g x -> g * df x) dy v)
-  pure (RVec o (VS.map f v))
-vmap1HR _ _ _ _ = error "vmap1HR: scalar input"
-
--- | 非空ベクトルノード列を vadd で畳む。
-foldVadd :: Ctx s -> [Rval] -> ST s Rval
-foldVadd _   []       = error "foldVadd: empty"
-foldVadd _   [x]      = pure x
-foldVadd ctx (x:y:xs) = vaddHR ctx x y >>= \z -> foldVadd ctx (z : xs)
-
--- ===========================================================================
--- スカラ演算 (随伴付き)
--- ===========================================================================
-
-cstS :: Ctx s -> Double -> ST s Rval
-cstS ctx x = do { i <- fresh ctx; pure (RScal i x) }
-
-binS :: Ctx s -> (Double -> Double -> Double) -> (Double -> Double -> (Double, Double))
-     -> Rval -> Rval -> ST s Rval
-binS ctx f df (RScal aid a) (RScal bid b) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    g <- readAdjS adj o
-    when (g /= 0) $ do
-      let (da, db) = df a b
-      bumpA adj aid (VS.singleton (g * da))
-      bumpA adj bid (VS.singleton (g * db))
-  pure (RScal o (f a b))
-binS _ _ _ _ _ = error "binS: scalar expected"
-
-addS, subS, mulS :: Ctx s -> Rval -> Rval -> ST s Rval
-addS ctx = binS ctx (+) (\_ _ -> (1, 1))
-subS ctx = binS ctx (-) (\_ _ -> (1, -1))
-mulS ctx = binS ctx (*) (\a b -> (b, a))
-
--- | scalar 除算 (a/b)。
-divByS :: Ctx s -> Rval -> Rval -> ST s Rval
-divByS ctx = binS ctx (/) (\a b -> (1 / b, negate a / (b * b)))
-
-unS :: Ctx s -> (Double -> Double) -> (Double -> Double) -> Rval -> ST s Rval
-unS ctx f df (RScal aid a) = do
-  o <- fresh ctx
-  record ctx $ \adj -> do
-    g <- readAdjS adj o
-    when (g /= 0) $ bumpA adj aid (VS.singleton (g * df a))
-  pure (RScal o (f a))
-unS _ _ _ _ = error "unS: scalar expected"
-
-expS, logS :: Ctx s -> Rval -> ST s Rval
-expS ctx = unS ctx exp exp
-logS ctx = unS ctx log (\a -> 1 / a)
-
--- | 汎用スカラ単項 (Phase 54.11)。 @f@ と導関数 @f'@ を受ける ('unS' の公開形)。
-map1S :: Ctx s -> (Double -> Double) -> (Double -> Double) -> Rval -> ST s Rval
-map1S = unS
-
-mulConstS, addConstS :: Ctx s -> Double -> Rval -> ST s Rval
-mulConstS ctx c = unS ctx (* c) (const c)
-addConstS ctx c = unS ctx (+ c) (const 1)
diff --git a/src/Hanalyze/Model/HierarchicalCluster.hs b/src/Hanalyze/Model/HierarchicalCluster.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/HierarchicalCluster.hs
+++ /dev/null
@@ -1,215 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Model.HierarchicalCluster
--- Description : 凝集型階層クラスタリング (Agglomerative Hierarchical Clustering)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- 凝集型階層クラスタリング (Agglomerative Hierarchical Clustering)。
---
--- Lance-Williams update formula による O(n²) アルゴリズム。
--- 各ステップで最近接クラスタ対をマージし、 新クラスタへの距離を再計算する。
---
--- 対応 linkage:
---
---   * 'Single'   : d(i∪j, k) = min(d(i,k), d(j,k))
---   * 'Complete' : d(i∪j, k) = max(d(i,k), d(j,k))
---   * 'Average'  : (|i|·d(i,k) + |j|·d(j,k)) / (|i|+|j|)
---   * 'Ward'     : Lance-Williams 係数で分散最小化
---
--- 距離は Euclidean のみサポート (X の各行をサンプルとして二乗ユークリッド距離)。
-module Hanalyze.Model.HierarchicalCluster
-  ( Linkage (..)
-  , HClusterFit (..)
-  , fitHierarchical
-  , cutTree
-  ) where
-
-import qualified Data.Vector                  as V
-import qualified Data.Vector.Mutable          as MV
-import qualified Data.Vector.Unboxed.Mutable  as MU
-import qualified Numeric.LinearAlgebra        as LA
-import           Control.Monad                (forM_, when)
-import           Control.Monad.ST             (runST)
-import           Data.STRef                   (newSTRef, readSTRef, writeSTRef,
-                                               modifySTRef')
-import           Data.List                    (foldl')
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
-data Linkage = Single | Complete | Average | Ward
-             deriving (Show, Eq)
-
-data HClusterFit = HClusterFit
-  { hcMerges       :: ![(Int, Int)]  -- ^ マージ列 (n-1 個)。 ID は 0..n-1 が元サンプル、
-                                     --   以降 n, n+1, ... が新クラスタ
-  , hcHeights      :: ![Double]      -- ^ マージ時点での距離 (linkage に応じた値)
-  , hcLinkage      :: !Linkage
-  , hcNumOriginals :: !Int           -- ^ n_samples
-  } deriving (Show)
-
--- ===========================================================================
--- fit
--- ===========================================================================
-
--- | 階層クラスタリングを fit する。 X は n × p 行列、 各行が 1 サンプル。
-fitHierarchical :: Linkage -> LA.Matrix Double -> HClusterFit
-fitHierarchical link xs =
-  let n = LA.rows xs
-      d0 = initialDistance link xs
-  in agglomerate link n d0
-
--- | 樹形図を K クラスタに切り、 各サンプルのクラスタ ID を返す。
---   K = 1 → 全サンプル ID 0; K = n → 全サンプル別 ID。
-cutTree :: HClusterFit -> Int -> V.Vector Int
-cutTree fit k
-  | k <= 0 = V.replicate (hcNumOriginals fit) 0
-  | k >= n = V.generate n id
-  | otherwise =
-      let nMerges = n - k     -- K クラスタにするには n-K 回マージを適用
-          mergesUsed = take nMerges (hcMerges fit)
-          -- union-find 風: parent[i] = root cluster representative
-          parents = runST $ do
-            arr <- MV.replicate (2 * n) (-1 :: Int)
-            forM_ [0 .. n - 1] $ \i -> MV.write arr i i
-            forM_ (zip [n ..] mergesUsed) $ \(newId, (a, b)) -> do
-              ra <- findRoot arr a
-              rb <- findRoot arr b
-              MV.write arr ra newId
-              MV.write arr rb newId
-              MV.write arr newId newId
-            V.generateM n (findRoot arr)
-          uniqRoots = foldr (\r acc -> if r `elem` acc then acc else r:acc) [] (V.toList parents)
-          roots = zip uniqRoots [0 ..]
-          lookupId r = case lookup r roots of
-            Just i  -> i
-            Nothing -> 0
-      in V.map lookupId parents
-  where
-    n = hcNumOriginals fit
-    findRoot arr i = do
-      p <- MV.read arr i
-      if p == i then pure i else findRoot arr p
-
--- ===========================================================================
--- 内部: 距離行列の構築
--- ===========================================================================
-
--- | 初期距離行列 (n × n)。 二乗ユークリッド距離。
---   Ward は二乗距離を使うのが定義どおり。 他 linkage は √ を取って通常距離にする。
-initialDistance :: Linkage -> LA.Matrix Double -> LA.Matrix Double
-initialDistance link xs =
-  let n = LA.rows xs
-      sqDist i j =
-        let r = LA.flatten (xs LA.? [i]) - LA.flatten (xs LA.? [j])
-        in LA.sumElements (r * r)
-      raw = LA.build (n, n)
-              (\i j -> sqDist (round i) (round j) :: Double)
-  in case link of
-       Ward -> raw           -- squared
-       _    -> LA.cmap sqrt raw
-
--- ===========================================================================
--- 内部: 凝集アルゴリズム
--- ===========================================================================
-
-agglomerate :: Linkage -> Int -> LA.Matrix Double -> HClusterFit
-agglomerate link n d0 = runST $ do
-  -- Phase 17.2 改善:
-  --   * 距離行列を MU (Unboxed Mutable Vector Double) で flat 配列に
-  --   * active set を Unboxed Mutable Vector Int でコンパクトに保持
-  --     (毎ステップ tail 切詰めの代わりに、 in-place で a,b 位置を最後と入替え)
-  --   * unsafeRead / unsafeWrite で境界チェック排除
-  --   * inner loop の STRef 更新を local accumulator (Int * 2 + Double) で減らす
-  let !totalIds = 2 * n - 1
-  dist  <- MU.unsafeNew (totalIds * totalIds)
-  -- 初期化: ∞
-  forM_ [0 .. totalIds * totalIds - 1] $ \k -> MU.unsafeWrite dist k (1/0 :: Double)
-  sizes <- MU.replicate totalIds (1 :: Int)
-  forM_ [0 .. n - 1] $ \i ->
-    forM_ [0 .. n - 1] $ \j ->
-      when (i /= j) $
-        MU.unsafeWrite dist (i * totalIds + j) (LA.atIndex d0 (i, j))
-  -- active: 先頭 `activeLen` 要素が active な ID
-  active <- MU.unsafeNew totalIds
-  forM_ [0 .. n - 1] $ \i -> MU.unsafeWrite active i i
-  activeLenRef <- newSTRef n
-  mergesRef    <- newSTRef ([] :: [(Int, Int)])
-  heightsRef   <- newSTRef ([] :: [Double])
-  forM_ [0 .. n - 2] $ \step -> do
-    let !nextId = n + step
-    !alen <- readSTRef activeLenRef
-    -- find argmin。 active[0 .. alen-1] のペアを直接走査
-    bestRef <- newSTRef ((-1) :: Int, (-1) :: Int, 1/0 :: Double, (-1) :: Int, (-1) :: Int)
-    -- (a, b, bestDist, posA, posB)  posA/posB は active 内の位置
-    forM_ [0 .. alen - 2] $ \pi_ -> do
-      !i <- MU.unsafeRead active pi_
-      forM_ [pi_ + 1 .. alen - 1] $ \pj -> do
-        !j <- MU.unsafeRead active pj
-        !d <- MU.unsafeRead dist (i * totalIds + j)
-        (_, _, !best, _, _) <- readSTRef bestRef
-        when (d < best) $ writeSTRef bestRef (i, j, d, pi_, pj)
-    (!a, !b, !h, !pa, !pb) <- readSTRef bestRef
-    modifySTRef' mergesRef  ((a, b) :)
-    modifySTRef' heightsRef ((reportHeight link h) :)
-    !na <- MU.unsafeRead sizes a
-    !nb <- MU.unsafeRead sizes b
-    MU.unsafeWrite sizes nextId (na + nb)
-    -- active から a, b を削除し nextId を追加: pb を末尾と swap で除去、
-    -- 同様に pa を新末尾と swap、 alen 減 2、 末尾に nextId を入れて alen 増 1
-    -- ※ pa < pb 不変 (内側 loop が pj > pi)
-    !lastPos <- pure (alen - 1)
-    !valLast <- MU.unsafeRead active lastPos
-    MU.unsafeWrite active pb valLast
-    !secondLast <- pure (alen - 2)
-    !valSecond <- MU.unsafeRead active secondLast
-    -- pa の位置は pb と入替えで動いていない (pa < pb なので)
-    MU.unsafeWrite active pa valSecond
-    MU.unsafeWrite active secondLast nextId
-    writeSTRef activeLenRef (alen - 1)  -- 2 削除 + 1 追加 = -1
-    !alenNew <- readSTRef activeLenRef
-    -- Lance-Williams update: active[0 .. alenNew - 1] (末尾は nextId)
-    let !nextRow = nextId * totalIds
-    forM_ [0 .. alenNew - 2] $ \pk -> do
-      !k <- MU.unsafeRead active pk
-      !dak <- MU.unsafeRead dist (a * totalIds + k)
-      !dbk <- MU.unsafeRead dist (b * totalIds + k)
-      !nk  <- MU.unsafeRead sizes k
-      let !dNew = lanceWilliams link (na, nb, nk) dak dbk h
-      MU.unsafeWrite dist (nextRow + k) dNew
-      MU.unsafeWrite dist (k * totalIds + nextId) dNew
-  merges  <- reverse <$> readSTRef mergesRef
-  heights <- reverse <$> readSTRef heightsRef
-  pure HClusterFit
-    { hcMerges       = merges
-    , hcHeights      = heights
-    , hcLinkage      = link
-    , hcNumOriginals = n
-    }
-  where
-    reportHeight Ward h = sqrt (max 0 h)
-    reportHeight _    h = h
-
--- | Lance-Williams recurrence:
---   d(i∪j, k) = α_i d(i,k) + α_j d(j,k) + β d(i,j) + γ |d(i,k) − d(j,k)|
-lanceWilliams :: Linkage
-              -> (Int, Int, Int)   -- sizes (n_a, n_b, n_k)
-              -> Double            -- d(a, k)
-              -> Double            -- d(b, k)
-              -> Double            -- d(a, b)
-              -> Double
-lanceWilliams link (na, nb, nk) dak dbk dab =
-  case link of
-    Single   -> min dak dbk
-    Complete -> max dak dbk
-    Average  ->
-      let naD = fromIntegral na; nbD = fromIntegral nb
-      in (naD * dak + nbD * dbk) / (naD + nbD)
-    Ward ->
-      let naD = fromIntegral na; nbD = fromIntegral nb
-          nkD = fromIntegral nk
-          tot = naD + nbD + nkD
-      in ((naD + nkD) * dak + (nbD + nkD) * dbk - nkD * dab) / tot
diff --git a/src/Hanalyze/Model/KNN.hs b/src/Hanalyze/Model/KNN.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/KNN.hs
+++ /dev/null
@@ -1,134 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Model.KNN
--- Description : k近傍法 (k-Nearest Neighbours、 回帰 + 分類、 brute force ユークリッド距離)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- k-Nearest Neighbours (回帰 + 分類、 brute force ユークリッド距離).
---
--- @
--- import qualified Hanalyze.Model.KNN as KNN
--- let knnR = KNN.fitKNNR 5 xTrain yTrain
---     yR   = KNN.predictKNNR knnR xTest
--- @
---
--- /Complexity/: O(n_test · n_train · d)。 KD-tree は scope 外。
-module Hanalyze.Model.KNN
-  ( KNNRegressor (..)
-  , KNNClassifier (..)
-  , fitKNNR
-  , fitKNNC
-  , predictKNNR
-  , predictKNNC
-  , predictKNNCProbs
-  ) where
-
-import qualified Data.Vector.Unboxed   as VU
-import qualified Numeric.LinearAlgebra as LA
-import qualified Data.Map.Strict       as Map
-import           Data.List             (foldl', sortBy, nub, sort)
-import           Data.Ord              (comparing)
-import           Data.Text             (Text)
-
--- ---------------------------------------------------------------------------
--- Types
--- ---------------------------------------------------------------------------
-
-data KNNRegressor = KNNRegressor
-  { knnRK :: !Int
-  , knnRX :: !(LA.Matrix Double)
-  , knnRY :: !(VU.Vector Double)
-  } deriving (Show)
-
-data KNNClassifier = KNNClassifier
-  { knnCK          :: !Int
-  , knnCX          :: !(LA.Matrix Double)
-  , knnCY          :: !(VU.Vector Int)
-  , knnCClasses    :: ![Int]
-  , knnCClassNames :: ![Text]   -- ^ クラス名 (df|-> が levels 注入・空=数値表示)。
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- Fit
--- ---------------------------------------------------------------------------
-
-fitKNNR :: Int -> LA.Matrix Double -> VU.Vector Double -> KNNRegressor
-fitKNNR k x y = KNNRegressor k x y
-
-fitKNNC :: Int -> LA.Matrix Double -> VU.Vector Int -> KNNClassifier
-fitKNNC k x y = KNNClassifier
-  { knnCK          = k
-  , knnCX          = x
-  , knnCY          = y
-  , knnCClasses    = sort (nub (VU.toList y))
-  , knnCClassNames = []          -- df|-> 経路が reqLabelWithLevels で後から注入。
-  }
-
--- ---------------------------------------------------------------------------
--- Predict helpers
--- ---------------------------------------------------------------------------
-
-rowVec :: LA.Matrix Double -> Int -> LA.Vector Double
-rowVec x i = LA.flatten (x LA.? [i])
-
--- | クエリ点に対し、 訓練データ各行までの距離 (二乗) と元 index のペア
--- を返す。
-distancesSq :: LA.Matrix Double -> LA.Vector Double -> [(Int, Double)]
-distancesSq xTrain q =
-  let !n = LA.rows xTrain
-  in [ (i, let v = rowVec xTrain i - q in LA.dot v v)
-     | i <- [0 .. n - 1] ]
-
-kNearest :: Int -> LA.Matrix Double -> LA.Vector Double -> [Int]
-kNearest k xTrain q =
-  let ds = sortBy (comparing snd) (distancesSq xTrain q)
-  in map fst (take k ds)
-
--- ---------------------------------------------------------------------------
--- Predict (regression)
--- ---------------------------------------------------------------------------
-
-predictKNNR :: KNNRegressor -> LA.Matrix Double -> VU.Vector Double
-predictKNNR knn xTest =
-  let !nT = LA.rows xTest
-      !k  = knnRK knn
-      !xT = knnRX knn
-      !yT = knnRY knn
-      pred1 i =
-        let q   = rowVec xTest i
-            ids = kNearest k xT q
-            ys  = [ yT VU.! j | j <- ids ]
-        in sum ys / fromIntegral (length ys)
-  in VU.generate nT pred1
-
--- ---------------------------------------------------------------------------
--- Predict (classification)
--- ---------------------------------------------------------------------------
-
-predictKNNCProbs :: KNNClassifier
-                 -> LA.Matrix Double
-                 -> [Map.Map Int Double]
-predictKNNCProbs knn xTest =
-  let !nT = LA.rows xTest
-      !k  = knnCK knn
-      !xT = knnCX knn
-      !yT = knnCY knn
-      counts1 i =
-        let q   = rowVec xTest i
-            ids = kNearest k xT q
-            cs  = [ yT VU.! j | j <- ids ]
-            !nk = fromIntegral (length cs) :: Double
-            mp  = foldl' (\m c -> Map.insertWith (+) c 1 m)
-                          Map.empty cs
-        in Map.map (/ nk) mp
-  in [ counts1 i | i <- [0 .. nT - 1] ]
-
-predictKNNC :: KNNClassifier -> LA.Matrix Double -> VU.Vector Int
-predictKNNC knn xTest =
-  let probs = predictKNNCProbs knn xTest
-      majority m =
-        case sortBy (flip (comparing snd)) (Map.toList m) of
-          ((c, _) : _) -> c
-          []           -> 0
-  in VU.fromList (map majority probs)
diff --git a/src/Hanalyze/Model/Kernel.hs b/src/Hanalyze/Model/Kernel.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Kernel.hs
+++ /dev/null
@@ -1,279 +0,0 @@
-{-# LANGUAGE StrictData #-}
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.Kernel
--- Description : GP/SVM/カーネル法で共通のカーネル語彙 (RBF/Matern52/Periodic/Linear/Poly)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- 共有カーネル語彙 (GP / SVM / カーネル法で共通) — Phase 75.18 で 'Model.GP'
--- から分離。
---
--- GP 族の定常/内積カーネル ('RBF' / 'Matern52' / 'Periodic' / 'Linear' / 'Poly') と
--- そのハイパーパラメータ 'KernelParams' (ℓ / σ_f² / period / ARD per-dim ℓ) を集約する。
--- 'GPParams' (= 'KernelParams' + 観測ノイズ σ_n²) に依存しないので、 SVM 等
--- ノイズを持たないカーネル法はこのモジュールだけを import すればよい
--- ('Model.GP' を import しない)。
---
--- 評価関数:
---
---   * 'kernelFn'            — 1D 入力の @k(x, x')@。
---   * 'buildKernelMatrix'   — 1D の Gram 行列 @K(xs, xs')@。
---   * 'applyKernel'         — 二乗距離行列 → カーネル行列 (距離カーネル専用)。
---   * 'kernelOfParams'      — 固定パラメータの @s ↦ k(s)@ (距離カーネル専用・INLINE)。
---   * 'ardScaleXY'          — ARD 列スケーリング。
---   * 'buildKernelMatrixMV' — 多入力 Gram 行列 (全カーネル)。
---   * 'kEvalMV'             — 多入力の点対点評価 @k(a, b)@ (全カーネル・SVM 等の汎用経路)。
---
--- 距離カーネル (RBF/Matern52/Periodic) は二乗距離から、 内積カーネル
--- (Linear/Poly) は内積から評価する。 'applyKernel' / 'kernelOfParams' は距離専用で、
--- 内積カーネルを渡すと error (multi-input gram は 'buildKernelMatrixMV' が内積経路へ
--- 分岐するためそこには到達しない)。
-module Hanalyze.Model.Kernel
-  ( -- * カーネル型
-    Kernel (..)
-  , kernelName
-    -- * カーネルハイパーパラメータ
-  , KernelParams (..)
-  , defaultKernelParams
-    -- * 評価
-  , kernelFn
-  , buildKernelMatrix
-  , applyKernel
-  , kernelOfParams
-  , ardScaleXY
-  , buildKernelMatrixMV
-  , kEvalMV
-  ) where
-
-import           Data.Text (Text)
-import qualified Data.Text                    as T
-import qualified Numeric.LinearAlgebra        as LA
-import qualified Hanalyze.Stat.KernelDist as KD
-import qualified Data.Vector.Storable         as VS
-import qualified Data.Vector.Storable.Mutable as VSM
-import           Control.Monad.ST             (runST)
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
--- | GP / SVM 族のカーネル種別。
-data Kernel
-  = RBF
-    -- ^ Squared exponential: @k(x,x') = σ_f² exp(−r²/(2ℓ²))@.
-    --   Best for smooth functions; the most commonly used kernel.
-  | Matern52
-    -- ^ Matérn 5/2: @k(x,x') = σ_f²(1+√5 r/ℓ+5r²/(3ℓ²)) exp(−√5 r/ℓ)@.
-    --   Slightly rougher than RBF; common in physical systems.
-  | Periodic
-    -- ^ Periodic: @k(x,x') = σ_f² exp(−2 sin²(π r/p)/ℓ²)@.
-    --   For periodic patterns; set 'kpPeriod' appropriately.
-  | Linear
-    -- ^ Linear (dot-product): @k(x,x') = σ_f² (x·x')@. A non-stationary
-    --   kernel; with SVM gives a linear decision boundary. (Phase 75.14)
-  | Poly !Int
-    -- ^ Polynomial of degree @d@: @k(x,x') = (γ (x·x') + 1)^d@ with
-    --   @γ = 1/(2ℓ²)@ (shared with the SVM γ convention). A
-    --   non-stationary kernel. (Phase 75.14)
-  deriving (Show, Eq)
-
--- | Display name of a kernel.
-kernelName :: Kernel -> Text
-kernelName RBF       = "RBF"
-kernelName Matern52  = "Mat\xe9rn 5/2"
-kernelName Periodic  = "Periodic"
-kernelName Linear    = "Linear"
-kernelName (Poly d)  = "Poly(" <> T.pack (show d) <> ")"
-
--- | カーネルハイパーパラメータ (観測ノイズ σ_n² は含まない)。
-data KernelParams = KernelParams
-  { kpLengthScale  :: Double
-    -- ^ Isotropic length scale @ℓ@; larger means smoother. Used unless
-    --   'kpLengthScales' is 'Just' (= ARD), in which case the per-dim
-    --   vector overrides this for multi-input kernel evaluation.
-  , kpSignalVar    :: Double
-    -- ^ Signal variance @σ_f²@; the variability of the function values.
-  , kpPeriod       :: Double
-    -- ^ Period @p@ (only used by the @Periodic@ kernel).
-  , kpLengthScales :: Maybe (LA.Vector Double)
-    -- ^ Per-dim length scales for ARD (Automatic Relevance
-    --   Determination). When 'Just' v, the multi-input kernel uses
-    --   @D_ARD[i,j] = Σ_d (X[i,d] − X'[j,d])² / ℓ_d²@ instead of the
-    --   isotropic distance / ℓ². Has no effect on the 1D 'kernelFn'
-    --   path. 'Nothing' = isotropic (default).
-  } deriving (Show)
-
--- | Default kernel hyperparameters: @ℓ = σ_f² = p = 1@, isotropic.
-defaultKernelParams :: KernelParams
-defaultKernelParams = KernelParams 1.0 1.0 1.0 Nothing
-
--- ---------------------------------------------------------------------------
--- 1D 評価
--- ---------------------------------------------------------------------------
-
--- | Evaluate the kernel function @k(x, x')@ for scalar inputs.
-kernelFn :: Kernel -> KernelParams -> Double -> Double -> Double
-kernelFn RBF p x x' =
-  let d = x - x'
-      l = kpLengthScale p
-  in kpSignalVar p * exp (-(d * d) / (2 * l * l))
-kernelFn Matern52 p x x' =
-  let d = abs (x - x')
-      l = kpLengthScale p
-      s = sqrt 5 * d / l
-  in kpSignalVar p * (1 + s + s * s / 3) * exp (-s)
-kernelFn Periodic p x x' =
-  let d = abs (x - x')
-      l = kpLengthScale p
-      s = sin (pi * d / kpPeriod p)
-  in kpSignalVar p * exp (-2 * s * s / (l * l))
-kernelFn Linear p x x' =
-  -- 内積カーネル: 1D では x·x' = x*x'。
-  kpSignalVar p * (x * x')
-kernelFn (Poly d) p x x' =
-  -- (γ x·x' + 1)^d, γ = 1/(2ℓ²)。1D では x·x' = x*x'。
-  let l = kpLengthScale p
-      g = 1 / (2 * l * l)
-  in (g * (x * x') + 1) ^^ d
-
--- | Build the kernel matrix @K(xs, xs')@ of shape @|xs| × |xs'|@.
---
--- Phase 11b (2026-05-14): fill a flat 'Storable.Vector' via @runST +
--- MVector@ instead of materialising the @|xs|·|xs'|@ lazy @[Double]@
--- list (one allocation per kernel call). 'kernelFn' itself is unchanged
--- so 'Periodic' (signed-difference dependent) keeps working.
-buildKernelMatrix :: Kernel -> KernelParams -> [Double] -> [Double] -> LA.Matrix Double
-buildKernelMatrix ker p xs xs' =
-  let xv = VS.fromList xs
-      yv = VS.fromList xs'
-      n  = VS.length xv
-      m  = VS.length yv
-      out = runST $ do
-        v <- VSM.unsafeNew (n * m)
-        let go !i !j
-              | i >= n    = pure ()
-              | j >= m    = go (i + 1) 0
-              | otherwise = do
-                  let xi = VS.unsafeIndex xv i
-                      yj = VS.unsafeIndex yv j
-                  VSM.unsafeWrite v (i * m + j) (kernelFn ker p xi yj)
-                  go i (j + 1)
-        go 0 0
-        VS.unsafeFreeze v
-  in LA.reshape m out
-
--- ---------------------------------------------------------------------------
--- 多入力 (multivariate) 評価
--- ---------------------------------------------------------------------------
-
--- | Apply the kernel function to an @m × n@ matrix of squared distances.
--- 距離カーネル (RBF/Matern52/Periodic) 専用。 内積カーネル (Linear/Poly) は
--- 二乗距離から復元できないため error (multi-input gram は 'buildKernelMatrixMV'
--- が内積経路へ分岐するためここには到達しない)。
-applyKernel :: Kernel -> KernelParams -> LA.Matrix Double -> LA.Matrix Double
-applyKernel RBF p d2 =
-  let l2 = kpLengthScale p ** 2
-      sf = kpSignalVar p
-  in KD.mapMatrix (\s -> sf * exp (- s / (2 * l2))) d2
-applyKernel Matern52 p d2 =
-  let l  = kpLengthScale p
-      sf = kpSignalVar p
-  in KD.mapMatrix (\s -> let r = sqrt (max 0 s)
-                             u = sqrt 5 * r / l
-                         in sf * (1 + u + u * u / 3) * exp (- u)) d2
-applyKernel Periodic p d2 =
-  let l  = kpLengthScale p
-      sf = kpSignalVar p
-      pr = kpPeriod p
-  in KD.mapMatrix (\s -> let r = sqrt (max 0 s)
-                             ss = sin (pi * r / pr)
-                         in sf * exp (- 2 * ss * ss / (l * l))) d2
-applyKernel Linear   _ _ = error "applyKernel: Linear は内積カーネル。buildKernelMatrixMV/kEvalMV を使うこと"
-applyKernel (Poly _) _ _ = error "applyKernel: Poly は内積カーネル。buildKernelMatrixMV/kEvalMV を使うこと"
-
--- | Apply ARD scaling to (X, X') if 'kpLengthScales' is 'Just'. Returns
--- the (possibly rescaled) matrices and a 'KernelParams' with @ℓ = 1@ so
--- that 'applyKernel' divides by 1 (the per-dim ℓ_d already absorbed into
--- the column scaling). 'Nothing' = isotropic, returns inputs and params
--- unchanged. The 'Periodic' kernel does not support ARD.
-ardScaleXY
-  :: Kernel -> KernelParams -> LA.Matrix Double -> LA.Matrix Double
-  -> (LA.Matrix Double, LA.Matrix Double, KernelParams)
-ardScaleXY Periodic p x y = (x, y, p)
-ardScaleXY _        p x y = case kpLengthScales p of
-  Nothing -> (x, y, p)
-  Just ls ->
-    let p_     = LA.cols x
-        lsExt  = if LA.size ls == p_
-                   then ls
-                   else LA.konst (kpLengthScale p) p_  -- safety fallback
-        invL   = LA.cmap (1 /) lsExt                 -- 1 / ℓ_d
-        scaleCols m = m LA.<> LA.diag invL
-        x'     = scaleCols x
-        y'     = scaleCols y
-        p'     = p { kpLengthScale = 1.0 }
-    in (x', y', p')
-
--- | Build the kernel matrix @K(X, X')@ of shape @|X| × |X'|@ from
--- multi-input matrices. @X@ is @n × p@; @X'@ is @m × p@.
---
--- When 'kpLengthScales' is 'Just', uses ARD: each input dimension is
--- scaled by @1 / ℓ_d@ before computing pairwise squared distances.
-buildKernelMatrixMV
-  :: Kernel -> KernelParams -> LA.Matrix Double -> LA.Matrix Double
-  -> LA.Matrix Double
-buildKernelMatrixMV Linear p x x' =
-  -- 内積カーネル: K = σ_f² X X'ᵀ (距離経路を通さない)。
-  LA.scale (kpSignalVar p) (x LA.<> LA.tr x')
-buildKernelMatrixMV (Poly d) p x x' =
-  -- (γ X X'ᵀ + 1)^d, γ = 1/(2ℓ²)。
-  let l = kpLengthScale p
-      g = 1 / (2 * l * l)
-  in LA.cmap (\ip -> (g * ip + 1) ^^ d) (x LA.<> LA.tr x')
-buildKernelMatrixMV ker p x x' =
-  let (xs, ys, p') = ardScaleXY ker p x x'
-  in applyKernel ker p' (KD.pairwiseSqDistXY xs ys)
-
--- | 多入力カーネル評価 @k(a, b)@ (全カーネル対応・SVM 等の汎用経路)。
--- 距離カーネル (RBF/Matern52/Periodic) は二乗距離、 内積カーネル (Linear/Poly)
--- は内積から評価する。 (Phase 75.14)
-kEvalMV :: Kernel -> KernelParams -> LA.Vector Double -> LA.Vector Double -> Double
-kEvalMV Linear   p a b = kpSignalVar p * (a LA.<.> b)
-kEvalMV (Poly d) p a b =
-  let l = kpLengthScale p
-      g = 1 / (2 * l * l)
-  in (g * (a LA.<.> b) + 1) ^^ d
-kEvalMV ker      p a b =
-  let d = a - b
-  in kernelOfParams ker p (d LA.<.> d)   -- 距離カーネル: s = ‖a−b‖²
-
--- | Specialized kernel function for a fixed parameter set, returning a
--- monomorphic @Double -> Double@ that GHC can inline tightly into the
--- @mkNoiseKernelFromD2@ inner loop (in 'Model.GP'). 距離カーネル専用。
-{-# INLINE kernelOfParams #-}
-kernelOfParams :: Kernel -> KernelParams -> (Double -> Double)
-kernelOfParams RBF p =
-  let !l2 = kpLengthScale p ** 2
-      !sf = kpSignalVar p
-      !inv2L2 = 1 / (2 * l2)
-  in \s -> sf * exp (- s * inv2L2)
-kernelOfParams Matern52 p =
-  let !l  = kpLengthScale p
-      !sf = kpSignalVar p
-      !invL = sqrt 5 / l
-  in \s -> let r = sqrt (max 0 s)
-               u = invL * r
-           in sf * (1 + u + u * u / 3) * exp (- u)
-kernelOfParams Periodic p =
-  let !l  = kpLengthScale p
-      !sf = kpSignalVar p
-      !pr = kpPeriod p
-      !invL2 = 1 / (l * l)
-      !invPr = pi / pr
-  in \s -> let r  = sqrt (max 0 s)
-               ss = sin (invPr * r)
-           in sf * exp (- 2 * ss * ss * invL2)
-kernelOfParams Linear   _ = error "kernelOfParams: Linear は内積カーネル。kEvalMV を使うこと"
-kernelOfParams (Poly _) _ = error "kernelOfParams: Poly は内積カーネル。kEvalMV を使うこと"
diff --git a/src/Hanalyze/Model/KernelRegression.hs b/src/Hanalyze/Model/KernelRegression.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/KernelRegression.hs
+++ /dev/null
@@ -1,485 +0,0 @@
-{-# LANGUAGE StrictData #-}
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.KernelRegression
--- Description : カーネル回帰 (Nadaraya-Watson / kernel ridge regression)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Kernel regression — Nadaraya-Watson and kernel ridge regression.
---
---   * 'Kernel'        — RBF / Matérn / triangular / Epanechnikov kernel
---     functions.
---   * 'nwRegression'  — Nadaraya-Watson (kernel-weighted moving average).
---   * 'kernelRidge'   — kernel ridge regression
---     @ŷ(x*) = k(x*)ᵀ (K + λI)⁻¹ y@.
---
--- Both are non-parametric smooth nonlinear regressors. Unlike 'Hanalyze.Model.GP',
--- they do not produce uncertainty estimates.
---
--- NB: この 'Kernel' は回帰スムージング用 (Gaussian/Epanechnikov/…)。
--- GP/SVM 族の共有カーネル (RBF/Matérn5/2/Periodic/Linear/Poly) は別モジュール
--- 'Hanalyze.Model.Kernel' (Phase 75.18 で分離)。
-module Hanalyze.Model.KernelRegression
-  ( Kernel (..)
-  , kernelEval
-  , kernelFromSqDist
-  , nwRegression
-  , nwRegressionMulti
-  , KernelRidgeFit (..)
-  , kernelRidge
-  , predictKernelRidge
-  , gridSearchBandwidth
-  , autoBandwidthBrent
-    -- * Multi-output (1D input, multiple Y columns)
-  , KernelRidgeFitMulti (..)
-  , kernelRidgeMulti
-  , predictKernelRidgeMulti
-  , fittedKernelRidgeMulti
-  , r2Multi
-  , autoTuneKernelRidgeMulti
-  , defaultHGrid
-  , defaultLamGrid
-    -- * Multi-input (primary API; X is @n × p@, Y is @n × q@)
-  , gramMatrixMV
-  , gramMatrixMVXY
-  , KernelRidgeFitMV (..)
-  , kernelRidgeMV
-  , predictKernelRidgeMV
-  , fittedKernelRidgeMV
-  , nwRegressionMV
-  ) where
-
-import qualified Data.Vector as V
-import qualified Numeric.LinearAlgebra as LA
-import qualified Hanalyze.Optim.LineSearch as LS
-import qualified Hanalyze.Optim.Common     as OC
-import qualified Hanalyze.Stat.KernelDist  as KD
-import qualified Hanalyze.Stat.Cholesky    as Chol
-
--- ---------------------------------------------------------------------------
--- カーネル関数
--- ---------------------------------------------------------------------------
-
--- | Supported kernels. The bandwidth @h@ is passed separately at the
--- call site.
-data Kernel
-  = Gaussian       -- ^ @exp(-u²/2)@ (= RBF, infinite support).
-  | Epanechnikov   -- ^ @0.75 (1-u²)@ on @|u| ≤ 1@.
-  | Triangular     -- ^ @1 - |u|@ on @|u| ≤ 1@.
-  | Uniform        -- ^ @0.5@ on @|u| ≤ 1@ (coarsest).
-  | TriCube        -- ^ @(1-|u|³)³@ on @|u| ≤ 1@.
-  deriving (Show, Eq)
-
--- | Evaluate the kernel at scaled squared distance @s = ‖x − x'‖² / h²@.
--- Generalizes 'kernelEval' to multivariate inputs: every supported
--- kernel is radially symmetric, so the kernel value depends only on
--- @‖x − x'‖ / h@.
---
--- For the Gaussian kernel this avoids the redundant @sqrt@; for kernels
--- with bounded support (Epanechnikov / Triangular / Uniform / TriCube)
--- the boundary check uses @s ≤ 1@.
-kernelFromSqDist :: Kernel -> Double -> Double
-kernelFromSqDist k s = case k of
-  Gaussian     -> exp (-0.5 * s) / sqrt (2 * pi)
-  Epanechnikov -> if s <= 1 then 0.75 * (1 - s) else 0
-  Triangular   -> if s <= 1 then 1 - sqrt s else 0
-  Uniform      -> if s <= 1 then 0.5 else 0
-  TriCube      -> if s <= 1
-                    then let u = sqrt s
-                             t = 1 - u * u * u
-                         in t * t * t
-                    else 0
-
--- | Evaluate the kernel at @u = (x - x_i) / h@.
-kernelEval :: Kernel -> Double -> Double
-kernelEval k u = case k of
-  Gaussian     -> exp (-0.5 * u * u) / sqrt (2 * pi)
-  Epanechnikov -> if abs u <= 1 then 0.75 * (1 - u * u) else 0
-  Triangular   -> if abs u <= 1 then 1 - abs u else 0
-  Uniform      -> if abs u <= 1 then 0.5 else 0
-  TriCube      -> if abs u <= 1
-                    then let t = 1 - (abs u)^(3::Int)
-                         in t * t * t
-                    else 0
-
--- ---------------------------------------------------------------------------
--- Nadaraya-Watson
--- ---------------------------------------------------------------------------
-
--- | Single-output Nadaraya-Watson kernel regression.
---
--- @ŷ(x*) = Σᵢ K_h(x* - xᵢ) yᵢ / Σᵢ K_h(x* - xᵢ)@
---
--- Delegates to 'nwRegressionMulti' by promoting @y@ to a one-column
--- matrix.
-nwRegression :: Kernel
-             -> Double             -- ^ Bandwidth @h@ (@> 0@).
-             -> V.Vector Double    -- ^ Training inputs.
-             -> V.Vector Double    -- ^ Training targets.
-             -> V.Vector Double    -- ^ Prediction inputs.
-             -> V.Vector Double    -- ^ Predictions.
-nwRegression kern h xs ys xNew =
-  let yMat = LA.asColumn (LA.fromList (V.toList ys))
-      mat  = nwRegressionMulti kern h xs yMat xNew
-  in V.fromList (LA.toList (LA.flatten (mat LA.¿ [0])))
-
--- | Multi-output Nadaraya-Watson: reuse the same weight matrix across
--- every output column. With @W@ of shape @m × n@ and @Y@ of shape
--- @n × q@, the result is the row-normalized product @W · Y@ of shape
--- @m × q@.
-nwRegressionMulti :: Kernel
-                  -> Double               -- ^ Bandwidth @h@.
-                  -> V.Vector Double      -- ^ Training inputs (length @n@).
-                  -> LA.Matrix Double     -- ^ Training response @Y@ (@n × q@).
-                  -> V.Vector Double      -- ^ Prediction inputs (length @m@).
-                  -> LA.Matrix Double     -- ^ Predictions (@m × q@).
-nwRegressionMulti kern h xs ys xNew =
-  let n  = V.length xs
-      m  = V.length xNew
-      q  = LA.cols ys
-      wMat = LA.fromLists
-               [ [ kernelEval kern ((xStar - xi) / h)
-                 | xi <- V.toList xs ]
-               | xStar <- V.toList xNew ]   -- (m × n)
-      num  = wMat LA.<> ys                  -- (m × q)
-      dens = LA.toList (wMat LA.#> LA.konst 1 n)
-      rows = [ if d == 0 then replicate q 0
-                 else [ (num `LA.atIndex` (i, j)) / d | j <- [0 .. q - 1] ]
-             | (i, d) <- zip [0 .. m - 1] dens ]
-  in LA.fromLists rows
-
--- ---------------------------------------------------------------------------
--- Kernel Ridge regression
--- ---------------------------------------------------------------------------
-
--- | Kernel ridge regression fit; carries everything needed to predict.
-data KernelRidgeFit = KernelRidgeFit
-  { krKernel :: Kernel
-  , krH      :: Double
-  , krLambda :: Double
-  , krXs     :: V.Vector Double   -- ^ Training inputs.
-  , krAlpha  :: LA.Vector Double  -- ^ Solution @α = (K + λI)⁻¹ y@.
-  } deriving (Show)
-
--- | Build the Gram matrix @K_{ij} = K_h(x_i - x_j)@.
-gramMatrix :: Kernel -> Double -> V.Vector Double -> LA.Matrix Double
-gramMatrix kern h xs =
-  let n = V.length xs
-      xv = V.toList xs
-  in (n LA.>< n)
-       [ kernelEval kern ((xi - xj) / h)
-       | xi <- xv, xj <- xv ]
-
--- | Single-output kernel ridge regression. Delegates to
--- 'kernelRidgeMulti' by promoting @y@ to a one-column matrix and taking
--- column 0 of the resulting @α@ matrix.
-kernelRidge :: Kernel
-            -> Double             -- ^ Bandwidth @h@.
-            -> Double             -- ^ Ridge penalty @λ@.
-            -> V.Vector Double    -- ^ Training inputs.
-            -> V.Vector Double    -- ^ Training targets.
-            -> KernelRidgeFit
-kernelRidge kern h lam xs ys =
-  let yMat = LA.asColumn (LA.fromList (V.toList ys))
-      mf   = kernelRidgeMulti kern h lam xs yMat
-      a    = LA.flatten (krmAlpha mf LA.¿ [0])
-  in KernelRidgeFit kern h lam xs a
-
--- | Predict at new inputs from a 'KernelRidgeFit'.
-predictKernelRidge :: KernelRidgeFit -> V.Vector Double -> V.Vector Double
-predictKernelRidge fit xNew =
-  V.map predict xNew
-  where
-    xs    = krXs fit
-    h     = krH fit
-    kern  = krKernel fit
-    alpha = krAlpha fit
-    predict xStar =
-      let kVec = LA.fromList
-                   [ kernelEval kern ((xStar - xi) / h)
-                   | xi <- V.toList xs ]
-      in kVec LA.<.> alpha
-
--- ---------------------------------------------------------------------------
--- Bandwidth selection
--- ---------------------------------------------------------------------------
-
--- | Pick the bandwidth @h@ by leave-one-out cross-validation. Simple
--- grid search: returns the candidate with the smallest LOO RMSE.
-gridSearchBandwidth
-  :: Kernel
-  -> V.Vector Double      -- ^ Training inputs.
-  -> V.Vector Double      -- ^ Training targets.
-  -> [Double]             -- ^ Candidate bandwidths.
-  -> (Double, Double)     -- ^ @(best h, best LOO RMSE)@.
-gridSearchBandwidth kern xs ys hs =
-  let results = [(h, looErrNW kern xs ys h) | h <- hs]
-      best = head [ pair | pair <- results
-                         , snd pair == minimum (map snd results) ]
-  in best
-
--- | NW LOO-CV loss as a continuous function of @h@; shared with
--- 'autoBandwidthBrent'.
-looErrNW :: Kernel -> V.Vector Double -> V.Vector Double -> Double -> Double
-looErrNW kern xs ys h =
-  let n = V.length xs
-      yPred = V.imap
-        (\i _ ->
-          let xs'  = V.ifilter (\j _ -> j /= i) xs
-              ys'  = V.ifilter (\j _ -> j /= i) ys
-              xi   = xs V.! i
-              pred = nwRegression kern h xs' ys' (V.singleton xi)
-          in V.head pred)
-        xs
-      err = V.zipWith (\y yh -> (y - yh)^(2::Int)) ys yPred
-  in sqrt (V.sum err / fromIntegral n)
-
--- | Continuously optimize the bandwidth @h@ with Brent's method
--- (minimizing the LOO-CV loss). Assumes the bracket @[h_lo, h_hi]@ is
--- unimodal. Avoids enumerating discrete candidates the way
--- 'gridSearchBandwidth' does.
---
--- Returns @(best h, best LOO RMSE)@.
-autoBandwidthBrent
-  :: Kernel
-  -> V.Vector Double    -- ^ Training inputs.
-  -> V.Vector Double    -- ^ Training targets.
-  -> Double             -- ^ Lower bound @h_lo@.
-  -> Double             -- ^ Upper bound @h_hi@.
-  -> (Double, Double)
-autoBandwidthBrent kern xs ys hLo hHi =
-  let cfg = LS.defaultBrentConfig { LS.bcMaxIter = 80, LS.bcTol = 1e-6 }
-      result = LS.brent cfg (\[h] -> looErrNW kern xs ys h) hLo hHi
-      hStar  = head (OC.orBest result)
-  in (hStar, OC.orValue result)
-
--- ---------------------------------------------------------------------------
--- 多出力 Kernel Ridge (Phase T2)
--- ---------------------------------------------------------------------------
-
--- | Multi-output kernel ridge regression. With @Y@ of shape @n × q@,
--- solves each column independently but shares the Gram matrix @K@.
-data KernelRidgeFitMulti = KernelRidgeFitMulti
-  { krmKernel :: Kernel
-  , krmH      :: Double
-  , krmLambda :: Double
-  , krmXs     :: V.Vector Double
-  , krmAlpha  :: LA.Matrix Double   -- α (n × q)
-  } deriving (Show)
-
--- | Solve @(K + λI)⁻¹ Y@ once and reuse for every column (fast).
-kernelRidgeMulti :: Kernel -> Double -> Double
-                 -> V.Vector Double -> LA.Matrix Double
-                 -> KernelRidgeFitMulti
-kernelRidgeMulti kern h lam xs ys =
-  let n     = V.length xs
-      kMat  = gramMatrix kern h xs
-      regK  = kMat + LA.scale lam (LA.ident n)
-      -- regK is SPD (K is PSD, λI is PD). Use Cholesky-based solve;
-      -- jitter retry handles ill-conditioned bandwidths.
-      alpha = Chol.cholSolveJitter regK ys
-  in KernelRidgeFitMulti kern h lam xs alpha
-
--- | Predict @Ŷ@ for new inputs from a 'KernelRidgeFitMulti'.
-predictKernelRidgeMulti :: KernelRidgeFitMulti -> V.Vector Double
-                        -> LA.Matrix Double
-predictKernelRidgeMulti fit xNew =
-  let xs    = krmXs fit
-      h     = krmH fit
-      kern  = krmKernel fit
-      alpha = krmAlpha fit
-      kMat  = LA.fromLists
-                [ [ kernelEval kern ((xStar - xi) / h)
-                  | xi <- V.toList xs ]
-                | xStar <- V.toList xNew ]
-  in kMat LA.<> alpha
-
--- | Fitted values at the training inputs (= @ŷ_train@).
-fittedKernelRidgeMulti :: KernelRidgeFitMulti -> LA.Matrix Double
-fittedKernelRidgeMulti fit = predictKernelRidgeMulti fit (krmXs fit)
-
--- | Multi-output R² returned as a length-@q@ vector. @Y@ observed and
--- @Ŷ@ predicted both have shape @n × q@.
-r2Multi :: LA.Matrix Double -> LA.Matrix Double -> V.Vector Double
-r2Multi ys yhat =
-  let n  = LA.rows ys
-      q  = LA.cols ys
-      colR2 j =
-        let yc  = LA.toList (LA.flatten (ys     LA.¿ [j]))
-            yhc = LA.toList (LA.flatten (yhat   LA.¿ [j]))
-            mu  = sum yc / fromIntegral n
-            sst = sum [(y - mu)^(2::Int) | y <- yc]
-            sse = sum [(y - p)^(2::Int) | (y, p) <- zip yc yhc]
-        in if sst == 0 then 0 else 1 - sse / sst
-  in V.fromList [ colR2 j | j <- [0 .. q - 1] ]
-
--- | Joint @(h, λ)@ grid search using the closed-form LOOCV. Computes the
--- hat-matrix diagonal once per
--- 全 q 出力の LOO 残差を一括評価。
---
--- 戻り値: (best fit, best h, best λ, best mean LOO MSE)
-autoTuneKernelRidgeMulti
-  :: Kernel
-  -> V.Vector Double      -- xs (n)
-  -> LA.Matrix Double     -- ys (n × q)
-  -> [Double]             -- h candidates
-  -> [Double]             -- λ candidates
-  -> (KernelRidgeFitMulti, Double, Double, Double)
-autoTuneKernelRidgeMulti kern xs ys hs lams =
-  let n   = V.length xs
-      q   = LA.cols ys
-      tot = fromIntegral (n * q) :: Double
-      score h lam =
-        let kMat = gramMatrix kern h xs
-            regK = kMat + LA.scale lam (LA.ident n)
-            ainv = LA.inv regK
-            hat  = kMat LA.<> ainv          -- (n × n)
-            diagH = LA.takeDiag hat
-            yhat = hat LA.<> ys             -- (n × q)
-            res  = ys - yhat                -- (n × q)
-            -- LOO 残差: r_i / (1 - H_ii)、列方向ブロードキャスト
-            denom = LA.cmap (\h_ii -> 1 - h_ii) diagH
-            invDenom = LA.cmap (\d -> if abs d < 1e-10 then 0 else 1/d) denom
-            scaler = LA.fromColumns (replicate q invDenom)
-            looR  = res * scaler
-            sse   = LA.sumElements (looR * looR)
-        in sse / tot
-      grid = [ (h, lam, score h lam) | h <- hs, lam <- lams ]
-      best@(bestH, bestL, bestS) = head [ p | p@(_,_,s) <- grid
-                                             , s == minimum (map (\(_,_,x) -> x) grid) ]
-      _ = best
-      fit  = kernelRidgeMulti kern bestH bestL xs ys
-  in (fit, bestH, bestL, bestS)
-
--- | Log-spaced bandwidth candidates. @defaultHGrid xs@ produces 30
--- candidates spanning the range of @xs@.
-defaultHGrid :: V.Vector Double -> [Double]
-defaultHGrid xs =
-  let xv  = V.toList xs
-      mn  = minimum xv
-      mx  = maximum xv
-      rng = mx - mn
-      lo  = max 1e-3 (rng / 100)
-      hi  = max (lo * 10) rng
-      n   = 30
-      lLo = log lo
-      lHi = log hi
-      step = (lHi - lLo) / fromIntegral (n - 1)
-  in [ exp (lLo + fromIntegral i * step) | i <- [0 .. n - 1 :: Int] ]
-
--- | Log-spaced ridge-penalty candidates (10 values from 1e-6 to 1).
-defaultLamGrid :: [Double]
-defaultLamGrid =
-  let n = 10
-      lLo = log 1e-6
-      lHi = log 1e0
-      step = (lHi - lLo) / fromIntegral (n - 1)
-  in [ exp (lLo + fromIntegral i * step) | i <- [0 .. n - 1 :: Int] ]
-
--- ---------------------------------------------------------------------------
--- Multi-input (multivariate X) API
---
--- These functions take @X@ as an @n × p@ matrix (rows = samples) and use a
--- single shared bandwidth @h@ across every input dimension. Distance
--- matrices are computed via 'Hanalyze.Stat.KernelDist' (BLAS GEMM) and the kernel
--- function is applied element-wise via 'LA.cmap'; no list traversals over
--- the @O(n²)@ pair set.
---
--- For axis-specific bandwidths, scale columns of @X@ by @1 / h_d@ before
--- calling these functions.
--- ---------------------------------------------------------------------------
-
--- | Multi-input Gram matrix @K[i, j] = κ(‖X[i,:] − X[j,:]‖ / h)@.
-gramMatrixMV :: Kernel -> Double -> LA.Matrix Double -> LA.Matrix Double
-gramMatrixMV kern h x =
-  let h2 = h * h
-      d2 = KD.pairwiseSqDist x
-  in LA.cmap (\s -> kernelFromSqDist kern (s / h2)) d2
-
--- | Multi-input cross Gram matrix @K[i, j] = κ(‖X[i,:] − Y[j,:]‖ / h)@.
-gramMatrixMVXY
-  :: Kernel -> Double
-  -> LA.Matrix Double   -- ^ Query @X_*@ (@m × p@).
-  -> LA.Matrix Double   -- ^ Training @X@ (@n × p@).
-  -> LA.Matrix Double   -- ^ Result (@m × n@).
-gramMatrixMVXY kern h xs ts =
-  let h2 = h * h
-      d2 = KD.pairwiseSqDistXY xs ts
-  in LA.cmap (\s -> kernelFromSqDist kern (s / h2)) d2
-
--- | Multi-input kernel ridge fit. Holds the training matrix and the
--- solution coefficients; @α@ has shape @n × q@.
-data KernelRidgeFitMV = KernelRidgeFitMV
-  { krmvKernel :: Kernel
-  , krmvH      :: Double
-  , krmvLambda :: Double
-  , krmvXs     :: LA.Matrix Double  -- ^ Training inputs (@n × p@).
-  , krmvAlpha  :: LA.Matrix Double  -- ^ @(K + λI)⁻¹ Y@ (@n × q@).
-  } deriving (Show)
-
--- | Multi-input multi-output kernel ridge regression.
---
--- @α = (K + λI)⁻¹ Y@ with @K = gramMatrixMV kern h X@. Solving once and
--- reusing across the @q@ output columns.
-kernelRidgeMV
-  :: Kernel
-  -> Double                 -- ^ Bandwidth @h@.
-  -> Double                 -- ^ Ridge penalty @λ@.
-  -> LA.Matrix Double       -- ^ Training inputs @X@ (@n × p@).
-  -> LA.Matrix Double       -- ^ Training response @Y@ (@n × q@).
-  -> KernelRidgeFitMV
-kernelRidgeMV kern h lam x y =
-  let n     = LA.rows x
-      kMat  = gramMatrixMV kern h x
-      regK  = kMat + LA.scale lam (LA.ident n)
-      -- SPD: K + λI. Use Cholesky-based solve.
-      alpha = Chol.cholSolveJitter regK y
-  in KernelRidgeFitMV kern h lam x alpha
-
--- | Predict @Ŷ = K_* α@ for new query inputs (@m × p@). Output shape is
--- @m × q@.
-predictKernelRidgeMV :: KernelRidgeFitMV -> LA.Matrix Double -> LA.Matrix Double
-predictKernelRidgeMV fit xNew =
-  gramMatrixMVXY (krmvKernel fit) (krmvH fit) xNew (krmvXs fit)
-    LA.<> krmvAlpha fit
-
--- | Fitted values at the training inputs.
-fittedKernelRidgeMV :: KernelRidgeFitMV -> LA.Matrix Double
-fittedKernelRidgeMV fit = predictKernelRidgeMV fit (krmvXs fit)
-
--- | Multi-input multi-output Nadaraya-Watson regression.
---
--- @ŷ(x*) = (Σⱼ K_h(x* − xⱼ) yⱼ) / Σⱼ K_h(x* − xⱼ)@, computed for every
--- query row in one pass via @W = K(X_*, X)@ then @W Y / row-sums@.
-nwRegressionMV
-  :: Kernel
-  -> Double                 -- ^ Bandwidth @h@.
-  -> LA.Matrix Double       -- ^ Training inputs @X@ (@n × p@).
-  -> LA.Matrix Double       -- ^ Training response @Y@ (@n × q@).
-  -> LA.Matrix Double       -- ^ Query inputs @X_*@ (@m × p@).
-  -> LA.Matrix Double       -- ^ Predictions (@m × q@).
-nwRegressionMV kern h xs ys xNew =
-  -- P35a (2026-05-07): replace @LA.diag safe LA.<> num@ (m×m dense
-  -- diag matrix + GEMM) with broadcast outer product → elementwise.
-  --
-  -- P35b explored further: fusing the @num@ and @denom@ GEMVs into a
-  -- single GEMM via @yAug = [ys | onesN]@ to traverse the 8 MB
-  -- weight matrix only once (it exceeds typical L3). It /regressed/
-  -- at q=1 (33.8 → 37 ms) because (a) @LA.|||@ allocates a fresh
-  -- 8 MB matrix, and (b) BLAS GEMM with k=2 RHS columns has higher
-  -- block-tiling overhead than two GEMV calls. For q ≫ 1 the fusion
-  -- would win, but the bench is q=1 so the unfused form stays.
-  --
-  -- The remaining bottleneck is @LA.cmap kernelFromSqDist@ over the
-  -- 1M-cell weight matrix — a per-element Haskell function call per
-  -- exp(). FFI'd vectorized exp (libmvec / SLEEF) would close the
-  -- 3.6× gap to sklearn but is out of scope here.
-  let !wMat   = gramMatrixMVXY kern h xNew xs           -- m × n
-      !num    = wMat LA.<> ys                           -- m × q
-      !onesN  = LA.konst 1 (LA.cols wMat) :: LA.Vector Double
-      !denom  = wMat LA.#> onesN                        -- m
-      !safe   = LA.cmap (\d -> if d == 0 then 1 else 1 / d) denom
-      !onesQ  = LA.konst 1 (LA.cols num) :: LA.Vector Double
-      !safeBc = LA.outer safe onesQ                     -- m × q
-  in safeBc * num
diff --git a/src/Hanalyze/Model/LM.hs b/src/Hanalyze/Model/LM.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/LM.hs
+++ /dev/null
@@ -1,277 +0,0 @@
--- |
--- Module      : Hanalyze.Model.LM
--- Description : 最小二乗法による線形回帰の fit・予測・信頼/予測区間
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Ordinary linear regression by least squares.
---
--- Solves @β = (XᵀX)⁻¹ Xᵀ y@ via hmatrix's @\\\\@ (LAPACK). Provides
--- confidence and prediction bands using
--- @t × √(s² xᵢᵀ(XᵀX)⁻¹xᵢ)@ and convenient adapters from a
--- @DataFrame@ for use from the CLI and report builder.
-module Hanalyze.Model.LM
-  ( LinearModel (..)
-  , CIBand (..)
-  , SmoothFit (..)
-    -- * Matrix-canonical fit
-  , fitLM
-  , predictLM
-    -- * Vector wrapper (1-output convenience)
-  , fitLMVec
-  , predictLMVec
-    -- * Design matrices
-  , designMatrix
-  , polyDesignMatrix
-  , multiPolyDesignMatrix
-  , linspace
-    -- * DataFrame helpers
-  , fitDataFrameLM
-  , confidenceBand
-  , confidenceBandAt
-  , predictionBandAt
-  , fitWithCI
-  , fitPolyWithSmooth
-  ) where
-
-import qualified DataFrame.Internal.DataFrame as DXD
-import Hanalyze.DataIO.Convert (getDoubleVec)
-import Hanalyze.Model.Core (FitResult (..), Model (..), Band (..),
-                   coefficientsV, residualsV)
-
-import Data.Text (Text)
-import qualified Data.Vector as V
-import qualified Numeric.LinearAlgebra as LA
-import Statistics.Distribution (quantile)
-import Statistics.Distribution.StudentT (studentT)
-
-data LinearModel = LinearModel
-  deriving (Show)
-
-instance Model LinearModel where
-  fit     _ = fitLM
-  predict _ = predictLM
-
--- | Ordinary Least Squares (Matrix canonical, 多出力対応):
--- B = (XᵀX)⁻¹ Xᵀ Y、各列を独立に解く。
-fitLM :: LA.Matrix Double -> LA.Matrix Double -> FitResult
-fitLM x y =
-  let beta  = x LA.<\> y                   -- p × q
-      yHat  = x LA.<> beta                 -- n × q
-      resid = y - yHat
-      r2    = computeR2Multi y yHat
-  in FitResult beta yHat resid r2
-
-predictLM :: LA.Matrix Double -> LA.Matrix Double -> LA.Matrix Double
-predictLM beta xNew = xNew LA.<> beta
-
--- | 単一出力 (Vector y) の便利ラッパ。@asColumn@ で 1 列行列に変換。
-fitLMVec :: LA.Matrix Double -> LA.Vector Double -> FitResult
-fitLMVec x y = fitLM x (LA.asColumn y)
-
--- | 1 出力での予測 (β は Vector)。
-predictLMVec :: LA.Vector Double -> LA.Matrix Double -> LA.Vector Double
-predictLMVec beta xNew = xNew LA.#> beta
-
--- | Build intercept + single predictor design matrix  [1, x].
-designMatrix :: V.Vector Double -> LA.Matrix Double
-designMatrix xs = LA.fromColumns
-  [ LA.konst 1.0 n
-  , LA.fromList (V.toList xs)
-  ]
-  where n = V.length xs
-
--- | Convenience: fit a simple LM directly from a DataFrame.
-fitDataFrameLM :: DXD.DataFrame -> Text -> Text -> Maybe FitResult
-fitDataFrameLM df xCol yCol = do
-  xVec <- getDoubleVec xCol df
-  yVec <- getDoubleVec yCol df
-  let dm = designMatrix xVec
-      y  = LA.fromList (V.toList yVec)
-  return (fitLMVec dm y)
-
-data CIBand = CIBand
-  { lowerBound :: [Double]
-  , upperBound :: [Double]
-  , ciLevel    :: Double
-  } deriving (Show)
-
--- | Pointwise confidence band for the mean response (1 出力前提)。
--- Formula: ŷᵢ ± t_{α/2, n−p} × sqrt(s² × xᵢᵀ (XᵀX)⁻¹ xᵢ)
---
--- 訓練設計行列上で評価する版 (= 各点の中心は fitted)。 grid 評価が要るときは
--- 'confidenceBandAt' を使う。
-confidenceBand :: LA.Matrix Double -> FitResult -> Double -> CIBand
-confidenceBand x res level = confidenceBandAt x res level x
-
--- | 訓練設計行列 @xTrain@ で推定した分散核 (s², (XᵀX)⁻¹, t 値) を、 別の
--- 評価点設計行列 @xEval@ の各行で band 化する。 中心は @xEval·β@、 半幅は
--- @t × √(s² × x₀ᵀ (XᵀX)⁻¹ x₀)@。 自由度・s² は訓練データで決まる。
---
--- ★grid 評価の核: 訓練点ではなく等間隔 grid の設計行列を @xEval@ に渡すと、
--- 回帰曲線・CI 帯が滑らかになる (= 疎・不均一データのガタつき解消)。 訓練点を
--- そのまま渡せば 'confidenceBand' と一致する (LM では @xTrain·β = fitted@)。
-confidenceBandAt
-  :: LA.Matrix Double  -- ^ 訓練設計行列 X (分散核の推定元)
-  -> FitResult         -- ^ fit 結果 (β / 残差)
-  -> Double            -- ^ 信頼水準 (例 0.95)
-  -> LA.Matrix Double  -- ^ 評価点設計行列 X₀ (band を評価する行)
-  -> CIBand
-confidenceBandAt xTrain res level xEval =
-  let df    = fromIntegral (LA.rows xTrain - LA.cols xTrain)
-      beta  = coefficientsV res
-      rs    = LA.toRows xEval
-      yHats = [ xi `LA.dot` beta | xi <- rs ]
-  -- df<=0 (飽和・過剰指定) は s²=0/0・studentT が例外 → CI 定義不能。
-  -- 幅ゼロ帯 (lo=hi=ŷ) を返し、 帯は線に潰す (呼び元は線のみ描く)。
-  in if df <= 0
-       then CIBand yHats yHats level
-       else
-         let resV  = residualsV res
-             s2    = (resV `LA.dot` resV) / df
-             xtxi  = LA.inv (LA.tr xTrain LA.<> xTrain)
-             tVal  = quantile (studentT df) ((1.0 + level) / 2.0)
-             se xi = tVal * sqrt (s2 * (xi `LA.dot` (xtxi LA.#> xi)))
-             los   = zipWith (\yh xi -> yh - se xi) yHats rs
-             his   = zipWith (\yh xi -> yh + se xi) yHats rs
-         in CIBand los his level
-
--- | 予測区間 (prediction interval) 版の 'confidenceBandAt'。 半幅に**観測分散**
--- @σ̂²@ を 1 つ加える: @t × √(s² × (1 + x₀ᵀ (XᵀX)⁻¹ x₀))@ (CI には @1 +@ が無い)。
--- = 新規観測 1 点が入る区間 (平均の信頼区間より広い)。 statsmodels の
--- @get_prediction().summary_frame()['obs_ci_lower/upper']@ と一致する。
--- (hanalyze-portable)
-predictionBandAt
-  :: LA.Matrix Double  -- ^ 訓練設計行列 X (分散核の推定元)
-  -> FitResult         -- ^ fit 結果 (β / 残差)
-  -> Double            -- ^ 信頼水準 (例 0.95)
-  -> LA.Matrix Double  -- ^ 評価点設計行列 X₀ (band を評価する行)
-  -> CIBand
-predictionBandAt xTrain res level xEval =
-  let df    = fromIntegral (LA.rows xTrain - LA.cols xTrain)
-      beta  = coefficientsV res
-      rs    = LA.toRows xEval
-      yHats = [ xi `LA.dot` beta | xi <- rs ]
-  -- df<=0 は CI/PI 定義不能 → 幅ゼロ帯 (線のみ)。 'confidenceBandAt' と同方針。
-  in if df <= 0
-       then CIBand yHats yHats level
-       else
-         let resV  = residualsV res
-             s2    = (resV `LA.dot` resV) / df
-             xtxi  = LA.inv (LA.tr xTrain LA.<> xTrain)
-             tVal  = quantile (studentT df) ((1.0 + level) / 2.0)
-             se xi = tVal * sqrt (s2 * (1 + xi `LA.dot` (xtxi LA.#> xi)))   -- ★CI との差は (1 +)
-             los   = zipWith (\yh xi -> yh - se xi) yHats rs
-             his   = zipWith (\yh xi -> yh + se xi) yHats rs
-         in CIBand los his level
-
--- | Fit LM and compute confidence band in one step.
-fitWithCI :: Double -> DXD.DataFrame -> Text -> Text -> Maybe (FitResult, CIBand)
-fitWithCI level df xCol yCol = do
-  xVec <- getDoubleVec xCol df
-  yVec <- getDoubleVec yCol df
-  let dm  = designMatrix xVec
-      y   = LA.fromList (V.toList yVec)
-      res = fitLMVec dm y
-  return (res, confidenceBand dm res level)
-
--- | Polynomial design matrix [1, x, x², …, xᵈ].
-polyDesignMatrix :: Int -> V.Vector Double -> LA.Matrix Double
-polyDesignMatrix degree xs = LA.fromColumns
-  [ LA.fromList [ x ^ k | x <- V.toList xs ]
-  | k <- [0 .. degree]
-  ]
-
--- | Multi-column polynomial design matrix.
--- Builds [1, x1, x1², …, x1^d1, x2, …, x2^d2, …] from a list of (column, degree) pairs.
-multiPolyDesignMatrix :: [(V.Vector Double, Int)] -> LA.Matrix Double
-multiPolyDesignMatrix [] = error "multiPolyDesignMatrix: empty predictor list"
-multiPolyDesignMatrix colDegs@((firstXs, _) : _) =
-  LA.fromColumns (intercept : concatMap polyExpand colDegs)
-  where
-    n           = V.length firstXs
-    intercept   = LA.konst 1.0 n
-    polyExpand (xs, deg) =
-      [ LA.fromList [ x ^ k | x <- V.toList xs ] | k <- [1 .. deg] ]
-
--- | Grid of evenly spaced values from lo to hi.
-linspace :: Double -> Double -> Int -> [Double]
-linspace lo hi n
-  | n <= 1    = [lo]
-  | otherwise = [ lo + fromIntegral i * (hi - lo) / fromIntegral (n - 1)
-                | i <- [0 .. n - 1] ]
-
--- | Pre-computed smooth curve data for plotting (evaluated on a fine grid).
-data SmoothFit = SmoothFit
-  { sfX       :: [Double]
-  , sfFit     :: [Double]
-  , sfLower   :: [Double]
-  , sfUpper   :: [Double]
-  , sfHasBand :: Bool
-  } deriving (Show)
-
--- | Fit polynomial LM of given degree and compute a smooth curve with optional band
--- on a fine grid of nGrid points for clean visualisation.
-fitPolyWithSmooth
-  :: Band
-  -> Int
-  -> DXD.DataFrame
-  -> Text
-  -> Text
-  -> Maybe (FitResult, SmoothFit)
-fitPolyWithSmooth band nGrid df xCol yCol = do
-  xVec <- getDoubleVec xCol df
-  yVec <- getDoubleVec yCol df
-  let degree = 1
-      dm     = polyDesignMatrix degree xVec
-      y      = LA.fromList (V.toList yVec)
-      res    = fitLMVec dm y
-      beta   = coefficientsV res
-
-      xLa    = LA.fromList (V.toList xVec)
-      xGrid  = V.fromList (linspace (LA.minElement xLa) (LA.maxElement xLa) nGrid)
-      dmG    = polyDesignMatrix degree xGrid
-      yGrid  = LA.toList (dmG LA.#> beta)
-
-      dfStat = fromIntegral (LA.rows dm - LA.cols dm) :: Double
-      resV   = residualsV res
-      s2     = (resV `LA.dot` resV) / dfStat
-      xtxi   = LA.inv (LA.tr dm LA.<> dm)
-      gRows  = LA.toRows dmG
-
-      computeBand level isPI
-        -- df<=0 (飽和) は s²=0/0・studentT が例外 → 帯を線に潰す (lo=hi=yGrid)。
-        | dfStat <= 0 = (yGrid, yGrid)
-        | otherwise   =
-            let tVal   = quantile (studentT dfStat) ((1.0 + level) / 2.0)
-                extra  = if isPI then 1.0 else 0.0
-                halfW xi = tVal * sqrt (s2 * (extra + xi `LA.dot` (xtxi LA.#> xi)))
-                los    = zipWith (\yh xi -> yh - halfW xi) yGrid gRows
-                his    = zipWith (\yh xi -> yh + halfW xi) yGrid gRows
-            in (los, his)
-
-  case band of
-    NoBand ->
-      return (res, SmoothFit (V.toList xGrid) yGrid yGrid yGrid False)
-    CI level ->
-      let (los, his) = computeBand level False
-      in return (res, SmoothFit (V.toList xGrid) yGrid los his True)
-    PI level ->
-      let (los, his) = computeBand level True
-      in return (res, SmoothFit (V.toList xGrid) yGrid los his True)
-
--- | 各列ごとに R² を計算 (多出力対応)。
-computeR2Multi :: LA.Matrix Double -> LA.Matrix Double -> LA.Vector Double
-computeR2Multi y yHat =
-  let q = LA.cols y
-  in LA.fromList
-       [ let yj    = LA.flatten (y    LA.¿ [j])
-             yhj   = LA.flatten (yHat LA.¿ [j])
-             resid = yj - yhj
-             yMean = LA.sumElements yj / fromIntegral (LA.size yj)
-             dev   = LA.cmap (subtract yMean) yj
-             ssRes = resid `LA.dot` resid
-             ssTot = dev   `LA.dot` dev
-         in if ssTot == 0 then 0
-              else 1.0 - ssRes / ssTot
-       | j <- [0 .. q - 1] ]
diff --git a/src/Hanalyze/Model/LM/Diagnostics.hs b/src/Hanalyze/Model/LM/Diagnostics.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/LM/Diagnostics.hs
+++ /dev/null
@@ -1,278 +0,0 @@
--- |
--- Module      : Hanalyze.Model.LM.Diagnostics
--- Description : 線形回帰の推論統計量 (標準誤差・t/p 値・F 統計量・AIC/BIC・レバレッジ・Cook's distance)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Inference and residual diagnostics for ordinary linear regression.
---
--- Provides standard errors, t / p-values, F-statistic, information
--- criteria (AIC / BIC), leverage / hat-diagonal, standardised
--- residuals, and Cook's distance. All multi-output operators
--- (@q@ output columns) follow the @Matrix p × q@ canonical convention,
--- with @Vector p@ wrappers for the @q = 1@ case.
-module Hanalyze.Model.LM.Diagnostics
-  ( -- * t-quantile
-    ciTValue
-    -- * Per-coefficient inference (Multi-output canonical)
-  , CoefStats (..)
-  , lmSigmaSqMulti
-  , lmCovarianceMulti
-  , lmStdErrorsMulti
-  , lmCoefStatsMulti
-    -- * 1-output convenience wrappers
-  , lmStdErrors
-  , lmCoefStats
-    -- * Whole-model F-statistic
-  , FStat (..)
-  , lmFStatistic
-    -- * Information criteria
-  , ICs (..)
-  , lmInformationCriteria
-  , lmInformationCriteriaMulti
-    -- * Residual diagnostics
-  , hatDiagonal
-  , standardizedResiduals
-  , cooksDistance
-    -- * Predictor utilities
-  , predictorStdDevs
-  ) where
-
-import Hanalyze.Model.Core (FitResult (..))
-import qualified Numeric.LinearAlgebra as LA
-import qualified Statistics.Distribution as SD
-import qualified Statistics.Distribution.FDistribution as FD
-import Statistics.Distribution.StudentT (studentT)
-
--- ---------------------------------------------------------------------------
--- t-quantile
--- ---------------------------------------------------------------------------
-
--- | Two-sided Student-t quantile @t_{α/2, df}@ at confidence
--- @level@ (e.g. @0.95@) and degrees of freedom @df@.
-ciTValue :: Double -> Int -> Double
-ciTValue level df =
-  SD.quantile (studentT (fromIntegral df)) ((1.0 + level) / 2.0)
-
--- ---------------------------------------------------------------------------
--- Helpers shared across diagnostics
--- ---------------------------------------------------------------------------
-
--- | Per-output residual variance @σ²_k = RSS_k / (n − p)@. Returns a
--- length-@q@ vector.
-lmSigmaSqMulti :: FitResult -> LA.Vector Double
-lmSigmaSqMulti res =
-  let r       = residuals res
-      n       = LA.rows r
-      p       = LA.rows (coefficients res)
-      df      = fromIntegral (n - p) :: Double
-      cols    = LA.toColumns r
-      ssRes c = c `LA.dot` c
-  in LA.fromList [ ssRes c / df | c <- cols ]
-
--- | Per-output coefficient covariance matrices. Returns a list of
--- @q@ symmetric @p × p@ matrices, one per output column:
--- @Cov_k = σ²_k × (XᵀX)⁻¹@.
-lmCovarianceMulti :: LA.Matrix Double -> FitResult -> [LA.Matrix Double]
-lmCovarianceMulti x res =
-  let xtxi  = LA.inv (LA.tr x LA.<> x)
-      sig2s = LA.toList (lmSigmaSqMulti res)
-  in [ LA.scale s2 xtxi | s2 <- sig2s ]
-
--- ---------------------------------------------------------------------------
--- Standard errors
--- ---------------------------------------------------------------------------
-
--- | Per-coefficient, per-output standard errors as a @p × q@ matrix:
--- @SE_{jk} = √(diag(Cov_k)_j)@.
-lmStdErrorsMulti :: LA.Matrix Double -> FitResult -> LA.Matrix Double
-lmStdErrorsMulti x res =
-  let covs = lmCovarianceMulti x res
-      cols = [ LA.cmap sqrt (LA.takeDiag c) | c <- covs ]
-  in LA.fromColumns cols
-
--- | 1-output convenience: standard errors as a length-@p@ vector.
-lmStdErrors :: LA.Matrix Double -> FitResult -> LA.Vector Double
-lmStdErrors x res = LA.flatten (lmStdErrorsMulti x res)
-
--- ---------------------------------------------------------------------------
--- Coefficient stats (SE / t / two-sided p)
--- ---------------------------------------------------------------------------
-
--- | Per-coefficient inference triple: standard error, Wald @t@ value,
--- and two-sided @p@ value @2 × (1 − F_t(|t|; df))@.
-data CoefStats = CoefStats
-  { csSE     :: !Double
-  , csTValue :: !Double
-  , csPValue :: !Double
-  } deriving (Show, Eq)
-
--- | Per-output 'CoefStats' for every coefficient. Returns a list of
--- @q@ lists, each of length @p@.
-lmCoefStatsMulti :: LA.Matrix Double -> FitResult -> [[CoefStats]]
-lmCoefStatsMulti x res =
-  let n      = LA.rows x
-      p      = LA.cols x
-      df     = fromIntegral (n - p) :: Double
-      tDist  = studentT df
-      betaCs = LA.toColumns (coefficients res)
-      seCs   = LA.toColumns (lmStdErrorsMulti x res)
-      pair beta se =
-        zipWith
-          (\b s ->
-              let t  = if s == 0 then 0 else b / s
-                  pv = 2.0 * (1.0 - SD.cumulative tDist (abs t))
-              in CoefStats s t pv)
-          (LA.toList beta) (LA.toList se)
-  in zipWith pair betaCs seCs
-
--- | 1-output convenience: 'CoefStats' for every coefficient.
-lmCoefStats :: LA.Matrix Double -> FitResult -> [CoefStats]
-lmCoefStats x res = head (lmCoefStatsMulti x res)
-
--- ---------------------------------------------------------------------------
--- F-statistic (whole-model)
--- ---------------------------------------------------------------------------
-
--- | Whole-model F-statistic and its right-tail @p@ value:
--- @F = ((TSS − RSS)/(p − 1)) / (RSS/(n − p))@,
--- @F ~ F(p − 1, n − p)@.
-data FStat = FStat
-  { fsValue  :: !Double
-  , fsPValue :: !Double
-  , fsDf1    :: !Int
-  , fsDf2    :: !Int
-  } deriving (Show, Eq)
-
--- | Whole-model F-statistic per output column. The first design-matrix
--- column is assumed to be the intercept (so the effective number of
--- predictors is @p − 1@). For @p ≤ 1@ or @n ≤ p@ returns @F = 0@,
--- @p = 1@.
-lmFStatistic :: LA.Matrix Double -> FitResult -> [FStat]
-lmFStatistic x res =
-  let n    = LA.rows x
-      p    = LA.cols x
-      df1  = p - 1
-      df2  = n - p
-      yMat = fitted res + residuals res
-      yCs  = LA.toColumns yMat
-      rCs  = LA.toColumns (residuals res)
-      go yj rj =
-        if df1 <= 0 || df2 <= 0
-          then FStat 0 1 (max df1 0) (max df2 0)
-          else
-            let yMean = LA.sumElements yj / fromIntegral (LA.size yj)
-                dev   = LA.cmap (subtract yMean) yj
-                tss   = dev `LA.dot` dev
-                rss   = rj  `LA.dot` rj
-                ess   = tss - rss
-                fVal  = (ess / fromIntegral df1) / (rss / fromIntegral df2)
-                pVal  = if rss == 0
-                          then 0
-                          else SD.complCumulative
-                                 (FD.fDistribution df1 df2) fVal
-            in FStat fVal pVal df1 df2
-  in zipWith go yCs rCs
-
--- ---------------------------------------------------------------------------
--- Information criteria (Gaussian LM)
--- ---------------------------------------------------------------------------
-
--- | Gaussian log-likelihood, AIC, and BIC under the standard
--- @ε ~ N(0, σ²)@ assumption.
-data ICs = ICs
-  { icLogLik :: !Double
-  , icAIC    :: !Double
-  , icBIC    :: !Double
-  } deriving (Show, Eq)
-
--- | Per-output information criteria.
---
--- @
--- logLik = −n/2 × (log(2π) + log(RSS/n) + 1)
--- AIC    = 2k − 2 × logLik              (k = p + 1, σ² counted)
--- BIC    = k × log(n) − 2 × logLik
--- @
-lmInformationCriteriaMulti :: FitResult -> [ICs]
-lmInformationCriteriaMulti res =
-  let r    = residuals res
-      n    = LA.rows r
-      p    = LA.rows (coefficients res)
-      k    = fromIntegral (p + 1) :: Double
-      nD   = fromIntegral n       :: Double
-      cols = LA.toColumns r
-      go c =
-        let rss    = c `LA.dot` c
-            logLik = -nD / 2.0 *
-                       (log (2.0 * pi) + log (rss / nD) + 1.0)
-            aic    = 2.0 * k - 2.0 * logLik
-            bic    = k * log nD - 2.0 * logLik
-        in ICs logLik aic bic
-  in map go cols
-
--- | 1-output convenience.
-lmInformationCriteria :: FitResult -> ICs
-lmInformationCriteria = head . lmInformationCriteriaMulti
-
--- ---------------------------------------------------------------------------
--- Residual diagnostics
--- ---------------------------------------------------------------------------
-
--- | Hat-matrix diagonal @h_ii = xᵢᵀ (XᵀX)⁻¹ xᵢ@. Returns a length-@n@
--- vector independent of the response.
-hatDiagonal :: LA.Matrix Double -> LA.Vector Double
-hatDiagonal x =
-  let xtxi = LA.inv (LA.tr x LA.<> x)
-      rows = LA.toRows x
-  in LA.fromList [ xi `LA.dot` (xtxi LA.#> xi) | xi <- rows ]
-
--- | Internally studentised residual @r̃_i = r_i / (σ × √(1 − h_ii))@.
--- 1-output only (multi-output leverage is the same; the standardisation
--- divides by per-column @σ@). Returns a length-@n@ vector.
-standardizedResiduals :: LA.Matrix Double -> FitResult -> LA.Vector Double
-standardizedResiduals x res =
-  let n      = LA.rows x
-      p      = LA.cols x
-      rj     = LA.flatten (residuals res)        -- assumes q = 1
-      rss    = rj `LA.dot` rj
-      sigma  = sqrt (rss / fromIntegral (n - p))
-      h      = hatDiagonal x
-      one h_ = max 0.0 (1.0 - h_)
-  in LA.fromList
-       [ if sigma == 0 || one hi == 0
-           then 0
-           else ri / (sigma * sqrt (one hi))
-       | (ri, hi) <- zip (LA.toList rj) (LA.toList h) ]
-
--- | Cook's distance @D_i = (r̃_i² / p) × (h_ii / (1 − h_ii))@.
--- 1-output only. Returns a length-@n@ vector.
-cooksDistance :: LA.Matrix Double -> FitResult -> LA.Vector Double
-cooksDistance x res =
-  let p    = fromIntegral (LA.cols x) :: Double
-      h    = hatDiagonal x
-      rTil = standardizedResiduals x res
-  in LA.fromList
-       [ let denom = max 0.0 (1.0 - hi)
-         in if denom == 0
-              then 0
-              else (rTi * rTi / p) * (hi / denom)
-       | (rTi, hi) <- zip (LA.toList rTil) (LA.toList h) ]
-
--- ---------------------------------------------------------------------------
--- Predictor utilities
--- ---------------------------------------------------------------------------
-
--- | Per-column sample standard deviation of the design matrix
--- (length @p@). Useful for standardised contribution
--- @|β_j × sd(x_j)| / Σ|β_k × sd(x_k)|@. The intercept column is
--- typically constant, so its entry is @0@.
-predictorStdDevs :: LA.Matrix Double -> LA.Vector Double
-predictorStdDevs x =
-  let n  = fromIntegral (LA.rows x) :: Double
-      cs = LA.toColumns x
-      sd c =
-        let mu  = LA.sumElements c / n
-            dev = LA.cmap (subtract mu) c
-            v   = (dev `LA.dot` dev) / max 1.0 (n - 1.0)
-        in sqrt v
-  in LA.fromList (map sd cs)
diff --git a/src/Hanalyze/Model/LatentClassAnalysis.hs b/src/Hanalyze/Model/LatentClassAnalysis.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/LatentClassAnalysis.hs
+++ /dev/null
@@ -1,170 +0,0 @@
--- |
--- Module      : Hanalyze.Model.LatentClassAnalysis
--- Description : EM アルゴリズムによる潜在クラス分析 (LCA、R poLCA 相当)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Latent Class Analysis (LCA) via EM algorithm (Phase 32-A2)。
---
--- カテゴリ変数の潜在クラスクラスタリング。 @K@ 個の潜在クラスを仮定し、
--- 各クラスでの各 categorical 特徴の条件付き分布 @P(X_j | class)@ を推定する。
--- R `poLCA` 相当。
---
--- ## モデル
---
--- @
---   P(X_i) = Σ_k π_k · Π_j ρ_{k, j, X_{i,j}}
--- @
---
--- ここで @π_k@ はクラス混合重み、 @ρ_{k,j,l}@ はクラス @k@ で特徴 @j@ が
--- 水準 @l@ を取る確率。
---
--- ## EM
---
--- - **E-step**: posterior @γ_{i,k} = π_k Π_j ρ_{k,j,X_{i,j}} / Σ_{k'} (...)@
--- - **M-step**: @π_k ← (1/n) Σ_i γ_{i,k}@、
---   @ρ_{k,j,l} ← Σ_i γ_{i,k} [X_{i,j} = l] / Σ_i γ_{i,k}@
---
--- Reference: Linzer-Lewis (2011) "poLCA: An R package for polytomous
--- variable latent class analysis". J Stat Softw 42(10).
-module Hanalyze.Model.LatentClassAnalysis
-  ( LCAFit (..)
-  , fitLCA
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import qualified System.Random.MWC     as MWC
-import           Control.Monad         (replicateM)
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
-data LCAFit = LCAFit
-  { lcaPi              :: !(LA.Vector Double)       -- ^ class mixing weights (length K)
-  , lcaRho             :: ![LA.Matrix Double]       -- ^ per feature: K × L (length J)
-  , lcaResponsibilities :: !(LA.Matrix Double)      -- ^ posterior γ (n × K)
-  , lcaIterations      :: !Int
-  , lcaConverged       :: !Bool
-  , lcaLogLik          :: !Double
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- fitLCA
--- ---------------------------------------------------------------------------
-
--- | @K@ クラス、 @L@ 水準の LCA を EM で fit。 入力 @X@ は @n@ 行 @J@ 列の
--- 0-indexed カテゴリ値 (`[[Int]]`、 全要素 ∈ @[0, L-1]@)。
---
--- 初期化はランダム (Dirichlet(1) ≈ uniform-on-simplex の近似で MWC を使う)。
--- 同じ seed で再現性あり。
-fitLCA
-  :: Int                  -- ^ K (classes)
-  -> Int                  -- ^ L (levels per feature)
-  -> [[Int]]              -- ^ X (n × J)
-  -> Int                  -- ^ max EM iterations
-  -> Double               -- ^ tolerance on log-likelihood diff
-  -> MWC.GenIO
-  -> IO LCAFit
-fitLCA k l xRaw maxIter tol gen = do
-  let n = length xRaw
-      j = if n > 0 then length (head xRaw) else 0
-  -- 初期化
-  pi0  <- randomSimplex k gen
-  rho0 <- replicateM j (randomRowStochastic k l gen)
-  let xMat = LA.fromLists [map fromIntegral row | row <- xRaw]
-      go !it !pVec !rhoList !prevLL = do
-        let (gamma, ll) = eStep xMat pVec rhoList l
-            (pNew, rhoNew) = mStep xMat gamma l
-            converged = abs (ll - prevLL) < tol
-        if it >= maxIter || converged
-          then pure (pVec, rhoList, gamma, it, converged, ll)
-          else go (it + 1) pNew rhoNew ll
-  -- 初期 ll は -inf で 1 回目は必ず更新される
-  (pFinal, rhoFinal, gamFinal, iters, conv, llFinal) <-
-    go 0 pi0 rho0 (-1 / 0)
-  pure LCAFit
-    { lcaPi              = pFinal
-    , lcaRho             = rhoFinal
-    , lcaResponsibilities = gamFinal
-    , lcaIterations      = iters
-    , lcaConverged       = conv
-    , lcaLogLik          = llFinal
-    }
-
--- | E-step: per-row posterior @γ_{i,k}@ と log-likelihood。
--- log-space で stable: @log P(X_i | k) = Σ_j log ρ_{k, j, X_{i,j}}@
-eStep
-  :: LA.Matrix Double  -- ^ X (n × J)、 0/1/.../L-1 を Double で
-  -> LA.Vector Double  -- ^ π
-  -> [LA.Matrix Double] -- ^ ρ (J 個の K × L)
-  -> Int               -- ^ L
-  -> (LA.Matrix Double, Double)
-eStep xMat pVec rhoList _ =
-  let n = LA.rows xMat
-      k = LA.size pVec
-      logPi = LA.cmap (\p -> log (max 1e-300 p)) pVec
-      logPx_ik i kk =
-        sum [ log (max 1e-300
-                     (LA.atIndex (rhoList !! jj)
-                        (kk, floor (LA.atIndex xMat (i, jj)))))
-            | jj <- [0 .. length rhoList - 1] ]
-      logUnnormRow i = LA.fromList
-        [ LA.atIndex logPi kk + logPx_ik i kk | kk <- [0 .. k - 1] ]
-      rows = [logUnnormRow i | i <- [0 .. n - 1]]
-      logSumExpV v =
-        let mx = LA.maxElement v
-        in mx + log (LA.sumElements (LA.cmap (\x -> exp (x - mx)) v))
-      perRowLL = [logSumExpV r | r <- rows]
-      gammaRows =
-        [ LA.cmap (\x -> exp (x - lse)) r
-        | (r, lse) <- zip rows perRowLL ]
-      gamma = LA.fromRows gammaRows
-      ll = sum perRowLL
-  in (gamma, ll)
-
--- | M-step: γ から π / ρ を更新。
-mStep
-  :: LA.Matrix Double   -- ^ X (n × J)
-  -> LA.Matrix Double   -- ^ γ (n × K)
-  -> Int                -- ^ L
-  -> (LA.Vector Double, [LA.Matrix Double])
-mStep xMat gamma l =
-  let n   = LA.rows xMat
-      j   = LA.cols xMat
-      k   = LA.cols gamma
-      ones = LA.konst 1 n :: LA.Vector Double
-      gSum = LA.tr gamma LA.#> ones   -- length K = Σ_i γ_{i,k}
-      pNew = LA.scale (1 / fromIntegral n) gSum
-      -- 各特徴 j の ρ (K × L) を再推定
-      rhoFor jj =
-        let countMat = LA.fromLists
-              [ [ sum [ LA.atIndex gamma (i, kk)
-                      | i <- [0 .. n - 1]
-                      , floor (LA.atIndex xMat (i, jj)) == ll ]
-                | ll <- [0 .. l - 1] ]
-              | kk <- [0 .. k - 1] ]
-            denom = LA.cmap (\g -> max 1e-300 g) gSum
-        in LA.fromColumns
-             [ LA.flatten (countMat LA.¿ [c]) / denom
-             | c <- [0 .. l - 1] ]
-      rhoNew = [rhoFor jj | jj <- [0 .. j - 1]]
-  in (pNew, rhoNew)
-
--- ---------------------------------------------------------------------------
--- 初期化ヘルパ
--- ---------------------------------------------------------------------------
-
--- | 長さ @k@ の simplex 上の uniform ランダム vector (= Dir(1) 近似)。
--- 単純に @k@ 個の uniform を引いて正規化。
-randomSimplex :: Int -> MWC.GenIO -> IO (LA.Vector Double)
-randomSimplex k gen = do
-  rs <- replicateM k (MWC.uniformR (1e-3, 1.0 :: Double) gen)
-  let s = sum rs
-  pure (LA.fromList (map (/ s) rs))
-
--- | K × L 行 stochastic matrix のランダム生成。 各行を randomSimplex。
-randomRowStochastic :: Int -> Int -> MWC.GenIO -> IO (LA.Matrix Double)
-randomRowStochastic k l gen = do
-  rows <- replicateM k (randomSimplex l gen)
-  pure (LA.fromRows rows)
diff --git a/src/Hanalyze/Model/LiNGAM/Bootstrap.hs b/src/Hanalyze/Model/LiNGAM/Bootstrap.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/LiNGAM/Bootstrap.hs
+++ /dev/null
@@ -1,194 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns      #-}
--- |
--- Module      : Hanalyze.Model.LiNGAM.Bootstrap
--- Description : BootstrapLiNGAM (エッジ出現頻度・平均係数・符号一致率による DAG confidence 診断)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- BootstrapLiNGAM: 'DirectLiNGAM' を B 個の bootstrap サンプルに対し fit し、
---   エッジ毎の出現頻度 (confidence) と平均係数を出す。
---
--- ## アルゴリズム
---
--- 1. B 回の bootstrap サンプル (行を with-replacement で n 個抽出) を生成
--- 2. 各サンプルで 'fitDirectLiNGAM' を呼ぶ
--- 3. エッジ (j → i) ごとに:
---    * 出現頻度 = (|B[i, j]| > threshold となった bootstrap の数) / B
---    * 平均係数 = 出現した bootstrap での B[i, j] の平均
---    * 符号一致率 = sign の合致率 (符号の不安定性を診断)
---
--- ## 出力
---
--- 'BootstrapResult' は 'edgeProbability' / 'edgeMeanWeight' / 'signConsistency'
--- の 3 つの p × p 行列を保持。 これらを使って 「確からしい因果関係 のみ
--- 採用する DAG」 を構築できる。
---
--- ## リファレンス
---
--- Shimizu (2014) "Bayesian estimation of causal direction in acyclic structural
--- equation models with individual-specific confounder variables and
--- non-Gaussian distributions" (BootstrapLiNGAM の運用紹介)。
--- Python 実装は cdt15/lingam の `lingam/bootstrap.py`。
-module Hanalyze.Model.LiNGAM.Bootstrap
-  ( BootstrapConfig (..)
-  , BootstrapResult (..)
-  , defaultBootstrapConfig
-  , fitBootstrapLiNGAM
-  , fitBootstrapLiNGAMPure
-  , confidenceDAG
-  ) where
-
-import qualified Numeric.LinearAlgebra      as LA
-import qualified System.Random.MWC          as MWC
-import           Control.Monad              (replicateM)
-import           Control.Monad.ST           (runST)
-import qualified Data.Vector                as V
-
-import qualified Hanalyze.Model.LiNGAM.Direct as DL
-import qualified Hanalyze.Model.DAG           as DAG
-
--- ===========================================================================
--- 設定
--- ===========================================================================
-
-data BootstrapConfig = BootstrapConfig
-  { bcNumBootstraps :: !Int
-    -- ^ B (resample 回数)、 default 100
-  , bcDirectCfg     :: !DL.DirectLiNGAMConfig
-    -- ^ 各 bootstrap で使う DirectLiNGAM 設定
-  , bcEdgeThreshold :: !Double
-    -- ^ |B[i, j]| > thr のとき「エッジあり」 と数える、 default 0.05
-  , bcSeed          :: !(Maybe Int)
-  } deriving (Show)
-
-defaultBootstrapConfig :: BootstrapConfig
-defaultBootstrapConfig = BootstrapConfig
-  { bcNumBootstraps = 100
-  , bcDirectCfg     = DL.defaultDirectLiNGAMConfig
-  , bcEdgeThreshold = 0.05
-  , bcSeed          = Just 42
-  }
-
--- ===========================================================================
--- 結果
--- ===========================================================================
-
-data BootstrapResult = BootstrapResult
-  { brEdgeProbability :: !(LA.Matrix Double)
-    -- ^ p × p、 (i, j) = エッジ j → i の出現頻度 (0..1)
-  , brEdgeMeanWeight  :: !(LA.Matrix Double)
-    -- ^ p × p、 (i, j) = エッジが出現した bootstrap における B[i, j] の平均
-  , brSignConsistency :: !(LA.Matrix Double)
-    -- ^ p × p、 (i, j) = エッジが出現した bootstrap での符号合致率
-    --   (1.0 = 全部同符号、 0.5 = 半々)
-  , brNumBootstraps   :: !Int
-  } deriving (Show)
-
--- ===========================================================================
--- 主実装
--- ===========================================================================
-
-fitBootstrapLiNGAM :: BootstrapConfig -> LA.Matrix Double -> IO BootstrapResult
-fitBootstrapLiNGAM cfg xs = do
-  let !n = LA.rows xs
-      !p = LA.cols xs
-      !b = bcNumBootstraps cfg
-      !thr = bcEdgeThreshold cfg
-  gen <- case bcSeed cfg of
-    Just s  -> MWC.initialize (V.fromList [fromIntegral s])
-    Nothing -> MWC.createSystemRandom
-  -- 各 bootstrap の B 行列を集める
-  bMats <- replicateM b $ do
-    idxs <- V.replicateM n (MWC.uniformR (0, n - 1) gen)
-    let !resample = xs LA.? V.toList idxs
-        !fit      = DL.fitDirectLiNGAM (bcDirectCfg cfg) resample
-    pure (DL.dlB fit)
-  let !probMat = computeEdgeProbability thr p bMats
-      !meanMat = computeEdgeMeanWeight  thr p bMats
-      !signMat = computeSignConsistency thr p bMats
-  pure BootstrapResult
-    { brEdgeProbability = probMat
-    , brEdgeMeanWeight  = meanMat
-    , brSignConsistency = signMat
-    , brNumBootstraps   = b
-    }
-
--- | 'fitBootstrapLiNGAM' の **seed 純粋版** (Phase 77.C・@df |->@ 用)。 @bcSeed@ (既定 42・
---   'Nothing' は 42 fallback) で 'runST'+MWC。 同 seed で IO 版とビット一致 (乱数列は monad 非依存)。
-fitBootstrapLiNGAMPure :: BootstrapConfig -> LA.Matrix Double -> BootstrapResult
-fitBootstrapLiNGAMPure cfg xs = runST $ do
-  let !n = LA.rows xs
-      !p = LA.cols xs
-      !b = bcNumBootstraps cfg
-      !thr = bcEdgeThreshold cfg
-  gen <- MWC.initialize (V.fromList [fromIntegral (maybe 42 id (bcSeed cfg))])
-  bMats <- replicateM b $ do
-    idxs <- V.replicateM n (MWC.uniformR (0, n - 1) gen)
-    let !resample = xs LA.? V.toList idxs
-    pure (DL.dlB (DL.fitDirectLiNGAM (bcDirectCfg cfg) resample))
-  pure BootstrapResult
-    { brEdgeProbability = computeEdgeProbability thr p bMats
-    , brEdgeMeanWeight  = computeEdgeMeanWeight  thr p bMats
-    , brSignConsistency = computeSignConsistency thr p bMats
-    , brNumBootstraps   = b
-    }
-
--- | 「出現頻度 ≥ probThreshold かつ符号合致率 ≥ signThreshold」 のエッジだけ
---   採用した DAG を構築。 重みは 'brEdgeMeanWeight' を使う。
-confidenceDAG
-  :: Double           -- 出現頻度閾値 (例 0.7)
-  -> Double           -- 符号合致率閾値 (例 0.8)
-  -> BootstrapResult
-  -> DAG.DAG
-confidenceDAG probThr signThr res =
-  let !p     = LA.rows (brEdgeProbability res)
-      f i j
-        | i == j                                     = 0
-        | LA.atIndex (brEdgeProbability res) (i, j) < probThr = 0
-        | LA.atIndex (brSignConsistency res) (i, j) < signThr = 0
-        | otherwise = LA.atIndex (brEdgeMeanWeight res) (i, j)
-      w = LA.build (p, p) (\i j -> f (round i) (round j) :: Double)
-  in DAG.mkDAG w
-
--- ===========================================================================
--- 内部: 集計
--- ===========================================================================
-
-computeEdgeProbability :: Double -> Int -> [LA.Matrix Double] -> LA.Matrix Double
-computeEdgeProbability thr p bMats =
-  let !n = fromIntegral (length bMats) :: Double
-      f i j
-        | i == j    = 0
-        | otherwise =
-            let !cnt = length [ () | b <- bMats
-                                   , abs (LA.atIndex b (i, j)) > thr ]
-            in fromIntegral cnt / n
-  in LA.build (p, p) (\i j -> f (round i) (round j) :: Double)
-
-computeEdgeMeanWeight :: Double -> Int -> [LA.Matrix Double] -> LA.Matrix Double
-computeEdgeMeanWeight thr p bMats =
-  let f i j
-        | i == j    = 0
-        | otherwise =
-            let vs = [ LA.atIndex b (i, j)
-                     | b <- bMats
-                     , abs (LA.atIndex b (i, j)) > thr ]
-            in if null vs then 0 else sum vs / fromIntegral (length vs)
-  in LA.build (p, p) (\i j -> f (round i) (round j) :: Double)
-
-computeSignConsistency :: Double -> Int -> [LA.Matrix Double] -> LA.Matrix Double
-computeSignConsistency thr p bMats =
-  let f i j
-        | i == j    = 0
-        | otherwise =
-            let vs = [ LA.atIndex b (i, j)
-                     | b <- bMats
-                     , abs (LA.atIndex b (i, j)) > thr ]
-            in if null vs then 0
-               else let !nPos = length (filter (> 0) vs)
-                        !nNeg = length (filter (< 0) vs)
-                        !tot  = nPos + nNeg
-                    in if tot == 0 then 0
-                       else fromIntegral (max nPos nNeg) / fromIntegral tot
-  in LA.build (p, p) (\i j -> f (round i) (round j) :: Double)
diff --git a/src/Hanalyze/Model/LiNGAM/Direct.hs b/src/Hanalyze/Model/LiNGAM/Direct.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/LiNGAM/Direct.hs
+++ /dev/null
@@ -1,299 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns      #-}
--- |
--- Module      : Hanalyze.Model.LiNGAM.Direct
--- Description : DirectLiNGAM (Shimizu 2011) による線形非ガウシアン因果探索
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- DirectLiNGAM (Shimizu et al. 2011) による線形非ガウシアン因果探索。
---
--- ## 前提モデル
---
--- 観測 X ∈ ℝ^(n×p) が **線形 + acyclic + 非ガウシアン独立 noise** な構造方程式
--- モデル X = B X + e に従う (B は適切な行/列順列で下三角化可能、 e の各成分は
--- 互いに独立かつ非ガウシアン)。 このとき DirectLiNGAM は ICA を経由せず、
--- 残差独立性 (差分相互情報量) の最大化で因果順序を 1 変数ずつ確定する。
---
--- ## アルゴリズム概要
---
--- 1. 候補集合 U = {0..p-1}、 因果順序 K = []
--- 2. p 回 loop:
---    a. searchCausalOrder で M(m) = -Σ_{j∈U,j≠m} min(0, ΔMI(x_m,x_j,r_{mj},r_{jm}))²
---       を最大化する m を選ぶ
---    b. U の各 i ≠ m について x_i ← residual(x_i, x_m) (m で残差化)
---    c. K に m を追加、 U から m を除く
--- 3. K から B 行列を OLS で組み上げる (causal order に従い順に回帰)
---
--- ## ΔMI (差分相互情報量)
---
--- 標準化後の x_i, x_j と残差 r_{ij}, r_{ji} (互いに片方を片方で回帰した残差)
--- に対し:
---
--- > ΔMI(x_i, x_j, r_{ij}, r_{ji}) = [H(x_j) + H(r_{ij}/σ_{r_{ij}})]
--- >                                - [H(x_i) + H(r_{ji}/σ_{r_{ji}})]
---
--- H は Hyvärinen (1998) の maximum entropy 近似:
---
--- > H(u) = (1 + log 2π)/2 - k1·(E[log cosh u] - γ)² - k2·(E[u·exp(-u²/2)])²
--- > k1 = 79.047, k2 = 7.4129, γ = 0.37457
---
--- ## リファレンス
---
--- Shimizu et al. (2011) "DirectLiNGAM: A direct method for learning a linear
--- non-Gaussian structural equation model", JMLR 12. Python 実装は
--- cdt15/lingam の `lingam/direct_lingam.py` で動作対応を確認した。
---
--- ## 落とし穴メモ
---
--- * 観測変数が **完全ガウシアン** だと ΔMI ≈ 0 となり順序が一意決まらない。
---   ガウシアン応答には Phase 30 の causal inference (介入効果) や PC algorithm
---   等を使う
--- * **n < 100** だと entropy の sample 推定が不安定。 n ≥ 200 推奨
--- * 行列 B は **causal order の根本変数を 0 行目** に置く慣習。 出力の
---   dlB[K[j], K[i]] = β_i (i < j) で表される (= 影響先 ← 影響元 規約)
-module Hanalyze.Model.LiNGAM.Direct
-  ( DirectLiNGAMConfig (..)
-  , DirectLiNGAMFit (..)
-  , defaultDirectLiNGAMConfig
-  , fitDirectLiNGAM
-  , dlDAG
-  -- helpers (re-export 不要時は internal だが、 単体テスト用に公開)
-  , entropyApprox
-  , diffMutualInfo
-  , olsResidual
-  , standardize
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import           Data.List             (foldl')
-
-import qualified Hanalyze.Model.DAG    as DAG
-
--- ===========================================================================
--- 公開型
--- ===========================================================================
-
--- | DirectLiNGAM の設定。
-data DirectLiNGAMConfig = DirectLiNGAMConfig
-  { dlcPruneThr :: !Double
-    -- ^ |B_ij| < 'dlcPruneThr' は隣接行列で 0 と扱う。 default 0.05。
-  } deriving (Show)
-
-defaultDirectLiNGAMConfig :: DirectLiNGAMConfig
-defaultDirectLiNGAMConfig = DirectLiNGAMConfig
-  { dlcPruneThr = 0.05
-  }
-
--- | DirectLiNGAM の推定結果。
-data DirectLiNGAMFit = DirectLiNGAMFit
-  { dlOrder     :: ![Int]
-    -- ^ 推定 causal order (topological)。 K[0] が最も外生的、 K[p-1] が
-    --   最も末端 (どの変数からも影響を受ける可能性のある変数)
-  , dlB         :: !(LA.Matrix Double)
-    -- ^ 構造方程式係数行列 (p × p)。 X_i = Σ_j dlB[i, j] · X_j + e_i。
-    --   causal order に従い適切な行/列順列で下三角化可能
-  , dlAdjacency :: !(LA.Matrix Double)
-    -- ^ |dlB| > dlcPruneThr の 0/1 マスク
-  , dlResiduals :: !(LA.Matrix Double)
-    -- ^ 各サンプルの推定残差 e_i (n × p)。 独立性検定の事後評価に使う
-  } deriving (Show)
-
--- ===========================================================================
--- 主アルゴリズム
--- ===========================================================================
-
--- | DirectLiNGAM を fit する。 X は n × p 行列 (各列 = 1 変数)。
---
--- 計算量: 因果順序探索 O(p² · n) per iteration × p iterations = O(p³ · n)
--- (entropy 評価 + 残差化が dominant)。
--- | 'DirectLiNGAMFit' を 'Hanalyze.Model.DAG.DAG' 表現に変換 (threshold は
---   元の 'dlcPruneThr' を再利用)。
-dlDAG :: DirectLiNGAMConfig -> DirectLiNGAMFit -> DAG.DAG
-dlDAG cfg fit = DAG.fromBMatrix (dlcPruneThr cfg) (dlB fit)
-
-fitDirectLiNGAM :: DirectLiNGAMConfig -> LA.Matrix Double -> DirectLiNGAMFit
-fitDirectLiNGAM cfg xs =
-  let !p = LA.cols xs
-      !n = LA.rows xs
-      -- 各列を Vector に分解した可変リスト (residualize 用)
-      cols0 :: [LA.Vector Double]
-      cols0 = [ LA.flatten (xs LA.¿ [j]) | j <- [0 .. p - 1] ]
-      -- 主 loop: cols / activeU / order を順次更新
-      (order, _finalCols) = causalOrderLoop cols0 [0 .. p - 1] []
-      -- 元の X から causal order に従い B 行列を OLS で組み立て
-      bMat    = estimateB xs order
-      adjMat  = buildAdjacency (dlcPruneThr cfg) bMat
-      -- 残差: e = X - X·B^T (行ベクトル view、 単純な線形変換)
-      resid   = xs - xs LA.<> LA.tr bMat
-      _ = n  -- shadow warn 防止
-  in DirectLiNGAMFit
-       { dlOrder     = order
-       , dlB         = bMat
-       , dlAdjacency = adjMat
-       , dlResiduals = resid
-       }
-
--- | causal order を 1 つずつ確定する主ループ。
---   引数:
---     cols    : 現在の (残差化された) 列ベクトルのリスト (length p、 元 index で並ぶ)
---     activeU : まだ確定していない元 index のリスト
---     orderRev: これまでに確定した順序 (逆順、 後で reverse)
-causalOrderLoop
-  :: [LA.Vector Double]   -- 現状の列ベクトル
-  -> [Int]                -- active 集合
-  -> [Int]                -- 確定済 (逆順)
-  -> ([Int], [LA.Vector Double])
-causalOrderLoop cols activeU orderRev
-  | null activeU = (reverse orderRev, cols)
-  | length activeU == 1 =
-      (reverse (head activeU : orderRev), cols)
-  | otherwise =
-      let !m = searchCausalOrder cols activeU
-          xm = cols !! m
-          -- m 以外の active で残差化
-          colsNew = [ if j `elem` activeU && j /= m
-                        then olsResidual (cols !! j) xm
-                        else cols !! j
-                    | j <- [0 .. length cols - 1] ]
-          activeNew = [ j | j <- activeU, j /= m ]
-      in causalOrderLoop colsNew activeNew (m : orderRev)
-
--- | 候補集合 activeU から、 「最も外生的 (= 他から残差化された後の独立性が
---   崩れにくい)」 index を 1 つ返す。
---   M(m) = -Σ_{j∈U, j≠m} min(0, ΔMI(x_m,x_j,r_{mj},r_{jm}))² を最大化。
-searchCausalOrder :: [LA.Vector Double] -> [Int] -> Int
-searchCausalOrder cols activeU =
-  let !scores = [ (m, score m) | m <- activeU ]
-      score m =
-        let xm = cols !! m
-            xmStd = standardize xm
-            contribs =
-              [ let xj = cols !! j
-                    xjStd = standardize xj
-                    rmj = olsResidual xmStd xjStd   -- xm を xj で残差化
-                    rjm = olsResidual xjStd xmStd   -- xj を xm で残差化
-                    dmi = diffMutualInfo xmStd xjStd rmj rjm
-                in min 0 dmi ** 2
-              | j <- activeU, j /= m ]
-        in negate (sum contribs)
-  in fst (foldl' pickMax (head scores) (tail scores))
-  where
-    pickMax acc@(_, s0) cur@(_, s1)
-      | s1 > s0   = cur
-      | otherwise = acc
-
--- | 差分相互情報量 ΔMI = [H(xj) + H(rij/σ)] - [H(xi) + H(rji/σ)]。
---   入力 xi/xj は標準化済、 rij/rji は **標準化前**の残差。
-diffMutualInfo
-  :: LA.Vector Double  -- xi (標準化済)
-  -> LA.Vector Double  -- xj (標準化済)
-  -> LA.Vector Double  -- rij = xi - β xj 残差
-  -> LA.Vector Double  -- rji = xj - β xi 残差
-  -> Double
-diffMutualInfo xi xj rij rji =
-  let !hxi  = entropyApprox xi
-      !hxj  = entropyApprox xj
-      !srij = stdSafe rij
-      !srji = stdSafe rji
-      !hrij = entropyApprox (LA.scale (1 / srij) rij)
-      !hrji = entropyApprox (LA.scale (1 / srji) rji)
-  in (hxj + hrij) - (hxi + hrji)
-  where
-    stdSafe v =
-      let s = LA.norm_2 (v - LA.scalar (LA.sumElements v / fromIntegral (LA.size v)))
-                / sqrt (fromIntegral (LA.size v))
-      in if s > 1e-12 then s else 1.0
-
--- | Hyvärinen (1998) maximum entropy 近似:
---   H(u) = (1 + log 2π)/2 - k1·(E[log cosh u] - γ)² - k2·(E[u·exp(-u²/2)])²
---   u は事前に標準化されていることが前提。
-entropyApprox :: LA.Vector Double -> Double
-entropyApprox u =
-  let !k1    = 79.047
-      !k2    = 7.4129
-      !gamma = 0.37457
-      !n     = fromIntegral (LA.size u) :: Double
-      !logCosh = LA.sumElements (LA.cmap (\v -> log (cosh v)) u) / n
-      !uExp    = LA.sumElements (u * LA.cmap (\v -> exp (-v * v / 2)) u) / n
-  in (1 + log (2 * pi)) / 2
-     - k1 * (logCosh - gamma) ** 2
-     - k2 * uExp ** 2
-
--- | OLS による残差: r = xi - (Cov(xi,xj) / Var(xj)) · xj
-olsResidual :: LA.Vector Double -> LA.Vector Double -> LA.Vector Double
-olsResidual xi xj =
-  let !n   = fromIntegral (LA.size xi) :: Double
-      !mxi = LA.sumElements xi / n
-      !mxj = LA.sumElements xj / n
-      !ci  = xi - LA.scalar mxi
-      !cj  = xj - LA.scalar mxj
-      !cov = ci `LA.dot` cj / n
-      !var = cj `LA.dot` cj / n
-      !beta = if var > 1e-12 then cov / var else 0
-  in xi - LA.scale beta xj
-
--- | 中心化 + 標準偏差で割る (zero-mean, unit-variance)。
-standardize :: LA.Vector Double -> LA.Vector Double
-standardize v =
-  let !n  = fromIntegral (LA.size v) :: Double
-      !mu = LA.sumElements v / n
-      !c  = v - LA.scalar mu
-      !s  = sqrt (c `LA.dot` c / n)
-      !sd = if s > 1e-12 then s else 1.0
-  in LA.scale (1 / sd) c
-
--- ===========================================================================
--- B 行列 + 隣接行列
--- ===========================================================================
-
--- | causal order に従い B 行列を OLS で組み立てる。
---   B[K[j], K[i]] = OLS 回帰 X[:,K[j]] ~ X[:,K[0..j-1]] の i 番目係数。
-estimateB :: LA.Matrix Double -> [Int] -> LA.Matrix Double
-estimateB xs order =
-  let !p    = LA.cols xs
-      bRows = [ buildRow j | j <- [0 .. p - 1] ]
-      buildRow j =
-        let kj   = order !! j
-            -- 影響元候補: order の j より前
-            parents = take j order
-        in if null parents
-             then LA.fromList (replicate p 0)
-             else
-               let parentMat = LA.fromColumns
-                     [ LA.flatten (xs LA.¿ [pIdx]) | pIdx <- parents ]
-                   target = LA.flatten (xs LA.¿ [kj])
-                   beta = olsBeta parentMat target
-                   coefVec = replicate p 0
-                   -- beta を parent 位置に散布
-                   updates = zip parents (LA.toList beta)
-                   filled = foldl' (\acc (idx, v) -> setAt acc idx v) coefVec updates
-               in LA.fromList filled
-      -- 行は K の順序、 列は元 variable index。
-      -- bRows[j] は variable K[j] の行ベクトル → reorder で元 variable index 順に
-      origOrderMat = LA.fromRows
-        [ bRows !! posInOrder i | i <- [0 .. p - 1] ]
-      posInOrder i = case lookup i (zip order [0 ..]) of
-        Just k  -> k
-        Nothing -> 0   -- unreachable
-  in origOrderMat
-
--- | OLS 係数: β = (XᵀX)⁻¹ Xᵀy
-olsBeta :: LA.Matrix Double -> LA.Vector Double -> LA.Vector Double
-olsBeta x y =
-  let xtx = LA.tr x LA.<> x
-      xty = LA.tr x LA.#> y
-  in LA.flatten (LA.linearSolveLS xtx (LA.asColumn xty))
-
-setAt :: [a] -> Int -> a -> [a]
-setAt xs i v = take i xs ++ [v] ++ drop (i + 1) xs
-
--- | |B_ij| > threshold で 1、 以外 0 の隣接行列。 対角は 0 に固定。
-buildAdjacency :: Double -> LA.Matrix Double -> LA.Matrix Double
-buildAdjacency thr b =
-  let !p = LA.rows b
-      f i j
-        | i == j    = 0
-        | abs (LA.atIndex b (i, j)) > thr = 1
-        | otherwise = 0
-  in LA.build (p, p) (\i j -> f (round i) (round j) :: Double)
diff --git a/src/Hanalyze/Model/LiNGAM/ICA.hs b/src/Hanalyze/Model/LiNGAM/ICA.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/LiNGAM/ICA.hs
+++ /dev/null
@@ -1,245 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns      #-}
--- |
--- Module      : Hanalyze.Model.LiNGAM.ICA
--- Description : ICA-LiNGAM (Shimizu 2006、原典版) by FastICA + Hungarian 順列
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- ICA-LiNGAM (Shimizu et al. 2006、 LiNGAM の原典版) by FastICA。
---
--- ## アルゴリズム
---
--- 1. 観測 X (n × p) に対し FastICA で **分離行列 W** (= ICA unmixing) を求める
---    (元座標、 'Hanalyze.Math.ICA.icaUnmixing')
--- 2. **A = pinv(W)** を計算 (X = S · Aᵀ + mean)
--- 3. **行/列順列で下三角化**:
---    a. A の絶対値の **逆数** をコスト行列とし、 行・列順列で対角要素を
---       絶対値最大に揃える Hungarian-like (本実装は近似貪欲)
---    b. 順列適用後の A を対角要素で正規化、 B = I - A_perm⁻¹
---    c. B の下三角化のための **行順列** を別途決定 (= causal order)
--- 4. B 行列を pruning して隣接行列を返す
---
--- ## DirectLiNGAM との違い
---
--- DirectLiNGAM は ICA 不要で残差独立性 + 1 変数ずつ確定。 ICA-LiNGAM は ICA
--- (FastICA) で全成分を同時推定 → 順列で因果順序を後付けで決める。 ICA の
--- 収束性に依存するが、 因子数が多いときは並列度で有利な場合がある。
---
--- 行/列順列は **Hungarian (Kuhn-Munkres, O(p³))** で大域最適化する
--- ('Hanalyze.Math.Hungarian')。 cdt15/lingam の Python 実装は
--- @scipy.optimize.linear_sum_assignment(1 / |W|)@ で同等のことをしており、
--- コスト関数も @1 / (|W| + ε)@ で揃えている。 旧来の貪欲版 ('greedyAssignRows')
--- は @ilcUseHungarian = False@ で復元可能 (回帰確認・ベンチ比較用)。
---
--- ## リファレンス
---
--- Shimizu et al. (2006) "A Linear Non-Gaussian Acyclic Model for Causal
--- Discovery", JMLR 7. Python 実装は cdt15/lingam の `lingam/ica_lingam.py`。
-module Hanalyze.Model.LiNGAM.ICA
-  ( ICALiNGAMConfig (..)
-  , ICALiNGAMFit (..)
-  , fitICALiNGAMPure
-  , defaultICALiNGAMConfig
-  , fitICALiNGAM
-  , ilDAG
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import qualified Data.Vector.Unboxed   as VU
-import           Data.List             (sortBy)
-import           Data.Ord              (comparing, Down (..))
-
-import qualified Hanalyze.Math.ICA          as ICA
-import qualified Hanalyze.Math.Hungarian    as Hung
-import qualified Hanalyze.Model.DAG         as DAG
-
--- ===========================================================================
--- 設定 / 結果
--- ===========================================================================
-
-data ICALiNGAMConfig = ICALiNGAMConfig
-  { ilcPruneThr      :: !Double
-  , ilcICACfg        :: !ICA.ICAConfig
-  , ilcUseHungarian  :: !Bool
-    -- ^ True: 行順列を Hungarian (O(p³)) で大域最適化 (default、 推奨)。
-    --   False: 旧来の貪欲版を使う (回帰比較・ベンチ用)。
-  } deriving (Show)
-
-defaultICALiNGAMConfig :: ICALiNGAMConfig
-defaultICALiNGAMConfig = ICALiNGAMConfig
-  { ilcPruneThr     = 0.05
-  , ilcICACfg       = ICA.defaultICAConfig
-  , ilcUseHungarian = True
-  }
-
-data ICALiNGAMFit = ICALiNGAMFit
-  { ilOrder      :: ![Int]
-  , ilB          :: !(LA.Matrix Double)
-  , ilAdjacency  :: !(LA.Matrix Double)
-  , ilICAResult  :: !ICA.ICAResult
-  } deriving (Show)
-
--- ===========================================================================
--- 主実装
--- ===========================================================================
-
-fitICALiNGAM :: ICALiNGAMConfig -> LA.Matrix Double -> IO ICALiNGAMFit
-fitICALiNGAM cfg x = do
-  ica <- ICA.fitICA (ilcICACfg cfg) x
-  pure (assembleICALiNGAM cfg ica)
-
--- | 'fitICALiNGAM' の **seed 純粋版** (Phase 77.C・@df |->@ 用)。 'fitICAPure' (seed) で
---   FastICA を回す。 同 seed で IO 版とビット一致。
-fitICALiNGAMPure :: ICALiNGAMConfig -> LA.Matrix Double -> ICALiNGAMFit
-fitICALiNGAMPure cfg x = assembleICALiNGAM cfg (ICA.fitICAPure (ilcICACfg cfg) x)
-
--- | ICA 結果 → 'ICALiNGAMFit' の純粋組み立て (行順列 → 正規化 → 下三角化 → adjacency)。
-assembleICALiNGAM :: ICALiNGAMConfig -> ICA.ICAResult -> ICALiNGAMFit
-assembleICALiNGAM cfg ica =
-  let !w = ICA.icaUnmixing ica      -- (p × p)
-      !p = LA.rows w
-      -- step 3a: 対角絶対値最大化の行順列を決定。 Hungarian は大域最適、
-      -- 貪欲は p > 10 でしばしば劣化する (cdt15/lingam も Hungarian 採用)。
-      !rowPerm    = if ilcUseHungarian cfg
-                      then hungarianAssignRows w
-                      else greedyAssignRows w
-      !wPerm1     = permuteRows w rowPerm
-      -- step 3b: 各行を対角で正規化
-      !wNorm      = normalizeDiag wPerm1
-      -- B' = I - W_norm
-      !bPrime     = LA.ident p - wNorm
-      -- step 3c: bPrime の行順列を causal order に並べる
-      -- 下三角化: 順列の絶対値和が下三角寄りになるよう貪欲に並べ替え
-      !causal     = causalOrderFromTriangle bPrime
-      -- causal order で再順列した B を返す
-      !bReorder   = permuteRowsCols bPrime causal causal
-      -- 元 variable index に戻す
-      -- bPrime[i, j] は permuted index 上の値、 rowPerm を逆引きする必要あり
-      !bFinal     = restoreOriginalIndex p bPrime rowPerm causal
-      !adj        = adjMatrix (ilcPruneThr cfg) bFinal
-      _ = bReorder  -- 内部debug 用、 未使用
-  in ICALiNGAMFit
-    { ilOrder      = mapPerm causal rowPerm
-    , ilB          = bFinal
-    , ilAdjacency  = adj
-    , ilICAResult  = ica
-    }
-
--- | DAG への変換
-ilDAG :: ICALiNGAMConfig -> ICALiNGAMFit -> DAG.DAG
-ilDAG cfg fit = DAG.fromBMatrix (ilcPruneThr cfg) (ilB fit)
-
--- ===========================================================================
--- 内部: 順列ヘルパ
--- ===========================================================================
-
--- | Hungarian による行順列決定。 コスト C[i, j] = 1 / (|W[i, j]| + ε) で
---   'Hung.hungarianMin' を呼び、 row i → col j の割当を得てから
---   perm[j] = i に反転する (col j に row i を置く)。
---   cdt15/lingam の Python 実装 (scipy linear_sum_assignment(1/|W|)) と同型。
-hungarianAssignRows :: LA.Matrix Double -> [Int]
-hungarianAssignRows w =
-  let p        = LA.rows w
-      eps      = 1.0e-12
-      cost     = LA.build (p, p)
-                   (\i j -> 1.0 / (abs (LA.atIndex w (round i, round j)) + eps)
-                            :: Double)
-      assign   = Hung.hungarianMin cost  -- assign[i] = j (row i → col j)
-      pairs    = sortBy (comparing fst)
-                   [ (assign VU.! i, i) | i <- [0 .. p - 1] ]
-                                          -- (col j, row i)
-  in map snd pairs                        -- perm[j] = i
-
--- | 行順列の貪欲決定: 各列の絶対値最大要素を見て、 行と列を 1-1 対応させる
---   greedy assignment (Hungarian の近似版)。 戻り値 perm の意味:
---   「permuted index j に元 row index perm[j] を持ってくる」 (= rows ordering)。
-greedyAssignRows :: LA.Matrix Double -> [Int]
-greedyAssignRows w =
-  let p = LA.rows w
-      -- 候補を (元 row i, 元 col j, abs value) として絶対値降順に並べる
-      candidates :: [((Int, Int), Double)]
-      candidates = sortBy (comparing (Down . snd))
-        [ ((i, j), abs (LA.atIndex w (i, j)))
-        | i <- [0 .. p - 1], j <- [0 .. p - 1] ]
-      -- 貪欲: row と col を使用済にしながら (col j に row i を割当て)
-      assign :: [Int] -> [Int] -> [((Int, Int), Double)] -> [(Int, Int)]
-      assign _        _        []                = []
-      assign usedRows usedCols (((i, j), _):rest)
-        | i `elem` usedRows || j `elem` usedCols = assign usedRows usedCols rest
-        | otherwise = (j, i) : assign (i:usedRows) (j:usedCols) rest
-      pairs    = assign [] [] candidates           -- (col j, row i) のペア
-      sortedPairs = sortBy (comparing fst) pairs   -- col 昇順
-      perm        = map snd sortedPairs            -- perm[j] = i
-  in if length perm == p
-       then perm
-       else [0 .. p - 1]   -- fallback
-
--- | 行を perm で並べ替える (perm[i] = 元 index)。
-permuteRows :: LA.Matrix Double -> [Int] -> LA.Matrix Double
-permuteRows m perm = m LA.? perm
-
--- | 各行を対角要素で正規化する (W → W / diag(W))。
-normalizeDiag :: LA.Matrix Double -> LA.Matrix Double
-normalizeDiag w =
-  let p = LA.rows w
-      diags = [ LA.atIndex w (i, i) | i <- [0 .. p - 1] ]
-      f i j =
-        let d = diags !! i
-            v = LA.atIndex w (i, j)
-        in if abs d > 1e-12 then v / d else v
-  in LA.build (p, p) (\i j -> f (round i) (round j) :: Double)
-
--- | B から下三角化のための行順列を貪欲に決める。
---   各行の非零要素数が少ない行 (根) を先に置く戦略。
-causalOrderFromTriangle :: LA.Matrix Double -> [Int]
-causalOrderFromTriangle b =
-  let p = LA.rows b
-      scoreRow i =
-        sum [ abs (LA.atIndex b (i, j))
-            | j <- [0 .. p - 1], j /= i ]
-      sorted = sortBy (comparing snd)
-                 [ (i, scoreRow i) | i <- [0 .. p - 1] ]
-  in map fst sorted
-
--- | 行と列を同じ perm で並び替え (DAG 構造を保つ)。
-permuteRowsCols :: LA.Matrix Double -> [Int] -> [Int] -> LA.Matrix Double
-permuteRowsCols m rp cp =
-  let mR = m LA.? rp
-      mTr = LA.tr mR LA.? cp
-  in LA.tr mTr
-
--- | 元の variable index に戻す。
---   permuted index 上での B → original index 上での B。
-restoreOriginalIndex
-  :: Int
-  -> LA.Matrix Double    -- B_prime (permuted index 上)
-  -> [Int]               -- rowPerm: permuted_i ← original_rowPerm[i]
-  -> [Int]               -- causal: permuted index 上での causal order
-  -> LA.Matrix Double
-restoreOriginalIndex p bPrime rowPerm _causal =
-  -- bPrime は rowPerm で permuted されている。 inverse perm で元に戻す。
-  let invPerm = invertPerm rowPerm
-      f i j   = LA.atIndex bPrime (invPerm !! i, invPerm !! j)
-  in LA.build (p, p) (\i j -> f (round i) (round j) :: Double)
-
-invertPerm :: [Int] -> [Int]
-invertPerm perm =
-  let p = length perm
-      pairs = zip perm [0 ..]
-      sorted = sortBy (comparing fst) pairs
-  in map snd sorted ++ replicate (p - length sorted) 0
-
--- | original index 上での causal order (= permuted causal を rowPerm で戻す)
-mapPerm :: [Int] -> [Int] -> [Int]
-mapPerm causal rowPerm = map (rowPerm !!) causal
-
--- | adjacency 行列 (|B| > thr のマスク)
-adjMatrix :: Double -> LA.Matrix Double -> LA.Matrix Double
-adjMatrix thr b =
-  let p = LA.rows b
-      f i j
-        | i == j                          = 0
-        | abs (LA.atIndex b (i, j)) > thr = 1
-        | otherwise                       = 0
-  in LA.build (p, p) (\i j -> f (round i) (round j) :: Double)
diff --git a/src/Hanalyze/Model/LiNGAM/MultiGroup.hs b/src/Hanalyze/Model/LiNGAM/MultiGroup.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/LiNGAM/MultiGroup.hs
+++ /dev/null
@@ -1,192 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns      #-}
--- |
--- Module      : Hanalyze.Model.LiNGAM.MultiGroup
--- Description : MultiGroupLiNGAM (Shimizu 2012、群間で共通 DAG 構造・係数値のみ異なる LiNGAM 拡張)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- MultiGroupLiNGAM (Shimizu 2012): 複数群 (group) で **共通の DAG 構造** を
---   仮定し、 群間で係数値は異なる可能性を許す LiNGAM 拡張。
---
--- ## モデル
---
--- 群 g = 1..G について、 観測 X^(g) は同じ causal order に従う SEM:
---
--- > X^(g) = B^(g) · X^(g) + e^(g)
---
--- 各 B^(g) の非零パターン (= DAG 構造) は **全群共通** を仮定するが、 値は
--- 群ごとに異なってよい。 これは半導体現場の「異なる工場 / 装置号機 / 世代で
--- 同じ因果構造、 効き量だけ違う」 という想定とマッチする。
---
--- ## アルゴリズム
---
--- 1. 各群 X^(g) について 'fitDirectLiNGAM' を独立に実行 → B^(g)、 K^(g)
--- 2. 全群の K^(g) を集約して **多数決で共通 causal order** を確定
---    (本実装: 各位置 j の頻度最大ノードを選び、 不一致時は位置 j の総合的
---    平均スコアを再計算)
--- 3. 共通 order に従い、 各群で再度 OLS で B^(g) を組み直す
--- 4. **共通 adjacency**: 各群で |B^(g)[i, j]| > thr となるエッジ数が
---    全群のうち過半数なら採用
---
--- ## リファレンス
---
--- Shimizu (2012) "Joint estimation of linear non-Gaussian acyclic models",
--- Neurocomputing 81. Python 実装は cdt15/lingam の `lingam/multi_group_lingam.py`。
-module Hanalyze.Model.LiNGAM.MultiGroup
-  ( MultiGroupConfig (..)
-  , MultiGroupFit (..)
-  , defaultMultiGroupConfig
-  , fitMultiGroupLiNGAM
-  , mgCommonDAG
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import           Data.List             (foldl', sort, group, sortBy)
-import           Data.Ord              (comparing, Down (..))
-
-import qualified Hanalyze.Model.LiNGAM.Direct as DL
-import qualified Hanalyze.Model.DAG           as DAG
-
--- ===========================================================================
--- 設定 / 結果
--- ===========================================================================
-
-data MultiGroupConfig = MultiGroupConfig
-  { mgcDirectCfg :: !DL.DirectLiNGAMConfig
-  , mgcMajority  :: !Double
-    -- ^ adjacency 多数決閾値 (0..1)、 default 0.5
-  } deriving (Show)
-
-defaultMultiGroupConfig :: MultiGroupConfig
-defaultMultiGroupConfig = MultiGroupConfig
-  { mgcDirectCfg = DL.defaultDirectLiNGAMConfig
-  , mgcMajority  = 0.5
-  }
-
-data MultiGroupFit = MultiGroupFit
-  { mgGroupFits      :: ![DL.DirectLiNGAMFit]
-    -- ^ 各群独立 fit 結果
-  , mgCommonOrder    :: ![Int]
-    -- ^ 多数決で確定した共通 causal order
-  , mgGroupBMats     :: ![LA.Matrix Double]
-    -- ^ 共通 order で再 fit した各群 B 行列
-  , mgCommonAdj      :: !(LA.Matrix Double)
-    -- ^ 多数決による共通 adjacency マスク (0/1)
-  } deriving (Show)
-
--- ===========================================================================
--- 主実装
--- ===========================================================================
-
-fitMultiGroupLiNGAM :: MultiGroupConfig -> [LA.Matrix Double] -> MultiGroupFit
-fitMultiGroupLiNGAM cfg groups =
-  let !groupFits = [ DL.fitDirectLiNGAM (mgcDirectCfg cfg) g | g <- groups ]
-      !p         = if null groupFits then 0 else LA.cols (DL.dlB (head groupFits))
-      !commonOrd = majorityOrder p (map DL.dlOrder groupFits)
-      -- 共通 order に従って各群で B を再度 OLS で組み立てる
-      !commonBs  = [ refitWithOrder commonOrd g | g <- groups ]
-      !commonAdj = majorityAdjacency
-                    (mgcMajority cfg)
-                    (DL.dlcPruneThr (mgcDirectCfg cfg))
-                    commonBs
-  in MultiGroupFit
-       { mgGroupFits   = groupFits
-       , mgCommonOrder = commonOrd
-       , mgGroupBMats  = commonBs
-       , mgCommonAdj   = commonAdj
-       }
-
--- | 共通 adjacency に基づく DAG 表現。 重みは全群 B の平均を使う。
-mgCommonDAG :: MultiGroupFit -> DAG.DAG
-mgCommonDAG fit =
-  let !bs   = mgGroupBMats fit
-      !adj  = mgCommonAdj fit
-      !p    = LA.rows adj
-      !g    = fromIntegral (length bs) :: Double
-      !meanB = LA.scale (1 / g) (foldl' (+) (LA.konst 0 (p, p)) bs)
-      f i j
-        | i == j                        = 0
-        | LA.atIndex adj (i, j) == 0    = 0
-        | otherwise                     = LA.atIndex meanB (i, j)
-      w = LA.build (p, p) (\i j -> f (round i) (round j) :: Double)
-  in DAG.mkDAG w
-
--- ===========================================================================
--- 内部
--- ===========================================================================
-
--- | 多数決で共通 causal order を決める。 各位置 j で最頻 node を取り、
---   重複が出たら未確定 node を残りから追加する fallback。
-majorityOrder :: Int -> [[Int]] -> [Int]
-majorityOrder p orders
-  | null orders = [0 .. p - 1]
-  | otherwise =
-      let posCount j = [ ord !! j | ord <- orders, length ord > j ]
-          mostFreq xs =
-            let !grouped = sortBy (comparing (Down . length))
-                             (group (sort xs))
-            in case grouped of
-                 ((h:_):_) -> h
-                 _         -> 0
-          go acc unused j
-            | j >= p = reverse acc
-            | otherwise =
-                let !cand = mostFreq (posCount j)
-                in if cand `elem` unused
-                     then go (cand : acc) (filter (/= cand) unused) (j + 1)
-                     else
-                       -- fallback: 残りから一番低 index
-                       case unused of
-                         []      -> reverse acc
-                         (h : _) ->
-                           go (h : acc) (filter (/= h) unused) (j + 1)
-      in go [] [0 .. p - 1] 0
-
--- | 指定 causal order に従い X から B を OLS で組み立て直す。
-refitWithOrder :: [Int] -> LA.Matrix Double -> LA.Matrix Double
-refitWithOrder order x =
-  let !p = LA.cols x
-      mkRow j =
-        let kj   = order !! j
-            parents = take j order
-        in if null parents
-             then LA.fromList (replicate p 0)
-             else
-               let pm = LA.fromColumns
-                     [ LA.flatten (x LA.¿ [pIdx]) | pIdx <- parents ]
-                   y  = LA.flatten (x LA.¿ [kj])
-                   beta = LA.flatten
-                     (LA.linearSolveLS (LA.tr pm LA.<> pm)
-                        (LA.asColumn (LA.tr pm LA.#> y)))
-                   updates = zip parents (LA.toList beta)
-                   coefV   = replicate p 0
-                   filled  = foldl' (\acc (i, v) -> set acc i v) coefV updates
-               in LA.fromList filled
-      bRows = [ mkRow j | j <- [0 .. p - 1] ]
-      pos i = case lookup i (zip order [0 ..]) of
-                Just k -> k
-                Nothing -> 0
-      origOrderMat = LA.fromRows [ bRows !! pos i | i <- [0 .. p - 1] ]
-  in origOrderMat
-  where
-    set xs i v = take i xs ++ [v] ++ drop (i + 1) xs
-
--- | 多数決による共通 adjacency: |B^(g)[i, j]| > thr が 全群中 majorityRatio
---   以上の比率で起こったら 1。
-majorityAdjacency
-  :: Double                -- majority ratio (0..1)
-  -> Double                -- B threshold
-  -> [LA.Matrix Double]
-  -> LA.Matrix Double
-majorityAdjacency majRatio thr bs =
-  let !p = LA.rows (head bs)
-      !g = fromIntegral (length bs) :: Double
-      f i j
-        | i == j    = 0
-        | otherwise =
-            let cnt = length [ () | b <- bs
-                                  , abs (LA.atIndex b (i, j)) > thr ]
-                rate = fromIntegral cnt / g
-            in if rate >= majRatio then 1 else 0
-  in LA.build (p, p) (\i j -> f (round i) (round j) :: Double)
diff --git a/src/Hanalyze/Model/LiNGAM/Pairwise.hs b/src/Hanalyze/Model/LiNGAM/Pairwise.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/LiNGAM/Pairwise.hs
+++ /dev/null
@@ -1,109 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns      #-}
--- |
--- Module      : Hanalyze.Model.LiNGAM.Pairwise
--- Description : Pairwise LiNGAM (Hyvärinen-Smith 2013、2 変数間の因果方向推定)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Pairwise LiNGAM: 2 変数間の因果方向 (x → y か y → x か) 推定。
---
--- ## アルゴリズム (Hyvärinen-Smith 2013)
---
--- 標準化された (x, y) について、 非ガウシアン独立性に基づき:
---
---   R(x → y) = - Cov(x³, y) · sign(Cov(x, y)) + Cov(x, y³)
---
--- の符号で方向を決定する近似的測度 (LIM, likelihood ratio approximation)。
---
--- * R > 0 → x → y
--- * R < 0 → y → x
--- * |R| 小 → 判定不能 (ガウシアン近接 or 弱依存)
---
--- 軽量で 2 変数の方向推定に直接使える。 3 変数以上には 'DirectLiNGAM' を使う。
---
--- ## リファレンス
---
--- Hyvärinen, A. & Smith, S. M. (2013) "Pairwise likelihood ratios for
--- estimation of non-Gaussian structural equation models", JMLR 14.
--- Python 実装は cdt15/lingam の `lingam/lim.py` (LIM = Likelihood-based
--- Independence Measure)。
-module Hanalyze.Model.LiNGAM.Pairwise
-  ( PairwiseDirection (..)
-  , PairwiseResult (..)
-  , pairwiseLiNGAM
-  , pairwiseScore
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
-data PairwiseDirection
-  = XtoY        -- ^ x → y
-  | YtoX        -- ^ y → x
-  | Inconclusive  -- ^ |score| < threshold
-  deriving (Show, Eq)
-
-data PairwiseResult = PairwiseResult
-  { prScore     :: !Double             -- ^ R(x → y) の値、 符号で方向決定
-  , prDirection :: !PairwiseDirection
-  , prMagnitude :: !Double             -- ^ |score|、 confidence の代理
-  } deriving (Show)
-
--- ===========================================================================
--- 実装
--- ===========================================================================
-
--- | Pairwise LiNGAM の主関数。 threshold 未満は Inconclusive。
-pairwiseLiNGAM
-  :: Double               -- threshold (default 0.0 = 符号だけで判定)
-  -> LA.Vector Double     -- x
-  -> LA.Vector Double     -- y
-  -> PairwiseResult
-pairwiseLiNGAM thr x y =
-  let !s = pairwiseScore x y
-      !mag = abs s
-      !dir
-        | mag < thr = Inconclusive
-        | s > 0     = XtoY
-        | otherwise = YtoX
-  in PairwiseResult { prScore = s, prDirection = dir, prMagnitude = mag }
-
--- | スコア R = -Cov(x³, y)·sign(Cov(x,y)) + Cov(x, y³)
---   x, y は内部で標準化される (zero-mean、 unit-variance)。
-pairwiseScore :: LA.Vector Double -> LA.Vector Double -> Double
-pairwiseScore xRaw yRaw =
-  let !x = standardize xRaw
-      !y = standardize yRaw
-      !x3 = x * x * x
-      !y3 = y * y * y
-      !cov_x_y   = covar x  y
-      !cov_x3_y  = covar x3 y
-      !cov_x_y3  = covar x  y3
-      !sgn = if cov_x_y >= 0 then 1.0 else (-1.0 :: Double)
-  in - cov_x3_y * sgn + cov_x_y3
-
--- ===========================================================================
--- 内部
--- ===========================================================================
-
-standardize :: LA.Vector Double -> LA.Vector Double
-standardize v =
-  let !n  = fromIntegral (LA.size v) :: Double
-      !mu = LA.sumElements v / n
-      !c  = v - LA.scalar mu
-      !s  = sqrt (c `LA.dot` c / n)
-      !sd = if s > 1e-12 then s else 1.0
-  in LA.scale (1 / sd) c
-
-covar :: LA.Vector Double -> LA.Vector Double -> Double
-covar a b =
-  let !n  = fromIntegral (LA.size a) :: Double
-      !ma = LA.sumElements a / n
-      !mb = LA.sumElements b / n
-      !ca = a - LA.scalar ma
-      !cb = b - LA.scalar mb
-  in ca `LA.dot` cb / n
diff --git a/src/Hanalyze/Model/LiNGAM/Parce.hs b/src/Hanalyze/Model/LiNGAM/Parce.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/LiNGAM/Parce.hs
+++ /dev/null
@@ -1,218 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns      #-}
--- |
--- Module      : Hanalyze.Model.LiNGAM.Parce
--- Description : ParceLiNGAM (Tashiro 2014、潜在交絡に頑健な bottom-up + HSIC LiNGAM 拡張)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- ParceLiNGAM (Tashiro et al. 2014): **潜在交絡 (unobserved confounders) に
---   頑健な** LiNGAM 拡張。
---
--- ## モデル
---
--- 通常の LiNGAM は @X = B X + e@ で e の各成分独立を要求する。 潜在交絡が
--- ある場合、 観測 X だけ見ると e が独立に見えず DirectLiNGAM は誤った因果
--- 順序を出すことがある。 ParceLiNGAM は:
---
--- > X = B X + Λ · f + e
---
--- ここで f が潜在交絡変数。
---
--- ## アルゴリズム (v0.2、 bottom-up + HSIC、 cdt15/lingam 準拠)
---
--- cdt15/lingam の `lingam/bottom_up_parce_lingam.py` を参照実装とする
--- bottom-up 探索:
---
--- 1. 候補集合 U = {0, .., p-1} を初期化
--- 2. 各候補 j ∈ U について、 残り @U \\ {j}@ の変数で x_j を OLS 回帰した
---    残差 R を作る。 「x_j が最も下流 (sink)」 ならば
---    @{x_i : i ∈ U \\ {j}}@ と R は独立になるはず
--- 3. 独立度を @hsicAggregate (x_{U \\ {j}}, R)@ で測る (HSIC 総和)。
---    最小のものを最も下流の候補 j* として選ぶ
--- 4. その HSIC 集約値が threshold 'pcAcceptThr' を下回れば j* を順序末尾に
---    追加して U から削除。 そうでなければ探索停止
--- 5. 未確定の変数群は **unresolved group** ('pcUnresolvedGroup') として
---    まとめて返す (潜在交絡で順序が同定不能)
---
--- v0.1 (per-pair OLS + Pairwise LiNGAM) は **削除** した。 v0.2 は
--- リファレンス実装と同じ「集合 vs 単変量残差」 の依存判定に切替。
---
--- ## 独立性判定の妥協点
---
--- cdt15/lingam では HSIC を gamma 近似で p 値化し Fisher 法で合成する。
--- v0.2 では HSIC **統計量の総和** を直接スコアとして使い、 閾値で判定する
--- (実装軽量化、 p 値の校正は将来課題)。 相対比較 (どの候補が最も独立か)
--- は機能する。 absolute threshold はサンプル数 / 分散依存なので、 ユーザは
--- 'pcAcceptThr' をデータに合わせて調整する想定。
---
--- ## リファレンス
---
--- Tashiro et al. (2014) "ParceLiNGAM: A causal ordering method robust against
--- latent confounders", Neural Computation 26(1).
--- cdt15/lingam の `lingam/bottom_up_parce_lingam.py`。
-module Hanalyze.Model.LiNGAM.Parce
-  ( ParceConfig (..)
-  , ParceFit (..)
-  , defaultParceConfig
-  , fitParceLiNGAM
-  , parceDAG
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import           Data.List             (foldl', sortBy)
-import           Data.Ord              (comparing)
-
-import qualified Hanalyze.Math.HSIC    as HSIC
-import qualified Hanalyze.Model.DAG    as DAG
-
--- ===========================================================================
--- 設定 / 結果
--- ===========================================================================
-
-data ParceConfig = ParceConfig
-  { pcRelRatio :: !Double
-    -- ^ 受理判定の相対比閾値。 best 候補の HSIC 集約値が 2 番目候補の値の
-    --   pcRelRatio 倍未満なら sink として受理。 default 0.5
-    --   (best が 2nd の半分未満で「明瞭に独立」 と判断)。
-    --
-    --   絶対 HSIC の値はサンプル数 / 分散 / median bandwidth に強く依存する
-    --   ため、 v0.2 では絶対閾値を捨て **相対比のみ** で判定する。
-    --   |U| = 2 のときは 2 候補のうち小さい方/大きい方が pcRelRatio 未満
-    --   なら受理 (= 自然な「明瞭差」 検出)。
-  , pcPruneThr :: !Double
-    -- ^ B 行列 pruning 閾値、 default 0.05
-  } deriving (Show)
-
-defaultParceConfig :: ParceConfig
-defaultParceConfig = ParceConfig
-  { pcRelRatio = 0.5
-  , pcPruneThr = 0.05
-  }
-
-data ParceFit = ParceFit
-  { pcOrder            :: ![Int]
-    -- ^ 確定できた causal order (sink → source の順で逆に並んだものを
-    --   さらに反転 → source → sink の順)。 unresolved group があるときは
-    --   その後ろに連結 (Spec 互換のため任意順で末尾追加)
-  , pcB                :: !(LA.Matrix Double)
-    -- ^ 構造方程式係数行列。 unresolved 群内の係数は OLS で仮置きされる
-    --   (確定的順序が無いので解釈は控えめに)
-  , pcAdjacency        :: !(LA.Matrix Double)
-  , pcUnresolvedGroup  :: ![Int]
-    -- ^ 潜在交絡で順序が同定不能と判定された変数群 (空ならば全変数確定)
-  } deriving (Show)
-
--- ===========================================================================
--- 主実装
--- ===========================================================================
-
-fitParceLiNGAM :: ParceConfig -> LA.Matrix Double -> ParceFit
-fitParceLiNGAM cfg x =
-  let !p          = LA.cols x
-      (sinkList, leftover) = bottomUpSearch cfg x [0 .. p - 1]
-      -- sinkList は新しく見つけた順に **prepend** しているので、
-      -- 自然と「upstream → downstream」 (source → sink) の順に並ぶ。
-      -- leftover (確定できなかった残り) を先頭に置く: 長さ 1 なら単なる
-      -- source、 長さ ≥ 2 なら **潜在交絡で順序不能** のグループ。
-      !fullOrder         = leftover ++ sinkList
-      !unresolved        = if length leftover > 1 then leftover else []
-      !bMat       = buildBFromOrder p x fullOrder
-      !adjMat     = adjFromB (pcPruneThr cfg) bMat
-  in ParceFit
-       { pcOrder           = fullOrder
-       , pcB               = bMat
-       , pcAdjacency       = adjMat
-       , pcUnresolvedGroup = unresolved
-       }
-
--- | DAG 表現を返す。
-parceDAG :: ParceConfig -> ParceFit -> DAG.DAG
-parceDAG cfg fit = DAG.fromBMatrix (pcPruneThr cfg) (pcB fit)
-
--- ===========================================================================
--- bottom-up 探索
--- ===========================================================================
-
--- | 候補集合 U から sink を 1 つずつ削り出す。
---   戻り値: (確定した sink を upstream→downstream の順で並べたリスト、
---   残り未確定 U)。 ※ prepend で蓄積するため、 最後に見つけたもの
---   (=最も upstream に近い) が先頭、 最初に見つけたもの (=最も downstream)
---   が末尾、 つまり自然な source → sink 順。
-bottomUpSearch
-  :: ParceConfig
-  -> LA.Matrix Double
-  -> [Int]                   -- 初期 U (全変数 index)
-  -> ([Int], [Int])
-bottomUpSearch cfg x = go []
-  where
-    go !sinks u
-      | length u <= 1 = (sinks, u)         -- 1 個以下なら確定済とみなす
-      | otherwise =
-          let scored      = sortBy (comparing snd)
-                              [ (j, scoreSink x u j) | j <- u ]
-              (jStar, sB) = head scored
-              sNext       = snd (scored !! 1)
-              accept      = sB < pcRelRatio cfg * sNext
-          in if accept
-               then go (jStar : sinks) (filter (/= jStar) u)
-               else (sinks, u)              -- 明瞭な sink が無い → halt
-
--- | 候補 j を sink と仮定したときの「他変数 U\\{j} ⊥ R_j」 の HSIC 集約値。
---   R_j = x_j を x_{U\\{j}} で OLS 回帰した残差。
-scoreSink :: LA.Matrix Double -> [Int] -> Int -> Double
-scoreSink x u j =
-  let others = filter (/= j) u
-      xj     = LA.flatten (x LA.¿ [j])
-      xRest  = LA.fromColumns [ LA.flatten (x LA.¿ [k]) | k <- others ]
-      r      = partialResidual xj xRest
-  in HSIC.hsicAggregate xRest r
-
--- ===========================================================================
--- 内部ヘルパ
--- ===========================================================================
-
--- | y を Z (n × q 行列) に OLS 回帰した残差。
-partialResidual :: LA.Vector Double -> LA.Matrix Double -> LA.Vector Double
-partialResidual y z =
-  let xtx  = LA.tr z LA.<> z
-      xty  = LA.tr z LA.#> y
-      beta = LA.flatten (LA.linearSolveLS xtx (LA.asColumn xty))
-  in y - z LA.#> beta
-
--- | causal order に従い OLS で B 行列を構築 (DirectLiNGAM と同手順)。
-buildBFromOrder :: Int -> LA.Matrix Double -> [Int] -> LA.Matrix Double
-buildBFromOrder p x order =
-  let mkRow j =
-        let kj      = order !! j
-            parents = take j order
-        in if null parents
-             then LA.fromList (replicate p 0)
-             else
-               let pm = LA.fromColumns
-                     [ LA.flatten (x LA.¿ [pi_]) | pi_ <- parents ]
-                   y  = LA.flatten (x LA.¿ [kj])
-                   xtx = LA.tr pm LA.<> pm
-                   xty = LA.tr pm LA.#> y
-                   beta = LA.flatten
-                            (LA.linearSolveLS xtx (LA.asColumn xty))
-                   updates = zip parents (LA.toList beta)
-                   coefV   = replicate p 0
-                   filled  = foldl' (\acc (i, v) -> set acc i v) coefV updates
-               in LA.fromList filled
-      bRows = [ mkRow j | j <- [0 .. p - 1] ]
-      pos i = case lookup i (zip order [0 ..]) of
-                Just k  -> k
-                Nothing -> 0
-  in LA.fromRows [ bRows !! pos i | i <- [0 .. p - 1] ]
-  where
-    set xs i v = take i xs ++ [v] ++ drop (i + 1) xs
-
-adjFromB :: Double -> LA.Matrix Double -> LA.Matrix Double
-adjFromB thr b =
-  let p = LA.rows b
-      f i j
-        | i == j                          = 0
-        | abs (LA.atIndex b (i, j)) > thr = 1
-        | otherwise                       = 0
-  in LA.build (p, p) (\i j -> f (round i) (round j) :: Double)
diff --git a/src/Hanalyze/Model/LiNGAM/VAR.hs b/src/Hanalyze/Model/LiNGAM/VAR.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/LiNGAM/VAR.hs
+++ /dev/null
@@ -1,104 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns      #-}
--- |
--- Module      : Hanalyze.Model.LiNGAM.VAR
--- Description : VAR-LiNGAM (Hyvärinen et al. 2010) — 時系列データに対する LiNGAM 拡張
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- VAR-LiNGAM (Hyvärinen et al. 2010): 時系列データに対する LiNGAM 拡張。
---
--- ## モデル
---
--- 観測時系列 Y_t ∈ ℝ^K は以下の SVAR (構造 VAR) に従う:
---
--- > Y_t = Σ_{l=1..p} A_l^* · Y_{t-l} + B_0 · Y_t + e_t
---
--- ここで B_0 は同時刻因果 (contemporaneous causal effect、 acyclic + LiNGAM)、
--- e_t は非ガウシアン独立 noise。 通常の reduced-form VAR(p) と関係:
---
--- > Y_t = Σ_l A_l · Y_{t-l} + u_t,   u_t = (I - B_0)⁻¹ · e_t
---
--- なので u_t に LiNGAM を適用すれば B_0 が求まり、 A_l^* も A_l と B_0 から
--- 回収できる。
---
--- ## アルゴリズム
---
--- 1. Phase 35 の 'Hanalyze.Model.VAR.fitVAR' で reduced-form VAR(p) を fit
--- 2. 残差 u_t (= 'varResiduals') に 'fitDirectLiNGAM' を適用 → B_0 と
---    causal order を取得
--- 3. 構造 lag 行列を A_l^* = (I - B_0) · A_l で復元 (l=1..p)
---
--- ## リファレンス
---
--- Hyvärinen et al. (2010) "Estimation of a Structural Vector Autoregression
--- Model Using Non-Gaussianity", JMLR 11. Python 実装は cdt15/lingam の
--- `lingam/var_lingam.py`。
-module Hanalyze.Model.LiNGAM.VAR
-  ( VARLiNGAMConfig (..)
-  , VARLiNGAMFit (..)
-  , defaultVARLiNGAMConfig
-  , fitVARLiNGAM
-  , vlDAG
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-
-import qualified Hanalyze.Model.VAR           as V
-import qualified Hanalyze.Model.LiNGAM.Direct as DL
-import qualified Hanalyze.Model.DAG           as DAG
-
--- ===========================================================================
--- 設定 / 結果
--- ===========================================================================
-
-data VARLiNGAMConfig = VARLiNGAMConfig
-  { vlcLagOrder  :: !Int
-    -- ^ VAR の lag 数 p (≥ 1)
-  , vlcDirectCfg :: !DL.DirectLiNGAMConfig
-  } deriving (Show)
-
-defaultVARLiNGAMConfig :: VARLiNGAMConfig
-defaultVARLiNGAMConfig = VARLiNGAMConfig
-  { vlcLagOrder  = 1
-  , vlcDirectCfg = DL.defaultDirectLiNGAMConfig
-  }
-
-data VARLiNGAMFit = VARLiNGAMFit
-  { vlVARFit          :: !V.VARFit
-    -- ^ Phase 35 の reduced-form VAR(p) fit 結果
-  , vlContempLiNGAM   :: !DL.DirectLiNGAMFit
-    -- ^ 残差 u_t に対する DirectLiNGAM 結果 (= 同時刻因果 B_0)
-  , vlB0              :: !(LA.Matrix Double)
-    -- ^ 同時刻因果係数 (K × K)、 = vlContempLiNGAM の dlB
-  , vlStructuralLags  :: ![LA.Matrix Double]
-    -- ^ 構造 lag 行列 A_l^* = (I - B_0) · A_l (length = p)
-  , vlContempOrder    :: ![Int]
-  } deriving (Show)
-
--- ===========================================================================
--- 主実装
--- ===========================================================================
-
-fitVARLiNGAM :: VARLiNGAMConfig -> LA.Matrix Double -> VARLiNGAMFit
-fitVARLiNGAM cfg y =
-  let !varFit = V.fitVAR (vlcLagOrder cfg) y
-      !resid  = V.varResiduals varFit
-      !lgFit  = DL.fitDirectLiNGAM (vlcDirectCfg cfg) resid
-      !b0     = DL.dlB lgFit
-      !k      = V.varK varFit
-      !iMinusB0 = LA.ident k - b0
-      !structLags =
-        [ iMinusB0 LA.<> al | al <- V.varCoefs varFit ]
-  in VARLiNGAMFit
-       { vlVARFit         = varFit
-       , vlContempLiNGAM  = lgFit
-       , vlB0             = b0
-       , vlStructuralLags = structLags
-       , vlContempOrder   = DL.dlOrder lgFit
-       }
-
--- | 同時刻因果 (B_0) の DAG 表現を返す。 lag 部分は含まない (時間方向は別の
---   表現が必要、 v0.1 では同時刻のみ DAG 化)。
-vlDAG :: VARLiNGAMConfig -> VARLiNGAMFit -> DAG.DAG
-vlDAG cfg fit = DAG.fromBMatrix (DL.dlcPruneThr (vlcDirectCfg cfg)) (vlB0 fit)
diff --git a/src/Hanalyze/Model/MDS.hs b/src/Hanalyze/Model/MDS.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/MDS.hs
+++ /dev/null
@@ -1,105 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.MDS
--- Description : MDS (多次元尺度構成法) の高レベルモデル型 (Phase 75.21)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- MDS の高レベルモデル型 (Phase 75.21)。
---
--- 低レベルの行列カーネル ('mdsClassical' / 'mdsSammon' / 'euclideanDist') は
--- 'Hanalyze.Stat.MDS' に置き、 ここは @df |-> mds cfg cols@ で使う
--- **モデル型** 'MDSResult' (= 'Hanalyze.Model.PCA.PCAResult' と同格) と
--- その設定 'MDSConfig' を提供する。
---
--- MDS (多次元尺度構成法) = サンプル間の **距離 (非類似度) を保ったまま** 高次元
--- データを 2D へ配置する可視化・次元圧縮。 'MDSClassical' (Torgerson・ユークリッド
--- 距離なら PCA と等価) と 'MDSSammon' (小距離重視の非線形版) を選べる。 結果は
--- 埋め込み (MDS1/MDS2) に加え **元データ (群色付け用の列を含む)** を保持し、
--- plot 側で @toPlot m@ (単色散布) / @toPlot (mdsView m <> mdsGroupBy \"g\")@ (群色) に使う。
-module Hanalyze.Model.MDS
-  ( -- * 手法と設定
-    MDSMethod (..)
-  , MDSConfig (..)
-  , defaultMDS
-    -- ** 再 export (Sammon パラメータ)
-  , SammonConfig (..)
-  , defaultSammonConfig
-    -- * モデル型
-  , MDSResult (..)
-  , runMDS
-  ) where
-
-import           Data.Text (Text)
-import qualified Data.Text             as T
-import qualified Data.Vector           as V
-import qualified Numeric.LinearAlgebra as LA
-import qualified DataFrame.Internal.DataFrame  as DX
-
-import           Hanalyze.DataIO.Convert (getDoubleVec)
-import qualified Hanalyze.Stat.MDS       as S
-import           Hanalyze.Stat.MDS       (SammonConfig (..), defaultSammonConfig)
-
--- ===========================================================================
--- 手法と設定
--- ===========================================================================
-
--- | MDS の手法選択。 'MDSClassical' = 古典 MDS (Torgerson・固有分解)、
--- 'MDSSammon' = Sammon 写像 (小距離重視の非線形・勾配降下)。
-data MDSMethod = MDSClassical | MDSSammon
-  deriving (Show, Eq)
-
--- | MDS の設定。 手法 ('mdsMethod') と、 'MDSSammon' 選択時に使う Sammon
--- パラメータ ('mdsSammon') を持つ (他の config 同様レコード型・裸の直和を
--- spec 引数にしない)。 k=2 固定・距離はユークリッドのみ (現状実装どおり)。
-data MDSConfig = MDSConfig
-  { mdsMethod :: !MDSMethod      -- ^ 古典 / Sammon。
-  , mdsSammon :: !SammonConfig   -- ^ 'MDSSammon' 選択時の勾配降下パラメータ。
-  } deriving (Show)
-
--- | 既定設定: 古典 MDS・Sammon パラメータは既定。
-defaultMDS :: MDSConfig
-defaultMDS = MDSConfig MDSClassical defaultSammonConfig
-
--- ===========================================================================
--- モデル型
--- ===========================================================================
-
--- | 学習済 MDS。 2D 埋め込み (MDS1/MDS2) に加え、 **元データ ('mdsSourceFrame')** を
--- 保持して plot 側の群色付け ('mdsGroupBy') に使う。 'Hanalyze.Model.PCA.PCAResult'
--- と同格のモデル型 (df 型ではない)。
-data MDSResult = MDSResult
-  { mdsMethodUsed  :: !MDSMethod          -- ^ 使った手法。
-  , mdsEmbedding   :: !(LA.Matrix Double) -- ^ 埋め込み (n × 2)。
-  , mdsFeatures    :: ![Text]             -- ^ 入力に使った特徴列名。
-  , mdsSourceFrame :: !DX.DataFrame       -- ^ 元データ (群色付け用に保持)。
-  }
-
--- | @runMDS cfg frame cols@ — frame の特徴列 @cols@ を行列化し、 ユークリッド
--- 距離 → 古典 / Sammon MDS で 2D 埋め込みを得る。 列が無い / 長さ不揃いなら 'Left'。
-runMDS :: MDSConfig -> DX.DataFrame -> [Text] -> Either String MDSResult
-runMDS _   _     []   = Left "MDS: 特徴列が空です (1 列以上必要)"
-runMDS cfg frame cols = do
-  colVecs <- mapM getCol cols
-  let lens = map length colVecs
-  if not (allEq lens)
-    then Left ("MDS: 特徴列の長さが不揃いです: " <> show lens)
-    else do
-      let n    = head lens
-          xMat = LA.fromLists [ [ v !! i | v <- colVecs ] | i <- [0 .. n - 1] ]
-          d    = S.euclideanDist xMat
-          emb  = case mdsMethod cfg of
-                   MDSClassical -> S.mdsClassical d 2
-                   MDSSammon    -> S.mdsSammon (mdsSammon cfg) d 2
-      Right MDSResult
-        { mdsMethodUsed  = mdsMethod cfg
-        , mdsEmbedding   = emb
-        , mdsFeatures    = cols
-        , mdsSourceFrame = frame
-        }
-  where
-    getCol c = case V.toList <$> getDoubleVec c frame of
-      Just vs -> Right vs
-      Nothing -> Left ("MDS: 数値列が見つかりません: " <> T.unpack c)
-    allEq []     = True
-    allEq (x:xs) = all (== x) xs
diff --git a/src/Hanalyze/Model/MultiGP.hs b/src/Hanalyze/Model/MultiGP.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/MultiGP.hs
+++ /dev/null
@@ -1,325 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.MultiGP
--- Description : Multi-output Gaussian processes (共有 HP / per-output 独立 HP の 2 戦略)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Multi-output Gaussian processes.
---
--- Two strategies are offered; pick by how outputs should share
--- hyperparameters:
---
---   * __Shared-HP (default)__ — @fitMultiGP@ / @fitMultiGPMV@.
---     RBF only. A /single/ HP optimisation maximises the pooled marginal
---     likelihood @Σ_q log p(y_q | θ)@, and the resulting Cholesky factor
---     of @Ky@ is reused for every output's posterior solve. Mirrors
---     scikit-learn's @GaussianProcessRegressor.fit(X, Y::(n,q))@. About
---     @q@-fold faster than the per-output variant when @q > 1@.
---
---   * __Per-output independent HPs__ — @fitMultiGPIndep@ /
---     @fitMultiGPMVIndep@. Supports any 'Kernel' kind. Each output
---     runs its own LBFGS HP fit, so per-task flexibility is preserved
---     at @q × O(LBFGS)@ cost.
---
--- Both treat outputs as independent likelihoods (@B = I@ in the
--- Intrinsic Coregionalization Model). Co-kriging / LMC kernels with
--- learned cross-output correlations are not implemented.
-module Hanalyze.Model.MultiGP
-  ( MultiGPModel (..)
-    -- * Default (shared-HP, RBF only)
-  , MultiGPResult (..)
-  , mgpStd
-  , fitMultiGP
-  , predictMultiGP
-  , MultiGPResultMV (..)
-  , fitMultiGPMV
-    -- * Per-output independent HPs (any kernel)
-  , fitMultiGPIndep
-  , fitMultiGPMVIndep
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import Hanalyze.Model.GP (Kernel (..), GPModel (..), GPParams (..),
-                 GPResult (..),
-                 fitGP, optimizeGP, initParamsFromData, initParamsFromDataMV,
-                 GPResultMV (..), fitGPMV, optimizeGPMVCached)
-import qualified Hanalyze.Stat.KernelDist as KD
-import qualified Hanalyze.Stat.Cholesky   as Chol
-import qualified Hanalyze.Optim.LBFGS     as LBFGS
-import qualified Hanalyze.Optim.Common    as OC
-import           System.IO.Unsafe (unsafePerformIO)
-
--- | Multi-output GP model with a per-output set of hyperparameters.
--- All outputs share the same kernel /type/ for simplicity; their
--- length-scales etc. are still optimized independently.
-data MultiGPModel = MultiGPModel
-  { mgpKernel :: Kernel
-  , mgpParams :: [GPParams]   -- ^ Hyperparameters per output.
-  } deriving (Show)
-
--- | Per-output GP fit results.
-data MultiGPResult = MultiGPResult
-  { mgpMean   :: [[Double]]   -- ^ Predictive means, one list per output (length @q@).
-  , mgpLower  :: [[Double]]   -- ^ 95 % lower band (@mean − 2σ@) per output.
-  , mgpUpper  :: [[Double]]   -- ^ 95 % upper band (@mean + 2σ@) per output.
-  , mgpModels :: [GPModel]    -- ^ Underlying per-output 'GPModel's.
-  } deriving (Show)
-
--- | Recover the per-output predictive standard deviation @σ@ from the
--- @mean@ / @upper@ bands.
-mgpStd :: MultiGPResult -> [[Double]]
-mgpStd r = zipWith (zipWith (\m u -> (u - m) / 2)) (mgpMean r) (mgpUpper r)
-
--- | Fit a multi-output GP with shared RBF hyperparameters (default API).
---
--- This is the 1D-input wrapper around 'fitMultiGPMV'. A single HP set
--- is learned by maximising the pooled marginal likelihood over all
--- @q@ outputs, then one Cholesky factor of @Ky = K + σ_n² I@ is
--- reused for each output's posterior solve.
---
--- For per-output independent HPs (any kernel kind), use
--- 'fitMultiGPIndep'.
-fitMultiGP :: [Double]      -- ^ Training inputs (1D).
-           -> [[Double]]    -- ^ Per-output training values (length @q@).
-           -> [Double]      -- ^ Test inputs.
-           -> MultiGPResult
-fitMultiGP trainX trainYs testX =
-  let xMat   = LA.asColumn (LA.fromList trainX)
-      tMat   = LA.asColumn (LA.fromList testX)
-      yVecs  = map LA.fromList trainYs
-      r      = fitMultiGPMV xMat yVecs tMat
-  in MultiGPResult
-       { mgpMean   = map LA.toList (mgpmvMean   r)
-       , mgpLower  = map LA.toList (mgpmvLower  r)
-       , mgpUpper  = map LA.toList (mgpmvUpper  r)
-       , mgpModels = mgpmvModels r
-       }
-
--- | Fit a multi-output GP with per-output independent hyperparameters.
---
--- Each output runs its own 'optimizeGP' LBFGS loop, then is predicted
--- at @testX@. Supports any 'Kernel' kind. Use this when outputs need
--- distinct length-scales / noise levels.
---
--- For sklearn-style shared-HP behaviour (RBF, single HP optimisation,
--- much faster when @q > 1@), use 'fitMultiGP'.
-fitMultiGPIndep :: Kernel        -- ^ Kernel kind shared by every output.
-                -> [Double]      -- ^ Training inputs (1D).
-                -> [[Double]]    -- ^ Per-output training values (length @q@).
-                -> [Double]      -- ^ Test inputs.
-                -> MultiGPResult
-fitMultiGPIndep kern trainX trainYs testX =
-  let perOutput :: [Double] -> (GPModel, GPResult)
-      perOutput trainY =
-        let p0   = initParamsFromData trainX trainY
-            pOpt = optimizeGP kern trainX trainY p0
-            mdl  = GPModel kern pOpt
-            res  = fitGP mdl trainX trainY testX
-        in (mdl, res)
-      pairs   = map perOutput trainYs
-      models  = map fst pairs
-      results = map snd pairs
-  in MultiGPResult
-       { mgpMean   = map gpMean   results
-       , mgpLower  = map gpLower  results
-       , mgpUpper  = map gpUpper  results
-       , mgpModels = models
-       }
-
--- | Re-predict an existing 'MultiGPModel' at new test inputs (no
--- re-fitting).
-predictMultiGP :: MultiGPModel
-               -> [Double]    -- ^ Training inputs.
-               -> [[Double]]  -- ^ Per-output training values.
-               -> [Double]    -- ^ Test inputs.
-               -> MultiGPResult
-predictMultiGP mgp trainX trainYs testX =
-  let kern    = mgpKernel mgp
-      models  = zipWith (\p _ -> GPModel kern p) (mgpParams mgp) trainYs
-      results = zipWith3 (\m _ ty -> fitGP m trainX ty testX)
-                         models trainYs trainYs
-  in MultiGPResult
-       { mgpMean   = map gpMean   results
-       , mgpLower  = map gpLower  results
-       , mgpUpper  = map gpUpper  results
-       , mgpModels = models
-       }
-
--- ---------------------------------------------------------------------------
--- Multi-input (multivariate X) API
--- ---------------------------------------------------------------------------
-
--- | Multi-input multi-output GP fit result. Per-output mean / band
--- vectors (length @m@), with the optimized 'GPModel' that produced them.
-data MultiGPResultMV = MultiGPResultMV
-  { mgpmvMean   :: [LA.Vector Double]
-  , mgpmvLower  :: [LA.Vector Double]
-  , mgpmvUpper  :: [LA.Vector Double]
-  , mgpmvModels :: [GPModel]
-  } deriving (Show)
-
--- | Multi-output GP fit with multivariate input and /shared/ RBF
--- hyperparameters (default API).
---
--- Mirrors @sklearn.gaussian_process.GaussianProcessRegressor@'s
--- @fit(X, Y::(n,q))@ behaviour: one HP optimisation against the
--- pooled marginal likelihood @Σ_q log p(y_q | θ)@, then a single
--- Cholesky factor of @Ky = K + σ_n² I@ reused for every output's
--- posterior solve. Roughly @q@-fold faster than 'fitMultiGPMVIndep'
--- when @q > 1@.
---
--- RBF only. For other kernels (Matérn 5/2, periodic) or per-output
--- length-scales, use 'fitMultiGPMVIndep'.
-fitMultiGPMV
-  :: LA.Matrix Double          -- ^ Training @X@ (@n × p@).
-  -> [LA.Vector Double]        -- ^ Per-output training values (length @q@).
-  -> LA.Matrix Double          -- ^ Test inputs (@m × p@).
-  -> MultiGPResultMV
-fitMultiGPMV trainX trainYs testX =
-  let q       = length trainYs
-      yMat    = LA.fromColumns trainYs                   -- n × q
-      sharedD = KD.pairwiseSqDist trainX
-      -- Use the first output as the reference for HP initial values
-      -- (any output works; the result of the joint optimisation is
-      -- the same).
-      p0      = case trainYs of
-                  (y0 : _) -> initParamsFromDataMV trainX y0
-                  []       -> error "fitMultiGPMV: no outputs"
-      pOpt    = optimizeRBFAnalyticMulti sharedD trainX yMat p0
-      mdl     = GPModel RBF pOpt
-      results = [ fitGPMV mdl trainX yi testX | yi <- trainYs ]
-  in MultiGPResultMV
-       { mgpmvMean   = map gpmvMean   results
-       , mgpmvLower  = map gpmvLower  results
-       , mgpmvUpper  = map gpmvUpper  results
-       , mgpmvModels = replicate q mdl  -- shared model
-       }
-
--- | Like 'Hanalyze.Model.GP.optimizeRBFAnalytic' but the marginal likelihood is
--- the /sum/ over @q@ outputs sharing one kernel — single HP fit.
---
--- Internally factor Ky once per LBFGS step, solve @α = Ky⁻¹ Y@ as one
--- @n × q@ RHS, and assemble the gradient via
--- @∇L = ½ tr((α αᵀ − q Ky⁻¹) ∂Ky/∂θ)@.
-optimizeRBFAnalyticMulti
-  :: LA.Matrix Double          -- ^ Pre-computed @D = pairwiseSqDist trainX@.
-  -> LA.Matrix Double          -- ^ Training @X@ (used only for shape; actual
-                               --   computations go through @D@).
-  -> LA.Matrix Double          -- ^ @Y@ (@n × q@), one column per output.
-  -> GPParams                  -- ^ Initial params.
-  -> GPParams
-optimizeRBFAnalyticMulti d2 trainX yMat p0 =
-  let n     = LA.rows trainX
-      q     = LA.cols yMat
-      qD    = fromIntegral q :: Double
-      cfg   = optimizerConfig
-      u0v   = LA.fromList
-                [ log (gpLengthScale p0)
-                , log (gpSignalVar  p0)
-                , log (gpNoiseVar   p0) ]
-
-      buildK uv =
-        let !ll  = exp (uv `LA.atIndex` 0)
-            !sf2 = exp (uv `LA.atIndex` 1)
-            !sn2 = exp (uv `LA.atIndex` 2)
-            !inv2L2 = 1 / (2 * ll * ll)
-            !kMat = LA.cmap (\s -> sf2 * exp (- s * inv2L2)) d2
-            !kyM  = kMat + LA.scale sn2 (LA.ident n)
-        in (ll, sf2, sn2, kMat, kyM)
-
-      objV uv =
-        let (_, _, _, _, kyM) = buildK uv
-        in case Chol.cholFactor kyM of
-             Nothing -> -1e30
-             Just r  ->
-               let logDet = 2 * sum (map log (LA.toList (LA.takeDiag r)))
-                   alpha  = Chol.cholSolveWithFactor r yMat   -- n × q
-                   -- Σ_q y_qᵀ α_q  =  trace(Yᵀ α)  =  elementwise sum (Y ⊙ α)
-                   dataFit = LA.sumElements (yMat * alpha)
-               in -0.5 * dataFit - 0.5 * qD * logDet
-                  - fromIntegral n * qD / 2 * log (2 * pi)
-
-      gradV uv =
-        let (ll, _sf2, sn2, kMat, kyM) = buildK uv
-        in case Chol.cholFactor kyM of
-             Nothing -> LA.fromList [0, 0, 0]
-             Just r  ->
-               let alpha = Chol.cholSolveWithFactor r yMat       -- n × q
-                   kyInv = Chol.cholSolveWithFactor r (LA.ident n)
-                   -- Σ_q (α_qᵀ V α_q) = elementwise sum of (α ⊙ (V α))
-                   sumAVA v =
-                     let vAlpha = v LA.<> alpha                 -- n × q
-                     in LA.sumElements (alpha * vAlpha)
-                   -- ∂Ky/∂(log ℓ)
-                   !invL2 = 1 / (ll * ll)
-                   !vL    = LA.scale invL2 (kMat * d2)
-                   !aVa_L = sumAVA vL
-                   !tr_L  = LA.sumElements (kyInv * vL)
-                   !gLogL = 0.5 * (aVa_L - qD * tr_L)
-                   -- ∂Ky/∂(log σ_f²) = K
-                   !aVa_K = sumAVA kMat
-                   !tr_K  = LA.sumElements (kyInv * kMat)
-                   !gLogSf = 0.5 * (aVa_K - qD * tr_K)
-                   -- ∂Ky/∂(log σ_n²) = σ_n² I
-                   !aVa_I = LA.sumElements (alpha * alpha)        -- ‖α‖²_F
-                   !tr_I  = LA.sumElements (LA.takeDiag kyInv)
-                   !gLogSn = 0.5 * sn2 * (aVa_I - qD * tr_I)
-               in LA.fromList [gLogL, gLogSf, gLogSn]
-
-      result = unsafePerformIO $ LBFGS.runLBFGSWithV cfg objV gradV u0v
-      uOpt   = OC.orBest result
-  in p0
-       { gpLengthScale = exp (uOpt !! 0)
-       , gpSignalVar   = exp (uOpt !! 1)
-       , gpNoiseVar    = exp (uOpt !! 2)
-       }
-  where
-    optimizerConfig =
-      LBFGS.defaultLBFGSConfig
-        { LBFGS.lbDir   = OC.Maximize
-        , LBFGS.lbStop  = OC.defaultStopCriteria
-                            { OC.stMaxIter = 200, OC.stTolFun = 1e-8 }
-        }
-
--- | Multi-output GP fit with multivariate input and /independent/
--- per-output hyperparameters.
---
--- Each output column runs its own LBFGS HP optimisation via
--- @optimizeGPMVCached@. Supports any 'Kernel' kind (RBF / Matérn 5/2
--- / periodic). Costs @q × O(LBFGS)@; use 'fitMultiGPMV' (shared HP)
--- for a roughly @q@-fold speed-up when outputs are homogeneous.
---
--- The pairwise distance matrix @D = pairwiseSqDist X@ is shared
--- across the @q@ per-output optimisations to save @(q − 1) × O(n²)@
--- work.
-fitMultiGPMVIndep
-  :: Kernel
-  -> LA.Matrix Double          -- ^ Training @X@ (@n × p@).
-  -> [LA.Vector Double]        -- ^ Per-output training values (length @q@).
-  -> LA.Matrix Double          -- ^ Test inputs (@m × p@).
-  -> MultiGPResultMV
-fitMultiGPMVIndep kern trainX trainYs testX =
-  let -- @D = pairwiseSqDist trainX@ is shared across all q outputs
-      -- (trainX is the same input matrix), so we compute it once and
-      -- pass it into 'optimizeGPMVCached'. Each output's HP loop then
-      -- re-uses the same @D@ instead of recomputing it inside its own
-      -- per-output cache. Saves @(q − 1) × O(n²)@ work for kernel that
-      -- uses the isotropic length scale.
-      sharedD = KD.pairwiseSqDist trainX
-      perOutput :: LA.Vector Double -> (GPModel, GPResultMV)
-      perOutput trainY =
-        let p0   = initParamsFromDataMV trainX trainY
-            pOpt = optimizeGPMVCached kern (Just sharedD) trainX trainY p0
-            mdl  = GPModel kern pOpt
-            res  = fitGPMV mdl trainX trainY testX
-        in (mdl, res)
-      pairs   = map perOutput trainYs
-      models  = map fst pairs
-      results = map snd pairs
-  in MultiGPResultMV
-       { mgpmvMean   = map gpmvMean   results
-       , mgpmvLower  = map gpmvLower  results
-       , mgpmvUpper  = map gpmvUpper  results
-       , mgpmvModels = models
-       }
diff --git a/src/Hanalyze/Model/MultiLM.hs b/src/Hanalyze/Model/MultiLM.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/MultiLM.hs
+++ /dev/null
@@ -1,82 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.MultiLM
--- Description : Multivariate (multi-output) linear regression — 列別 OLS + 残差共分散推定
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Multivariate (multi-output) linear regression.
---
--- @Y = XB + E@ with @Y@ of shape @n × q@ (@q@ outputs), @X@ of shape
--- @n × p@, @B@ of shape @p × q@ and @E@ of shape @n × q@.
---
--- Solves each column independently by OLS (column-wise OLS) and
--- additionally estimates the residual covariance matrix @Σ@, which is
--- used for joint multi-output predictive intervals.
---
--- The API matches 'Hanalyze.Model.LM', so @fitLM@ can be called directly; this
--- module merely exposes the additional multi-output information
--- (@Σ@, correlation matrix).
-module Hanalyze.Model.MultiLM
-  ( MultiFit (..)
-  , fitMultiLM
-  , predictMultiLM
-  , residualCovariance
-  , residualCorrelation
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import Hanalyze.Model.Core (FitResult (..))
-import qualified Hanalyze.Model.LM as LM
-
--- | Augmented result for multi-output linear regression.
-data MultiFit = MultiFit
-  { mfFit         :: FitResult        -- ^ Underlying matrix-based fit.
-  , mfResidCov    :: LA.Matrix Double -- ^ Residual covariance @Σ@ (@q × q@).
-  , mfResidCor    :: LA.Matrix Double -- ^ Residual correlation matrix (@q × q@).
-  , mfNumOutputs  :: Int              -- ^ Number of responses @q@.
-  , mfNumPredict  :: Int              -- ^ Number of predictors @p@.
-  , mfNumSamples  :: Int              -- ^ Number of observations @n@.
-  } deriving (Show)
-
--- | Multi-output linear regression: @Y = XB + E@.
--- Delegates to 'LM.fitLM' and additionally returns the residual
--- covariance.
-fitMultiLM :: LA.Matrix Double  -- ^ Design matrix @X@ (@n × p@).
-           -> LA.Matrix Double  -- ^ Response @Y@ (@n × q@).
-           -> MultiFit
-fitMultiLM x y =
-  let fit = LM.fitLM x y
-      res = residuals fit
-      n   = LA.rows y
-      q   = LA.cols y
-      p   = LA.cols x
-      df  = max 1 (n - p)   -- 自由度補正
-      -- Σ = (1/(n-p)) * Eᵀ E
-      sigma = LA.scale (1 / fromIntegral df)
-                       (LA.tr res LA.<> res)
-      -- 相関行列: D⁻¹ Σ D⁻¹ where D = diag(sqrt(diag(Σ)))
-      diagS = [ sqrt (sigma `LA.atIndex` (i, i))
-              | i <- [0 .. q - 1] ]
-      corr  = LA.fromLists
-        [ [ if di == 0 || dj == 0 then 0
-            else (sigma `LA.atIndex` (i, j)) / (di * dj)
-          | j <- [0 .. q - 1]
-          , let dj = diagS !! j ]
-        | i <- [0 .. q - 1]
-        , let di = diagS !! i ]
-  in MultiFit fit sigma corr q p n
-
--- | Predict @Ŷ@ (@m × q@) for new inputs @X_new@ (@m × p@). A thin
--- wrapper around 'LM.predictLM'.
-predictMultiLM :: MultiFit -> LA.Matrix Double -> LA.Matrix Double
-predictMultiLM mf xNew =
-  LM.predictLM (coefficients (mfFit mf)) xNew
-
--- | Residual covariance matrix (alias for 'mfResidCov').
-residualCovariance :: MultiFit -> LA.Matrix Double
-residualCovariance = mfResidCov
-
--- | Residual correlation matrix.
-residualCorrelation :: MultiFit -> LA.Matrix Double
-residualCorrelation = mfResidCor
diff --git a/src/Hanalyze/Model/MultiOutput.hs b/src/Hanalyze/Model/MultiOutput.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/MultiOutput.hs
+++ /dev/null
@@ -1,85 +0,0 @@
--- |
--- Module      : Hanalyze.Model.MultiOutput
--- Description : Common foundation for multi-output regression (単出力 ↔ 多出力変換 + 評価指標)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Common foundation for multi-output regression.
---
--- Design policy:
---
---   * Each model's /primary/ API takes the response @Y@ as
---     @LA.Matrix Double@ (@n × q@) and returns a matrix; the @q = 1@ case
---     is a specialization.
---   * The single-output API (@V.Vector Double@) is a thin wrapper that
---     promotes the response to a one-column matrix via 'asMultiY' /
---     'fromMultiY' and reuses the multi-output implementation.
---   * Per-output evaluation metrics (R² etc.) are collected here.
-module Hanalyze.Model.MultiOutput
-  ( -- * 単出力 ↔ 多出力 変換
-    asMultiY
-  , fromMultiY
-  , asMultiYV
-    -- * Multi-output evaluation metrics
-  , rmseMulti
-  , r2Multi
-  , mseMulti
-  ) where
-
-import qualified Data.Vector as V
-import qualified Numeric.LinearAlgebra as LA
-
--- ---------------------------------------------------------------------------
--- 変換
--- ---------------------------------------------------------------------------
-
--- | Promote a 1D 'V.Vector' to an @n × 1@ matrix.
---
--- >>> import qualified Data.Vector as V
--- >>> LA.rows (asMultiY (V.fromList [1.0, 2.0, 3.0]))
--- 3
--- >>> LA.cols (asMultiY (V.fromList [1.0, 2.0, 3.0]))
--- 1
-asMultiY :: V.Vector Double -> LA.Matrix Double
-asMultiY = LA.asColumn . LA.fromList . V.toList
-
--- | Promote an hmatrix 'LA.Vector' to an @n × 1@ matrix.
-asMultiYV :: LA.Vector Double -> LA.Matrix Double
-asMultiYV = LA.asColumn
-
--- | Convert an @n × 1@ matrix back to a 1D vector. When @q ≠ 1@, returns
--- the first column.
-fromMultiY :: LA.Matrix Double -> V.Vector Double
-fromMultiY m
-  | LA.cols m == 0 = V.empty
-  | otherwise      = V.fromList (LA.toList (LA.flatten (m LA.¿ [0])))
-
--- ---------------------------------------------------------------------------
--- 評価指標
--- ---------------------------------------------------------------------------
-
--- | Whole-matrix MSE: sum-of-squares divided by @n × q@.
-mseMulti :: LA.Matrix Double -> LA.Matrix Double -> Double
-mseMulti ys yhat =
-  let n = LA.rows ys
-      q = LA.cols ys
-      r = ys - yhat
-  in LA.sumElements (r * r) / fromIntegral (n * q)
-
--- | Whole-matrix RMSE.
-rmseMulti :: LA.Matrix Double -> LA.Matrix Double -> Double
-rmseMulti ys yhat = sqrt (mseMulti ys yhat)
-
--- | Per-column R² (vector of length @q@).
-r2Multi :: LA.Matrix Double -> LA.Matrix Double -> V.Vector Double
-r2Multi ys yhat =
-  let n  = LA.rows ys
-      q  = LA.cols ys
-      colR2 j =
-        let yc  = LA.toList (LA.flatten (ys   LA.¿ [j]))
-            yhc = LA.toList (LA.flatten (yhat LA.¿ [j]))
-            mu  = sum yc / fromIntegral n
-            sst = sum [(y - mu)^(2::Int) | y <- yc]
-            sse = sum [(y - p)^(2::Int) | (y, p) <- zip yc yhc]
-        in if sst == 0 then 0 else 1 - sse / sst
-  in V.fromList [ colR2 j | j <- [0 .. q - 1] ]
diff --git a/src/Hanalyze/Model/Multivariate.hs b/src/Hanalyze/Model/Multivariate.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Multivariate.hs
+++ /dev/null
@@ -1,186 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.Multivariate
--- Description : Specialized multivariate regression — Reduced-Rank Regression / PLS / CCA
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Specialized multivariate regression: Reduced-Rank Regression, PLS,
--- and CCA.
---
--- These all express the relationship between a multi-response @Y@
--- (@n × q@) and multi-predictor @X@ (@n × p@) via a low-rank structure.
---
---   * 'reducedRankRegression' — @B = U_r V_rᵀ@ (rank-@r@ constraint).
---   * 'pls'                   — extracts directions of maximum
---     @X@-@Y@ covariance one at a time.
---   * 'cca'                   — canonical pairs maximizing @X@-@Y@
---     correlation.
-module Hanalyze.Model.Multivariate
-  ( -- * Reduced Rank Regression
-    RRRFit (..)
-  , reducedRankRegression
-  , predictRRR
-    -- * Partial Least Squares
-  , PLSFit (..)
-  , pls
-  , predictPLS
-    -- * Canonical Correlation Analysis
-  , CCAFit (..)
-  , cca
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-
--- ---------------------------------------------------------------------------
--- Reduced Rank Regression
--- ---------------------------------------------------------------------------
-
--- | Reduced-Rank Regression result. The coefficient matrix @B@ is
--- constrained to rank @r@.
-data RRRFit = RRRFit
-  { rrrBeta :: LA.Matrix Double  -- ^ @B@ of shape @p × q@ (rank @≤ r@).
-  , rrrU    :: LA.Matrix Double  -- ^ Left factor (@p × r@).
-  , rrrV    :: LA.Matrix Double  -- ^ Right factor (@q × r@).
-  , rrrRank :: Int               -- ^ Effective rank.
-  } deriving (Show)
-
--- | Reduced-Rank Regression: @B = U Vᵀ@ with rank @r@.
---
--- The OLS estimate @B̂@ is SVD-truncated to its top @r@ singular values:
--- @B̂_RRR = U_r Σ_r V_rᵀ@.
-reducedRankRegression :: Int                -- ^ Target rank @r@.
-                     -> LA.Matrix Double    -- ^ Design matrix @X@ (@n × p@).
-                     -> LA.Matrix Double    -- ^ Response @Y@ (@n × q@).
-                     -> RRRFit
-reducedRankRegression r x y =
-  let bOLS = x LA.<\> y                -- OLS: p × q
-      (u, sv, vt) = LA.svd bOLS
-      r' = min r (LA.size sv)
-      uR = u LA.?? (LA.All, LA.Take r')
-      sR = LA.subVector 0 r' sv
-      vR = (LA.tr vt) LA.?? (LA.All, LA.Take r')
-      bRRR = uR LA.<> LA.diag sR LA.<> LA.tr vR
-  in RRRFit bRRR uR vR r'
-
--- | Predict @Ŷ@ for new inputs from a 'RRRFit'.
-predictRRR :: RRRFit -> LA.Matrix Double -> LA.Matrix Double
-predictRRR fit xNew = xNew LA.<> rrrBeta fit
-
--- ---------------------------------------------------------------------------
--- Partial Least Squares (NIPALS algorithm)
--- ---------------------------------------------------------------------------
-
--- | PLS fit result.
-data PLSFit = PLSFit
-  { plsBeta :: LA.Matrix Double  -- ^ Regression coefficients (@p × q@).
-  , plsW    :: LA.Matrix Double  -- ^ Weights (@p × k@).
-  , plsT    :: LA.Matrix Double  -- ^ Scores (@n × k@).
-  , plsP    :: LA.Matrix Double  -- ^ Loadings (@p × k@).
-  , plsQ    :: LA.Matrix Double  -- ^ Y-loadings (@q × k@).
-  , plsK    :: Int               -- ^ Number of components extracted.
-  } deriving (Show)
-
--- | NIPALS-PLS (Wold 1975). Extracts @k@ components sequentially.
---
--- For each component:
---
---   1. @w = Xᵀ Y u / ‖Xᵀ Y u‖@ — the X-side weight (@u@ is the Y direction).
---   2. @t = X w@.
---   3. @p = Xᵀ t / (tᵀ t)@.
---   4. @q = Yᵀ t / (tᵀ t)@.
---   5. Deflate: @X ← X − t pᵀ@, @Y ← Y − t qᵀ@.
-pls :: Int                      -- ^ Number of components @k@.
-    -> LA.Matrix Double         -- ^ Design matrix @X@ (@n × p@).
-    -> LA.Matrix Double         -- ^ Response @Y@ (@n × q@).
-    -> PLSFit
-pls k x0 y0 =
-  let p = LA.cols x0
-      q = LA.cols y0
-      n = LA.rows x0
-      _ = n
-      go' iter xCur yCur ws ts ps qs
-        | iter >= k = (reverse ws, reverse ts, reverse ps, reverse qs)
-        | otherwise =
-            let u    = LA.flatten (yCur LA.¿ [0])
-                xtyu = LA.tr xCur LA.#> u
-                w    = if LA.norm_2 xtyu > 1e-12
-                         then LA.scale (1 / LA.norm_2 xtyu) xtyu
-                         else LA.fromList (replicate p 0)
-                t    = xCur LA.#> w
-                tt   = max 1e-12 (LA.dot t t)
-                pVec = LA.scale (1/tt) (LA.tr xCur LA.#> t)
-                qVec = LA.scale (1/tt) (LA.tr yCur LA.#> t)
-                xNew = xCur - LA.outer t pVec
-                yNew = yCur - LA.outer t qVec
-            in go' (iter + 1) xNew yNew (w:ws) (t:ts) (pVec:ps) (qVec:qs)
-      (wsL, tsL, psL, qsL) = go' 0 x0 y0 [] [] [] []
-      wM = LA.fromColumns wsL  -- p × k
-      tM = LA.fromColumns tsL  -- n × k
-      pM = LA.fromColumns psL  -- p × k
-      qM = LA.fromColumns qsL  -- q × k
-      -- 回帰係数: B = W (PᵀW)⁻¹ Qᵀ (Wold formula)
-      ptw = LA.tr pM LA.<> wM   -- k × k
-      bMat = wM LA.<> LA.inv ptw LA.<> LA.tr qM   -- p × q
-      _ = q
-  in PLSFit bMat wM tM pM qM k
-
--- | Predict @Ŷ@ for new inputs from a 'PLSFit'.
-predictPLS :: PLSFit -> LA.Matrix Double -> LA.Matrix Double
-predictPLS fit xNew = xNew LA.<> plsBeta fit
-
--- ---------------------------------------------------------------------------
--- Canonical Correlation Analysis
--- ---------------------------------------------------------------------------
-
--- | CCA fit result.
-data CCAFit = CCAFit
-  { ccaA       :: LA.Matrix Double  -- ^ X-side basis (@p × r@).
-  , ccaB       :: LA.Matrix Double  -- ^ Y-side basis (@q × r@).
-  , ccaCorr    :: LA.Vector Double  -- ^ Canonical correlations (length @r@).
-  , ccaScoresX :: LA.Matrix Double  -- ^ X scores (@n × r@).
-  , ccaScoresY :: LA.Matrix Double  -- ^ Y scores (@n × r@).
-  } deriving (Show)
-
--- | Canonical Correlation Analysis: find basis pairs @(a_k, b_k)@ that
--- maximize the correlation between @X@ and @Y@.
---
--- Algorithm:
---
---   1. Compute @C_xx = XᵀX/(n-1)@, @C_yy@, @C_xy@.
---   2. SVD of @M = C_xx^{−1/2} C_xy C_yy^{−1/2}@: @M = U Σ Vᵀ@.
---   3. @a = C_xx^{−1/2} U@, @b = C_yy^{−1/2} V@, correlations = @Σ@.
-cca :: LA.Matrix Double -> LA.Matrix Double -> CCAFit
-cca x y =
-  let n  = fromIntegral (LA.rows x) :: Double
-      _p = LA.cols x
-      _q = LA.cols y
-      -- 中心化
-      meanCol m = LA.fromList [LA.sumElements (LA.flatten (m LA.¿ [j])) / n
-                              | j <- [0 .. LA.cols m - 1]]
-      mxs = meanCol x
-      mys = meanCol y
-      cx0 i = LA.flatten (x LA.¿ [i]) - LA.scalar (mxs LA.! i)
-      cy0 i = LA.flatten (y LA.¿ [i]) - LA.scalar (mys LA.! i)
-      xC  = LA.fromColumns [cx0 i | i <- [0 .. LA.cols x - 1]]
-      yC  = LA.fromColumns [cy0 i | i <- [0 .. LA.cols y - 1]]
-      -- 共分散
-      cxx = LA.scale (1 / (n - 1)) (LA.tr xC LA.<> xC)
-      cyy = LA.scale (1 / (n - 1)) (LA.tr yC LA.<> yC)
-      cxy = LA.scale (1 / (n - 1)) (LA.tr xC LA.<> yC)
-      -- 平方根逆行列 (固有値分解で計算)
-      invSqrt sym =
-        let (eigs, evec) = LA.eigSH (LA.sym sym)
-            invSqrtVals = LA.fromList
-              [ if v > 1e-12 then 1 / sqrt v else 0
-              | v <- LA.toList eigs ]
-        in evec LA.<> LA.diag invSqrtVals LA.<> LA.tr evec
-      cxxIS = invSqrt cxx
-      cyyIS = invSqrt cyy
-      mMat  = cxxIS LA.<> cxy LA.<> cyyIS
-      (uM, sM, vtM) = LA.svd mMat
-      aMat = cxxIS LA.<> uM
-      bMat = cyyIS LA.<> LA.tr vtM
-      scoresX = xC LA.<> aMat
-      scoresY = yC LA.<> bMat
-  in CCAFit aMat bMat sM scoresX scoresY
diff --git a/src/Hanalyze/Model/NaiveBayes.hs b/src/Hanalyze/Model/NaiveBayes.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/NaiveBayes.hs
+++ /dev/null
@@ -1,166 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Model.NaiveBayes
--- Description : Naive Bayes 分類 (Gaussian + Multinomial)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Naive Bayes 分類 (Gaussian + Multinomial).
---
--- @
--- import qualified Hanalyze.Model.NaiveBayes as NB
--- let nb = NB.fitGNB x y                    -- 連続特徴: Gaussian
---     yhat = NB.predictNB nb x
---
--- let mnb = NB.fitMNB 1.0 xCounts yCount    -- カウント特徴: Multinomial (Laplace α)
--- @
-module Hanalyze.Model.NaiveBayes
-  ( -- * Gaussian NB
-    GaussianNB (..)
-  , fitGNB
-    -- * Multinomial NB
-  , MultinomialNB (..)
-  , fitMNB
-    -- * Predict (両対応)
-  , NBModel (..)
-  , predictNB
-  , predictNBLogProbs
-  ) where
-
-import qualified Data.Vector.Unboxed   as VU
-import qualified Numeric.LinearAlgebra as LA
-import           Data.Text             (Text)
-import           Data.List             (nub, sort, foldl')
-
--- ---------------------------------------------------------------------------
--- Gaussian NB
--- ---------------------------------------------------------------------------
-
--- | クラスごとに各特徴を独立 Gaussian と仮定。
-data GaussianNB = GaussianNB
-  { gnbClasses    :: ![Int]
-  , gnbLogPrior   :: ![Double]           -- ^ log π_c (classes 順)
-  , gnbMeans      :: ![LA.Vector Double] -- ^ 各クラスの μ (length d)
-  , gnbVars       :: ![LA.Vector Double] -- ^ 各クラスの σ² (length d)、 var smoothing 済
-  , gnbClassNames :: ![Text]             -- ^ クラス名 (df|-> が levels 注入・空=数値表示)。
-  } deriving (Show)
-
--- | sklearn 互換の var smoothing (最大 var の 1e-9 倍を全 var に加算)。
-varSmoothing :: Double
-varSmoothing = 1e-9
-
-fitGNB :: LA.Matrix Double -> VU.Vector Int -> GaussianNB
-fitGNB x y =
-  let !n        = VU.length y
-      !d        = LA.cols x
-      classes   = sort (nub (VU.toList y))
-      rows c    = [ i | i <- [0 .. n - 1], y VU.! i == c ]
-      meanV ids =
-        let m = LA.fromRows [ LA.flatten (x LA.? [i]) | i <- ids ]
-            nc = fromIntegral (length ids) :: Double
-        in LA.scale (1 / nc) (LA.fromList (map LA.sumElements (LA.toColumns m)))
-      varV ids mu =
-        let nc = fromIntegral (length ids) :: Double
-            sq i = let r = LA.flatten (x LA.? [i]) - mu
-                   in r * r
-            sumSq = sum (map sq ids)
-        in LA.scale (1 / nc) sumSq
-      mus  = [ meanV (rows c) | c <- classes ]
-      vrs0 = zipWith (\c mu -> varV (rows c) mu) classes mus
-      maxVar = maximum (map (LA.maxElement . LA.cmap abs) vrs0)
-      eps    = varSmoothing * maxVar + 1e-300
-      vrs    = map (LA.cmap (+ eps)) vrs0
-      priors = [ log (fromIntegral (length (rows c)) / fromIntegral n)
-               | c <- classes ]
-      _ = d  -- d は使わない (内部で LA.size に頼る)
-  in GaussianNB classes priors mus vrs []
-
--- | log p(x | c) = -1/2 Σ_j [ log(2π σ²_j) + (x_j - μ_j)² / σ²_j ]
-gnbLogLik :: GaussianNB -> LA.Vector Double -> [Double]
-gnbLogLik nb xv =
-  [ let r   = xv - mu
-        rsq = r * r
-        logT = LA.sumElements (LA.cmap log (LA.scale (2 * pi) vr))
-        chiT = LA.sumElements (rsq / vr)
-    in -0.5 * (logT + chiT)
-  | (mu, vr) <- zip (gnbMeans nb) (gnbVars nb) ]
-
--- ---------------------------------------------------------------------------
--- Multinomial NB
--- ---------------------------------------------------------------------------
-
--- | テキスト分類等のカウント特徴用。 ラプラス平滑化 α (典型 1.0)。
-data MultinomialNB = MultinomialNB
-  { mnbClasses    :: ![Int]
-  , mnbLogPrior   :: ![Double]
-  , mnbLogFeat    :: ![LA.Vector Double]   -- ^ log p(feature_j | c)
-  , mnbClassNames :: ![Text]               -- ^ クラス名 (df|-> が levels 注入・空=数値表示)。
-  } deriving (Show)
-
-fitMNB :: Double             -- ^ Laplace α
-       -> LA.Matrix Double  -- ^ 非負カウント (n × d)
-       -> VU.Vector Int     -- ^ y
-       -> MultinomialNB
-fitMNB alpha x y =
-  let !n       = VU.length y
-      !d       = LA.cols x
-      classes  = sort (nub (VU.toList y))
-      rows c   = [ i | i <- [0 .. n - 1], y VU.! i == c ]
-      sumRows ids =
-        foldl' (+) (LA.konst 0 d)
-          [ LA.flatten (x LA.? [i]) | i <- ids ]
-      featLog c =
-        let s     = sumRows (rows c)
-            !sNum = LA.cmap (+ alpha) s
-            !tot  = LA.sumElements sNum
-        in LA.cmap log (LA.scale (1 / tot) sNum)
-      priors = [ log (fromIntegral (length (rows c)) / fromIntegral n)
-               | c <- classes ]
-  in MultinomialNB classes priors [ featLog c | c <- classes ] []
-
-mnbLogLik :: MultinomialNB -> LA.Vector Double -> [Double]
-mnbLogLik nb xv =
-  [ LA.dot xv lf | lf <- mnbLogFeat nb ]
-
--- ---------------------------------------------------------------------------
--- 共通インターフェース
--- ---------------------------------------------------------------------------
-
-data NBModel = NBGaussian GaussianNB | NBMultinomial MultinomialNB
-  deriving (Show)
-
-nbClasses :: NBModel -> [Int]
-nbClasses (NBGaussian m)    = gnbClasses m
-nbClasses (NBMultinomial m) = mnbClasses m
-
-nbLogPriorAndLik :: NBModel -> LA.Vector Double -> ([Double], [Double])
-nbLogPriorAndLik (NBGaussian m) xv    = (gnbLogPrior m, gnbLogLik m xv)
-nbLogPriorAndLik (NBMultinomial m) xv = (mnbLogPrior m, mnbLogLik m xv)
-
-predictNBLogProbs :: NBModel -> LA.Matrix Double -> [[Double]]
-predictNBLogProbs nb x =
-  let !n = LA.rows x
-      row i = LA.flatten (x LA.? [i])
-      logits xv =
-        let (lp, ll) = nbLogPriorAndLik nb xv
-        in zipWith (+) lp ll
-      -- log-sum-exp 正規化
-      lse zs =
-        let !mx = maximum zs
-        in mx + log (sum [ exp (z - mx) | z <- zs ])
-      one i =
-        let zs = logits (row i)
-            z  = lse zs
-        in [ k - z | k <- zs ]
-  in [ one i | i <- [0 .. n - 1] ]
-
-predictNB :: NBModel -> LA.Matrix Double -> VU.Vector Int
-predictNB nb x =
-  let probs = predictNBLogProbs nb x
-      classes = nbClasses nb
-      pick zs =
-        let (cMax, _) = foldr1
-                          (\(c, v) (c', v') -> if v >= v' then (c, v) else (c', v'))
-                          (zip classes zs)
-        in cMax
-  in VU.fromList (map pick probs)
diff --git a/src/Hanalyze/Model/NeuralNetwork.hs b/src/Hanalyze/Model/NeuralNetwork.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/NeuralNetwork.hs
+++ /dev/null
@@ -1,502 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Model.NeuralNetwork
--- Description : Multi-Layer Perceptron (MLP) — feedforward neural network (mini-batch SGD + Adam)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Multi-Layer Perceptron (MLP) — feedforward neural network。
---
--- Mini-batch SGD + 自前 Adam で学習。 hmatrix Matrix/Vector で全演算。
---
--- 対応:
---
---   * 'fitMLPRegressor': 出力 1 次元の回帰 (MSE loss)
---   * 'fitMLPClassifier': 多クラス分類 (cross-entropy + softmax 出力)
---   * 'predictMLP': forward 推論
---
--- 隠れ層の活性化は ReLU 既定、 出力層は task に応じて自動 (回帰=Identity、
--- 分類=Softmax)。
-module Hanalyze.Model.NeuralNetwork
-  ( Activation (..)
-  , MLPConfig (..)
-  , defaultMLP
-  , Layer (..)
-  , MLPFit (..)
-  , MLPEpochEvent (..)
-  , fitMLPRegressor
-  , fitMLPRegressorWithCallback
-  , fitMLPRegressorPure
-  , fitMLPClassifier
-  , fitMLPClassifierWithCallback
-  , fitMLPClassifierPure
-  , predictMLP
-  , predictMLPClass
-  ) where
-
-import qualified Data.Vector             as V
-import qualified Data.Vector.Unboxed     as VU
-import           Data.Text               (Text)
-import qualified Numeric.LinearAlgebra   as LA
-import           Control.Monad           (forM_)
-import           Control.Monad.Primitive (PrimMonad, PrimState)
-import           Control.Monad.ST        (runST)
-import           Data.Primitive.MutVar   (newMutVar, readMutVar, writeMutVar,
-                                          modifyMutVar')
-import           Data.Word               (Word32)
-import qualified System.Random.MWC       as MWC
-import           System.Random.MWC       (initialize)
-import           System.Random.MWC.Distributions (standard)
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
-data Activation = ReLU | Sigmoid | Tanh | Identity | Softmax
-  deriving (Show, Eq)
-
-data Layer = Layer
-  { lyrW :: !(LA.Matrix Double)   -- (in × out)
-  , lyrB :: !(LA.Vector Double)   -- (out)
-  , lyrAct :: !Activation
-  } deriving (Show)
-
-data MLPConfig = MLPConfig
-  { mlpHidden    :: ![Int]
-  , mlpActHidden :: !Activation
-  , mlpLR        :: !Double
-  , mlpEpochs    :: !Int
-  , mlpBatch     :: !Int
-  , mlpL2        :: !Double
-  , mlpStandardize :: !Bool
-    -- ^ True で X を z-score 標準化してから学習 (predict 時は同じ
-    --   mean/std で逆変換)。 Phase 17.3 で追加、 default True。
-  } deriving (Show)
-
-defaultMLP :: MLPConfig
-defaultMLP = MLPConfig
-  { mlpHidden    = [16]
-  , mlpActHidden = ReLU
-  , mlpLR        = 0.01
-  , mlpEpochs    = 200
-  , mlpBatch     = 16
-  , mlpL2        = 0
-  , mlpStandardize = True
-  }
-
-data MLPFit = MLPFit
-  { mlpLayers   :: ![Layer]
-  , mlpLossHist :: ![Double]
-  , mlpClasses  :: ![Int]
-    -- ^ 分類器の場合の class label 順 (sorted)。 回帰時は空。
-  , mlpClassNames :: ![Text]
-    -- ^ クラス名 (df|-> が levels 注入・空=数値表示/回帰時は空)。
-  , mlpXMean    :: !(LA.Vector Double)
-    -- ^ X 標準化に使った列平均 (Phase 17.3、 標準化 off なら length 0)
-  , mlpXStd     :: !(LA.Vector Double)
-  , mlpYMean    :: !Double
-    -- ^ regressor の場合の y 平均 (標準化 off なら 0)
-  , mlpYStd     :: !Double
-  } deriving (Show)
-
--- ===========================================================================
--- 活性化
--- ===========================================================================
-
-applyAct :: Activation -> LA.Matrix Double -> LA.Matrix Double
-applyAct ReLU     = LA.cmap (\v -> max 0 v)
-applyAct Sigmoid  = LA.cmap (\v -> 1 / (1 + exp (-v)))
-applyAct Tanh     = LA.cmap tanh
-applyAct Identity = id
-applyAct Softmax  = softmaxRows
-
-actGrad :: Activation -> LA.Matrix Double -> LA.Matrix Double -> LA.Matrix Double
-actGrad ReLU     pre _   = LA.cmap (\v -> if v > 0 then 1 else 0) pre
-actGrad Sigmoid  _   out = out * (1 - out)
-actGrad Tanh     _   out = 1 - out * out
-actGrad Identity _   _   = LA.fromLists [[1 :: Double]]
-actGrad Softmax  _   _   = LA.fromLists [[1 :: Double]]
-
-softmaxRows :: LA.Matrix Double -> LA.Matrix Double
-softmaxRows m = LA.fromRows
-  [ let r = LA.flatten (m LA.? [i])
-        mx = LA.maxElement r
-        ex = LA.cmap (\v -> exp (v - mx)) r
-        s  = LA.sumElements ex
-    in LA.scale (1 / s) ex
-  | i <- [0 .. LA.rows m - 1] ]
-
--- ===========================================================================
--- 初期化
--- ===========================================================================
-
-initLayers :: PrimMonad m
-           => MWC.Gen (PrimState m) -> Int -> Int -> [Int] -> Activation -> Activation -> m [Layer]
-initLayers gen inDim outDim hidden hidAct outAct = do
-  let sizes = inDim : hidden ++ [outDim]
-      pairs = zip sizes (tail sizes)
-      acts  = replicate (length hidden) hidAct ++ [outAct]
-  mapM (\((nin, nout), act) -> do
-          let scale = sqrt (2 / fromIntegral nin)
-          ws <- mapM (\_ -> standard gen) [1 .. nin * nout]
-          let w = LA.scale scale
-                    (LA.fromLists (chunksOf nout ws))
-              b = LA.fromList (replicate nout 0)
-          pure (Layer w b act))
-       (zip pairs acts)
-  where
-    chunksOf _ [] = []
-    chunksOf n xs = take n xs : chunksOf n (drop n xs)
-
--- ===========================================================================
--- Forward pass
--- ===========================================================================
-
-forward :: [Layer] -> LA.Matrix Double -> [(LA.Matrix Double, LA.Matrix Double)]
-forward layers x = go x layers []
-  where
-    go _    []     acc = reverse acc
-    go inp (l:ls) acc =
-      let pre = addBias (inp LA.<> lyrW l) (lyrB l)
-          out = applyAct (lyrAct l) pre
-      in go out ls ((pre, out) : acc)
-
--- | Add bias vector (length = out) to every row of the (n × out) matrix.
-addBias :: LA.Matrix Double -> LA.Vector Double -> LA.Matrix Double
-addBias m b = m + LA.fromRows (replicate (LA.rows m) b)
-
--- ===========================================================================
--- Backprop (回帰 MSE)
--- ===========================================================================
-
--- | Backprop with MSE for regression OR cross-entropy with softmax for
---   classification. Output gradient at last layer differs by task:
---     reg:   dL/dz_out = (yhat - y) / n   (with Identity output)
---     class: dL/dz_out = (yhat - yOH) / n (softmax + CE simplification)
-backprop
-  :: [Layer]
-  -> LA.Matrix Double                       -- x (n × in)
-  -> LA.Matrix Double                       -- y (n × out) target
-  -> Bool                                   -- True = classification (softmax+CE)
-  -> Double                                 -- L2 weight
-  -> [(LA.Matrix Double, LA.Vector Double)] -- gradients (dW, dB) per layer
-backprop layers x y isClass l2 =
-  let cache = forward layers x   -- list of (pre, out) per layer
-      n     = fromIntegral (LA.rows x) :: Double
-      out_  = snd (last cache)
-      dPre_last
-        | isClass   = LA.scale (1/n) (out_ - y)
-        | otherwise = LA.scale (1/n) (out_ - y)   -- Identity output, same shape
-      -- walk backward
-      walk !dPre [] _ acc = acc
-      walk !dPre (l:ls) (c:cs) acc =
-        let -- input to layer l = (previous out) or x if first
-            inpToL = case cs of
-                       []      -> x
-                       (cPrev:_) -> snd cPrev
-            (preL, _) = c
-            dW = LA.tr inpToL LA.<> dPre + LA.scale l2 (lyrW l)
-            dB = LA.fromList [ LA.sumElements (dPre LA.¿ [j])
-                             | j <- [0 .. LA.cols dPre - 1] ]
-            -- propagate to previous layer
-            dOutPrev = dPre LA.<> LA.tr (lyrW l)
-            dPrePrev =
-              case ls of
-                []      -> dOutPrev  -- unused
-                (lPrev:_) ->
-                  let (prePrev, outPrev) = head cs
-                      g = actGrad (lyrAct lPrev) prePrev outPrev
-                  in dOutPrev * g
-        in walk dPrePrev ls cs ((dW, dB) : acc)
-      grads = walk dPre_last (reverse layers) (reverse cache) []
-  in grads
-
--- ===========================================================================
--- 学習ループ (Adam)
--- ===========================================================================
-
--- | Per-epoch event emitted by 'fitMLPRegressorWithCallback' /
--- 'fitMLPClassifierWithCallback'。 Phase 21 で追加。
-data MLPEpochEvent = MLPEpochEvent
-  { meEpoch     :: !Int
-    -- ^ 0-based epoch index (0..epochs-1)
-  , meTrainLoss :: !Double
-    -- ^ epoch 終端での full-batch training loss
-  , meValLoss   :: !(Maybe Double)
-    -- ^ validation split loss。 v1 では常に 'Nothing' (= reserved for future)
-  , meCurrentLR :: !Double
-    -- ^ そのときの学習率 (現在は constant scheduler のみ、 将来 LR scheduler
-    --   実装で意味が出る)
-  } deriving (Show)
-
-trainMLP
-  :: PrimMonad m
-  => MWC.Gen (PrimState m) -> MLPConfig
-  -> LA.Matrix Double -> LA.Matrix Double
-  -> Bool         -- isClass
-  -> (MLPEpochEvent -> m ())   -- per-epoch callback (no-op で旧挙動)
-  -> m ([Layer], [Double])
-trainMLP gen cfg x y isClass onEpoch = do
-  let inDim   = LA.cols x
-      outDim  = LA.cols y
-      outAct  = if isClass then Softmax else Identity
-  layers0 <- initLayers gen inDim outDim (mlpHidden cfg) (mlpActHidden cfg) outAct
-  -- Adam state per layer (mW, vW, mB, vB)
-  let zeroLike w = LA.scale 0 w
-      zeroLikeV v = LA.scale 0 v
-  state <- mapM (\l -> do
-                    mw <- newMutVar (zeroLike (lyrW l))
-                    vw <- newMutVar (zeroLike (lyrW l))
-                    mb <- newMutVar (zeroLikeV (lyrB l))
-                    vb <- newMutVar (zeroLikeV (lyrB l))
-                    pure (mw, vw, mb, vb)) layers0
-  layersRef <- newMutVar layers0
-  lossRef   <- newMutVar ([] :: [Double])
-  let n  = LA.rows x
-      lr = mlpLR cfg
-      b1 = 0.9
-      b2 = 0.999
-      eps = 1e-8
-  tRef <- newMutVar (0 :: Int)
-  forM_ [0 .. mlpEpochs cfg - 1] $ \epochIdx -> do
-    -- shuffle indices
-    idx <- fisherYates gen [0 .. n - 1]
-    let batches = chunksOf (mlpBatch cfg) idx
-    forM_ batches $ \batch -> do
-      let xb = x LA.? batch
-          yb = y LA.? batch
-      ls0 <- readMutVar layersRef
-      let grads = backprop ls0 xb yb isClass (mlpL2 cfg)
-      modifyMutVar' tRef (+1)
-      t <- readMutVar tRef
-      let tD = fromIntegral t :: Double
-          c1 = 1 - b1 ** tD
-          c2 = 1 - b2 ** tD
-      newLayers <-
-        mapM (\(l, (dW, dB), (mwR, vwR, mbR, vbR)) -> do
-                mw <- readMutVar mwR
-                vw <- readMutVar vwR
-                mb <- readMutVar mbR
-                vb <- readMutVar vbR
-                let mw' = LA.scale b1 mw + LA.scale (1 - b1) dW
-                    vw' = LA.scale b2 vw + LA.scale (1 - b2) (dW * dW)
-                    mb' = LA.scale b1 mb + LA.scale (1 - b1) dB
-                    vb' = LA.scale b2 vb + LA.scale (1 - b2) (dB * dB)
-                    mwHat = LA.scale (1 / c1) mw'
-                    vwHat = LA.scale (1 / c2) vw'
-                    mbHat = LA.scale (1 / c1) mb'
-                    vbHat = LA.scale (1 / c2) vb'
-                    wNew = lyrW l - LA.scale lr
-                             (mwHat / LA.cmap (\v -> sqrt v + eps) vwHat)
-                    bNew = lyrB l - LA.scale lr
-                             (mbHat / LA.cmap (\v -> sqrt v + eps) vbHat)
-                writeMutVar mwR mw'
-                writeMutVar vwR vw'
-                writeMutVar mbR mb'
-                writeMutVar vbR vb'
-                pure l { lyrW = wNew, lyrB = bNew })
-             (zip3 ls0 grads state)
-      writeMutVar layersRef newLayers
-    -- record epoch loss + per-epoch callback (Phase 21)
-    lsFinal <- readMutVar layersRef
-    let cache = forward lsFinal x
-        out_ = snd (last cache)
-        loss = if isClass
-                 then crossEntropyLoss out_ y
-                 else mseLoss out_ y
-    modifyMutVar' lossRef (loss :)
-    onEpoch MLPEpochEvent
-      { meEpoch     = epochIdx
-      , meTrainLoss = loss
-      , meValLoss   = Nothing
-      , meCurrentLR = lr
-      }
-  finalLayers <- readMutVar layersRef
-  losses <- readMutVar lossRef
-  pure (finalLayers, reverse losses)
-
-mseLoss :: LA.Matrix Double -> LA.Matrix Double -> Double
-mseLoss yhat y =
-  let d = yhat - y
-  in LA.sumElements (d * d) / fromIntegral (LA.rows y * LA.cols y)
-
-crossEntropyLoss :: LA.Matrix Double -> LA.Matrix Double -> Double
-crossEntropyLoss yhat y =
-  let safe = LA.cmap (\v -> log (max 1e-15 v)) yhat
-  in - LA.sumElements (y * safe) / fromIntegral (LA.rows y)
-
--- ===========================================================================
--- 公開 API
--- ===========================================================================
-
--- | X の列ごと平均と標準偏差 (n-1)。
-standardizeStats :: LA.Matrix Double -> (LA.Vector Double, LA.Vector Double)
-standardizeStats x =
-  let n   = LA.rows x
-      nD  = fromIntegral n :: Double
-      mean_ = LA.fromList
-        [ LA.sumElements (x LA.¿ [j]) / nD | j <- [0 .. LA.cols x - 1] ]
-      std_ = if n < 2
-               then LA.fromList (replicate (LA.cols x) 1)
-               else LA.fromList
-                      [ let c = LA.flatten (x LA.¿ [j]) - LA.scalar (mean_ `LA.atIndex` j)
-                            v = (c `LA.dot` c) / (nD - 1)
-                            s = sqrt v
-                        in if s > 1e-12 then s else 1
-                      | j <- [0 .. LA.cols x - 1] ]
-  in (mean_, std_)
-
-applyStandardize :: LA.Vector Double -> LA.Vector Double -> LA.Matrix Double
-                 -> LA.Matrix Double
-applyStandardize mean_ std_ x =
-  let n   = LA.rows x
-      mRow = LA.fromRows (replicate n mean_)
-      sRow = LA.fromRows (replicate n std_)
-  in (x - mRow) / sRow
-
-fitMLPRegressor
-  :: MLPConfig -> LA.Matrix Double -> LA.Vector Double
-  -> MWC.GenIO -> IO MLPFit
-fitMLPRegressor cfg x y gen =
-  fitMLPRegressorWithCallback cfg x y gen (\_ -> pure ())
-
--- | Phase 21 で追加。 epoch 終端ごとに 'MLPEpochEvent' を渡す callback 付き
--- regressor 学習。 既存 'fitMLPRegressor' は no-op callback で本関数を呼ぶ
--- 薄い wrapper として保持される。
-fitMLPRegressorWithCallback
-  :: PrimMonad m
-  => MLPConfig -> LA.Matrix Double -> LA.Vector Double
-  -> MWC.Gen (PrimState m)
-  -> (MLPEpochEvent -> m ())
-  -> m MLPFit
-fitMLPRegressorWithCallback cfg x y gen onEpoch = do
-  let (xMean, xStd) = if mlpStandardize cfg
-                        then standardizeStats x
-                        else (LA.fromList [], LA.fromList [])
-      xUse = if mlpStandardize cfg then applyStandardize xMean xStd x else x
-      yMat = LA.asColumn y
-  (layers, losses) <- trainMLP gen cfg xUse yMat False onEpoch
-  pure MLPFit
-    { mlpLayers   = layers
-    , mlpLossHist = losses
-    , mlpClasses  = []
-    , mlpClassNames = []
-    , mlpXMean    = xMean
-    , mlpXStd     = xStd
-    , mlpYMean    = 0
-    , mlpYStd     = 1
-    }
-
-fitMLPClassifier
-  :: MLPConfig -> LA.Matrix Double -> VU.Vector Int
-  -> MWC.GenIO -> IO MLPFit
-fitMLPClassifier cfg x y gen =
-  fitMLPClassifierWithCallback cfg x y gen (\_ -> pure ())
-
--- | Phase 21 で追加。 'fitMLPRegressorWithCallback' の classifier 版。
-fitMLPClassifierWithCallback
-  :: PrimMonad m
-  => MLPConfig -> LA.Matrix Double -> VU.Vector Int
-  -> MWC.Gen (PrimState m)
-  -> (MLPEpochEvent -> m ())
-  -> m MLPFit
-fitMLPClassifierWithCallback cfg x y gen onEpoch = do
-  let classes = uniqueSort (VU.toList y)
-      k       = length classes
-      n       = VU.length y
-      classIdx c = case lookup c (zip classes [0 ..]) of
-        Just i  -> i
-        Nothing -> 0
-      yOH = LA.fromLists
-              [ [ if j == classIdx (y VU.! i) then 1 else 0
-                | j <- [0 .. k - 1] ]
-              | i <- [0 .. n - 1] ]
-      (xMean, xStd) = if mlpStandardize cfg
-                        then standardizeStats x
-                        else (LA.fromList [], LA.fromList [])
-      xUse = if mlpStandardize cfg then applyStandardize xMean xStd x else x
-  (layers, losses) <- trainMLP gen cfg xUse yOH True onEpoch
-  pure MLPFit
-    { mlpLayers   = layers
-    , mlpLossHist = losses
-    , mlpClasses  = classes
-    , mlpClassNames = []
-    , mlpXMean    = xMean
-    , mlpXStd     = xStd
-    , mlpYMean    = 0
-    , mlpYStd     = 1
-    }
-
--- | 'fitMLPRegressor' の純粋版 (Phase 75.8)。 Word32 seed から @runST@ + MWC で重み初期化・
--- shuffle を決定的に閉じる ('fitRFVPure'/'nutsPure' と同方針・同 seed → ビット同一)。
--- IO 版は進捗 callback 用に残る。
-fitMLPRegressorPure :: MLPConfig -> LA.Matrix Double -> LA.Vector Double -> Word32 -> MLPFit
-fitMLPRegressorPure cfg x y seed =
-  runST (initialize (V.singleton seed)
-           >>= \gen -> fitMLPRegressorWithCallback cfg x y gen (\_ -> pure ()))
-
--- | 'fitMLPClassifier' の純粋版 (Phase 75.8)。 seed から @runST@ で決定的に学習。
-fitMLPClassifierPure :: MLPConfig -> LA.Matrix Double -> VU.Vector Int -> Word32 -> MLPFit
-fitMLPClassifierPure cfg x y seed =
-  runST (initialize (V.singleton seed)
-           >>= \gen -> fitMLPClassifierWithCallback cfg x y gen (\_ -> pure ()))
-
-predictMLP :: MLPFit -> LA.Matrix Double -> LA.Matrix Double
-predictMLP fit xNew =
-  let xUse = if LA.size (mlpXMean fit) > 0
-               then applyStandardize (mlpXMean fit) (mlpXStd fit) xNew
-               else xNew
-      cache = forward (mlpLayers fit) xUse
-      raw   = snd (last cache)
-      -- regressor の場合、 y も標準化して学習しているので戻す
-  in if null (mlpClasses fit) && mlpYStd fit /= 1
-       then LA.cmap (\v -> v * mlpYStd fit + mlpYMean fit) raw
-       else raw
-
-predictMLPClass :: MLPFit -> LA.Matrix Double -> V.Vector Int
-predictMLPClass fit xNew =
-  let probs = predictMLP fit xNew
-      classes = mlpClasses fit
-  in V.generate (LA.rows probs) $ \i ->
-       let row = LA.toList (LA.flatten (probs LA.? [i]))
-           (best, _) = foldr1 (\(j, p) (jb, pb) ->
-                                  if p > pb then (j, p) else (jb, pb))
-                       (zip [0 ..] row)
-       in classes !! best
-
--- ===========================================================================
--- helpers
--- ===========================================================================
-
-uniqueSort :: Ord a => [a] -> [a]
-uniqueSort = uniqAdj . sortL
-  where
-    sortL xs = foldr insertSorted [] xs
-    insertSorted x [] = [x]
-    insertSorted x ys@(y:rest)
-      | x <  y = x : ys
-      | x == y = ys
-      | otherwise = y : insertSorted x rest
-    uniqAdj []  = []
-    uniqAdj [a] = [a]
-    uniqAdj (a:b:rest)
-      | a == b = uniqAdj (b : rest)
-      | otherwise = a : uniqAdj (b : rest)
-
-chunksOf :: Int -> [a] -> [[a]]
-chunksOf _ [] = []
-chunksOf n xs = take n xs : chunksOf n (drop n xs)
-
-fisherYates :: PrimMonad m => MWC.Gen (PrimState m) -> [a] -> m [a]
-fisherYates gen xs =
-  let v0 = V.fromList xs
-  in go v0 (V.length v0 - 1)
-  where
-    go v 0 = pure (V.toList v)
-    go v i = do
-      j <- MWC.uniformR (0, i) gen
-      let vi = v V.! i
-          vj = v V.! j
-          v' = v V.// [(i, vj), (j, vi)]
-      go v' (i - 1)
diff --git a/src/Hanalyze/Model/PCA.hs b/src/Hanalyze/Model/PCA.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/PCA.hs
+++ /dev/null
@@ -1,180 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.PCA
--- Description : Principal Component Analysis (PCA) and related dimensionality reduction
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Principal Component Analysis (PCA) and related dimensionality
--- reduction.
---
--- @
--- import Hanalyze.Model.PCA
---
--- let pcaRes = pca True x  -- center + scale
---     loadings = pcaComponents pcaRes
---     scores   = pcaTransform pcaRes x  -- project x onto components
--- @
---
--- * 'pca' fits PCA to a centred (and optionally scaled) feature matrix.
--- * 'pcaTransform' projects new data onto the learned components.
--- * 'pcaInverse' reconstructs from scores back to feature space.
--- * @screePlot@ / @biplot@ integration via @Viz@ (separate module).
-module Hanalyze.Model.PCA
-  ( -- * PCA
-    PCAResult (..)
-  , PCAStandardize (..)
-  , pca
-  , pcaTransform
-  , pcaInverse
-  , pcaCumExplained
-    -- * Helpers
-  , standardizeFeatures
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-
--- | Standardisation mode for input features before SVD.
-data PCAStandardize
-  = NoStandardize
-    -- ^ Do not center or scale (only useful when columns already have
-    --   zero mean and comparable units).
-  | Center
-    -- ^ Subtract column means (default behaviour for PCA).
-  | CenterScale
-    -- ^ Subtract means and divide by sample standard deviations
-    --   (= standardised PCA, AKA correlation-matrix PCA).
-  deriving (Show, Eq)
-
--- | Result of fitting PCA. All matrices share the same number of
--- components @k@; if the user passed @k = Nothing@ then
--- @k = min(n, p)@.
-data PCAResult = PCAResult
-  { pcaMean       :: !(LA.Vector Double)
-    -- ^ Per-column mean of the training data (length @p@).
-  , pcaScale      :: !(LA.Vector Double)
-    -- ^ Per-column standard deviation (length @p@). All ones when
-    --   'pcaStandardize' is 'NoStandardize' / 'Center'.
-  , pcaStandardize :: !PCAStandardize
-  , pcaComponents :: !(LA.Matrix Double)
-    -- ^ Principal axes (@loadings@), shape @k × p@. Rows are unit
-    --   vectors; PC@i@ corresponds to row @i@.
-  , pcaSingularValues :: !(LA.Vector Double)
-    -- ^ Singular values @σ_i@, length @k@. Sorted descending.
-  , pcaExplainedVar :: !(LA.Vector Double)
-    -- ^ Variance of each component (= σ_i² / (n − 1)). Length @k@.
-  , pcaExplainedRatio :: !(LA.Vector Double)
-    -- ^ Fraction of total variance explained by each component, length
-    --   @k@. Sums to ≤ 1; equals 1 when k = rank(X).
-  , pcaNSamples   :: !Int
-  , pcaNFeatures  :: !Int
-  } deriving (Show)
-
--- | Center (and optionally scale) a feature matrix. Returns the
--- transformed matrix along with the column means and per-column
--- standard deviations.
-standardizeFeatures
-  :: PCAStandardize
-  -> LA.Matrix Double  -- ^ X (n × p)
-  -> (LA.Matrix Double, LA.Vector Double, LA.Vector Double)
-       -- ^ (Z, μ, σ).
-standardizeFeatures std x =
-  let n    = LA.rows x
-      p    = LA.cols x
-      ones = LA.konst 1 n :: LA.Vector Double
-      mu   = LA.scale (1 / fromIntegral n) (ones LA.<# x)
-      xC   = x - LA.fromRows (replicate n mu)
-  in case std of
-       NoStandardize ->
-         (x, LA.konst 0 p, LA.konst 1 p)
-       Center ->
-         (xC, mu, LA.konst 1 p)
-       CenterScale ->
-         let sd2 = LA.scale (1 / fromIntegral (n - 1))
-                     (LA.konst 1 n LA.<# (xC * xC))
-             sd  = LA.cmap (\v -> if v < 1e-12 then 1 else sqrt v) sd2
-             z   = xC LA.<> LA.diag (LA.cmap (1 /) sd)
-         in (z, mu, sd)
-
--- | Fit PCA on a feature matrix.
---
--- Internally uses thin SVD on the (centred / scaled) matrix so the
--- cost is @O(min(n²p, np²))@. The first @k@ rows of @Vᵀ@ are the
--- principal axes; the singular values @σ@ give component magnitudes.
-pca
-  :: PCAStandardize
-  -> Maybe Int          -- ^ k (number of components to keep). Nothing = all.
-  -> LA.Matrix Double   -- ^ X (n × p)
-  -> PCAResult
-pca std mK x =
-  let (z, mu, sd) = standardizeFeatures std x
-      n           = LA.rows z
-      p           = LA.cols z
-      -- Thin SVD: z = U S Vᵀ, where U is n×r, S is r-vector, V is p×r.
-      (u, s, vT)  = LA.thinSVD z
-      _           = u
-      kMax        = min (LA.rows z) (LA.cols z)
-      k           = min kMax (maybe kMax id mK)
-      -- Keep first k components.
-      sK          = LA.subVector 0 k s
-      -- 'thinSVD' returns Vᵀ as p × min(n,p); we want first k rows of
-      -- Vᵀ (= first k columns of V transposed).
-      vTk         = vT LA.?? (LA.All, LA.Take k)
-      components  = LA.tr vTk            -- k × p
-      -- Variance per component = σ² / (n − 1).
-      varK        = LA.cmap (\sv -> sv * sv / fromIntegral (max 1 (n - 1))) sK
-      totalVar    = LA.sumElements
-                      (LA.cmap (\sv -> sv * sv / fromIntegral (max 1 (n - 1))) s)
-      ratio       = if totalVar > 0
-                      then LA.scale (1 / totalVar) varK
-                      else LA.konst 0 k
-  in PCAResult
-       { pcaMean           = mu
-       , pcaScale          = sd
-       , pcaStandardize    = std
-       , pcaComponents     = components
-       , pcaSingularValues = sK
-       , pcaExplainedVar   = varK
-       , pcaExplainedRatio = ratio
-       , pcaNSamples       = n
-       , pcaNFeatures      = p
-       }
-
--- | Project new data onto the learned principal components.
--- Returns scores of shape @m × k@ where @m@ is the number of new
--- samples.
-pcaTransform :: PCAResult -> LA.Matrix Double -> LA.Matrix Double
-pcaTransform r x =
-  let m  = LA.rows x
-      mu = pcaMean r
-      sd = pcaScale r
-      xC = x - LA.fromRows (replicate m mu)
-      z  = case pcaStandardize r of
-             NoStandardize -> x
-             Center        -> xC
-             CenterScale   -> xC LA.<> LA.diag (LA.cmap (1 /) sd)
-  in z LA.<> LA.tr (pcaComponents r)        -- m × k
-
--- | Reconstruct from scores back to feature space (approximation when
--- not all components are kept). Inverse of 'pcaTransform' modulo
--- truncation error.
-pcaInverse :: PCAResult -> LA.Matrix Double -> LA.Matrix Double
-pcaInverse r scores =
-  let m       = LA.rows scores
-      mu      = pcaMean r
-      sd      = pcaScale r
-      zRecon  = scores LA.<> pcaComponents r          -- m × p
-      xRecon  = case pcaStandardize r of
-        NoStandardize -> zRecon
-        Center        -> zRecon + LA.fromRows (replicate m mu)
-        CenterScale   ->
-          let unscaled = zRecon LA.<> LA.diag sd
-          in unscaled + LA.fromRows (replicate m mu)
-  in xRecon
-
--- | Cumulative explained variance ratio (length k).
-pcaCumExplained :: PCAResult -> LA.Vector Double
-pcaCumExplained r =
-  let ratio = LA.toList (pcaExplainedRatio r)
-      cum   = scanl1 (+) ratio
-  in LA.fromList cum
diff --git a/src/Hanalyze/Model/PLS.hs b/src/Hanalyze/Model/PLS.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/PLS.hs
+++ /dev/null
@@ -1,405 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Model.PLS
--- Description : PLS (Partial Least Squares) — 応答 Y との共分散を最大化する低ランク回帰 (NIPALS)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Partial Least Squares (PLS) — chemometrics 標準の応答連動低ランク回帰。
---
--- PCA (`Hanalyze.Model.PCA`) は応答無視の分散最大化、 PLS は **応答 Y と
--- X の共分散を最大化**する低ランク射影。 多変量分光分析 / 材料設計の予測 +
--- 変数選択を 1 モデルで実現する。
---
--- アルゴリズム:
---
---   * 'NIPALS' (default): 反復的 power iteration、 sklearn `PLSRegression` と
---     数値一致しやすい
---   * 'SIMPLS' (Phase 9.5 で追加予定): de Jong 1993、 SVD ベース、 multi-Y で
---     直接的
---
--- 内部実装は hmatrix Matrix / Vector 演算で完結 (list 化しない)。
-module Hanalyze.Model.PLS
-  ( -- * Config
-    PLSAlgorithm (..)
-  , PLSConfig (..)
-  , defaultPLS
-    -- * Fit / predict
-  , PLSFit (..)
-  , fitPLS
-  , fitPLS1
-  , predictPLS
-  , predictPLS1
-    -- * CV による component 数選択
-  , PLSLambdaSelection (..)
-  , selectPLSComponentsCV
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import qualified System.Random.MWC     as MWC
-import           Data.List             (sortBy)
-import           Data.Ord              (comparing)
-import           Data.Text             (Text)
-import qualified Data.Text             as T
-
-import qualified Hanalyze.Stat.CV      as HCV
-
--- ===========================================================================
--- Config
--- ===========================================================================
-
-data PLSAlgorithm
-  = NIPALS   -- ^ 反復的 power iteration (default)
-  | SIMPLS   -- ^ de Jong 1993 (Phase 9.5 で追加)
-  deriving (Show, Eq)
-
-data PLSConfig = PLSConfig
-  { plsN_Components :: !Int
-  , plsAlgorithm    :: !PLSAlgorithm
-  , plsScale        :: !Bool           -- ^ True で X, Y を column-wise に標準化
-  , plsTol          :: !Double         -- ^ NIPALS 収束許容誤差
-  , plsMaxIter      :: !Int            -- ^ NIPALS 最大反復
-  } deriving (Show)
-
-defaultPLS :: PLSConfig
-defaultPLS = PLSConfig
-  { plsN_Components = 2
-  , plsAlgorithm    = NIPALS
-  , plsScale        = True
-  , plsTol          = 1e-8
-  , plsMaxIter      = 500
-  }
-
--- ===========================================================================
--- 結果型
--- ===========================================================================
-
-data PLSFit = PLSFit
-  { plsScoresT    :: !(LA.Matrix Double)  -- ^ T (n × K) X scores
-  , plsLoadingsP  :: !(LA.Matrix Double)  -- ^ P (p × K) X loadings
-  , plsLoadingsQ  :: !(LA.Matrix Double)  -- ^ Q (q × K) Y loadings
-  , plsWeightsW   :: !(LA.Matrix Double)  -- ^ W (p × K) X weights
-  , plsCoef       :: !(LA.Matrix Double)
-    -- ^ β (p × q) 回帰係数 (元スケール)。 @Ŷ = (X - X̄) · β + Ȳ@
-  , plsXMean      :: !(LA.Vector Double)  -- ^ X 列平均
-  , plsXStd       :: !(LA.Vector Double)  -- ^ X 列標準偏差 (plsScale=True なら、 そうでなければ 1)
-  , plsYMean      :: !(LA.Vector Double)
-  , plsYStd       :: !(LA.Vector Double)
-  , plsR2X        :: !(LA.Vector Double)  -- ^ 各 component の X 説明分散率
-  , plsR2Y        :: !(LA.Vector Double)  -- ^ 各 component の Y 説明分散率
-  , plsVIP        :: !(LA.Vector Double)  -- ^ 変数重要度 (Variable Importance in Projection)
-  , plsConfig     :: !PLSConfig
-  } deriving (Show)
-
--- ===========================================================================
--- 公開関数
--- ===========================================================================
-
--- | PLS fit (multi-output Y、 q ≥ 1)。
-fitPLS :: PLSConfig
-       -> LA.Matrix Double      -- ^ X (n × p)
-       -> LA.Matrix Double      -- ^ Y (n × q)
-       -> Either Text PLSFit
-fitPLS cfg x y
-  | LA.rows x /= LA.rows y =
-      Left "fitPLS: X and Y must have the same number of rows"
-  | LA.rows x < 2 =
-      Left "fitPLS: need at least 2 observations"
-  | plsN_Components cfg < 1 =
-      Left "fitPLS: n_components must be ≥ 1"
-  | plsN_Components cfg > min (LA.rows x - 1) (LA.cols x) =
-      Left (T.pack ("fitPLS: n_components (" <> show (plsN_Components cfg) <>
-                    ") exceeds min(n-1, p)"))
-  | otherwise =
-      case plsAlgorithm cfg of
-        NIPALS -> Right (nipalsFit cfg x y)
-        SIMPLS -> Left "fitPLS: SIMPLS not yet implemented (Phase 9.5)"
-
--- | 単出力 Y ショートカット (q = 1)。
-fitPLS1 :: PLSConfig -> LA.Matrix Double -> LA.Vector Double -> Either Text PLSFit
-fitPLS1 cfg x y = fitPLS cfg x (LA.asColumn y)
-
--- | 予測 (multi-output)。 `plsCoef` は元スケールの回帰係数なので、
---   X を中央化するだけで予測可能 (= scaling は不要、 coef が吸収済)。
-predictPLS :: PLSFit -> LA.Matrix Double -> LA.Matrix Double
-predictPLS fit xNew =
-  let nRow = LA.rows xNew
-      xCentered = xNew - LA.fromRows (replicate nRow (plsXMean fit))
-      yCentered = xCentered LA.<> plsCoef fit
-  in yCentered + LA.fromRows (replicate nRow (plsYMean fit))
-
-predictPLS1 :: PLSFit -> LA.Matrix Double -> LA.Vector Double
-predictPLS1 fit xNew = LA.flatten (predictPLS fit xNew)
-
--- ===========================================================================
--- NIPALS 実装
--- ===========================================================================
-
--- | NIPALS 内部実装。 中央化 + (option で) 標準化 → component loop → 後処理。
-nipalsFit :: PLSConfig -> LA.Matrix Double -> LA.Matrix Double -> PLSFit
-nipalsFit cfg xRaw yRaw =
-  let !n = LA.rows xRaw
-      !p = LA.cols xRaw
-      !q = LA.cols yRaw
-      k  = plsN_Components cfg
-
-      -- 列平均
-      xMean = LA.scale (1 / fromIntegral n) (LA.fromList
-                [ LA.sumElements (xRaw LA.¿ [j]) | j <- [0 .. p - 1] ])
-      yMean = LA.scale (1 / fromIntegral n) (LA.fromList
-                [ LA.sumElements (yRaw LA.¿ [j]) | j <- [0 .. q - 1] ])
-
-      xCentered = xRaw - LA.fromRows (replicate n xMean)
-      yCentered = yRaw - LA.fromRows (replicate n yMean)
-
-      -- 列標準偏差 (n-1 分母、 plsScale=False なら 1 ベクトル)
-      -- Bug fix (Phase 17.1): 旧実装 LA.sumElements (c LA.<> LA.tr c) は
-      -- n×n 行列 c_i c_j を生成 → sumElements で (Σ c)² になっていた。
-      -- 正しくは Σ c_i² = c `LA.dot` c。
-      colSD m mean_
-        | LA.rows m < 2 = LA.fromList (replicate (LA.cols m) 1)
-        | otherwise =
-            let nm = fromIntegral (LA.rows m - 1) :: Double
-                centered = m - LA.fromRows (replicate (LA.rows m) mean_)
-                sqSum = LA.fromList
-                  [ let c = LA.flatten (centered LA.¿ [j])
-                    in c `LA.dot` c
-                  | j <- [0 .. LA.cols m - 1] ]
-            in LA.cmap (\v -> let s = sqrt (v / nm) in if s > 1e-12 then s else 1) sqSum
-
-      xStd = if plsScale cfg then colSD xRaw xMean else LA.fromList (replicate p 1)
-      yStd = if plsScale cfg then colSD yRaw yMean else LA.fromList (replicate q 1)
-
-      xScaled = if plsScale cfg
-                  then xCentered / LA.fromRows (replicate n xStd)
-                  else xCentered
-      yScaled = if plsScale cfg
-                  then yCentered / LA.fromRows (replicate n yStd)
-                  else yCentered
-
-      -- component loop: 各 component で deflate しながら w, t, p, q を取り出す
-      (wMat, tMat, pMat, qMat) = nipalsLoop cfg k xScaled yScaled
-
-      -- 回帰係数 β = W (Pᵀ W)⁻¹ Qᵀ  (centered/scaled 空間)
-      ptw = LA.tr pMat LA.<> wMat       -- K × K
-      ptwInv = case LA.linearSolve ptw (LA.ident k) of
-        Just inv -> inv
-        Nothing  -> LA.scale 0 (LA.ident k)  -- singular なら 0
-      betaScaled = wMat LA.<> ptwInv LA.<> LA.tr qMat  -- p × q
-
-      -- R²X, R²Y を component 別に計算
-      ssTotalX = LA.sumElements (xScaled * xScaled)
-      ssTotalY = LA.sumElements (yScaled * yScaled)
-      r2X = LA.fromList
-        [ let tk = tMat LA.¿ [j]
-              pk = pMat LA.¿ [j]
-              recon = tk LA.<> LA.tr pk
-              ss = LA.sumElements (recon * recon)
-          in if ssTotalX > 0 then ss / ssTotalX else 0
-        | j <- [0 .. k - 1] ]
-      r2Y = LA.fromList
-        [ let tk = tMat LA.¿ [j]
-              qk = qMat LA.¿ [j]
-              recon = tk LA.<> LA.tr qk
-              ss = LA.sumElements (recon * recon)
-          in if ssTotalY > 0 then ss / ssTotalY else 0
-        | j <- [0 .. k - 1] ]
-
-      -- VIP: VIP_j = sqrt( p · Σ_k (W²_jk · SS_Y_k) / Σ_k SS_Y_k )
-      ssYPerComp = LA.fromList
-        [ let tk = tMat LA.¿ [j]
-              qk = qMat LA.¿ [j]
-              recon = tk LA.<> LA.tr qk
-          in LA.sumElements (recon * recon)
-        | j <- [0 .. k - 1] ]
-      ssYTotal = LA.sumElements ssYPerComp
-      vip = if ssYTotal > 0
-              then LA.fromList
-                [ let wj = LA.flatten (LA.tr wMat LA.¿ [j])  -- length K
-                      contribs = (wj * wj) * ssYPerComp
-                      total = LA.sumElements contribs
-                  in sqrt (fromIntegral p * total / ssYTotal)
-                | j <- [0 .. p - 1] ]
-              else LA.fromList (replicate p 0)
-
-      -- 元スケールの coef
-      coefOrig =
-        if plsScale cfg
-          then let xStdInv = LA.cmap (1 /) xStd
-                   yStdDiag = LA.diag yStd
-                   xStdDiagInv = LA.diag xStdInv
-               in xStdDiagInv LA.<> betaScaled LA.<> yStdDiag
-          else betaScaled
-
-  in PLSFit
-       { plsScoresT    = tMat
-       , plsLoadingsP  = pMat
-       , plsLoadingsQ  = qMat
-       , plsWeightsW   = wMat
-       , plsCoef       = coefOrig
-       , plsXMean      = xMean
-       , plsXStd       = xStd
-       , plsYMean      = yMean
-       , plsYStd       = yStd
-       , plsR2X        = r2X
-       , plsR2Y        = r2Y
-       , plsVIP        = vip
-       , plsConfig     = cfg
-       }
-
--- | NIPALS 反復ループ: scaled X, Y から K components を抽出。
-nipalsLoop
-  :: PLSConfig
-  -> Int                        -- K
-  -> LA.Matrix Double           -- X_scaled (n × p)
-  -> LA.Matrix Double           -- Y_scaled (n × q)
-  -> ( LA.Matrix Double  -- W (p × K)
-     , LA.Matrix Double  -- T (n × K)
-     , LA.Matrix Double  -- P (p × K)
-     , LA.Matrix Double  -- Q (q × K)
-     )
-nipalsLoop cfg k x0 y0 = go 0 x0 y0 [] [] [] []
-  where
-    go !i !x !y wAcc tAcc pAcc qAcc
-      | i >= k =
-          ( LA.fromColumns (reverse wAcc)
-          , LA.fromColumns (reverse tAcc)
-          , LA.fromColumns (reverse pAcc)
-          , LA.fromColumns (reverse qAcc)
-          )
-      | otherwise =
-          let (w, t, ploading, qloading) = nipalsOneComponent cfg x y
-              -- Deflate: E = E - t pᵀ, F = F - t qᵀ
-              x' = x - LA.asColumn t LA.<> LA.asRow ploading
-              y' = y - LA.asColumn t LA.<> LA.asRow qloading
-          in go (i + 1) x' y' (w : wAcc) (t : tAcc) (ploading : pAcc) (qloading : qAcc)
-
--- | NIPALS の 1 component 抽出。 power iteration で w, t, p, q を得る。
-nipalsOneComponent
-  :: PLSConfig
-  -> LA.Matrix Double
-  -> LA.Matrix Double
-  -> ( LA.Vector Double   -- w (p)
-     , LA.Vector Double   -- t (n)
-     , LA.Vector Double   -- p loading (p)
-     , LA.Vector Double   -- q loading (q)
-     )
-nipalsOneComponent cfg x y =
-  let -- 初期 u: Y の最初の列
-      u0 = LA.flatten (y LA.¿ [0])
-      (uFinal, _iter) = iterate' cfg x y u0 0
-      -- 最終 w 計算 (deflate 前の x, y で)
-      xtu = LA.tr x LA.#> uFinal
-      normXtu = sqrt (LA.sumElements (xtu * xtu))
-      w = if normXtu > 1e-12 then LA.scale (1 / normXtu) xtu
-                              else xtu
-      t = x LA.#> w
-      tt = LA.sumElements (t * t)
-      qy = if tt > 1e-12 then LA.scale (1 / tt) (LA.tr y LA.#> t)
-                         else LA.tr y LA.#> t
-      pload = if tt > 1e-12 then LA.scale (1 / tt) (LA.tr x LA.#> t)
-                            else LA.tr x LA.#> t
-  in (w, t, pload, qy)
-
--- | NIPALS の収束反復。 u を更新し続け、 |u_new - u| < tol で終了。
-iterate'
-  :: PLSConfig
-  -> LA.Matrix Double
-  -> LA.Matrix Double
-  -> LA.Vector Double   -- u
-  -> Int                -- iter count
-  -> (LA.Vector Double, Int)
-iterate' cfg x y u !i
-  | i >= plsMaxIter cfg = (u, i)
-  | otherwise =
-      let xtu = LA.tr x LA.#> u
-          normXtu = sqrt (LA.sumElements (xtu * xtu))
-          w = if normXtu > 1e-12 then LA.scale (1 / normXtu) xtu else xtu
-          t = x LA.#> w
-          tt = LA.sumElements (t * t)
-          ytt = LA.tr y LA.#> t
-          q = if tt > 1e-12 then LA.scale (1 / tt) ytt else ytt
-          fq = y LA.#> q
-          normFq = sqrt (LA.sumElements (fq * fq))
-          uNew = if normFq > 1e-12 then LA.scale (1 / normFq) fq else fq
-          diff = uNew - u
-          err = sqrt (LA.sumElements (diff * diff))
-      in if err < plsTol cfg
-           then (uNew, i + 1)
-           else iterate' cfg x y uNew (i + 1)
-
--- ===========================================================================
--- CV による component 数選択
--- ===========================================================================
-
-data PLSLambdaSelection = PLSLambdaSelection
-  { plsBestK   :: !Int
-  , plsCVMSEs  :: ![Double]
-  , plsCVSDs   :: ![Double]
-  , plsOneSeK  :: !Int
-  } deriving (Show)
-
--- | k-fold CV で component 数を 1..maxK の中から選ぶ。
-selectPLSComponentsCV
-  :: Int                       -- ^ k-fold の k
-  -> Int                       -- ^ maxK (component 数上限)
-  -> LA.Matrix Double          -- ^ X
-  -> LA.Matrix Double          -- ^ Y
-  -> MWC.GenIO
-  -> IO PLSLambdaSelection
-selectPLSComponentsCV kFold maxK xMat yMat gen = do
-  let n = LA.rows xMat
-  folds <- HCV.kFold kFold n gen
-  let perK kk =
-        let cfg = defaultPLS { plsN_Components = kk }
-            scores =
-              [ mseForFold cfg xMat yMat trainIdx testIdx
-              | (trainIdx, testIdx) <- folds, not (null testIdx)
-              ]
-            !nFolds = fromIntegral (length scores) :: Double
-            meanMSE = sum scores / nFolds
-            varN    = sum [(s - meanMSE) ** 2 | s <- scores] / max 1 (nFolds - 1)
-            !se     = sqrt (varN / nFolds)
-        in (meanMSE, se)
-      ks = [1 .. maxK]
-      stats = map perK ks
-      mses  = map fst stats
-      ses   = map snd stats
-      indexedMSEs = zip3 ks mses ses
-      sortedAsc   = sortBy (comparing (\(_, m, _) -> m)) indexedMSEs
-      (bestK_, bestMSE, bestSE) =
-        case sortedAsc of
-          (h:_) -> h
-          []    -> (1, 0, 0)
-      threshold = bestMSE + bestSE
-      -- 1-SE rule: 最も sparse な K (= 最小 K) で best MSE + 1·SE 以内
-      oneSe = case [k | (k, m, _) <- indexedMSEs, m <= threshold] of
-                [] -> bestK_
-                xs -> minimum xs
-  pure PLSLambdaSelection
-    { plsBestK   = bestK_
-    , plsCVMSEs  = mses
-    , plsCVSDs   = ses
-    , plsOneSeK  = oneSe
-    }
-
-mseForFold
-  :: PLSConfig
-  -> LA.Matrix Double
-  -> LA.Matrix Double
-  -> [Int]
-  -> [Int]
-  -> Double
-mseForFold cfg xMat yMat trainIdx testIdx =
-  let xTr = xMat LA.? trainIdx
-      yTr = yMat LA.? trainIdx
-      xTe = xMat LA.? testIdx
-      yTe = yMat LA.? testIdx
-  in case fitPLS cfg xTr yTr of
-       Left _    -> 1/0
-       Right fit ->
-         let yHat = predictPLS fit xTe
-             resid = yTe - yHat
-             nTe = fromIntegral (length testIdx) :: Double
-         in LA.sumElements (resid * resid) / nTe
diff --git a/src/Hanalyze/Model/PartialDependence.hs b/src/Hanalyze/Model/PartialDependence.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/PartialDependence.hs
+++ /dev/null
@@ -1,114 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.PartialDependence
--- Description : 任意モデル対応の Partial Dependence / ICE 純粋計算エンジン (model 非依存・非ゲート層)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- 部分従属 (Partial Dependence) / ICE の純粋計算エンジン — 任意モデル対応 (Phase 75.27)。
---
--- R @pdp::partial@ / sklearn @sklearn.inspection.partial_dependence@ 相当。 学習済モデルの
--- predict を「注目特徴を grid で振り、 他の特徴は訓練データの観測分布のまま」評価し、 全観測
--- 行で平均したものが PDP、 行ごとの曲線が ICE (individual conditional expectation)。
---
--- model 非依存 (predict 閉包のみを受ける) ゆえ **非ゲート層** に置き、 図化は
--- 'Hanalyze.Plot.ML' がゲート (@plot-integration@) 配下で担う。
---
--- @
--- import Hanalyze.Model.PartialDependence
---
--- -- 任意モデルの predict 閉包を渡す (R pdp の pred.fun 流)。
--- let r = partialDependence trainX (\\m -> map (predictRF rf) (LA.toLists m)) 0 40
--- in  (pdpGrid r, pdpMean r)          -- 特徴 0 の PDP 曲線
--- @
-module Hanalyze.Model.PartialDependence
-  ( -- * 結果型
-    PDPResult (..)
-    -- * 計算
-  , partialDependence
-  , partialDependenceGrid
-    -- * 変換
-  , centerICE
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import           Data.List             (transpose)
-
--- ===========================================================================
--- 結果型
--- ===========================================================================
-
--- | 部分従属の計算結果。 grid・PDP 平均曲線・ICE 個体曲線群をまとめて返す。
-data PDPResult = PDPResult
-  { pdpGrid :: ![Double]      -- ^ 注目特徴の grid 値 (長さ = grid 数)。
-  , pdpMean :: ![Double]      -- ^ PDP: 各 grid 値で全観測行の予測を平均 (長さ = grid 数)。
-  , pdpIce  :: ![[Double]]    -- ^ ICE: 観測行ごとの曲線 (n 本・各長さ = grid 数)。
-  } deriving (Eq, Show)
-
--- ===========================================================================
--- 計算
--- ===========================================================================
-
--- | 注目特徴 j の観測 @[min,max]@ を等間隔 grid にして PDP/ICE を計算する。
---   grid 数 <2 は 2 に切り上げ。 空データ・列外 index は空結果 ('PDPResult' [] [] [])。
-partialDependence
-  :: LA.Matrix Double                 -- ^ 訓練特徴行列 X (n 行 × p 列)。
-  -> (LA.Matrix Double -> [Double])   -- ^ predict: 行列の各行 → 予測値 (長さ = 行数)。
-  -> Int                              -- ^ 注目特徴の列 index j (0 始まり)。
-  -> Int                              -- ^ grid 数。
-  -> PDPResult
-partialDependence x predict j n
-  | LA.rows x == 0 || j < 0 || j >= LA.cols x = PDPResult [] [] []
-  | otherwise =
-      let col  = LA.toList (LA.toColumns x !! j)
-          lo   = minimum col
-          hi   = maximum col
-          m    = max 2 n
-          grid = [ lo + (hi - lo) * fromIntegral i / fromIntegral (m - 1)
-                 | i <- [0 .. m - 1] ]
-      in partialDependenceGrid x predict j grid
-
--- | grid を明示指定する版。 分位点 grid や任意評価点を渡したいときに使う。
---   空 grid・空データ・列外 index は空結果。
-partialDependenceGrid
-  :: LA.Matrix Double
-  -> (LA.Matrix Double -> [Double])
-  -> Int
-  -> [Double]                         -- ^ 注目特徴の評価 grid。
-  -> PDPResult
-partialDependenceGrid x predict j grid
-  | LA.rows x == 0 || j < 0 || j >= LA.cols x || null grid = PDPResult [] [] []
-  | otherwise =
-      let nrows  = LA.rows x
-          cols   = LA.toColumns x
-          -- 各 grid 値 g で X の j 列を定数 g に置換 → 全行 predict (長さ nrows)。
-          predsAtG g =
-            let xg = LA.fromColumns
-                       [ if c == j then LA.konst g nrows else col
-                       | (c, col) <- zip [0 ..] cols ]
-            in predict xg
-          byGrid = [ predsAtG g | g <- grid ]              -- grid × n
-          means  = [ sum ps / fromIntegral nrows | ps <- byGrid ]
-          ice    = transpose byGrid                        -- n × grid (行ごとの曲線)
-      in PDPResult grid means ice
-
--- ===========================================================================
--- 変換
--- ===========================================================================
-
--- | 中心化 ICE (c-ICE)。 各 ICE 曲線を **左端 (grid[0]) の値が 0** になるよう平行移動し、
---   PDP 平均も中心化後の ICE から取り直す。 個体間の傾き差を見やすくする
---   (sklearn @centered=True@ / R @ice()@ centered 相当)。 空結果はそのまま。
-centerICE :: PDPResult -> PDPResult
-centerICE r
-  | null (pdpGrid r) || null (pdpIce r) = r
-  | otherwise =
-      let ice'   = [ case curve of
-                       (c0 : _) -> map (subtract c0) curve
-                       []       -> curve
-                   | curve <- pdpIce r ]
-          nrows  = length ice'
-          means' = case ice' of
-                     [] -> []
-                     _  -> map (\col -> sum col / fromIntegral nrows) (transpose ice')
-      in r { pdpMean = means', pdpIce = ice' }
diff --git a/src/Hanalyze/Model/Quantile.hs b/src/Hanalyze/Model/Quantile.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Quantile.hs
+++ /dev/null
@@ -1,167 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.Quantile
--- Description : Quantile regression — Hunter & Lange (2000) MM 法による条件付き τ-分位点回帰
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Quantile regression.
---
--- Whereas OLS fits the conditional /mean/, quantile regression fits the
--- conditional @τ@-quantile (with @τ ∈ (0, 1)@). @τ = 0.5@ gives outlier-
--- robust median regression; @τ = 0.1 / 0.9@ estimate lower / upper
--- quantiles, useful for predictive intervals and heteroscedastic data.
---
--- Loss function (pinball / check loss):
---
--- > ρ_τ(u) = u (τ - 𝟙[u < 0])  =  τ u       if u ≥ 0
--- >                               (τ-1) u   if u < 0
---
--- Algorithm: Hunter & Lange (2000) Majorization-Minimization. Locally
--- approximate @|u|@ by a quadratic and iterate weighted least squares:
---
--- 1. β₀ = OLS 解で初期化
--- 2. 反復 k:
---    - r = y - X β_k
---    - w_i = 1 / (2 max(|r_i|, ε))
---    - y'_i = y_i + (τ - ½) / w_i
---    - β_{k+1} = (Xᵀ W X)⁻¹ Xᵀ W y'
--- 3. ||β_{k+1} - β_k|| < tol で停止 (max 100 iter)。
---
--- 評価指標 (Koenker-Machado 1999): R¹_τ = 1 - V̂_τ(model) / V̂_τ(intercept-only)
--- where V̂_τ(m) = Σ ρ_τ(r_i^m)。
-module Hanalyze.Model.Quantile
-  ( QRFit (..)
-  , fitQuantile
-  , predictQuantile
-  , pinballLoss
-  , pseudoR1
-  ) where
-
-import qualified Data.List                    as L
-import qualified Numeric.LinearAlgebra as LA
-import qualified Hanalyze.Stat.Cholesky        as Chol
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
--- | Quantile-regression fit result.
-data QRFit = QRFit
-  { qfTau     :: Double            -- ^ Quantile level @τ ∈ (0, 1)@.
-  , qfBeta    :: LA.Vector Double  -- ^ Coefficients.
-  , qfYHat    :: LA.Vector Double  -- ^ Fitted values @X β@.
-  , qfResid   :: LA.Vector Double  -- ^ Residuals @y − X β@.
-  , qfPinball :: Double            -- ^ Total pinball loss @V̂_τ@.
-  , qfR1      :: Double            -- ^ Koenker-Machado pseudo @R¹_τ@.
-  , qfIters   :: Int               -- ^ Number of iterations executed.
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- フィット
--- ---------------------------------------------------------------------------
-
--- | Fit a @τ@-quantile regression by Majorization-Minimization IRLS.
-fitQuantile :: Double             -- ^ Quantile level @τ ∈ (0, 1)@.
-            -> LA.Matrix Double   -- ^ Design matrix @X@ (must include the intercept column).
-            -> LA.Vector Double   -- ^ Response @y@.
-            -> QRFit
-fitQuantile tau x y
-  | tau <= 0 || tau >= 1 = error "fitQuantile: tau must be in (0, 1)"
-  | otherwise =
-      let !beta0      = x LA.<\> y         -- OLS 初期値
-          !eps        = 1e-6
-          !maxIter    = 100 :: Int
-          !tol        = 1e-7
-          !p          = LA.cols x
-          !onesP      = LA.konst 1 p :: LA.Vector Double
-          (betaF, k)  = loop beta0 0
-          loop b iter
-            | iter >= maxIter = (b, iter)
-            | otherwise =
-                let !r    = y - x LA.#> b
-                    -- w_i = 1 / (2 max(|r_i|, eps))
-                    !wVec = LA.cmap (\v -> 1 / (2 * max eps (abs v))) r
-                    -- y' = y + (tau - 0.5) / w
-                    !yp   = y + LA.cmap (\wi -> (tau - 0.5) / wi) wVec
-                    -- W^{1/2}.
-                    !sqW  = LA.cmap sqrt wVec
-                    -- B10a (2026-05-06): row-scaling of X via outer
-                    -- product (broadcast sqW across columns) instead
-                    -- of the previous "@LA.toRows x !! i@" + "@diag@"
-                    -- combination, which was @O(n² p)@ per iteration
-                    -- (76× slower than statsmodels on n=10k p=20).
-                    -- Now @O(n p)@ per iteration — single elementwise
-                    -- multiply with a fully-allocated outer product.
-                    !sqWBcast = LA.outer sqW onesP   -- n × p
-                    !xScaled  = sqWBcast * x         -- n × p
-                    !yScaled  = sqW * yp             -- length n
-                    -- Solve the SPD normal equations
-                    --   (X^T W X) β = X^T W y'
-                    -- via Cholesky rather than the general LSQ path
-                    -- '@LA.<\>@' (QR/dgels). For @p ≪ n@ the @p × p@
-                    -- @aMat@ is tiny and dpotrf is faster than dgels
-                    -- on the @n × p@ @xScaled@ matrix; this is the
-                    -- same trick GLM IRLS already uses.
-                    !aMat     = LA.tr xScaled LA.<> xScaled
-                    !rhs      = LA.asColumn (LA.tr xScaled LA.#> yScaled)
-                    !bNew     = LA.flatten (Chol.cholSolveJitter aMat rhs)
-                    !delta    = LA.norm_2 (bNew - b)
-                in if delta < tol then (bNew, iter + 1)
-                                  else loop bNew (iter + 1)
-          yhat = x LA.#> betaF
-          resid = y - yhat
-          loss  = pinballLoss tau (LA.toList resid)
-          -- baseline: intercept-only model with τ-quantile of y
-          ys    = LA.toList y
-          baseQ = quantile tau ys
-          baseR = [ yi - baseQ | yi <- ys ]
-          baseLoss = pinballLoss tau baseR
-          r1 = if baseLoss <= 1e-12 then 0
-               else 1 - loss / baseLoss
-      in QRFit
-           { qfTau     = tau
-           , qfBeta    = betaF
-           , qfYHat    = yhat
-           , qfResid   = resid
-           , qfPinball = loss
-           , qfR1      = r1
-           , qfIters   = k
-           }
-
--- | Predict at new inputs.
-predictQuantile :: QRFit -> LA.Matrix Double -> LA.Vector Double
-predictQuantile fit xNew = xNew LA.#> qfBeta fit
-
--- ---------------------------------------------------------------------------
--- 補助関数
--- ---------------------------------------------------------------------------
-
--- | Total pinball / check loss: @Σ ρ_τ(r_i)@.
-pinballLoss :: Double -> [Double] -> Double
-pinballLoss tau rs =
-  sum [ if r >= 0 then tau * r else (tau - 1) * r | r <- rs ]
-
--- | Empirical @τ@-quantile (simple linear-interpolation style).
-quantile :: Double -> [Double] -> Double
-quantile p xs
-  | null xs = 0
-  | otherwise =
-      -- Phase 11b (2026-05-14): replaced naive list quicksort with
-      -- 'Data.List.sort' (mergesort, O(n log n), O(n) space). Pivot-bias
-      -- could push the old version to O(n²) space.
-      let sorted = L.sort xs
-          n      = length sorted
-          ix     = p * fromIntegral (n - 1)
-          lo     = floor ix :: Int
-          hi     = min (n - 1) (lo + 1)
-          frac   = ix - fromIntegral lo
-      in (1 - frac) * (sorted !! lo) + frac * (sorted !! hi)
-
--- | Pseudo R¹_τ を別途計算 (model loss と baseline loss から)。
-pseudoR1 :: Double            -- ^ model V̂_τ
-         -> Double            -- ^ baseline (intercept-only) V̂_τ
-         -> Double
-pseudoR1 modelV baseV
-  | baseV <= 1e-12 = 0
-  | otherwise      = 1 - modelV / baseV
diff --git a/src/Hanalyze/Model/RFF.hs b/src/Hanalyze/Model/RFF.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/RFF.hs
+++ /dev/null
@@ -1,1051 +0,0 @@
-{-# LANGUAGE StrictData #-}
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.RFF
--- Description : Random Fourier Features (RFF) — Bochner の定理に基づく kernel の明示的特徴写像近似
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Random Fourier Features (RFF) — kernel approximation.
---
--- By Bochner's theorem, a stationary kernel
--- @k(x, x') = ∫ p(ω) e^{iω(x-x')} dω@ admits an explicit feature map
--- defined via @D@ frequencies @ω_j@ sampled from @p(ω)@ and uniform
--- phases @b_j@:
---
--- @
--- φ(x) = σ_f √(2/D) [cos(ω_j x + b_j)]_{j=1..D}
--- @
---
--- so that @k(x, x') ≈ φ(x)·φ(x')@ (Rahimi & Recht 2007).
---
--- Benefits:
---
---   * @O(n³)@ kernel computation reduces to @O(n D + D³)@ — linear in @n@.
---   * Ridge regression and GP posterior become @D@-dimensional linear
---     algebra.
---
--- This module supports both univariate and multivariate inputs (the
--- @MV@-suffixed APIs).
--- - 'sampleRFFRBF':      RBF カーネル (ω ~ N(0, 1/ℓ²))
--- - 'sampleRFFMatern52': Matérn 5/2 (ω ~ scaled t with df = 5)
--- - 'rffFeatures':  特徴行列 Φ を構築 (n × D)
--- - 'rffRidge':     RFF + Ridge 回帰 (=O(n³) Kernel Ridge の近似)
--- - 'rffGP':        RFF + ベイズ線形回帰 = GP 事後の近似 (mean + variance)
-module Hanalyze.Model.RFF
-  ( RFFKernel (..)
-  , RFFFeatures (..)
-  , rffDim
-    -- * Feature generation
-  , sampleRFFRBF
-  , sampleRFFMatern52
-  , sampleRFFRBFPure
-  , sampleRFFMatern52Pure
-  , rffFeatures
-  , rffApproxKernel
-    -- * RFF ridge regression (primary API: multi-output)
-  , RFFRidgeFit (..)
-  , rffRidge
-  , predictRFFRidge
-  , RFFRidgeFitMulti (..)
-  , rffRidgeMulti
-  , predictRFFRidgeMulti
-    -- * RFF GP (posterior mean + variance)
-  , RFFGPFit (..)
-  , rffGP
-  , predictRFFGP
-    -- * Multivariate input (@p@ dimensions)
-  , RFFFeaturesMV (..)
-  , sampleRFFRBFMV
-  , sampleRFFMatern52MV
-  , sampleRFFRBFMVPure
-  , sampleRFFMatern52MVPure
-  , rffFeaturesMV
-  , RFFRidgeFitMV (..)
-  , rffRidgeMV
-  , predictRFFRidgeMV
-  , RFFGPFitMV (..)
-  , rffGPMV
-  , predictRFFGPMV
-  , RFFRidgeFitMVMO (..)
-  , rffRidgeMVMulti
-  , predictRFFRidgeMVMulti
-    -- * Marginal-likelihood maximization (auto-tune ℓ, σ_f, σ_n)
-  , logMarginalLikRBFMV
-  , maximizeMarginalLikRBFMV
-  , maximizeMarginalLikRBFMV_DE
-  , MLikResult (..)
-    -- * LOOCV closed form (faster HP auto-tuning)
-  , loocvFromPhi
-  , loocvRFFRidgeMV
-  , gridSearchLOOCVRBFMV
-  , gridSearchLOOCVRBFMV_DE
-  , bayesOptLOOCVRBFMV
-  , lbfgsLOOCVRBFMV
-  , LOOCVResult (..)
-  ) where
-
-import Control.Exception (SomeException, try, evaluate)
-import           Control.Monad.Primitive      (PrimMonad, PrimState)
-import           Data.Word                    (Word32)
-import qualified Data.Vector as V
-import qualified Data.Vector.Storable         as VS
-import qualified Data.Vector.Storable.Mutable as VSM
-import           Control.Monad.ST             (runST)
-import qualified Numeric.LinearAlgebra as LA
-import qualified System.IO.Unsafe
-import System.IO.Unsafe (unsafePerformIO)
-import qualified System.Random.MWC
-import System.Random.MWC (GenIO, Gen, uniformR, initialize)
-import qualified System.Random.MWC.Distributions as MWCD
-import qualified Hanalyze.Optim.DifferentialEvolution as DEM
-import qualified Hanalyze.Optim.Common as OCM
-import qualified Hanalyze.Optim.BayesOpt as BO
-import qualified Hanalyze.Optim.LBFGS as LBFGS
-import qualified Hanalyze.Stat.Cholesky as Chol
-import qualified Hanalyze.Stat.KernelDist as KD
-import qualified Data.Vector.Algorithms.Intro as Intro
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
--- | Supported kernels for RFF approximation.
-data RFFKernel = RFFRBF | RFFMatern52
-  deriving (Show, Eq)
-
--- | All the information needed to evaluate an RFF feature map.
-data RFFFeatures = RFFFeatures
-  { rffKernel      :: RFFKernel
-  , rffOmegas      :: V.Vector Double   -- ^ Random frequencies @ω_j@ (length @D@).
-  , rffBs          :: V.Vector Double   -- ^ Random phases @b_j ∈ [0, 2π)@.
-  , rffSigmaF      :: Double            -- ^ Signal standard deviation @σ_f@.
-  , rffLengthScale :: Double            -- ^ Length scale @ℓ@.
-  } deriving (Show)
-
--- | Number of features @D@.
-rffDim :: RFFFeatures -> Int
-rffDim = V.length . rffOmegas
-
--- ---------------------------------------------------------------------------
--- 周波数サンプリング
--- ---------------------------------------------------------------------------
-
--- | Sample RFF features for the RBF kernel: @ω_j ~ N(0, 1/ℓ²)@,
--- @b_j ~ U(0, 2π)@.
---
--- 'PrimMonad' 汎用 (mwc は 'PrimMonad' 汎用ゆえ ST/IO 両経路で同コード)。
--- IO 呼び出しは @GenIO = Gen (PrimState IO)@ ゆえ従来どおり。 純粋 (seed) 経路は
--- 'sampleRFFRBFPure' (Phase 70.5 = 'gp' spec の RFF 近似象限を pure 'fitWith' で完結
--- させるため・[[kMeansPure]]/[[fitRFVPure]] と一貫)。
-sampleRFFRBF :: PrimMonad m
-             => Int      -- ^ Feature dimension @D@.
-             -> Double   -- ^ Length scale @ℓ@.
-             -> Double   -- ^ Signal SD @σ_f@.
-             -> Gen (PrimState m) -> m RFFFeatures
-sampleRFFRBF d ell sf gen = do
-  ws <- V.replicateM d (MWCD.normal 0 (1/ell) gen)
-  bs <- V.replicateM d (uniformR (0, 2*pi) gen)
-  return RFFFeatures
-    { rffKernel      = RFFRBF
-    , rffOmegas      = ws
-    , rffBs          = bs
-    , rffSigmaF      = sf
-    , rffLengthScale = ell
-    }
-
--- | Sample RFF features for the Matérn 5/2 kernel:
--- @ω = z/√u@ where @z ~ N(0, 1/ℓ²)@ and @u ~ Gamma(ν, ν)@ with @ν = 5/2@.
--- This is a scaled @df = 5@ Student-t distribution, matching the
--- spectral density.
-sampleRFFMatern52 :: PrimMonad m
-                  => Int -> Double -> Double -> Gen (PrimState m) -> m RFFFeatures
-sampleRFFMatern52 d ell sf gen = do
-  let nu = 2.5 :: Double
-  ws <- V.replicateM d $ do
-    z <- MWCD.normal 0 (1/ell) gen
-    -- mwc-random-distributions の gamma は (shape, scale) 渡し → mean = shape * scale
-    -- Gamma(ν, 1/ν) で mean = 1
-    u <- MWCD.gamma nu (1/nu) gen
-    return (z / sqrt u)
-  bs <- V.replicateM d (uniformR (0, 2*pi) gen)
-  return RFFFeatures
-    { rffKernel      = RFFMatern52
-    , rffOmegas      = ws
-    , rffBs          = bs
-    , rffSigmaF      = sf
-    , rffLengthScale = ell
-    }
-
--- | 純粋 (seed) 版 'sampleRFFRBF'。 同 seed → 同 'RFFFeatures' (ST/IO ビット一致)。
--- 'gp' spec の @GpRff@/@RidgeRff@ 象限を pure 'fitWith' で完結させる継ぎ目。
-sampleRFFRBFPure :: Int -> Double -> Double -> Word32 -> RFFFeatures
-sampleRFFRBFPure d ell sf seed =
-  runST (initialize (V.singleton seed) >>= sampleRFFRBF d ell sf)
-
--- | 純粋 (seed) 版 'sampleRFFMatern52'。
-sampleRFFMatern52Pure :: Int -> Double -> Double -> Word32 -> RFFFeatures
-sampleRFFMatern52Pure d ell sf seed =
-  runST (initialize (V.singleton seed) >>= sampleRFFMatern52 d ell sf)
-
--- ---------------------------------------------------------------------------
--- 特徴写像
--- ---------------------------------------------------------------------------
-
--- | Feature matrix @Φ ∈ ℝ^{n×D}@.
--- @φ(x) = σ_f √(2/D) [cos(ω_j x + b_j)]_{j=1..D}@.
---
--- Single-pass 'runST' implementation: avoids the @[Double]@
--- list-comprehension @(n × D)@ + 'LA.fromList' round-trip the
--- previous version performed.
-rffFeatures :: RFFFeatures -> [Double] -> LA.Matrix Double
-rffFeatures rff xs =
-  let d    = rffDim rff
-      sf   = rffSigmaF rff
-      coef = sf * sqrt (2 / fromIntegral d)
-      -- Convert input list / boxed Vectors to Storable for fast access.
-      xsV  = VS.fromList xs
-      n    = VS.length xsV
-      ws   = VS.fromList (V.toList (rffOmegas rff))
-      bs   = VS.fromList (V.toList (rffBs     rff))
-      out  = runST $ do
-        v <- VSM.new (n * d)
-        let go i j
-              | i >= n    = pure ()
-              | j >= d    = go (i + 1) 0
-              | otherwise = do
-                  let !x_  = xsV `VS.unsafeIndex` i
-                      !w_  = ws  `VS.unsafeIndex` j
-                      !b_  = bs  `VS.unsafeIndex` j
-                      !val = coef * cos (w_ * x_ + b_)
-                  VSM.unsafeWrite v (i * d + j) val
-                  go i (j + 1)
-        go 0 0
-        VS.unsafeFreeze v
-  in LA.reshape d out
-
--- | Kernel matrix approximated by RFF: @K[i,j] ≈ k(x_i, x_j) = φ(x_i)·φ(x_j)@.
-rffApproxKernel :: RFFFeatures -> [Double] -> LA.Matrix Double
-rffApproxKernel rff xs =
-  let phi = rffFeatures rff xs
-  in phi LA.<> LA.tr phi
-
--- ---------------------------------------------------------------------------
--- RFF Ridge 回帰
--- ---------------------------------------------------------------------------
-
--- | Single-output RFF ridge fit.
-data RFFRidgeFit = RFFRidgeFit
-  { rffrFeatures :: RFFFeatures
-  , rffrWeights  :: LA.Vector Double   -- ^ Weight vector (length @D@).
-  , rffrLambda   :: Double             -- ^ Ridge penalty @λ@.
-  } deriving (Show)
-
--- | Single-output RFF ridge regression. Delegates to 'rffRidgeMulti' by
--- promoting @y@ to a one-column matrix.
-rffRidge :: RFFFeatures -> [Double] -> [Double] -> Double -> RFFRidgeFit
-rffRidge rff xs ys lam =
-  let yMat = LA.asColumn (LA.fromList ys)
-      mf   = rffRidgeMulti rff xs yMat lam
-      w    = LA.flatten (rffrmWeights mf LA.¿ [0])
-  in RFFRidgeFit rff w lam
-
--- | Predict at new inputs from a 'RFFRidgeFit'.
-predictRFFRidge :: RFFRidgeFit -> [Double] -> [Double]
-predictRFFRidge fit xNew =
-  let phi  = rffFeatures (rffrFeatures fit) xNew
-      yhat = phi LA.#> rffrWeights fit
-  in LA.toList yhat
-
--- | Multi-output RFF ridge fit (1D inputs). @Y@ is @n × q@, weights @W@
--- are @D × q@.
-data RFFRidgeFitMulti = RFFRidgeFitMulti
-  { rffrmFeatures :: RFFFeatures
-  , rffrmWeights  :: LA.Matrix Double   -- ^ Weight matrix (@D × q@).
-  , rffrmLambda   :: Double             -- ^ Ridge penalty @λ@.
-  } deriving (Show)
-
--- | Multi-output RFF ridge regression: @W = (ΦᵀΦ + λI)⁻¹ Φᵀ Y@.
--- SPD system; solved via Cholesky with diagonal regularizer applied
--- in place (@addToDiagRFF@).
-rffRidgeMulti :: RFFFeatures -> [Double] -> LA.Matrix Double -> Double
-              -> RFFRidgeFitMulti
-rffRidgeMulti rff xs ys lam =
-  let phi   = rffFeatures rff xs           -- n × D
-      gram  = LA.tr phi LA.<> phi          -- D × D (SPD)
-      regK  = addToDiagRFF lam gram
-      rhs   = LA.tr phi LA.<> ys           -- D × q
-      w     = Chol.cholSolveJitter regK rhs
-  in RFFRidgeFitMulti rff w lam
-
--- | Multi-output prediction at new inputs from a 'RFFRidgeFitMulti'.
-predictRFFRidgeMulti :: RFFRidgeFitMulti -> [Double] -> LA.Matrix Double
-predictRFFRidgeMulti fit xNew =
-  let phi = rffFeatures (rffrmFeatures fit) xNew
-  in phi LA.<> rffrmWeights fit
-
--- ---------------------------------------------------------------------------
--- RFF GP (ベイズ線形回帰 with prior w ~ N(0, I))
--- ---------------------------------------------------------------------------
-
--- | Bayesian linear regression on RFF features (a Gaussian-process
--- approximation).
---
--- Prior: @w ~ N(0, I)@ (the @σ_f@ amplitude is already in the features).
---
--- Likelihood: @y = φᵀ w + ε@, @ε ~ N(0, σ_n²)@.
---
--- Posterior: @Σ⁻¹ = ΦᵀΦ / σ_n² + I@, @μ = Σ Φᵀ y / σ_n²@.
-data RFFGPFit = RFFGPFit
-  { rffgpFeatures :: RFFFeatures
-  , rffgpSigma    :: LA.Matrix Double   -- ^ Posterior covariance @Σ@ (@D × D@).
-  , rffgpMean     :: LA.Vector Double   -- ^ Posterior mean @μ@ (length @D@).
-  , rffgpSigmaN   :: Double             -- ^ Observation noise SD @σ_n@.
-  } deriving (Show)
-
--- | Fit an RFF-based Bayesian linear-regression GP.
-rffGP :: RFFFeatures -> [Double] -> [Double] -> Double -> RFFGPFit
-rffGP rff xs ys sigmaN =
-  let phi    = rffFeatures rff xs
-      d      = rffDim rff
-      sigN2  = sigmaN ^ (2 :: Int)
-      yV     = LA.fromList ys
-      sigInv = LA.scale (1 / sigN2) (LA.tr phi LA.<> phi)
-                 `LA.add` LA.ident d
-      sigma  = LA.inv sigInv
-      mu     = sigma LA.#> LA.scale (1 / sigN2) (LA.tr phi LA.#> yV)
-  in RFFGPFit
-       { rffgpFeatures = rff
-       , rffgpSigma    = sigma
-       , rffgpMean     = mu
-       , rffgpSigmaN   = sigmaN
-       }
-
--- | Per-test-point @(mean, variance of f)@. The observation-noise term
--- @σ_n²@ is /not/ added.
---
--- @mean = φ(x*)ᵀ μ@, @var = φ(x*)ᵀ Σ φ(x*)@.
-predictRFFGP :: RFFGPFit -> [Double] -> [(Double, Double)]
-predictRFFGP fit xNew =
-  let rff   = rffgpFeatures fit
-      phi   = rffFeatures rff xNew                  -- n_new × D
-      mu    = rffgpMean fit
-      sigma = rffgpSigma fit
-      means = LA.toList (phi LA.#> mu)
-      vars  = [ max 0 (LA.dot phi_i (sigma LA.#> phi_i))
-              | phi_i <- LA.toRows phi ]
-  in zip means vars
-
--- ---------------------------------------------------------------------------
--- 多変量入力 (p 次元) 対応 (Phase B-RFF)
--- ---------------------------------------------------------------------------
-
--- | Multivariate RFF feature-generation parameters. 'rffmvOmegas' is a
--- @p × D@ matrix; each column is one frequency vector @ω_j ∈ ℝ^p@.
-data RFFFeaturesMV = RFFFeaturesMV
-  { rffmvKernel      :: RFFKernel
-  , rffmvDim         :: Int                -- ^ Input dimension @p@.
-  , rffmvOmegas      :: LA.Matrix Double   -- ^ Frequencies (@p × D@).
-  , rffmvBs          :: V.Vector Double    -- ^ Phases @b_j@ (length @D@).
-  , rffmvSigmaF      :: Double             -- ^ Signal SD @σ_f@.
-  , rffmvLengthScale :: Double             -- ^ Shared length scale @ℓ@
-                                           --   (no ARD support yet).
-  } deriving (Show)
-
--- | Sample multivariate RFF features for the RBF kernel.
--- Each component @ω_j[k] ~ N(0, 1/ℓ²)@ independently.
-sampleRFFRBFMV
-  :: PrimMonad m
-  => Int -> Int -> Double -> Double -> Gen (PrimState m) -> m RFFFeaturesMV
-sampleRFFRBFMV p d ell sf gen = do
-  let total = p * d
-  ws <- V.replicateM total (MWCD.normal 0 (1/ell) gen)
-  bs <- V.replicateM d (uniformR (0, 2*pi) gen)
-  let omegaMat = LA.reshape d (LA.fromList (V.toList ws))
-  return RFFFeaturesMV
-    { rffmvKernel      = RFFRBF
-    , rffmvDim         = p
-    , rffmvOmegas      = omegaMat
-    , rffmvBs          = bs
-    , rffmvSigmaF      = sf
-    , rffmvLengthScale = ell
-    }
-
--- | Sample multivariate RFF features for the Matérn 5/2 kernel.
-sampleRFFMatern52MV
-  :: PrimMonad m
-  => Int -> Int -> Double -> Double -> Gen (PrimState m) -> m RFFFeaturesMV
-sampleRFFMatern52MV p d ell sf gen = do
-  let nu = 2.5 :: Double
-  ws <- V.replicateM (p * d) $ do
-    z <- MWCD.normal 0 (1/ell) gen
-    u <- MWCD.gamma nu (1/nu) gen
-    return (z / sqrt u)
-  bs <- V.replicateM d (uniformR (0, 2*pi) gen)
-  return RFFFeaturesMV
-    { rffmvKernel      = RFFMatern52
-    , rffmvDim         = p
-    , rffmvOmegas      = LA.reshape d (LA.fromList (V.toList ws))
-    , rffmvBs          = bs
-    , rffmvSigmaF      = sf
-    , rffmvLengthScale = ell
-    }
-
--- | 純粋 (seed) 版 'sampleRFFRBFMV'。
-sampleRFFRBFMVPure :: Int -> Int -> Double -> Double -> Word32 -> RFFFeaturesMV
-sampleRFFRBFMVPure p d ell sf seed =
-  runST (initialize (V.singleton seed) >>= sampleRFFRBFMV p d ell sf)
-
--- | 純粋 (seed) 版 'sampleRFFMatern52MV'。
-sampleRFFMatern52MVPure :: Int -> Int -> Double -> Double -> Word32 -> RFFFeaturesMV
-sampleRFFMatern52MVPure p d ell sf seed =
-  runST (initialize (V.singleton seed) >>= sampleRFFMatern52MV p d ell sf)
-
--- | Multivariate feature matrix: @X (n × p) → Φ (n × D)@.
--- @φ_j(x) = σ_f √(2/D) cos(ω_jᵀ x + b_j)@.
---
--- Implementation: a single fused @runST + MVector@ pass writes the
--- @n × D@ output. The previous version went through
--- @LA.toRows xo + list comp (r + bs) + LA.fromRows + LA.cmap cos +
--- LA.scale coef@, allocating four @n × D@ intermediates and one list
--- of @n@ row vectors per call. This single-pass version emits one
--- @n × D@ allocation and computes
--- @coef · cos(xoFlat[i,j] + bs[j])@ in place.
-rffFeaturesMV :: RFFFeaturesMV -> LA.Matrix Double -> LA.Matrix Double
-rffFeaturesMV rff x =
-  let d      = LA.cols (rffmvOmegas rff)
-      sf     = rffmvSigmaF rff
-      coef   = sf * sqrt (2 / fromIntegral d)
-      -- X @ Ω → n × D (BLAS GEMM, kept).
-      xo     = x LA.<> rffmvOmegas rff
-      n      = LA.rows xo
-      xoFlat = LA.flatten xo
-      -- Phases as a Storable Vector (length D) for O(1) indexing.
-      bs     = VS.fromList (V.toList (rffmvBs rff))
-      out    = runST $ do
-        v <- VSM.new (n * d)
-        let go i j
-              | i >= n    = pure ()
-              | j >= d    = go (i + 1) 0
-              | otherwise = do
-                  let !idx = i * d + j
-                      !z   = (xoFlat `VS.unsafeIndex` idx)
-                           + (bs     `VS.unsafeIndex` j)
-                      !val = coef * cos z
-                  VSM.unsafeWrite v idx val
-                  go i (j + 1)
-        go 0 0
-        VS.unsafeFreeze v
-  in LA.reshape d out
-
--- | Multivariate RFF ridge fit.
-data RFFRidgeFitMV = RFFRidgeFitMV
-  { rffrmvFeatures :: RFFFeaturesMV
-  , rffrmvWeights  :: LA.Vector Double   -- ^ Weights (length @D@).
-  , rffrmvLambda   :: Double             -- ^ Ridge penalty @λ@.
-  } deriving (Show)
-
--- | Single-output multivariate RFF ridge regression. Delegates to
--- 'rffRidgeMVMulti' by promoting @y@ to a one-column matrix.
-rffRidgeMV :: RFFFeaturesMV -> LA.Matrix Double -> [Double] -> Double
-           -> RFFRidgeFitMV
-rffRidgeMV rff x ys lam =
-  let yMat = LA.asColumn (LA.fromList ys)
-      mf   = rffRidgeMVMulti rff x yMat lam
-      w    = LA.flatten (rffrmvmWeights mf LA.¿ [0])
-  in RFFRidgeFitMV rff w lam
-
--- | Predict at new inputs from a 'RFFRidgeFitMV'.
-predictRFFRidgeMV :: RFFRidgeFitMV -> LA.Matrix Double -> [Double]
-predictRFFRidgeMV fit xNew =
-  let phi = rffFeaturesMV (rffrmvFeatures fit) xNew
-  in LA.toList (phi LA.#> rffrmvWeights fit)
-
--- | Multivariate-input RFF GP (Bayesian linear regression on RFF features).
--- The multi-input analogue of 'rffGP': same posterior algebra
--- (@Σ⁻¹ = ΦᵀΦ/σ_n² + I@, @μ = Σ Φᵀy/σ_n²@) but @Φ@ comes from
--- 'rffFeaturesMV'. Used by the @GpRff@ quadrant of the unified @gpMulti@
--- spec to provide a posterior-variance band under RFF approximation.
-data RFFGPFitMV = RFFGPFitMV
-  { rffgpmvFeatures :: RFFFeaturesMV
-  , rffgpmvSigma    :: LA.Matrix Double   -- ^ Posterior covariance @Σ@ (@D × D@).
-  , rffgpmvMean     :: LA.Vector Double   -- ^ Posterior mean @μ@ (length @D@).
-  , rffgpmvSigmaN   :: Double             -- ^ Observation noise SD @σ_n@.
-  } deriving (Show)
-
--- | Fit a multivariate-input RFF Bayesian-linear-regression GP.
-rffGPMV :: RFFFeaturesMV -> LA.Matrix Double -> [Double] -> Double -> RFFGPFitMV
-rffGPMV rff x ys sigmaN =
-  let phi    = rffFeaturesMV rff x
-      d      = LA.cols (rffmvOmegas rff)
-      sigN2  = sigmaN ^ (2 :: Int)
-      yV     = LA.fromList ys
-      sigInv = LA.scale (1 / sigN2) (LA.tr phi LA.<> phi) `LA.add` LA.ident d
-      sigma  = LA.inv sigInv
-      mu     = sigma LA.#> LA.scale (1 / sigN2) (LA.tr phi LA.#> yV)
-  in RFFGPFitMV
-       { rffgpmvFeatures = rff
-       , rffgpmvSigma    = sigma
-       , rffgpmvMean     = mu
-       , rffgpmvSigmaN   = sigmaN
-       }
-
--- | Per-test-point @(mean, variance of f)@ for an 'RFFGPFitMV'. The
--- observation-noise term @σ_n²@ is /not/ added (matching 'predictRFFGP').
-predictRFFGPMV :: RFFGPFitMV -> LA.Matrix Double -> [(Double, Double)]
-predictRFFGPMV fit xNew =
-  let phi   = rffFeaturesMV (rffgpmvFeatures fit) xNew
-      mu    = rffgpmvMean fit
-      sigma = rffgpmvSigma fit
-      means = LA.toList (phi LA.#> mu)
-      vars  = [ max 0 (LA.dot p (sigma LA.#> p)) | p <- LA.toRows phi ]
-  in zip means vars
-
--- | Multivariate-input multi-output RFF ridge fit. @X@ is @n × p@,
--- @Y@ is @n × q@, weights @W@ are @D × q@.
-data RFFRidgeFitMVMO = RFFRidgeFitMVMO
-  { rffrmvmFeatures :: RFFFeaturesMV
-  , rffrmvmWeights  :: LA.Matrix Double   -- ^ D × q
-  , rffrmvmLambda   :: Double
-  } deriving (Show)
-
--- | Multivariate-input multi-output RFF ridge regression:
--- @W = (ΦᵀΦ + λI)⁻¹ Φᵀ Y@.
---
--- The system is SPD by construction, so we solve via Cholesky rather
--- than the general LSQ path '(LA.<\>)'. The diagonal regularizer is
--- applied via @addToDiagRFF@ (in-place runST update) instead of
--- @gram + LA.scale lam (LA.ident d)@ which would allocate a fresh
--- @D × D@ identity.
-rffRidgeMVMulti :: RFFFeaturesMV -> LA.Matrix Double -> LA.Matrix Double
-                -> Double -> RFFRidgeFitMVMO
-rffRidgeMVMulti rff x ys lam =
-  let phi  = rffFeaturesMV rff x           -- n × D
-      gram = LA.tr phi LA.<> phi           -- D × D (SPD)
-      regK = addToDiagRFF lam gram          -- D × D
-      rhs  = LA.tr phi LA.<> ys            -- D × q
-      w    = Chol.cholSolveJitter regK rhs
-  in RFFRidgeFitMVMO rff w lam
-
--- | Add a scalar to the diagonal of a square matrix in a single
--- 'runST' pass (no fresh @D × D@ identity allocation). Mirrors
--- 'Hanalyze.Model.GP.addToDiag'; duplicated here to keep the modules
--- decoupled.
-addToDiagRFF :: Double -> LA.Matrix Double -> LA.Matrix Double
-addToDiagRFF c m =
-  let d    = LA.rows m
-      flat = LA.flatten m
-      out  = runST $ do
-        v <- VSM.new (d * d)
-        let copy i
-              | i >= d * d = pure ()
-              | otherwise  = do
-                  VSM.unsafeWrite v i (flat `VS.unsafeIndex` i)
-                  copy (i + 1)
-        copy 0
-        let bumpDiag i
-              | i >= d    = pure ()
-              | otherwise = do
-                  let !idx = i * d + i
-                  d_old <- VSM.unsafeRead v idx
-                  VSM.unsafeWrite v idx (d_old + c)
-                  bumpDiag (i + 1)
-        bumpDiag 0
-        VS.unsafeFreeze v
-  in LA.reshape d out
-
--- | Multi-output prediction at new inputs from a 'RFFRidgeFitMVMO'.
-predictRFFRidgeMVMulti :: RFFRidgeFitMVMO -> LA.Matrix Double -> LA.Matrix Double
-predictRFFRidgeMVMulti fit xNew =
-  let phi = rffFeaturesMV (rffrmvmFeatures fit) xNew
-  in phi LA.<> rffrmvmWeights fit
-
--- ---------------------------------------------------------------------------
--- 周辺尤度最大化 (RFF GP 流の HP チューニング、Phase 2)
--- ---------------------------------------------------------------------------
-
--- | Log marginal likelihood under the RBF kernel for multivariate input
--- @X@ (@n × p@) and observations @y@.
---
---   K_ij = σ_f² · exp(-‖x_i - x_j‖² / (2 ℓ²))
---   y | θ ~ N(0, K + σ_n² I)
---
---   log p(y|θ) = -½ yᵀ (K+σ_n² I)⁻¹ y - ½ log|K+σ_n² I| - n/2 log(2π)
---
--- Cholesky 分解で安定計算。ℓ が極小で K が特異化したら -∞ 近似値を返す。
-logMarginalLikRBFMV
-  :: LA.Matrix Double      -- ^ X (n × p)
-  -> LA.Vector Double      -- ^ y (n)
-  -> Double                -- ^ ℓ
-  -> Double                -- ^ σ_f
-  -> Double                -- ^ σ_n
-  -> Double
-logMarginalLikRBFMV x y ell sf sn =
-  let n     = LA.rows x
-      kMat  = rbfKernelMat x ell sf
-      cMat  = kMat + LA.scale (sn * sn) (LA.ident n)
-      -- Cholesky: cMat = Rᵀ R (R 上三角)。失敗時は jitter を加えて再試行。
-      tryChol c =
-        let result = unsafePerformIO $ try (evaluate (LA.chol (LA.sym c))) :: Either SomeException (LA.Matrix Double)
-        in case result of
-             Right r -> Just r
-             Left _  -> Nothing
-      mR = case tryChol cMat of
-             Just r  -> Just r
-             Nothing -> tryChol (cMat + LA.scale 1e-6 (LA.ident n))
-  in case mR of
-       Nothing -> -1e30  -- 特異 → ペナルティ
-       Just r  ->
-         let logDet  = 2 * sum (map log (LA.toList (LA.takeDiag r)))
-             alpha   = cMat LA.<\> y
-             dataFit = LA.dot y alpha
-         in -0.5 * dataFit - 0.5 * logDet
-            - fromIntegral n / 2 * log (2 * pi)
-
--- | RBF kernel matrix for inputs @X@ (@n × p@):
--- @K[i,j] = σ_f² · exp(−‖x_i − x_j‖² / (2ℓ²))@.
-rbfKernelMat :: LA.Matrix Double -> Double -> Double -> LA.Matrix Double
-rbfKernelMat x ell sf =
-  let sf2   = sf * sf
-      twol2 = 2 * ell * ell
-      d2    = KD.pairwiseSqDist x
-  in LA.cmap (\v -> sf2 * exp (negate v / twol2)) d2
-
--- | Marginal-likelihood maximization result.
-data MLikResult = MLikResult
-  { mlEll      :: !Double
-  , mlSigmaF   :: !Double
-  , mlSigmaN   :: !Double
-  , mlLogMlik  :: !Double
-  , mlGridPts  :: !Int      -- ^ 評価したグリッド点数 (debug 用)
-  } deriving (Show)
-
--- | Maximize the marginal likelihood by grid search over @(ℓ, σ_f, σ_n)@.
---
--- 戦略:
---
--- 1. ℓ は median pairwise distance を中心に log 等間隔で n_ℓ 点
--- 2. σ_f は std(y) を中心に log で n_σf 点
--- 3. σ_n は std(y)·{0.001..0.5} の log 等間隔で n_σn 点
--- 4. 全 n_ℓ × n_σf × n_σn 点で log-mlik を評価し最良を取る
--- 5. 最良点周辺で 1/3 の幅で同点数のグリッドを再探索 (1 段の coarse-to-fine)
---
--- デフォルトは (20, 8, 8) = 1280 点。最終的に 2560 点 (再探索込)。
--- n=200 までは数秒。
-maximizeMarginalLikRBFMV
-  :: LA.Matrix Double
-  -> LA.Vector Double
-  -> Maybe (Int, Int, Int)         -- ^ (n_ℓ, n_σf, n_σn). Default (20,8,8)
-  -> MLikResult
-maximizeMarginalLikRBFMV x y mGrid =
-  let (nL, nSF, nSN) = case mGrid of
-        Just g  -> g
-        Nothing -> (20, 8, 8)
-      yStd  = sampleStd (LA.toList y)
-      ellM  = max 1e-3 (medianPairwiseDist x)
-      sfM   = max 1e-6 yStd
-      -- Stage 1: 広めグリッド
-      ellGrid1 = logSpace (ellM * 0.05) (ellM * 20)   nL
-      sfGrid1  = logSpace (sfM  * 0.25) (sfM  * 4)    nSF
-      snGrid1  = logSpace (yStd * 1e-3) (yStd * 0.5)  nSN
-      stage1   = bestOver x y ellGrid1 sfGrid1 snGrid1
-      -- Stage 2: 最良点周辺で 1/3 幅
-      (ell1, sf1, sn1, _) = stage1
-      ellGrid2 = logSpace (ell1 / 3) (ell1 * 3) nL
-      sfGrid2  = logSpace (sf1  / 2) (sf1  * 2) nSF
-      snGrid2  = logSpace (sn1  / 3) (sn1  * 3) nSN
-      stage2   = bestOver x y ellGrid2 sfGrid2 snGrid2
-      (ell2, sf2, sn2, ml2) = stage2
-  in MLikResult ell2 sf2 sn2 ml2
-       (nL * nSF * nSN * 2)
-
--- | Differential-Evolution variant of 'maximizeMarginalLikRBFMV'.
---
--- coarse stage を Differential Evolution (`Hanalyze.Optim.DifferentialEvolution`) で
--- 行い、fine stage は従来通りグリッド。
---
--- DE の探索空間は log 空間 (log_ℓ, log_σ_f, log_σ_n) の 3 次元。
--- 評価予算は generations 引数で制御 (典型 30-100 で集団 30、合計 900-3000 評価)。
--- グリッド版より広範囲を効率的に探索でき、log-mlik の局所解にハマりにくい。
-maximizeMarginalLikRBFMV_DE
-  :: LA.Matrix Double
-  -> LA.Vector Double
-  -> Int                                -- ^ DE generations
-  -> System.Random.MWC.GenIO
-  -> IO MLikResult
-maximizeMarginalLikRBFMV_DE x y nGen gen = do
-  let yStd  = sampleStd (LA.toList y)
-      ellM  = max 1e-3 (medianPairwiseDist x)
-      sfM   = max 1e-6 yStd
-      -- log 空間の bounds (元の logSpace 範囲と一致)
-      bounds =
-        [ (log (ellM * 0.05),  log (ellM * 20))     -- log ℓ
-        , (log (sfM  * 0.25),  log (sfM  * 4))      -- log σ_f
-        , (log (yStd * 1e-3),  log (yStd * 0.5))    -- log σ_n
-        ]
-      -- 目的関数: log-mlik を最大化 → DE は最小化なので negate
-      obj [le, lsf, lsn] = negate (logMarginalLikRBFMV x y (exp le) (exp lsf) (exp lsn))
-      obj _              = 1e30
-  let cfg = (DEM.defaultDEConfig bounds)
-              { DEM.deStop = OCM.defaultStopCriteria { OCM.stMaxIter = nGen } }
-  r <- DEM.runDEWith cfg obj gen
-  let [le, lsf, lsn] = OCM.orBest r
-      ell0 = exp le
-      sf0  = exp lsf
-      sn0  = exp lsn
-      -- Stage 2 (fine grid) for refinement
-      ellGrid2 = logSpace (ell0 / 3) (ell0 * 3) 8
-      sfGrid2  = logSpace (sf0  / 2) (sf0  * 2) 6
-      snGrid2  = logSpace (sn0  / 3) (sn0  * 3) 6
-      (ell2, sf2, sn2, ml2) = bestOver x y ellGrid2 sfGrid2 snGrid2
-      totalEvals = OCM.orIters r * DEM.dePopSize cfg + 8 * 6 * 6
-  return $ MLikResult ell2 sf2 sn2 ml2 totalEvals
-
--- | Best @log p@ over the full Cartesian product of @(ellGrid, sfGrid, snGrid)@.
-bestOver
-  :: LA.Matrix Double -> LA.Vector Double
-  -> [Double] -> [Double] -> [Double]
-  -> (Double, Double, Double, Double)
-bestOver x y ells sfs sns =
-  let evaluations =
-        [ (ell, sf, sn, logMarginalLikRBFMV x y ell sf sn)
-        | ell <- ells, sf <- sfs, sn <- sns ]
-      best = foldr1 (\a@(_,_,_,la) b@(_,_,_,lb) ->
-                       if la >= lb then a else b) evaluations
-  in best
-
--- | Log-spaced @n@ points between @lo@ and @hi@.
-logSpace :: Double -> Double -> Int -> [Double]
-logSpace lo hi n
-  | n <= 1    = [lo]
-  | lo <= 0   = logSpace 1e-9 hi n  -- 安全フォールバック
-  | otherwise =
-      let lLo = log lo
-          lHi = log hi
-          step = (lHi - lLo) / fromIntegral (n - 1)
-      in [ exp (lLo + fromIntegral i * step) | i <- [0 .. n - 1] ]
-
--- | Median pairwise distance between rows (the standard median heuristic
--- for an RBF length scale).
--- | Phase 11b (2026-05-14): rewritten to use BLAS gram matrix
--- ('KD.pairwiseSqDist') + 'Intro.sort' on a flat 'VS.Vector'. The previous
--- implementation built an @O(n²)@ list of pair distances with @rows !! i@
--- (each @O(i)@) and ran a naive list quicksort, which exploded space to
--- @O(n²)@..@O(n³)@ thunks and OOM-killed WSL2 around @n=768@.
-medianPairwiseDist :: LA.Matrix Double -> Double
-medianPairwiseDist x =
-  let n = LA.rows x in
-  if n < 2 then 1.0 else
-    let d2  = KD.pairwiseSqDist x        -- n × n via BLAS GEMM
-        d2f = LA.flatten d2
-        m   = n * (n - 1) `div` 2
-        ds  = runST $ do
-          v <- VSM.unsafeNew m
-          let go !k !i !j
-                | i >= n - 1 = pure ()
-                | j >= n     = go k (i + 1) (i + 2)
-                | otherwise  = do
-                    let s = VS.unsafeIndex d2f (i * n + j)
-                    VSM.unsafeWrite v k (sqrt (max 0 s))
-                    go (k + 1) i (j + 1)
-          go 0 0 1
-          Intro.sort v
-          VS.unsafeFreeze v
-    in if VS.null ds then 1.0 else VS.unsafeIndex ds (m `div` 2)
-
-sampleStd :: [Double] -> Double
-sampleStd xs
-  | length xs <= 1 = 1.0
-  | otherwise =
-      let n = fromIntegral (length xs)
-          m = sum xs / n
-          v = sum [ (x - m) * (x - m) | x <- xs ] / (n - 1)
-      in if v <= 0 then 1.0 else sqrt v
-
-
--- ---------------------------------------------------------------------------
--- LOOCV 解析解 (Phase 3 — Ridge の closed-form leave-one-out cross-validation)
--- ---------------------------------------------------------------------------
-
--- | Result of LOOCV-based hyperparameter search.
-data LOOCVResult = LOOCVResult
-  { lcEll      :: !Double
-  , lcSigmaF   :: !Double   -- ^ 信号 sd (= std(y) を使う簡易版)
-  , lcLambda   :: !Double   -- ^ Ridge 正則化
-  , lcLOOCV    :: !Double   -- ^ LOOCV(λ) = mean square LOO residual
-  , lcGridPts  :: !Int
-  } deriving (Show)
-
--- | Closed-form LOOCV for RFF ridge regression using a Cholesky
--- factorization plus the hat-matrix diagonal.
---
---   H = Φ (ΦᵀΦ + λI)⁻¹ Φᵀ
---   ŷ = H y
---   LOOCV(λ) = (1/n) Σᵢ ((y_i - ŷ_i) / (1 - H_ii))²
---
--- 本関数は与えられた特徴行列 @feats@ (= 既に ω/b/σ_f が決まったもの) と
--- Ridge λ に対して LOOCV を返す。グリッドサーチ側ではこれを多数の λ で
--- 呼び出すが、Φ は 1 度だけ計算すれば良いので外側でキャッシュする。
-loocvRFFRidgeMV
-  :: RFFFeaturesMV
-  -> LA.Matrix Double           -- ^ X (n × p)
-  -> LA.Vector Double           -- ^ y (n)
-  -> Double                     -- ^ λ
-  -> Double
-loocvRFFRidgeMV feats x y lam =
-  let phi = rffFeaturesMV feats x      -- n × D
-  in loocvFromPhi phi y lam
-
--- | Φ から LOOCV を計算する内部実装 (グリッドサーチでキャッシュ用)。
--- Cholesky ベース (Φ_ridge = Φᵀ Φ + λI、A = chol(Φ_ridge))。
---   H = Φ Φ_ridge⁻¹ Φᵀ
---   T = Φ Φ_ridge⁻¹  → diag(H) = row-sum(T ⊙ Φ)
-loocvFromPhi :: LA.Matrix Double -> LA.Vector Double -> Double -> Double
-loocvFromPhi phi y lam =
-  let n     = LA.rows phi
-      d     = LA.cols phi
-      gram  = LA.tr phi LA.<> phi             -- D × D
-      regK  = gram + LA.scale lam (LA.ident d)
-      -- 解析解: w = regK⁻¹ Φᵀ y
-      w     = regK LA.<\> (LA.tr phi LA.#> y)
-      yhat  = phi LA.#> w
-      -- diag(H) = diag(Φ M Φᵀ) where M = regK⁻¹
-      -- T = Φ M  (n × D)。Φ M Φᵀ の対角 = row(T) · row(Φ)
-      tMat  = LA.tr (regK LA.<\> LA.tr phi)   -- T = Φ M、n × D
-      hDiag = LA.fromList
-                [ LA.dot (LA.flatten (tMat LA.? [i]))
-                         (LA.flatten (phi  LA.? [i]))
-                | i <- [0 .. n - 1] ]
-      -- 1 - H_ii の極小ガード
-      oneMinusH = LA.cmap (\h -> max 1e-12 (1 - h)) hDiag
-      resid     = y - yhat
-      ratios    = LA.toList resid `divList` LA.toList oneMinusH
-      sse       = sum [ r * r | r <- ratios ]
-  in sse / fromIntegral (max 1 n)
-  where
-    divList xs ys = zipWith (/) xs ys
-
--- | Search a log-spaced @(ℓ, λ)@ grid for the smallest LOOCV.
---
--- ℓ ごとに ω を新規サンプリングするため IO。グリッドサイズ default (8, 20):
--- ℓ 8 点 × λ 20 点 = 160 fit。各 fit O(n D + D³) で n=545, D=200 程度なら
--- 全体で数秒程度。
---
--- σ_f は std(y) 固定 (Ridge ↔ GP 等価では σ_f は ω 分散と一緒に動くべきだが、
--- λ で吸収できるので簡易化)。
-gridSearchLOOCVRBFMV
-  :: Int                               -- ^ p (入力次元)
-  -> Int                               -- ^ D (特徴次元)
-  -> LA.Matrix Double                  -- ^ X
-  -> LA.Vector Double                  -- ^ y
-  -> Maybe (Int, Int)                  -- ^ (n_ℓ, n_λ) default (8, 20)
-  -> GenIO
-  -> IO LOOCVResult
-gridSearchLOOCVRBFMV p d x y mGrid gen = do
-  let (nL, nLam) = case mGrid of { Just g -> g; Nothing -> (8, 20) }
-      yStd  = sampleStd (LA.toList y)
-      sf    = max 1e-9 yStd
-      ellM  = max 1e-3 (medianPairwiseDist x)
-      ellGrid = logSpace (ellM * 0.05) (ellM * 20)  nL
-      lamGrid = logSpace (yStd * 1e-6) (yStd * 10)  nLam
-  -- 各 ℓ について 1 度サンプリングしてから λ ループ
-  evals <- mapM (\ell -> do
-                   feats <- sampleRFFRBFMV p d ell sf gen
-                   let phi = rffFeaturesMV feats x
-                   let scoresAtLam = [ (ell, sf, lam, loocvFromPhi phi y lam)
-                                     | lam <- lamGrid ]
-                   return scoresAtLam)
-                ellGrid
-  let evaluations = concat evals
-      best = foldr1 (\a@(_,_,_,la) b@(_,_,_,lb) ->
-                       if la <= lb then a else b) evaluations
-      (bEll, bSf, bLam, bL) = best
-  return LOOCVResult
-    { lcEll = bEll
-    , lcSigmaF = bSf
-    , lcLambda = bLam
-    , lcLOOCV  = bL
-    , lcGridPts = nL * nLam
-    }
-
--- | Differential-Evolution variant of 'gridSearchLOOCVRBFMV'.
---
--- (log_ℓ, log_λ) の 2 次元空間を Differential Evolution で探索。
--- ω は ℓ ごとに新規サンプリング (RFF の特性上避けられない) のでコストは
--- グリッド版と同程度。グリッドの離散性が問題になる場合に有効。
-gridSearchLOOCVRBFMV_DE
-  :: Int                               -- ^ p (入力次元)
-  -> Int                               -- ^ D (特徴次元)
-  -> LA.Matrix Double                  -- ^ X
-  -> LA.Vector Double                  -- ^ y
-  -> Int                               -- ^ DE generations
-  -> System.Random.MWC.GenIO
-  -> IO LOOCVResult
-gridSearchLOOCVRBFMV_DE p d x y nGen gen = do
-  let yStd  = sampleStd (LA.toList y)
-      sf    = max 1e-9 yStd
-      ellM  = max 1e-3 (medianPairwiseDist x)
-      bounds =
-        [ (log (ellM * 0.05), log (ellM * 20))      -- log ℓ
-        , (log (yStd * 1e-6), log (yStd * 10))      -- log λ
-        ]
-  -- 目的関数: log-space で受けた (log_ell, log_lam) で LOOCV を返す。
-  -- ω サンプリングは IO を含むため `unsafePerformIO` を使うが、決定的シードを
-  -- 内部で固定しないと毎回違う値が出る。簡略化のため: ℓ ごとに 1 度だけ
-  -- サンプリングしたかったが、純粋関数化のため IO Ref キャッシュは省略。
-  -- 各 DE 評価で feats を再サンプル (ノイズが入るが、実用上は最終 best 周辺で
-  -- 十分平均化される)。
-  --
-  -- 評価をプリ計算: 候補集団のサイズ × generations 回 fresh sample。
-  let cfg = (DEM.defaultDEConfig bounds)
-              { DEM.deStop = OCM.defaultStopCriteria { OCM.stMaxIter = nGen } }
-  -- ω サンプリング用の固定シード生成器を別途準備
-  -- (DE 内のランダムは gen を共有、評価用の ω は新たに引く)
-  obj <- pure $ \[le, llam] ->
-    System.IO.Unsafe.unsafePerformIO $ do
-      let ell = exp le
-          lam = exp llam
-      feats <- sampleRFFRBFMV p d ell sf gen
-      let phi = rffFeaturesMV feats x
-      pure (loocvFromPhi phi y lam)
-  r <- DEM.runDEWith cfg obj gen
-  let [le, llam] = OCM.orBest r
-      bestEll = exp le
-      bestLam = exp llam
-      bestL   = OCM.orValue r
-  return LOOCVResult
-    { lcEll = bestEll
-    , lcSigmaF = sf
-    , lcLambda = bestLam
-    , lcLOOCV  = bestL
-    , lcGridPts = OCM.orIters r * DEM.dePopSize cfg
-    }
-
--- | Bayesian-optimization variant of 'gridSearchLOOCVRBFMV'
--- (金子流: 初期点 + GP 代理モデル + 獲得関数で評価回数を削減)。
---
--- グリッドの 160 点 (8 ℓ × 20 λ) に対し、 既定 30 評価 (init 8 + iter 22) で
--- 同等の @(ℓ, λ)@ を (log ℓ, log λ) の 2 次元 BO ('BO.bayesOptND') で求める。
---
--- **RFF + BO の肝**: RFF の周波数 ω~N(0, 1/ℓ) はランダムなので、 同じ @(ℓ,λ)@ でも
--- 引き直すと LOOCV が変わる (stochastic)。 BO は決定的目的関数を仮定するため、
--- ここでは **基底 ω₀~N(0,1) と bias b を 1 度だけ引いて固定**し、 ℓ ごとに
--- @ω = ω₀ / ℓ@ とスケールする。 これで LOOCV(ℓ,λ) は ℓ の決定的関数になり、 GP 代理
--- が綺麗に乗る。 ℓ ごとに ω を引き直す grid / DE 版 (上記) より MC ノイズが小さく
--- **むしろ安定**。 D を上げるほど RFF の分散は減る。
-bayesOptLOOCVRBFMV
-  :: Int                               -- ^ p (入力次元)
-  -> Int                               -- ^ D (特徴次元)
-  -> LA.Matrix Double                  -- ^ X
-  -> LA.Vector Double                  -- ^ y
-  -> Maybe (Int, Int)                  -- ^ (initPoints, iterations) default (8, 22) = 30 評価
-  -> System.Random.MWC.GenIO
-  -> IO LOOCVResult
-bayesOptLOOCVRBFMV p d x y mBudget gen = do
-  let (nInit, nIter) = case mBudget of { Just b -> b; Nothing -> (8, 22) }
-      yStd  = sampleStd (LA.toList y)
-      sf    = max 1e-9 yStd
-      ellM  = max 1e-3 (medianPairwiseDist x)
-      bounds =
-        [ (log (ellM * 0.05), log (ellM * 20))      -- log ℓ
-        , (log (yStd * 1e-6), log (yStd * 10))      -- log λ
-        ]
-  -- 基底周波数 ω₀~N(0,1) + bias b を 1 度だけ引いて固定 (= 決定的目的関数化)。
-  ws0 <- V.replicateM (p * d) (MWCD.normal 0 1 gen)
-  bs  <- V.replicateM d (uniformR (0, 2 * pi) gen)
-  let omega0 = LA.reshape d (LA.fromList (V.toList ws0))   -- p × d (ℓ=1 相当)
-      featsAt ell =
-        RFFFeaturesMV
-          { rffmvKernel      = RFFRBF
-          , rffmvDim         = p
-          , rffmvOmegas      = LA.scale (1 / ell) omega0   -- ω = ω₀ / ℓ
-          , rffmvBs          = bs
-          , rffmvSigmaF      = sf
-          , rffmvLengthScale = ell
-          }
-      objective [le, llam] =
-        let ell = exp le
-            lam = exp llam
-            phi = rffFeaturesMV (featsAt ell) x
-        in pure (loocvFromPhi phi y lam)
-      objective _ = pure (1 / 0)   -- 次元不一致は +∞ (起き得ないが total に)
-      cfg = BO.defaultBayesOptConfig
-              { BO.boInitPoints = nInit, BO.boIterations = nIter }
-  (_history, (bestXs, bestL)) <- BO.bayesOptND cfg 8 objective bounds gen
-  let (bestEll, bestLam) = case bestXs of
-        (le : llam : _) -> (exp le, exp llam)
-        _               -> (ellM, yStd * 1e-3)
-  return LOOCVResult
-    { lcEll = max 1e-6 bestEll
-    , lcSigmaF = sf
-    , lcLambda = max 1e-8 bestLam
-    , lcLOOCV  = bestL
-    , lcGridPts = nInit + nIter
-    }
-
--- | L-BFGS variant of 'gridSearchLOOCVRBFMV' (固定基底 + 数値勾配 L-BFGS の多始点)。
---
--- 'bayesOptLOOCVRBFMV' と同じく **基底 ω₀~N(0,1) を 1 度引いて固定**し ℓ で
--- スケールすることで LOOCV(log ℓ, log λ) を決定的・微分可能化し、 数値勾配 L-BFGS
--- ('LBFGS.runLBFGSNumeric'、 GP の 'optimizeGP' と同じ engine) を複数始点から回して
--- LOOCV 最小を採る。 GP の多始点 L-BFGS の RFF 版で、 評価は O(D³) なので大 n でも
--- スケーラブル (厳密 GP marginal likelihood の O(n³) を回避)。 grid の離散性も BO の
--- 粗いサロゲートも避け、 連続最適化で (ℓ,λ) を精密に当てる。
-lbfgsLOOCVRBFMV
-  :: Int                               -- ^ p (入力次元)
-  -> Int                               -- ^ D (特徴次元)
-  -> LA.Matrix Double                  -- ^ X
-  -> LA.Vector Double                  -- ^ y
-  -> Maybe Int                         -- ^ multi-start 数 (default 4)
-  -> System.Random.MWC.GenIO
-  -> IO LOOCVResult
-lbfgsLOOCVRBFMV p d x y mStarts gen = do
-  let nStarts = max 1 (case mStarts of { Just n -> n; Nothing -> 3 })
-      yStd    = sampleStd (LA.toList y)
-      sf      = max 1e-9 yStd
-      ellM    = max 1e-3 (medianPairwiseDist x)
-      logEll0 = log ellM
-      logLam0 = log (max 1e-12 (yStd * 1e-3))
-  -- 基底 ω₀ + bias を 1 度だけ引いて固定 (= 決定的・微分可能化)。
-  ws0 <- V.replicateM (p * d) (MWCD.normal 0 1 gen)
-  bs  <- V.replicateM d (uniformR (0, 2 * pi) gen)
-  let omega0 = LA.reshape d (LA.fromList (V.toList ws0))
-      featsAt ell =
-        RFFFeaturesMV
-          { rffmvKernel      = RFFRBF
-          , rffmvDim         = p
-          , rffmvOmegas      = LA.scale (1 / ell) omega0
-          , rffmvBs          = bs
-          , rffmvSigmaF      = sf
-          , rffmvLengthScale = ell
-          }
-      -- LOOCV (最小化対象、 lbDir 既定 = Minimize)。
-      obj [le, llam] = loocvFromPhi (rffFeaturesMV (featsAt (exp le)) x) y (exp llam)
-      obj _          = 1 / 0
-      -- 2D 目的なので maxIter は控えめで十分収束 (数値勾配が高 D で高コストなため
-      -- 評価数を抑える)。 multi-start で局所性をカバー。
-      cfg = LBFGS.defaultLBFGSConfig
-              { LBFGS.lbStop = OCM.defaultStopCriteria
-                                 { OCM.stMaxIter = 40, OCM.stTolFun = 1e-8 } }
-  -- 多始点: base + (nStarts-1) ランダム摂動 (log 空間 正規)。
-  perturbs <- mapM (\_ -> do
-                      ze <- MWCD.normal 0 1.5 gen
-                      zl <- MWCD.normal 0 2.0 gen
-                      pure [logEll0 + ze, logLam0 + zl])
-                   [1 .. nStarts - 1]
-  results <- mapM (LBFGS.runLBFGSNumeric cfg obj) ([logEll0, logLam0] : perturbs)
-  let isFin v = not (isNaN v || isInfinite v)
-      scored  = [ (OCM.orBest r, OCM.orValue r) | r <- results, isFin (OCM.orValue r) ]
-      (bestX, bestVal) = case scored of
-        [] -> ([logEll0, logLam0], obj [logEll0, logLam0])
-        _  -> foldr1 (\a b -> if snd a <= snd b then a else b) scored
-      (bLe, bLlam) = case bestX of
-        (a : b : _) -> (a, b)
-        _           -> (logEll0, logLam0)
-  return LOOCVResult
-    { lcEll     = max 1e-6 (exp bLe)
-    , lcSigmaF  = sf
-    , lcLambda  = max 1e-8 (exp bLlam)
-    , lcLOOCV   = bestVal
-    , lcGridPts = nStarts
-    }
diff --git a/src/Hanalyze/Model/RandomForest.hs b/src/Hanalyze/Model/RandomForest.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/RandomForest.hs
+++ /dev/null
@@ -1,436 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Model.RandomForest
--- Description : 回帰用 Random Forest (CART + bagging + random feature subset、行インデックス置換方式)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Random forest for regression (CART + bagging + random feature subset).
---
--- /Performance/: this module was ported in B9b from a list-based
--- implementation to a row-index permutation scheme, mirroring the
--- 'Hanalyze.Model.DecisionTree' refactor:
---
---   * Single shared @LA.Matrix Double@ feature matrix.
---   * @VU.Vector Int@ row indices recurse through subtrees.
---   * Per-feature best split via 'Data.Vector.Algorithms.Intro' sort
---     and incremental sum / sum-of-squares sweep.
---   * Bootstrap = random index Vector (no row data copied).
---
--- The classic 'fitRF' over @[[Double]] / [Double]@ is preserved as a
--- backwards-compatibility wrapper that calls 'fitRFV'.
-module Hanalyze.Model.RandomForest
-  ( -- * Single regression tree
-    Tree (..)
-  , RFConfig (..)
-  , defaultRandomForest
-  , buildTree
-  , buildTreeV
-  , predictTree
-    -- * Forest
-  , RandomForest (..)
-  , fitRF
-  , fitRFV
-  , fitRFPure
-  , fitRFVPure
-  , predictRF
-  , featureImportance
-  , rfPermutationImportance
-  , defaultFeatureNames
-  ) where
-
-import qualified Data.Vector                  as V
-import qualified Data.Vector.Mutable          as VM
-import qualified Data.Vector.Unboxed          as VU
-import qualified Data.Vector.Unboxed.Mutable  as VUM
-import qualified Data.Vector.Algorithms.Intro as Intro
-import qualified Numeric.LinearAlgebra        as LA
-import qualified System.Random.MWC            as MWC
-import           Control.Monad                (replicateM)
-import           Control.Monad.Primitive      (PrimMonad, PrimState)
-import           Control.Monad.ST             (runST)
-import           Data.Word                    (Word32)
-import           Data.Text                    (Text)
-import qualified Data.Text                    as T
-
--- ---------------------------------------------------------------------------
--- Types
--- ---------------------------------------------------------------------------
-
--- | A regression tree node.
-data Tree
-  = Leaf !Double
-  | Node !Int !Double !Tree !Tree
-  deriving (Show)
-
--- | Random-forest configuration.
-data RFConfig = RFConfig
-  { rfTrees      :: !Int
-  , rfMaxDepth   :: !Int
-  , rfMinSamples :: !Int
-  , rfMtry       :: !(Maybe Int)
-  , rfBootstrap  :: !Bool
-  } deriving (Show)
-
-defaultRandomForest :: RFConfig
-defaultRandomForest = RFConfig
-  { rfTrees      = 100
-  , rfMaxDepth   = 12
-  , rfMinSamples = 3
-  , rfMtry       = Nothing
-  , rfBootstrap  = True
-  }
-
-data RandomForest = RandomForest
-  { rfTreesV         :: ![Tree]
-  , rfNFeatures      :: !Int
-  , rfImportance     :: !(V.Vector Double)  -- ^ impurity/split ベース (MDI 相当・R IncNodePurity)。
-  , rfPermImportance :: !(V.Vector Double)  -- ^ permutation ベース (MSE 増加・R %IncMSE・sklearn permutation_importance)。
-  , rfFeatureNames   :: ![Text]             -- ^ 特徴列名。 df|-> 経路が実列名を設定、 低レベル行列 fit は 'defaultFeatureNames' ("f1"..)。
-  } deriving (Show)
-
--- | 名前を持たない行列入力の既定特徴名 ("f1", "f2", …・1 始まり = R/sklearn 慣例)。
-defaultFeatureNames :: Int -> [Text]
-defaultFeatureNames d = [ "f" <> T.pack (show k) | k <- [1 .. d] ]
-
--- ---------------------------------------------------------------------------
--- Vector-based fit (primary)
--- ---------------------------------------------------------------------------
-
--- | IO ラッパ。 ロジックは 'PrimMonad' 汎用の 'fitRFVM' を共有
--- (mwc は 'PrimMonad' 汎用ゆえ ST/IO 両経路で同コード)。
-fitRFV :: RFConfig
-       -> LA.Matrix Double
-       -> VU.Vector Double
-       -> MWC.GenIO
-       -> IO RandomForest
-fitRFV = fitRFVM
-
--- | 'PrimMonad' 汎用の forest 本体。 'fitRFV' (IO) / 'fitRFVPure' (ST) が共有。
--- 乱数 (gen) は bootstrap index のみで使う。 木構築 'buildTreeV' と feature
--- importance は純粋ゆえ ST/IO でビット同一。
-fitRFVM :: PrimMonad m
-        => RFConfig
-        -> LA.Matrix Double
-        -> VU.Vector Double
-        -> MWC.Gen (PrimState m)
-        -> m RandomForest
-fitRFVM cfg x y gen = do
-  let !n = VU.length y
-      !d = LA.cols x
-  trees <- replicateM (rfTrees cfg) $ do
-    !idx <- if rfBootstrap cfg
-              then bootstrapIdxM n gen
-              else pure (VU.enumFromN 0 n)
-    pure $! buildTreeV cfg x y idx 0
-  -- permutation importance は列シャッフルに gen を使う (bootstrap の後・seed 決定的)。
-  !perm <- permImportanceRegM x y trees gen
-  pure RandomForest
-    { rfTreesV         = trees
-    , rfNFeatures      = d
-    , rfImportance     = importanceOf d trees
-    , rfPermImportance = perm
-    , rfFeatureNames   = defaultFeatureNames d
-    }
-
--- | Backwards-compatible list-based fit.
-fitRF :: RFConfig -> [[Double]] -> [Double] -> MWC.GenIO -> IO RandomForest
-fitRF cfg xs ys gen
-  | null xs   = pure emptyForest
-  | otherwise = fitRFV cfg (LA.fromLists xs) (VU.fromList ys) gen
-
--- | 純粋・決定的な行列入力 forest。 同じ @seed@ なら必ず同じ 'RandomForest'。
--- 'fitRFVM' を 'ST' で走らせ 'runST' で閉じる
--- ([[phase-50-mcmc-purification-status]] の 'nutsPure' と同方針)。
-fitRFVPure :: RFConfig
-           -> LA.Matrix Double
-           -> VU.Vector Double
-           -> Word32
-           -> RandomForest
-fitRFVPure cfg x y seed =
-  runST (MWC.initialize (V.singleton seed) >>= fitRFVM cfg x y)
-
--- | 純粋・決定的な list 入力 forest (list 版 'fitRF' の seed 純粋版)。
-fitRFPure :: RFConfig -> [[Double]] -> [Double] -> Word32 -> RandomForest
-fitRFPure cfg xs ys seed
-  | null xs   = emptyForest
-  | otherwise = fitRFVPure cfg (LA.fromLists xs) (VU.fromList ys) seed
-
--- | 空データ時の forest (全フィールド空)。
-emptyForest :: RandomForest
-emptyForest = RandomForest [] 0 V.empty V.empty []
-
--- | Single-tree builder kept for the symmetry of the old API. Most
--- callers should use 'fitRFV'.
-buildTree :: RFConfig -> [[Double]] -> [Double] -> MWC.GenIO -> IO Tree
-buildTree cfg rows ys gen
-  | null rows = pure (Leaf 0)
-  | otherwise = do
-      let !x = LA.fromLists rows
-          !y = VU.fromList ys
-          !n = VU.length y
-      idx <- if rfBootstrap cfg
-               then bootstrapIdxM n gen
-               else pure (VU.enumFromN 0 n)
-      pure (buildTreeV cfg x y idx 0)
-
-bootstrapIdxM :: PrimMonad m => Int -> MWC.Gen (PrimState m) -> m (VU.Vector Int)
-bootstrapIdxM n gen =
-  VU.replicateM n (MWC.uniformR (0, n - 1) gen)
-
--- ---------------------------------------------------------------------------
--- Recursive build
--- ---------------------------------------------------------------------------
-
-buildTreeV :: RFConfig
-           -> LA.Matrix Double
-           -> VU.Vector Double
-           -> VU.Vector Int
-           -> Int
-           -> Tree
-buildTreeV cfg x y idx depth =
-  let !n      = VU.length idx
-      !subY   = VU.map (y VU.!) idx
-      !meanY  = if n == 0 then 0
-                          else VU.sum subY / fromIntegral n
-      !varY   = varianceUS subY
-  in if n <= rfMinSamples cfg
-       || depth >= rfMaxDepth cfg
-       || varY < 1e-12
-       then Leaf meanY
-       else
-         let !d    = LA.cols x
-             !mtry = case rfMtry cfg of
-                       Just m  -> max 1 (min d m)
-                       Nothing -> max 1 (d `div` 3)
-             !featIxs = pickFeats d mtry depth n
-             !mBest   = bestSplitVRF featIxs x y idx
-         in case mBest of
-              Nothing             -> Leaf meanY
-              Just (j, thr, _)    ->
-                let (lIdx, rIdx) = partitionByFeat x idx j thr
-                in if VU.null lIdx || VU.null rIdx
-                     then Leaf meanY
-                     else Node j thr
-                            (buildTreeV cfg x y lIdx (depth + 1))
-                            (buildTreeV cfg x y rIdx (depth + 1))
-
--- | Deterministic pseudo-random feature subset using an LCG seeded by
--- @(depth, n)@. Different nodes typically see different subsets,
--- which is the decorrelation that random forests need at split time.
--- Tree-level randomness comes from 'bootstrapIdx', which threads
--- through 'MWC.GenIO'.
-pickFeats :: Int -> Int -> Int -> Int -> VU.Vector Int
-pickFeats d mtry depth n
-  | mtry >= d = VU.enumFromN 0 d
-  | otherwise =
-      let seed0 = depth * 1009 + n * 31 + 1
-          step !s = (s * 1103515245 + 12345) `mod` (2 ^ (31 :: Int))
-          go !s !chosen !left
-            | left == 0 = chosen
-            | otherwise =
-                let !s' = step s
-                    !i  = s' `mod` d
-                in if i `VU.elem` chosen
-                     then go s' chosen left
-                     else go s' (chosen `VU.snoc` i) (left - 1)
-      in go seed0 VU.empty mtry
-
-partitionByFeat :: LA.Matrix Double
-                -> VU.Vector Int
-                -> Int
-                -> Double
-                -> (VU.Vector Int, VU.Vector Int)
-partitionByFeat x idx feat thr =
-  let pred_ i = LA.atIndex x (i, feat) <= thr
-  in VU.partition pred_ idx
-
--- ---------------------------------------------------------------------------
--- Best split
--- ---------------------------------------------------------------------------
-
-bestSplitVRF :: VU.Vector Int
-             -> LA.Matrix Double
-             -> VU.Vector Double
-             -> VU.Vector Int
-             -> Maybe (Int, Double, Double)
-bestSplitVRF featIxs x y idx
-  | VU.length idx < 2 = Nothing
-  | otherwise =
-      let go best j =
-            case bestSplitFeatureRF x y idx j of
-              Nothing       -> best
-              Just (thr, g) ->
-                case best of
-                  Nothing                       -> Just (j, thr, g)
-                  Just (_, _, gPrev) | g > gPrev -> Just (j, thr, g)
-                                    | otherwise -> best
-      in VU.foldl' go Nothing featIxs
-
--- | Per-feature best split for regression: maximise variance reduction
--- via single sort + linear sweep with running sum / sum-of-squares.
-bestSplitFeatureRF :: LA.Matrix Double
-                   -> VU.Vector Double
-                   -> VU.Vector Int
-                   -> Int
-                   -> Maybe (Double, Double)
-bestSplitFeatureRF x y idx feat = runST $ do
-  let !n = VU.length idx
-  pairs <- VUM.new n
-  let valOf i = LA.atIndex x (i, feat)
-      yOf  i = y VU.! i
-      fill !k
-        | k == n = pure ()
-        | otherwise = do
-            let !i = VU.unsafeIndex idx k
-            VUM.unsafeWrite pairs k (valOf i, yOf i)
-            fill (k + 1)
-  fill 0
-  Intro.sortBy (\a b -> compare (fst a) (fst b)) pairs
-  pairsF <- VU.unsafeFreeze pairs
-
-  let !sumY     = VU.sum (VU.map snd pairsF)
-      !sumY2    = VU.sum (VU.map (\(_, v) -> v * v) pairsF)
-      !nD       = fromIntegral n :: Double
-      !parentSS = sumY2 - sumY * sumY / nD
-
-  let sweep !k !sumYL !sumY2L !bestThr !bestGain
-        | k >= n - 1 = pure (bestThr, bestGain)
-        | otherwise = do
-            let (v_k,  yk) = VU.unsafeIndex pairsF k
-                (v_k1, _)  = VU.unsafeIndex pairsF (k + 1)
-                !sumYL'  = sumYL  + yk
-                !sumY2L' = sumY2L + yk * yk
-            if v_k == v_k1
-              then sweep (k + 1) sumYL' sumY2L' bestThr bestGain
-              else do
-                let !nL  = fromIntegral (k + 1) :: Double
-                    !nR  = nD - nL
-                    !sumYR  = sumY  - sumYL'
-                    !sumY2R = sumY2 - sumY2L'
-                    !ssL    = sumY2L' - sumYL' * sumYL' / nL
-                    !ssR    = sumY2R  - sumYR  * sumYR  / nR
-                    !gain   = parentSS - ssL - ssR
-                    !thr    = (v_k + v_k1) / 2
-                if gain > bestGain
-                  then sweep (k + 1) sumYL' sumY2L' thr  gain
-                  else sweep (k + 1) sumYL' sumY2L' bestThr bestGain
-  (thr, gain) <- sweep 0 0 0 0 (negate (1.0 / 0.0))
-  pure $ if gain == negate (1.0 / 0.0)
-           then Nothing
-           else Just (thr, gain)
-
--- ---------------------------------------------------------------------------
--- Variance helper
--- ---------------------------------------------------------------------------
-
-varianceUS :: VU.Vector Double -> Double
-varianceUS v
-  | VU.length v <= 1 = 0
-  | otherwise =
-      let !n  = fromIntegral (VU.length v) :: Double
-          !mu = VU.sum v / n
-      in VU.foldl' (\acc x -> acc + (x - mu) ^ (2 :: Int)) 0 v / n
-
--- ---------------------------------------------------------------------------
--- Predict
--- ---------------------------------------------------------------------------
-
-predictTree :: Tree -> [Double] -> Double
-predictTree (Leaf v)         _  = v
-predictTree (Node j thr l r) xs =
-  if (xs !! j) <= thr then predictTree l xs else predictTree r xs
-
-predictRF :: RandomForest -> [Double] -> Double
-predictRF rf xs =
-  let preds = map (`predictTree` xs) (rfTreesV rf)
-      n     = length preds
-  in if n == 0 then 0 else sum preds / fromIntegral n
-
-featureImportance :: RandomForest -> V.Vector Double
-featureImportance rf =
-  let raw = rfImportance rf
-      tot = V.sum raw
-  in if tot <= 0 then raw else V.map (/ tot) raw
-
--- | Permutation importance (= 列を無作為置換したときの MSE 増加) を正の総和で
--- 正規化して返す。 全て非正なら raw のまま (負 = その特徴が予測に無寄与)。
--- R @randomForest %IncMSE@ / sklearn @permutation_importance@ 同方式。
-rfPermutationImportance :: RandomForest -> V.Vector Double
-rfPermutationImportance rf =
-  let raw = rfPermImportance rf
-      tot = V.sum (V.filter (> 0) raw)
-  in if tot <= 0 then raw else V.map (/ tot) raw
-
--- ---------------------------------------------------------------------------
--- Permutation importance (MSE 増加ベース)
--- ---------------------------------------------------------------------------
-
--- | 各特徴列を無作為置換し、 forest の MSE 増加量を測る (純粋・'PrimMonad')。
--- gen は列シャッフルにのみ使う。 同 seed → ビット同一。
-permImportanceRegM :: PrimMonad m
-                   => LA.Matrix Double -> VU.Vector Double -> [Tree]
-                   -> MWC.Gen (PrimState m) -> m (V.Vector Double)
-permImportanceRegM x y trees gen
-  | LA.rows x == 0 || null trees = pure (V.replicate (LA.cols x) 0)
-  | otherwise = do
-      let !base = forestMSE x y trees
-      scores <- mapM (\j -> do
-                         xp <- permuteColM j x gen
-                         pure $! forestMSE xp y trees - base)
-                     [0 .. LA.cols x - 1]
-      pure (V.fromList scores)
-
--- | forest の平均二乗誤差 (行毎に木予測を平均)。
-forestMSE :: LA.Matrix Double -> VU.Vector Double -> [Tree] -> Double
-forestMSE x y trees =
-  let !n = LA.rows x
-      !k = length trees
-      rowPred i =
-        let row   = LA.toList (LA.flatten (x LA.? [i]))
-            preds = map (`predictTree` row) trees
-        in if k == 0 then 0 else sum preds / fromIntegral k
-      sse = sum [ (rowPred i - y VU.! i) ^ (2 :: Int) | i <- [0 .. n - 1] ]
-  in if n == 0 then 0 else sse / fromIntegral n
-
--- | 列 j を Fisher-Yates で置換した行列を返す (他列は不変)。
-permuteColM :: PrimMonad m
-            => Int -> LA.Matrix Double -> MWC.Gen (PrimState m) -> m (LA.Matrix Double)
-permuteColM j x gen = do
-  let cols0 = LA.toColumns x
-      colj  = VU.fromList (LA.toList (cols0 !! j))
-  shuf <- fisherYatesM gen colj
-  let newCols = [ if kk == j then LA.fromList (VU.toList shuf) else cols0 !! kk
-                | kk <- [0 .. length cols0 - 1] ]
-  pure (LA.fromColumns newCols)
-
--- | 可変ベクトル上の Fisher-Yates シャッフル ('PrimMonad'・gen 決定的)。
-fisherYatesM :: PrimMonad m
-             => MWC.Gen (PrimState m) -> VU.Vector Double -> m (VU.Vector Double)
-fisherYatesM gen v0 = do
-  mv <- VU.thaw v0
-  let go i | i <= 0    = pure ()
-           | otherwise = do
-               j <- MWC.uniformR (0, i) gen
-               VUM.swap mv i j
-               go (i - 1)
-  go (VUM.length mv - 1)
-  VU.freeze mv
-
--- ---------------------------------------------------------------------------
--- Importance accumulation (per split, simple count)
--- ---------------------------------------------------------------------------
-
--- | 全木の split 特徴を 1 回の可変ベクトル走査で集計 (純粋)。 旧 'IORef'
--- 版を 'runST' + 可変ベクトルへ置換 (count の和は可換ゆえ木順不問で同値)。
-importanceOf :: Int -> [Tree] -> V.Vector Double
-importanceOf d trees = runST $ do
-  v <- VM.replicate d 0.0
-  let walk (Leaf _)       = pure ()
-      walk (Node j _ l r) = do
-        VM.modify v (+ 1.0) j
-        walk l
-        walk r
-  mapM_ walk trees
-  V.freeze v
diff --git a/src/Hanalyze/Model/RandomForestClassifier.hs b/src/Hanalyze/Model/RandomForestClassifier.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/RandomForestClassifier.hs
+++ /dev/null
@@ -1,236 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Model.RandomForestClassifier
--- Description : Random Forest 分類版 — DecisionTree の bootstrap aggregation + OOB error + permutation importance
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Random Forest **分類版**。
---
--- bootstrap aggregation of 'Hanalyze.Model.DecisionTree' (CART 分類)。
--- OOB (Out-of-Bag) error と permutation importance を併せて返す。
-module Hanalyze.Model.RandomForestClassifier
-  ( RFCConfig (..)
-  , defaultRFCConfig
-  , RFClassifierFit (..)
-  , fitRFClassifier
-  , fitRFClassifierPure
-  , predictRFClassifier
-  ) where
-
-import qualified Data.Vector                 as V
-import qualified Data.Vector.Unboxed         as VU
-import qualified Numeric.LinearAlgebra       as LA
-import qualified Data.Map.Strict             as Map
-import           Data.List                   (nub, sort, group, sortBy, foldl')
-import           Data.Ord                    (comparing, Down (..))
-import           Data.Text                   (Text)
-import           Data.Word                   (Word32)
-import qualified System.Random.MWC           as MWC
-import           Control.Monad               (forM, replicateM)
-import           Control.Monad.Primitive     (PrimMonad, PrimState)
-import           Control.Monad.ST            (runST)
-
-import qualified Hanalyze.Model.DecisionTree as DT
-import           Hanalyze.Model.RandomForest (defaultFeatureNames)
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
-data RFCConfig = RFCConfig
-  { rfcNTrees   :: !Int
-  , rfcMaxDepth :: !(Maybe Int)
-  , rfcMinSplit :: !Int
-  } deriving (Show)
-
-defaultRFCConfig :: RFCConfig
-defaultRFCConfig = RFCConfig
-  { rfcNTrees   = 100
-  , rfcMaxDepth = Just 10
-  , rfcMinSplit = 2
-  }
-
-data RFClassifierFit = RFClassifierFit
-  { rfcTrees          :: ![DT.DTree]
-  , rfcOOBSamples     :: ![[Int]]
-  , rfcClasses        :: ![Int]
-  , rfcOOBError       :: !Double
-  , rfcImportance     :: !(LA.Vector Double)  -- ^ permutation importance (OOB accuracy 低下)。
-  , rfcGiniImportance :: !(LA.Vector Double)  -- ^ MDI (mean decrease in gini・木構造から純粋計算・sklearn feature_importances_ 同方式)。
-  , rfcFeatureNames   :: ![Text]              -- ^ 特徴列名。 行列 fit は 'defaultFeatureNames' ("f1"..)。 実列名は df|-> 化 (後続) で。
-  , rfcConfig         :: !RFCConfig
-  } deriving (Show)
-
--- ===========================================================================
--- fit
--- ===========================================================================
-
--- | IO ラッパ。 ロジックは 'PrimMonad' 汎用の 'fitRFClassifierM' を共有。
-fitRFClassifier
-  :: RFCConfig
-  -> LA.Matrix Double
-  -> VU.Vector Int
-  -> MWC.GenIO
-  -> IO RFClassifierFit
-fitRFClassifier = fitRFClassifierM
-
--- | 純粋・決定的な forest 分類器 (同 @seed@ → ビット同一)。 回帰の 'fitRFVPure' と同方針
--- ([[phase-50-mcmc-purification-status]])。 df|-> ('Fit RFCSpec') 経路が使う。
-fitRFClassifierPure
-  :: RFCConfig -> LA.Matrix Double -> VU.Vector Int -> Word32 -> RFClassifierFit
-fitRFClassifierPure cfg x y seed =
-  runST (MWC.initialize (V.singleton seed) >>= fitRFClassifierM cfg x y)
-
--- | 'PrimMonad' 汎用の forest 分類器本体。 gen は bootstrap index と permutation の
--- 列シャッフルにのみ使う (木構築・OOB・gini は純粋ゆえ ST/IO でビット同一)。
-fitRFClassifierM
-  :: PrimMonad m
-  => RFCConfig
-  -> LA.Matrix Double
-  -> VU.Vector Int
-  -> MWC.Gen (PrimState m)
-  -> m RFClassifierFit
-fitRFClassifierM cfg x y gen = do
-  let n = LA.rows x
-      p = LA.cols x
-      classes = sort (nub (VU.toList y))
-      dtCfg = DT.defaultDecisionTree
-        { DT.dtMaxDepth        = rfcMaxDepth cfg
-        , DT.dtMinSamplesSplit = rfcMinSplit cfg
-        }
-  results <- forM [1 .. rfcNTrees cfg] $ \_ -> do
-    idxs <- replicateM n (MWC.uniformR (0, n - 1) gen)
-    let x'  = x LA.? idxs
-        y'  = VU.fromList [ y VU.! i | i <- idxs ]
-        tree = DT.fitDTV dtCfg x' y'
-        oob  = filter (`notElem` idxs) [0 .. n - 1]
-    pure (tree, oob)
-  let trees    = [ t | (t, _) <- results ]
-      oobLists = [ o | (_, o) <- results ]
-      oobErr   = computeOOB x y trees oobLists
-  -- permutation importance: fixed-seed gen for reproducibility per feature
-  imp <- permImportance gen x y trees
-  pure RFClassifierFit
-    { rfcTrees          = trees
-    , rfcOOBSamples     = oobLists
-    , rfcClasses        = classes
-    , rfcOOBError       = oobErr
-    , rfcImportance     = imp
-    , rfcGiniImportance = giniImportance p trees
-    , rfcFeatureNames   = defaultFeatureNames p
-    , rfcConfig         = cfg
-    }
-
--- | 各サンプルを多数決で予測。
-predictRFClassifier :: RFClassifierFit -> LA.Matrix Double -> V.Vector Int
-predictRFClassifier fit xNew =
-  V.generate (LA.rows xNew) $ \i ->
-    let row = LA.toList (LA.flatten (xNew LA.? [i]))
-    in majority [ DT.predictDT t row | t <- rfcTrees fit ]
-
--- ===========================================================================
--- 内部
--- ===========================================================================
-
-computeOOB
-  :: LA.Matrix Double -> VU.Vector Int -> [DT.DTree] -> [[Int]] -> Double
-computeOOB x y trees oobLists =
-  let n = LA.rows x
-      voteFor s =
-        let voters = [ t | (t, oob) <- zip trees oobLists, s `elem` oob ]
-        in if null voters then Nothing
-           else
-             let row = LA.toList (LA.flatten (x LA.? [s]))
-             in Just (majority [ DT.predictDT t row | t <- voters ])
-      voted = [ (s, p) | s <- [0 .. n - 1]
-                       , Just p <- [voteFor s] ]
-      nTotal = length voted
-      nErr   = length [ () | (s, p) <- voted, p /= (y VU.! s) ]
-  in if nTotal == 0 then 0 else fromIntegral nErr / fromIntegral nTotal
-
-majority :: [Int] -> Int
-majority xs =
-  let grouped = map (\g -> (head g, length g)) (group (sort xs))
-  in case sortBy (comparing (Down . snd)) grouped of
-       ((c, _) : _) -> c
-       []           -> 0
-
--- | Mean Decrease in Impurity (gini) per feature, summed over all trees
--- (sklearn @feature_importances_@ 同方式・木構造から純粋計算)。 各内部ノードの
--- 重み付き gini 減少 @n·(imp − (nL/n)·impL − (nR/n)·impR)@ を分割特徴に加算し、
--- 全木ぶん合計 → 合計 1 に正規化。 'DT.DTree' の 75.23 拡張 (dnN/dnImpurity) を使う。
-giniImportance :: Int -> [DT.DTree] -> LA.Vector Double
-giniImportance p trees =
-  let m0 = Map.fromList [ (j, 0 :: Double) | j <- [0 .. p - 1] ]
-      go m (DT.DLeaf{}) = m
-      go m (DT.DNode { DT.dnFeature = j, DT.dnLeft = l, DT.dnRight = r
-                     , DT.dnN = nn, DT.dnImpurity = imp }) =
-        let n    = fromIntegral nn :: Double
-            nL   = fromIntegral (nodeN l)
-            nR   = fromIntegral (nodeN r)
-            dec  = if n <= 0 then 0
-                   else n * (imp - (nL / n) * nodeImp l - (nR / n) * nodeImp r)
-            m'   = Map.insertWith (+) j dec m
-        in go (go m' l) r
-      accM = foldl' go m0 trees
-      raw  = [ Map.findWithDefault 0 j accM | j <- [0 .. p - 1] ]
-      tot  = sum raw
-  in LA.fromList (if tot <= 0 then raw else map (/ tot) raw)
-
--- | ノードのサンプル数 / gini 不純度 (葉・内部で共通アクセス)。
-nodeN :: DT.DTree -> Int
-nodeN (DT.DLeaf{ DT.dlN = n }) = n
-nodeN (DT.DNode{ DT.dnN = n }) = n
-
-nodeImp :: DT.DTree -> Double
-nodeImp (DT.DLeaf{ DT.dlImpurity = i }) = i
-nodeImp (DT.DNode{ DT.dnImpurity = i }) = i
-
-permImportance
-  :: PrimMonad m
-  => MWC.Gen (PrimState m) -> LA.Matrix Double -> VU.Vector Int -> [DT.DTree]
-  -> m (LA.Vector Double)
-permImportance gen x y trees = do
-  let p = LA.cols x
-      baseAcc = forestAccuracy x y trees
-  scores <- forM [0 .. p - 1] $ \j -> do
-    xPerm <- permuteColumn j gen x
-    let acc = forestAccuracy xPerm y trees
-    pure (baseAcc - acc)
-  pure (LA.fromList scores)
-
-forestAccuracy :: LA.Matrix Double -> VU.Vector Int -> [DT.DTree] -> Double
-forestAccuracy x y trees =
-  let n = LA.rows x
-      preds =
-        [ let row = LA.toList (LA.flatten (x LA.? [i]))
-          in majority [ DT.predictDT t row | t <- trees ]
-        | i <- [0 .. n - 1] ]
-      correct = length [ () | (p_, i) <- zip preds [0 ..]
-                            , p_ == (y VU.! i) ]
-  in fromIntegral correct / fromIntegral n
-
-permuteColumn :: PrimMonad m
-              => Int -> MWC.Gen (PrimState m) -> LA.Matrix Double -> m (LA.Matrix Double)
-permuteColumn j gen x = do
-  let col = LA.toList (LA.flatten (x LA.¿ [j]))
-  shuf <- fisherYates gen col
-  let newCol = LA.fromList shuf
-      cols = [ if k == j then newCol else LA.flatten (x LA.¿ [k])
-             | k <- [0 .. LA.cols x - 1] ]
-  pure (LA.fromColumns cols)
-
-fisherYates :: PrimMonad m => MWC.Gen (PrimState m) -> [a] -> m [a]
-fisherYates gen xs =
-  let v0 = V.fromList xs
-  in go v0 (V.length v0 - 1)
-  where
-    go v 0 = pure (V.toList v)
-    go v i = do
-      j <- MWC.uniformR (0, i) gen
-      let vi = v V.! i
-          vj = v V.! j
-          v' = v V.// [(i, vj), (j, vi)]
-      go v' (i - 1)
diff --git a/src/Hanalyze/Model/Regularized.hs b/src/Hanalyze/Model/Regularized.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Regularized.hs
+++ /dev/null
@@ -1,679 +0,0 @@
-{-# LANGUAGE StrictData #-}
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Model.Regularized
--- Description : 正則化回帰 (Ridge / Lasso / Elastic Net) を単一 API に統合したモジュール
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Regularized regression (Ridge / Lasso / Elastic Net) in one module.
---
--- The penalty is encoded as the sum type 'Penalty', and 'fitRegularized'
--- handles all four models:
---
--- > NoPen                          -- ordinary OLS
--- > L2 lambda                      -- Ridge regression
--- > L1 lambda                      -- Lasso regression
--- > ElasticNet lambda1 lambda2     -- Elastic Net (L1 + L2)
---
--- Ridge has a closed form; Lasso and Elastic Net use coordinate descent.
---
--- 注意: Lasso / Elastic Net は X の列スケールに敏感。事前に
--- standardize (各列を平均 0、分散 1 に) しておくのが一般的。
-module Hanalyze.Model.Regularized
-  ( Penalty (..)
-  , RegFit (..)
-  , fitRegularized
-  , fitRidge
-  , fitElasticNet
-  , predictRegularized
-  , standardize
-  , unstandardizeBeta
-    -- * Multi-output (primary API)
-  , RegFitMulti (..)
-  , fitRegularizedMulti
-  , fitRegularizedMultiWith
-  , predictRegularizedMulti
-  , regFitFromMulti
-    -- * Convergence-controlled API
-  , fitRegularizedWith
-    -- * Regularization path
-  , regularizationPath
-
-    -- * λ 自動選択 (Phase 4.4、 request/150)
-  , PenaltyKind (..)
-  , LambdaSelection (..)
-  , selectLambdaCV
-  , selectLambdaCVPure
-
-    -- * Phase 31: CD 内部プリミティブの再利用 (RegularizedAdvanced 用)
-  , softThreshold
-  , cdLoop
-  , mkRegFit
-  , fitOLS
-  , fitLasso
-  ) where
-
-import qualified Data.Vector                  as V
-import qualified Data.Vector.Storable         as VS
-import qualified Data.Vector.Storable.Mutable as VSM
-import qualified Numeric.LinearAlgebra        as LA
-import           Control.Monad                (forM_, when)
-import           Control.Monad.Primitive      (PrimMonad, PrimState)
-import           Control.Monad.ST             (runST)
-import           Data.List                    (foldl', sortBy)
-import           Data.Ord                     (comparing)
-import           Data.Word                    (Word32)
-import           System.IO.Unsafe             (unsafePerformIO)
-import qualified System.Random.MWC            as MWC
-import qualified Hanalyze.Stat.CV             as HCV
-
--- ---------------------------------------------------------------------------
--- ペナルティ型
--- ---------------------------------------------------------------------------
-
--- | Regularization penalty.
-data Penalty
-  = NoPen                       -- ^ Ordinary OLS (@λ = 0@).
-  | L2 Double                   -- ^ Ridge: @0.5 λ ‖β‖₂²@.
-  | L1 Double                   -- ^ Lasso: @λ ‖β‖₁@.
-  | ElasticNet Double Double    -- ^ Elastic Net: @λ₁ ‖β‖₁ + 0.5 λ₂ ‖β‖₂²@.
-  deriving (Show, Eq)
-
--- | Regularized-regression fit result.
-data RegFit = RegFit
-  { rfBeta    :: LA.Vector Double
-  , rfYHat    :: LA.Vector Double
-  , rfResid   :: LA.Vector Double
-  , rfR2      :: Double
-  , rfPenalty :: Penalty
-  , rfNonZero :: Int           -- ^ Number of @|β_j| > 1e-8@ (Lasso sparsity).
-  , rfIters   :: Int           -- ^ Iteration count (coordinate descent;
-                               --   0 for closed-form solvers).
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- メイン API
--- ---------------------------------------------------------------------------
-
--- | Single-output regularized-regression fit (sklearn-compatible
--- defaults @maxIter = 1000@, @tol = 1e-4@). Delegates to
--- 'fitRegularizedMulti' by promoting @y@ to a one-column matrix and
--- returns column 0 as a 'RegFit'.
-fitRegularized :: Penalty -> LA.Matrix Double -> LA.Vector Double -> RegFit
-fitRegularized pen x y =
-  regFitFromMulti 0 (fitRegularizedMulti pen x (LA.asColumn y))
-
--- | Single-output regularized-regression fit with explicit convergence
--- controls (only meaningful for Lasso / Elastic Net).
-fitRegularizedWith
-  :: Int -> Double -> Penalty -> LA.Matrix Double -> LA.Vector Double
-  -> RegFit
-fitRegularizedWith maxIter tol pen x y =
-  regFitFromMulti 0
-    (fitRegularizedMultiWith maxIter tol pen x (LA.asColumn y))
-
--- | Single-output prediction.
-predictRegularized :: RegFit -> LA.Matrix Double -> LA.Vector Double
-predictRegularized fit xNew = xNew LA.#> rfBeta fit
-
--- ---------------------------------------------------------------------------
--- OLS (NoPen)
--- ---------------------------------------------------------------------------
-
--- | Plain ordinary-least-squares fit (no penalty).
-fitOLS :: LA.Matrix Double -> LA.Vector Double -> RegFit
-fitOLS x y =
-  let beta = LA.flatten (x LA.<\> LA.asColumn y)
-      yHat = x LA.#> beta
-      r    = y - yHat
-  in mkRegFit beta yHat r y NoPen 0
-
--- ---------------------------------------------------------------------------
--- Ridge (closed form)
--- ---------------------------------------------------------------------------
-
--- | Ridge regression: @β = (XᵀX + λI)⁻¹ Xᵀy@.
-fitRidge :: Double -> LA.Matrix Double -> LA.Vector Double -> RegFit
-fitRidge lambda x y =
-  let p    = LA.cols x
-      xtx  = LA.tr x LA.<> x
-      reg  = xtx + LA.scale lambda (LA.ident p)
-      xty  = LA.tr x LA.#> y
-      beta = LA.flatten (reg LA.<\> LA.asColumn xty)
-      yHat = x LA.#> beta
-      r    = y - yHat
-  in mkRegFit beta yHat r y (L2 lambda) 0
-
--- ---------------------------------------------------------------------------
--- Lasso (Coordinate Descent + Soft-thresholding)
--- ---------------------------------------------------------------------------
-
--- | Soft-threshold operator: @S(z, γ) = sign(z) × max(|z| − γ, 0)@.
-softThreshold :: Double -> Double -> Double
-softThreshold z gamma
-  | z >  gamma = z - gamma
-  | z < -gamma = z + gamma
-  | otherwise  = 0
-
--- | Lasso regression: @β = argmin (1/2n) ‖y − Xβ‖² + λ ‖β‖₁@.
---
--- Solved by coordinate descent (one update per @β_j@):
---
--- @
--- r   = y − X β
--- ρ_j = (1/n) X_jᵀ r + β_j × (1/n) ‖X_j‖²
--- β_j ← S(ρ_j, λ) / ((1/n) ‖X_j‖²)
--- @
-fitLasso :: Double                -- ^ Penalty @λ@.
-         -> LA.Matrix Double      -- ^ Design matrix @X@.
-         -> LA.Vector Double      -- ^ Response @y@.
-         -> Int                   -- ^ Maximum CD iterations.
-         -> Double                -- ^ Convergence tolerance.
-         -> RegFit
-fitLasso lambda x y maxIter tol =
-  let (betaFinal, iters) = cdLoop x y maxIter tol
-                             (\rho cSq -> softThreshold rho lambda / cSq)
-      yHat = x LA.#> betaFinal
-      r    = y - yHat
-  in mkRegFit betaFinal yHat r y (L1 lambda) iters
-
--- ---------------------------------------------------------------------------
--- Elastic Net (Coordinate Descent)
--- ---------------------------------------------------------------------------
-
--- | Elastic-Net regression:
--- @β = argmin (1/2n) ‖y − Xβ‖² + λ₁ ‖β‖₁ + 0.5 λ₂ ‖β‖²@.
---
--- Coordinate descent update:
--- @β_j ← S(ρ_j, λ₁) / ((1/n) ‖X_j‖² + λ₂)@.
-fitElasticNet :: Double -> Double -> LA.Matrix Double -> LA.Vector Double
-              -> Int -> Double -> RegFit
-fitElasticNet lambda1 lambda2 x y maxIter tol =
-  let (betaFinal, iters) = cdLoop x y maxIter tol
-                             (\rho cSq -> softThreshold rho lambda1
-                                          / (cSq + lambda2))
-      yHat = x LA.#> betaFinal
-      r    = y - yHat
-  in mkRegFit betaFinal yHat r y (ElasticNet lambda1 lambda2) iters
-
--- ---------------------------------------------------------------------------
--- Shared CD loop with incremental residual maintenance
--- ---------------------------------------------------------------------------
-
--- | Coordinate descent loop shared by 'fitLasso' and 'fitElasticNet'.
---
--- The caller supplies a /closed-form coordinate update/ @upd ρ_j cSq_j@
--- that returns @β_j_new@ given the partial-residual correlation @ρ_j@
--- and the column-norm @cSq_j = ‖X_j‖²/n@.
---
--- Implementation (R2): the inner sweep runs in 'IO' on
--- 'Data.Vector.Storable.Mutable' buffers. Both @β@ and the residual
--- @r = y − Xβ@ are updated in place, and the columns of @X@ are looked
--- up through a boxed 'Data.Vector.Vector' for @O(1)@ indexing (the
--- previous list-based @cols !! j@ paid @O(p)@ per coordinate). This is
--- the moral equivalent of sklearn's Cython coordinate-descent inner
--- loop; the user-visible behaviour is identical to the prior Vector
--- implementation up to floating-point rounding.
-cdLoop
-  :: LA.Matrix Double                  -- X (n × p)
-  -> LA.Vector Double                  -- y
-  -> Int                               -- max iterations
-  -> Double                            -- tolerance on |Δβ|₂
-  -> (Double -> Double -> Double)      -- (ρ, cSq) → β_j_new
-  -> (LA.Vector Double, Int)
-cdLoop x y maxIter tol upd
-  | LA.rows x >= 4 * LA.cols x =
-      cdLoopGram x y maxIter tol upd      -- n ≫ p: Gram precompute
-  | otherwise                  = cdLoopResidual x y maxIter tol upd
-
--- | Coordinate descent maintaining the @n@-dimensional residual
--- @r = y − Xβ@. Best when @n@ is small (the residual update is
--- @O(n)@ per coord; the alternative 'cdLoopGram' keeps a length-@p@
--- prediction vector and pays @O(p)@ per coord).
-cdLoopResidual
-  :: LA.Matrix Double -> LA.Vector Double -> Int -> Double
-  -> (Double -> Double -> Double)
-  -> (LA.Vector Double, Int)
-cdLoopResidual x y maxIter tol upd = unsafePerformIO $ do
-  let nRows  = LA.rows x
-      n      = fromIntegral nRows :: Double
-      p      = LA.cols x
-      colsB  = V.fromList (LA.toColumns x)        -- O(1) indexing
-      -- F1: per-column squared sum via 1 GEMV instead of p
-      -- 'sumElements (c*c)' calls. ones_n^T (X⊙X) gives length-p
-      -- vector of column sums; divide by n.
-      onesN  = LA.konst 1 nRows :: LA.Vector Double
-      colSqN = LA.scale (1 / n) (onesN LA.<# (x * x))
-
-  -- Mutable buffer for β (single-index updates each coordinate step).
-  bMut <- VS.thaw (LA.konst 0 p :: LA.Vector Double)
-
-  -- The residual r is kept as an /immutable/ 'LA.Vector Double' between
-  -- coordinate updates so that @r ← r − d · x_j@ can use BLAS axpy
-  -- (a single optimized call) rather than a per-element Haskell loop.
-  let sweep r = do
-        beforeSnap <- VS.freeze bMut
-        let stepCoord rCur j = do
-              let xj  = colsB V.! j
-                  cSq = colSqN `LA.atIndex` j
-              bjOld <- VSM.unsafeRead bMut j
-              let rho   = (xj LA.<.> rCur) / n + bjOld * cSq
-                  bjNew = upd rho cSq
-                  d     = bjNew - bjOld
-              if d == 0
-                then return rCur
-                else do
-                  VSM.unsafeWrite bMut j bjNew
-                  -- BLAS axpy: r' = r - d * x_j. Tried fusing via
-                  -- 'VS.zipWith' (one alloc instead of two) but it
-                  -- was 1.6× slower — hmatrix's @(-)@ + @LA.scale@
-                  -- chain dispatches to BLAS @daxpy@/@dscal@ which
-                  -- are SIMD-vectorised at the C level, beating any
-                  -- pure Haskell per-element loop on n ≥ 1000.
-                  return (rCur - LA.scale d xj)
-        rEnd <- foldM' stepCoord r [0 .. p - 1]
-        afterSnap <- VS.freeze bMut
-        return (beforeSnap, afterSnap, rEnd)
-
-  let go k r = do
-        if k >= maxIter
-          then return k
-          else do
-            (before, after, r') <- sweep r
-            let diff = LA.norm_2 (after - before)
-            if diff < tol then return (k + 1) else go (k + 1) r'
-
-  iters     <- go 0 y     -- initial residual = y (since β₀ = 0)
-  betaFinal <- VS.freeze bMut
-  return (betaFinal, iters)
-  where
-    -- Strict foldM that discards no intermediate results (folds an
-    -- accumulator @r@ through @f@).
-    foldM' :: Monad m => (b -> a -> m b) -> b -> [a] -> m b
-    foldM' _ acc []     = return acc
-    foldM' f acc (z:zs) = do
-      acc' <- f acc z
-      acc' `seq` foldM' f acc' zs
-
--- | Coordinate descent with /precomputed/ Gram matrix
--- @G = XᵀX@ (p × p) and @v = Xᵀy@ (length p).
---
--- For @n ≫ p@ this is dramatically faster than 'cdLoopResidual'
--- because each coordinate update touches a length-@p@ prediction
--- vector @q = G β@ rather than the length-@n@ residual. With
--- @n = 10000, p = 50@ the per-coord work goes from @O(n)@ to
--- @O(p)@ — roughly 200× less arithmetic per inner step. Mirrors
--- sklearn's @Lasso(precompute=True)@.
---
--- Setup cost: forming @G@ is @O(np²)@ (one BLAS GEMM /
--- @LA.tr x \<\> x@); for the @p × p = 50 × 50@ Gram matrix at
--- @n = 10k@ that's ~25 million flops, amortised over the inner
--- coordinate-descent sweeps.
-cdLoopGram
-  :: LA.Matrix Double -> LA.Vector Double -> Int -> Double
-  -> (Double -> Double -> Double)
-  -> (LA.Vector Double, Int)
-cdLoopGram x y maxIter tol upd = unsafePerformIO $ do
-  let nRows = LA.rows x
-      nD    = fromIntegral nRows :: Double
-      p     = LA.cols x
-      gMat  = LA.tr x LA.<> x                -- p × p (SPD)
-      vVec  = LA.tr x LA.#> y                -- length p
-      diagG = LA.takeDiag gMat                -- length p (= ‖X_j‖²)
-      -- Per-column views of @G@ for the @q = G β@ rank-1 update.
-      gCols = V.fromList (LA.toColumns gMat)  -- O(1) column access
-
-  bMut <- VS.thaw (LA.konst 0 p :: LA.Vector Double)
-  -- @q[k] = (G β)[k]@. Maintained incrementally: a coord update
-  -- @β_j ← β_j + d@ shifts @q ← q + d · G[:, j]@.
-  qMut <- VS.thaw (LA.konst 0 p :: LA.Vector Double)
-
-  let stepCoord !maxDelta j = do
-        bjOld <- VSM.unsafeRead bMut j
-        qj    <- VSM.unsafeRead qMut j
-        let !cSq = (diagG `LA.atIndex` j) / nD
-            -- ρ_j = (X_jᵀ r) / n + β_j cSq, where
-            -- X_jᵀ r = X_jᵀ y − X_jᵀ X β = v_j − q_j (linear in β)
-            !rho   = (vVec `LA.atIndex` j - qj) / nD + bjOld * cSq
-            !bjNew = upd rho cSq
-            !d     = bjNew - bjOld
-            !ad    = abs d
-            !newMax = if ad > maxDelta then ad else maxDelta
-        if d == 0
-          then return newMax
-          else do
-            VSM.unsafeWrite bMut j bjNew
-            -- BLAS axpy on @q@: @q ← q + d · G[:, j]@ via a short
-            -- mutable loop (p elements; for typical p ≤ 100 the
-            -- BLAS dispatch overhead would dominate).
-            let gCol = gCols V.! j
-            let go !k
-                  | k >= p    = pure ()
-                  | otherwise = do
-                      qk <- VSM.unsafeRead qMut k
-                      VSM.unsafeWrite qMut k
-                        (qk + d * (gCol `VS.unsafeIndex` k))
-                      go (k + 1)
-            go 0
-            return newMax
-
-  let sweep = do
-        let go !mx !j
-              | j >= p    = pure mx
-              | otherwise = do
-                  mx' <- stepCoord mx j
-                  go mx' (j + 1)
-        go 0 0
-
-  let loop !k = do
-        if k >= maxIter
-          then return k
-          else do
-            mxDelta <- sweep
-            -- Convergence on max |Δβ_j| (sklearn's default test).
-            -- Avoids the per-sweep @before/after freeze + norm_2@ that
-            -- 'cdLoopResidual' performs.
-            if mxDelta < tol then return (k + 1) else loop (k + 1)
-
-  iters     <- loop 0
-  betaFinal <- VS.freeze bMut
-  return (betaFinal, iters)
-
--- ---------------------------------------------------------------------------
--- 共通ヘルパ
--- ---------------------------------------------------------------------------
-
-mkRegFit :: LA.Vector Double -> LA.Vector Double -> LA.Vector Double
-         -> LA.Vector Double -> Penalty -> Int -> RegFit
-mkRegFit beta yHat r y pen iters =
-  let mu   = LA.sumElements y / fromIntegral (LA.size y)
-      ssT  = LA.sumElements ((y - LA.scalar mu) ^ (2 :: Int))
-      ssR  = LA.sumElements (r ^ (2 :: Int))
-      r2   = if ssT == 0 then 0 else 1 - ssR / ssT
-      nz   = length [v | v <- LA.toList beta, abs v > 1e-8]
-  in RegFit beta yHat r r2 pen nz iters
-
--- ---------------------------------------------------------------------------
--- Standardization
--- ---------------------------------------------------------------------------
-
--- | Standardize each column to mean 0 and standard deviation 1.
---
--- Returns @(X_std, column means, column sds)@. The transformation is
--- @X_std = (X − μ) / σ@; use 'unstandardizeBeta' to map coefficients
--- back to the original scale.
-standardize :: LA.Matrix Double
-            -> (LA.Matrix Double, V.Vector Double, V.Vector Double)
-standardize x =
-  let n     = LA.rows x
-      p     = LA.cols x
-      means = V.fromList
-        [ LA.sumElements (LA.flatten (x LA.¿ [j])) / fromIntegral n
-        | j <- [0 .. p - 1] ]
-      sds   = V.fromList
-        [ let c   = LA.flatten (x LA.¿ [j])
-              mu  = means V.! j
-              var = LA.sumElements ((c - LA.scalar mu) ^ (2 :: Int))
-                    / fromIntegral (n - 1)
-          in sqrt var
-        | j <- [0 .. p - 1] ]
-      cols' = [ let c   = LA.flatten (x LA.¿ [j])
-                    mu  = means V.! j
-                    sd  = sds V.! j
-                in (c - LA.scalar mu) / LA.scalar (if sd == 0 then 1 else sd)
-              | j <- [0 .. p - 1] ]
-      xStd  = LA.fromColumns cols'
-  in (xStd, means, sds)
-
--- | Map coefficients fitted in standardized space back to the original
--- scale: @β_orig_j = β_std_j / σ_j@. The intercept must be adjusted
--- separately, outside this helper.
-unstandardizeBeta :: V.Vector Double -> LA.Vector Double -> LA.Vector Double
-unstandardizeBeta sds betaStd =
-  let p = LA.size betaStd
-  in LA.fromList
-       [ (betaStd `LA.atIndex` j) / (sds V.! j)
-       | j <- [0 .. p - 1] ]
-
--- ---------------------------------------------------------------------------
--- 多出力対応 (主 API)
--- ---------------------------------------------------------------------------
-
--- | Multi-output regularized-regression fit result.
--- Y は n × q、係数 B は p × q、予測 Ŷ = X B。
--- 'rfmFits' は列ごとの単出力 'RegFit' (R²、|β|>0 の数、反復回数を提供)。
-data RegFitMulti = RegFitMulti
-  { rfmFits     :: [RegFit]            -- ^ 列ごとの単出力 fit
-  , rfmBeta     :: LA.Matrix Double    -- ^ p × q
-  , rfmYHat     :: LA.Matrix Double    -- ^ n × q
-  , rfmResid    :: LA.Matrix Double    -- ^ n × q
-  , rfmR2       :: [Double]            -- ^ 列ごとの R²
-  , rfmPenalty  :: Penalty
-  } deriving (Show)
-
--- | Multi-output regularized regression with sklearn-compatible default
--- convergence parameters (@maxIter = 1000@, @tol = 1e-4@). Use
--- 'fitRegularizedMultiWith' to override.
---
--- - OLS / Ridge: 行列形式 1 回の線形求解で全 q 列を一括処理 (高速)。
--- - Lasso / Elastic Net: 列ごと座標降下 (列間に依存なし、独立並列可)。
-fitRegularizedMulti :: Penalty -> LA.Matrix Double -> LA.Matrix Double
-                    -> RegFitMulti
-fitRegularizedMulti = fitRegularizedMultiWith 1000 1e-4
-
--- | Multi-output regularized regression with explicit convergence
--- controls (@maxIter@, @tol@). Affects only Lasso / Elastic Net (the
--- iterative coordinate-descent paths). OLS / Ridge are direct solves
--- and ignore these parameters.
-fitRegularizedMultiWith
-  :: Int                    -- ^ Maximum CD iterations (default 1000).
-  -> Double                 -- ^ Convergence tolerance @|Δβ|₂@ (default 1e-4).
-  -> Penalty
-  -> LA.Matrix Double -> LA.Matrix Double
-  -> RegFitMulti
-fitRegularizedMultiWith maxIter tol pen x y = case pen of
-  NoPen        -> fitOLSMulti x y
-  L2 lambda    -> fitRidgeMulti lambda x y
-  L1 lambda    -> fitColumnwise (fitLasso lambda) maxIter tol pen x y
-  ElasticNet l1 l2 -> fitColumnwise (fitElasticNet l1 l2) maxIter tol pen x y
-
--- | Multi-output prediction.
-predictRegularizedMulti :: RegFitMulti -> LA.Matrix Double -> LA.Matrix Double
-predictRegularizedMulti mf xNew = xNew LA.<> rfmBeta mf
-
--- | Extract column @j@ of a 'RegFitMulti' as a 'RegFit'.
-regFitFromMulti :: Int -> RegFitMulti -> RegFit
-regFitFromMulti j mf
-  | j < length (rfmFits mf) = rfmFits mf !! j
-  | otherwise = error ("regFitFromMulti: column " ++ show j ++ " out of range")
-
--- | Matrix-form OLS: @B = X \\ Y@ in a single LAPACK call.
-fitOLSMulti :: LA.Matrix Double -> LA.Matrix Double -> RegFitMulti
-fitOLSMulti x y =
-  let beta = x LA.<\> y
-  in mkRegFitMulti beta x y NoPen (replicate (LA.cols y) 0)
-
--- | 行列形式の Ridge: B = (XᵀX + λI)⁻¹ XᵀY (1 回の Cholesky/LU)。
-fitRidgeMulti :: Double -> LA.Matrix Double -> LA.Matrix Double -> RegFitMulti
-fitRidgeMulti lambda x y =
-  let p    = LA.cols x
-      reg  = LA.tr x LA.<> x + LA.scale lambda (LA.ident p)
-      xty  = LA.tr x LA.<> y
-      beta = reg LA.<\> xty
-  in mkRegFitMulti beta x y (L2 lambda) (replicate (LA.cols y) 0)
-
--- | 列ごと CD (Lasso / Elastic Net 用)。
---
--- @maxIter@ / @tol@ は呼び元から指定する (旧版は 1000 / 1e-7 を hardcoded
--- していたが、これは sklearn の規定値 1000 / 1e-4 より tol 側が 1000×
--- 厳しく、bench 比較が不公平だったため明示パラメタ化)。
-fitColumnwise
-  :: (LA.Matrix Double -> LA.Vector Double -> Int -> Double -> RegFit)
-  -> Int                    -- ^ @maxIter@
-  -> Double                 -- ^ @tol@
-  -> Penalty
-  -> LA.Matrix Double -> LA.Matrix Double
-  -> RegFitMulti
-fitColumnwise fitCol maxIter tol pen x y =
-  let q     = LA.cols y
-      fits  = [ fitCol x (LA.flatten (y LA.¿ [j])) maxIter tol
-              | j <- [0 .. q - 1] ]
-      bMat  = LA.fromColumns [rfBeta f | f <- fits]
-      yHat  = LA.fromColumns [rfYHat f | f <- fits]
-      res   = LA.fromColumns [rfResid f | f <- fits]
-      r2s   = [rfR2 f | f <- fits]
-  in RegFitMulti fits bMat yHat res r2s pen
-
--- | 共通: B 行列から RegFitMulti を組み立て。各列の R² と非零係数数も計算。
-mkRegFitMulti :: LA.Matrix Double -> LA.Matrix Double -> LA.Matrix Double
-              -> Penalty -> [Int] -> RegFitMulti
-mkRegFitMulti beta x y pen iters =
-  let yHat  = x LA.<> beta
-      res   = y - yHat
-      q     = LA.cols y
-      colFit j =
-        let b   = LA.flatten (beta LA.¿ [j])
-            yh  = LA.flatten (yHat LA.¿ [j])
-            rj  = LA.flatten (res LA.¿ [j])
-            yj  = LA.flatten (y LA.¿ [j])
-        in mkRegFit b yh rj yj pen (iters !! j)
-      fits  = [colFit j | j <- [0 .. q - 1]]
-  in RegFitMulti fits beta yHat res [rfR2 f | f <- fits] pen
-
--- ---------------------------------------------------------------------------
--- Regularization path
--- ---------------------------------------------------------------------------
-
--- | 与えられた λ の系列に対して係数推移を計算する (regularization path)。
--- 戻り値: 各 λ に対する係数ベクトル。
---
--- 利用例 (Ridge):
---
--- @
--- let lams = [10 ** (-4 + 0.1 * i) | i <- [0..60]]
---     path = regularizationPath L2 lams xMat yVec
--- -- path :: [(Double, [Double])]  -- (λ, [β₀, β₁, ...])
--- @
-regularizationPath
-  :: (Double -> Penalty)         -- ^ λ → Penalty (e.g. @L2@, @L1@,
-                                 --   @\\l -> ElasticNet (l*α) (l*(1-α))@)
-  -> [Double]                    -- ^ λ 系列
-  -> LA.Matrix Double            -- ^ X (intercept 列付き)
-  -> LA.Vector Double            -- ^ y
-  -> [(Double, [Double])]        -- ^ [(λ, 係数ベクトル)]
-regularizationPath mkPen lambdas x y =
-  [ (lam, LA.toList (rfBeta (fitRegularized (mkPen lam) x y)))
-  | lam <- lambdas ]
-
-
--- ===========================================================================
--- λ 自動選択 (Phase 4.4、 request/150)
--- ===========================================================================
-
--- | Penalty の "形" (λ 抜き)。 'selectLambdaCV' の grid 探索で λ を変化させる
--- 際の penalty family を指定する。
-data PenaltyKind
-  = KindRidge                -- ^ Ridge (= 'L2' λ)
-  | KindLasso                -- ^ Lasso (= 'L1' λ)
-  | KindElasticNet !Double   -- ^ ElasticNet。 @α@ = L1 比率 (0 ≤ α ≤ 1)。
-                             --   total penalty = λ·(α·L1 + (1-α)/2·L2)、 内部で
-                             --   'ElasticNet' (α·λ) ((1-α)·λ) に展開。
-  deriving (Show, Eq)
-
--- | λ 自動選択の結果。
-data LambdaSelection = LambdaSelection
-  { lsBestLambda  :: !Double      -- ^ CV MSE が最小の λ
-  , lsLambdas     :: ![Double]    -- ^ 検証した λ 値 (入力順)
-  , lsCVScores    :: ![Double]    -- ^ 各 λ の CV MSE (lsLambdas と対応)
-  , lsCVScoreSE   :: ![Double]    -- ^ 各 λ の CV MSE の標準誤差 (fold 間 SD)
-  , lsOneSeLambda :: !Double      -- ^ 1-SE rule の λ (best ± 1·SE 範囲内で
-                                  --   最大スパース = 最大 λ)
-  , lsKind        :: !PenaltyKind -- ^ 入力 PenaltyKind を保持 (canvas 側参照用)
-  } deriving (Show)
-
--- | k-fold CV で λ を自動選択。
---
--- 入力 'PenaltyKind' に従って λ grid を Ridge/Lasso/EN の 'Penalty' に展開し、
--- 各 λ について k-fold CV を実行、 fold 平均 MSE を計算する。
---
--- 返り値の 'lsBestLambda' は MSE 最小の λ、 'lsOneSeLambda' は 1-SE rule
--- (= best MSE から 1·SE 以内で最大スパースな λ) の λ。
-selectLambdaCV
-  :: PrimMonad m
-  => Int               -- ^ k-fold の k (≥ 2)
-  -> PenaltyKind       -- ^ Ridge / Lasso / ElasticNet
-  -> [Double]          -- ^ 検証する λ grid (log-spaced 推奨)
-  -> LA.Matrix Double  -- ^ X (n × p)
-  -> LA.Vector Double  -- ^ y (n)
-  -> MWC.Gen (PrimState m)  -- ^ shuffle 用 (ST/IO 両用)
-  -> m LambdaSelection
-selectLambdaCV k kind lambdas xMat yVec gen = do
-  let n = LA.rows xMat
-  folds <- HCV.kFold k n gen
-  let perLambda lam =
-        let scores =
-              [ mseForFold (penaltyOf kind lam) xMat yVec trainIdx testIdx
-              | (trainIdx, testIdx) <- folds, not (null testIdx)
-              ]
-            !nFolds = fromIntegral (length scores) :: Double
-            mean   = sum scores / nFolds
-            varN   = sum [(s - mean) ** 2 | s <- scores] / max 1 (nFolds - 1)
-            !se    = sqrt (varN / nFolds)
-        in (mean, se)
-      stats = map perLambda lambdas
-      mses  = map fst stats
-      ses   = map snd stats
-      indexedMSEs = zip3 lambdas mses ses
-      sortedAsc   = sortBy (comparing (\(_, m, _) -> m)) indexedMSEs
-      (bestL, bestMSE, bestSE) =
-        case sortedAsc of
-          (h:_) -> h
-          []    -> (0, 0, 0)
-      threshold = bestMSE + bestSE
-      -- 1-SE λ: best から 1·SE 以内の λ のうち最大 (= 最大スパース)
-      oneSe =
-        let cands = [ lam | (lam, m, _) <- indexedMSEs, m <= threshold ]
-        in if null cands then bestL else maximum cands
-  pure LambdaSelection
-    { lsBestLambda  = bestL
-    , lsLambdas     = lambdas
-    , lsCVScores    = mses
-    , lsOneSeLambda = oneSe
-    , lsCVScoreSE   = ses
-    , lsKind        = kind
-    }
-
--- | 純粋 (seed) 版 'selectLambdaCV'。 同 seed → 同 λ 選択 (ST/IO ビット一致)。
--- 罰則回帰の高レベル spec (Phase 70.7 = `df |-> lasso …`) を pure 'fitWith' で完結
--- させる継ぎ目 (GP の `AutoCV` / `kMeansPure` / `fitRFVPure` と一貫)。
-selectLambdaCVPure
-  :: Int -> PenaltyKind -> [Double] -> LA.Matrix Double -> LA.Vector Double
-  -> Word32 -> LambdaSelection
-selectLambdaCVPure k kind lambdas xMat yVec seed =
-  runST (MWC.initialize (V.singleton seed) >>= selectLambdaCV k kind lambdas xMat yVec)
-
--- | 内部 helper: PenaltyKind と λ から具体 'Penalty' を組み立てる。
-penaltyOf :: PenaltyKind -> Double -> Penalty
-penaltyOf KindRidge          lam = L2 lam
-penaltyOf KindLasso          lam = L1 lam
-penaltyOf (KindElasticNet a) lam = ElasticNet (a * lam) ((1 - a) * lam)
-
--- | 1 fold の MSE を返す。 train index で fit、 test index で predict + 残差²平均。
-mseForFold
-  :: Penalty
-  -> LA.Matrix Double
-  -> LA.Vector Double
-  -> [Int]            -- train 行 index
-  -> [Int]            -- test 行 index
-  -> Double
-mseForFold pen xMat yVec trainIdx testIdx =
-  let xTr = xMat LA.? trainIdx
-      yTr = LA.fromList [ yVec LA.! i | i <- trainIdx ]
-      xTe = xMat LA.? testIdx
-      yTe = LA.fromList [ yVec LA.! i | i <- testIdx ]
-      fit = fitRegularized pen xTr yTr
-      yHat = predictRegularized fit xTe
-      resid = yTe - yHat
-      nTe = fromIntegral (length testIdx) :: Double
-  in LA.sumElements (resid * resid) / nTe
diff --git a/src/Hanalyze/Model/RegularizedAdvanced.hs b/src/Hanalyze/Model/RegularizedAdvanced.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/RegularizedAdvanced.hs
+++ /dev/null
@@ -1,268 +0,0 @@
--- |
--- Module      : Hanalyze.Model.RegularizedAdvanced
--- Description : 高度な罰則項回帰 (Phase 31) — Adaptive Lasso / MCP / SCAD / Group Lasso
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- 高度な罰則項回帰 (Phase 31): Adaptive Lasso / MCP / SCAD / Group Lasso。
---
--- 既存 'Hanalyze.Model.Regularized' (Lasso/Ridge/Elastic Net + CV λ 選択、
--- Phase 4 で実装) を補完する変数選択型の罰則項群。 JMP "Generalized
--- Regression" platform / R `ncvreg` / `grpreg` / `glmnet` (adaptive オプション)
--- 相当。
---
--- ## 共通の前提
---
--- - 罰則項は Lasso 同様 X の列スケールに敏感。 呼び出し側で
---   'Hanalyze.Model.Regularized.standardize' しておく
--- - 内部 CD は 'Hanalyze.Model.Regularized.cdLoop' を流用 (Adaptive Lasso は
---   列再重み付け、 MCP / SCAD は per-coord non-convex threshold)
--- - Group Lasso は block CD で別ループ (Yuan-Lin 2006 algorithm)
---
--- Reference:
---   Zou (2006), Zhang (2010), Fan-Li (2001), Yuan-Lin (2006),
---   Breheny-Huang (2011) "Coordinate descent algorithms for non-convex
---   penalized regression". Ann. Appl. Stat. 5:232-253.
-module Hanalyze.Model.RegularizedAdvanced
-  ( -- * Adaptive Lasso (Zou 2006)
-    fitAdaptiveLasso
-  , adaptiveWeightsFromOLS
-    -- * MCP (Zhang 2010)
-  , fitMCP
-    -- * SCAD (Fan-Li 2001)
-  , fitSCAD
-    -- * Group Lasso (Yuan-Lin 2006)
-  , fitGroupLasso
-  ) where
-
-import qualified Numeric.LinearAlgebra        as LA
-import           Hanalyze.Model.Regularized
-                   (RegFit (..), Penalty (..), softThreshold, cdLoop,
-                    mkRegFit, fitOLS, fitLasso)
-
--- ---------------------------------------------------------------------------
--- 31-A1: Adaptive Lasso
--- ---------------------------------------------------------------------------
-
--- | Adaptive Lasso (Zou 2006): @argmin (1/2n)|y - Xβ|² + λ Σ w_j |β_j|@。
---
--- 解法: column reweighting trick — @x_j' = x_j / w_j@ で変形すると標準
--- Lasso になり、 解 @β_j' = β_j · w_j@ から @β_j = β_j' / w_j@ で復元できる。
--- 既存 'fitLasso' をそのまま流用するので追加 CD ループ不要。
---
--- @w_j@ は典型的に OLS pilot 推定値から構築する ('adaptiveWeightsFromOLS')。
---
--- 注意: @w_j = 0@ は "罰則ゼロ" ではなく実装上 "@β_j = 0@ 強制" として扱う
--- (列 j を 0 vector に潰すため)。 罰則ゼロにしたい場合は @w_j@ を非常に
--- 小さい正値にする。
-fitAdaptiveLasso
-  :: Double                -- ^ @λ@
-  -> LA.Vector Double      -- ^ weights @w@ (length @p@、 全 @≥ 0@)
-  -> LA.Matrix Double      -- ^ X (n × p)
-  -> LA.Vector Double      -- ^ y
-  -> Int                   -- ^ max CD iterations
-  -> Double                -- ^ tolerance
-  -> RegFit
-fitAdaptiveLasso lambda w x y maxIter tol =
-  let invW   = LA.cmap (\wj -> if wj <= 0 then 0 else 1 / wj) w
-      xRew   = x LA.<> LA.diag invW
-      lassoF = fitLasso lambda xRew y maxIter tol
-      -- 変形空間の解 β' を元の空間の β = β' / w に戻す
-      betaP  = rfBeta lassoF
-      beta   = invW * betaP
-      yHat   = x LA.#> beta
-      r      = y - yHat
-  in mkRegFit beta yHat r y (L1 lambda) (rfIters lassoF)
-
--- | OLS pilot 推定値から Adaptive Lasso 重み @w_j = 1 / |β̂_j^OLS|^γ@ を構築。
--- 典型値 @γ = 1@。 OLS が定義できないケース (@n < p@) では事前に Ridge pilot
--- に切り替えるなど呼び出し側で工夫する。 0 除算回避のため @|β̂| ≤ 1e-8@ の
--- 場合は floor @1e-8@ を使う。
-adaptiveWeightsFromOLS
-  :: Double                -- ^ @γ@ (typical 1.0)
-  -> LA.Matrix Double -> LA.Vector Double -> LA.Vector Double
-adaptiveWeightsFromOLS gamma x y =
-  let beta0 = rfBeta (fitOLS x y)
-  in LA.cmap (\b -> 1 / (max 1e-8 (abs b) ** gamma)) beta0
-
--- ---------------------------------------------------------------------------
--- 31-A2: MCP (Minimax Concave Penalty、 Zhang 2010)
--- ---------------------------------------------------------------------------
-
--- | MCP non-convex 罰則:
---
--- @
---   p_{λ,γ}(β) = λ |β| - β²/(2γ)   if |β| ≤ γλ
---              = γλ²/2              if |β| > γλ
--- @
---
--- @γ → ∞@ で Lasso に縮退、 @γ → 1@ で hard-threshold 寄りになる。 典型値
--- @γ ∈ [2, 5]@。
---
--- Coordinate descent 更新 (Breheny-Huang 2011, with column-norm @cSq@):
---
--- @
---   z = ρ_j
---   β_j = S(z, λ) / (cSq - 1/γ)   if |z| ≤ γλ·cSq
---       = z / cSq                  if |z| > γλ·cSq
--- @
---
--- 前提: @cSq > 1/γ@ (= 罰則項の凹性を局所凸性が上回る)。 標準化 @X@ (cSq ≈ 1)
--- で @γ > 1@ なら自動的に満たす。 違反時は inner CD が発散する可能性があり、
--- 呼び出し側で @standardize@ + @γ ≥ 3@ を推奨。
-fitMCP
-  :: Double                -- ^ @λ@
-  -> Double                -- ^ @γ@ (concavity、 推奨 @≥ 3@)
-  -> LA.Matrix Double      -- ^ X
-  -> LA.Vector Double      -- ^ y
-  -> Int                   -- ^ max CD iterations
-  -> Double                -- ^ tolerance
-  -> RegFit
-fitMCP lambda gamma x y maxIter tol =
-  let upd rho cSq =
-        let z      = rho
-            thresh = gamma * lambda * cSq
-        in if abs z <= thresh
-             then
-               let denom = cSq - 1 / gamma
-               in if denom <= 0
-                    then z / cSq                       -- 非凸時は OLS 解で fallback
-                    else softThreshold z lambda / denom
-             else z / cSq
-      (betaFinal, iters) = cdLoop x y maxIter tol upd
-      yHat = x LA.#> betaFinal
-      r    = y - yHat
-  in mkRegFit betaFinal yHat r y (L1 lambda) iters
-
--- ---------------------------------------------------------------------------
--- 31-A3: SCAD (Smoothly Clipped Absolute Deviation、 Fan-Li 2001)
--- ---------------------------------------------------------------------------
-
--- | SCAD non-convex 罰則 (区分三次):
---
--- @
---   p'_{λ,a}(|β|) = λ                    if |β| ≤ λ
---                 = (aλ - |β|)/(a-1)     if λ < |β| ≤ aλ
---                 = 0                    if |β| > aλ
--- @
---
--- 典型値 @a = 3.7@ (Fan-Li 2001 推奨)。
---
--- Coordinate descent 更新 (Breheny-Huang 2011):
---
--- @
---   z = ρ_j
---   if |z| ≤ λ·(1 + cSq) :        β_j = S(z, λ) / cSq        -- Lasso 領域
---   elif |z| ≤ a·λ·cSq :          β_j = S(z, aλ/(a-1)) / (cSq - 1/(a-1))
---   else :                         β_j = z / cSq               -- OLS 領域
--- @
-fitSCAD
-  :: Double                -- ^ @λ@
-  -> Double                -- ^ @a@ (= 3.7 推奨)
-  -> LA.Matrix Double
-  -> LA.Vector Double
-  -> Int -> Double
-  -> RegFit
-fitSCAD lambda a x y maxIter tol =
-  let upd rho cSq =
-        let z = rho
-            absZ = abs z
-        in if absZ <= lambda * (1 + cSq)
-             then softThreshold z lambda / cSq
-             else if absZ <= a * lambda * cSq
-                    then
-                      let denom = cSq - 1 / (a - 1)
-                          thr   = a * lambda / (a - 1)
-                      in if denom <= 0
-                           then z / cSq
-                           else softThreshold z thr / denom
-                    else z / cSq
-      (betaFinal, iters) = cdLoop x y maxIter tol upd
-      yHat = x LA.#> betaFinal
-      r    = y - yHat
-  in mkRegFit betaFinal yHat r y (L1 lambda) iters
-
--- ---------------------------------------------------------------------------
--- 31-A4: Group Lasso (Yuan-Lin 2006)
--- ---------------------------------------------------------------------------
-
--- | Group Lasso: @argmin (1/2n)|y - Xβ|² + λ Σ_g √|g| · |β_g|₂@
--- (group ごと L2 ノルムの和で penalize、 group 全体を 0 / non-0 にする)。
---
--- 解法: block coordinate descent。 各 group @g@ について部分残差
--- @r_g = r + X_g β_g@ を作り、 group 更新
---
--- @
---   z_g = X_gᵀ r_g / n
---   β_g_new = (1 - λ √|g| / |z_g|₂)_+ · z_g / cSq_g
--- @
---
--- ここで @cSq_g = |X_g|² / n@ (group 内列ノルム合計、 簡易には 1 を仮定)、
--- @(·)_+@ は max(·, 0)。 Yuan-Lin 2006 の uncorrelated-within-group 想定で
--- 動く simplified version。
---
--- @groups@ は @[[Int]]@ で、 各内側リストが列 index の集合 (重複・順不同可)。
--- 列 index が複数 group に現れた場合は最初の group のみ扱われる。
-fitGroupLasso
-  :: Double                -- ^ @λ@
-  -> [[Int]]               -- ^ group 分割 (列 index)
-  -> LA.Matrix Double      -- ^ X (n × p)
-  -> LA.Vector Double      -- ^ y
-  -> Int                   -- ^ max iterations
-  -> Double                -- ^ tolerance
-  -> RegFit
-fitGroupLasso lambda groups x y maxIter tol =
-  let n       = LA.rows x
-      nD      = fromIntegral n :: Double
-      p       = LA.cols x
-      -- group ごとに前計算する design submatrix と column-norm sum
-      gPrep   = [ (gValid, x LA.¿ gValid, gSize gValid)
-                | g <- groups
-                , let gValid = [j | j <- g, j >= 0, j < p]
-                , not (null gValid) ]
-      gSize g = sqrt (fromIntegral (length g))   -- √|g|
-      -- 反復: β_g を block 更新
-      step beta resid =
-        foldl
-          (\(bAcc, rAcc) (gIdx, xG, gW) ->
-              let -- 部分残差 r_g = r + X_g β_g
-                  bG     = LA.fromList [ LA.atIndex bAcc j | j <- gIdx ]
-                  rG     = rAcc + xG LA.#> bG
-                  z      = LA.tr xG LA.#> rG / LA.scalar nD
-                  zNorm  = LA.norm_2 z
-                  cSqG   = LA.sumElements (xG * xG) / nD
-                  thr    = lambda * gW
-                  bGnew  = if zNorm <= thr || cSqG <= 0
-                             then LA.konst 0 (LA.size z)
-                             else LA.scale ((1 - thr / zNorm) / cSqG) z
-                  -- 残差を新 β_g で更新: r ← r - X_g (β_g_new - β_g)
-                  rNew   = rG - xG LA.#> bGnew
-                  bAcc'  = updateIndices bAcc gIdx (LA.toList bGnew)
-              in (bAcc', rNew))
-          (beta, resid) gPrep
-      loop !k !beta !resid =
-        if k >= maxIter
-          then (beta, k)
-          else
-            let (betaNew, residNew) = step beta resid
-                diff = LA.norm_2 (betaNew - beta)
-            in if diff < tol
-                 then (betaNew, k + 1)
-                 else loop (k + 1) betaNew residNew
-      beta0 = LA.konst 0 p
-      (betaFinal, iters) = loop 0 beta0 y
-      yHat  = x LA.#> betaFinal
-      r     = y - yHat
-  in mkRegFit betaFinal yHat r y (L1 lambda) iters
-
--- | Vector の特定 index 群を新値で置き換える (immutable 経由)。 Group Lasso
--- 専用のため module 内部 helper。
-updateIndices :: LA.Vector Double -> [Int] -> [Double] -> LA.Vector Double
-updateIndices v idx vals =
-  let xs = LA.toList v
-      m  = zip idx vals
-      n  = length xs
-      lookupNew j = case lookup j m of
-        Just nv -> nv
-        Nothing -> xs !! j
-  in LA.fromList [ lookupNew j | j <- [0 .. n - 1] ]
diff --git a/src/Hanalyze/Model/Reliability.hs b/src/Hanalyze/Model/Reliability.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Reliability.hs
+++ /dev/null
@@ -1,268 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Model.Reliability
--- Description : 信頼性解析 — 加速寿命試験モデル群 (Arrhenius / Eyring / Inverse Power Law)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- 信頼性解析: 加速寿命試験のモデル群。
---
--- ストレス変数 (温度 / 電圧 / 湿度等) と寿命の関係を回帰し、 使用条件下での
--- 寿命予測や加速係数を計算する。
---
--- 提供するモデル:
---
---   * 'fitArrhenius' — 温度ストレス: @t = A · exp(Ea / (k_B · T))@
---   * 'fitEyring' — 温度 + 1 ストレス: 半導体 EM 等
---   * 'fitInversePower' — 電圧 / 機械応力: @t = A · S^(-n)@
---
--- いずれも対数寿命を線形モデルとして fit する (古典的アプローチ)。
--- 寿命分布の指定が必要な場合は 'Hanalyze.Model.Weibull' の MLE 結果を
--- 入力として渡すバリアント (本モジュールの提供外、 別フェーズで検討)。
-module Hanalyze.Model.Reliability
-  ( -- * Arrhenius
-    ArrheniusFit (..)
-  , fitArrhenius
-  , accelerationFactor
-    -- * Eyring
-  , EyringFit (..)
-  , fitEyring
-    -- * Inverse Power Law
-  , InversePowerFit (..)
-  , fitInversePower
-    -- * 共通定数
-  , kBoltzmann
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import           Data.Text (Text)
-
--- ===========================================================================
--- 共通定数
--- ===========================================================================
-
--- | Boltzmann 定数 (eV/K)。 Arrhenius / Eyring で温度ストレスに使う。
-kBoltzmann :: Double
-kBoltzmann = 8.617333262145e-5
-
--- ===========================================================================
--- Arrhenius モデル
--- ===========================================================================
-
--- | Arrhenius fit: @t = A · exp(Ea / (k_B · T))@
-data ArrheniusFit = ArrheniusFit
-  { afA      :: !Double  -- ^ 前指数因子 A
-  , afEa     :: !Double  -- ^ 活性化エネルギー Ea (eV)
-  , afLogLik :: !Double  -- ^ 対数尤度 (Gaussian residual 仮定)
-  , afN      :: !Int     -- ^ 観測 (温度 × 寿命) 数
-  } deriving (Show)
-
--- | Arrhenius モデルの fit。
---
--- 入力: @[(temperature_K, [lifetimes])]@ の対、 温度ごとに複数寿命を観測。
--- 解法: log t = log A + Ea/k_B · (1/T) を OLS で解く (= 線形回帰)。
--- 戻り値: @A@ と @Ea (eV)@ の点推定、 log-likelihood (Gaussian residual 仮定)。
---
--- 失敗条件:
---
---   * 入力が空または全観測 0 個 → Left
---   * 温度水準が 1 種類しかない (= 傾き決定不能) → Left
---   * 任意の温度 ≤ 0 や寿命 ≤ 0 → Left (log 取得不能)
-fitArrhenius :: [(Double, [Double])] -> Either Text ArrheniusFit
-fitArrhenius input = do
-  () <- if null input then Left "fitArrhenius: empty input" else Right ()
-  let allPairs =
-        [ (t, life)
-        | (t, lives) <- input
-        , life <- lives
-        ]
-  () <- if null allPairs
-          then Left "fitArrhenius: no lifetime observations across all temperatures"
-          else Right ()
-  () <- if any (\(t, l) -> t <= 0 || l <= 0) allPairs
-          then Left "fitArrhenius: temperatures and lifetimes must all be > 0"
-          else Right ()
-  let distinctTemps = length (nubByDouble (map fst allPairs))
-  () <- if distinctTemps < 2
-          then Left "fitArrhenius: need at least 2 distinct temperatures"
-          else Right ()
-  -- (x, y) = (1/T, log t)
-  let xs = map (\(t, _) -> 1 / t) allPairs
-      ys = map (\(_, l) -> log l) allPairs
-      n  = length allPairs
-      meanX = sum xs / fromIntegral n
-      meanY = sum ys / fromIntegral n
-      sxx = sum [ (x - meanX) ** 2 | x <- xs ]
-      sxy = sum [ (x - meanX) * (y - meanY) | (x, y) <- zip xs ys ]
-  () <- if sxx <= 0
-          then Left "fitArrhenius: zero variance in 1/T (numerical issue)"
-          else Right ()
-  let b1     = sxy / sxx                     -- slope = Ea / k_B
-      b0     = meanY - b1 * meanX            -- intercept = log A
-      a      = exp b0
-      ea     = b1 * kBoltzmann
-      yHat   = [ b0 + b1 * x | x <- xs ]
-      sse    = sum [ (y - yh) ** 2 | (y, yh) <- zip ys yHat ]
-      sigma2 = if n > 2 then sse / fromIntegral (n - 2) else sse / fromIntegral n
-      ll     = -0.5 * fromIntegral n * (log (2 * pi * sigma2) + 1)
-  Right ArrheniusFit
-    { afA      = a
-    , afEa     = ea
-    , afLogLik = ll
-    , afN      = n
-    }
-
--- | 重複除去 (浮動小数点許容なし、 完全一致のみ)。
-nubByDouble :: [Double] -> [Double]
-nubByDouble = go []
-  where
-    go acc []     = reverse acc
-    go acc (x:xs) | x `elem` acc = go acc xs
-                  | otherwise    = go (x : acc) xs
-
--- | 加速係数 AF = exp(Ea/k_B · (1/T_use - 1/T_test))
-accelerationFactor :: ArrheniusFit -> Double -> Double -> Double
-accelerationFactor fit tUse tTest =
-  exp (afEa fit / kBoltzmann * (1/tUse - 1/tTest))
-
--- ===========================================================================
--- Eyring モデル (Phase 2.6)
--- ===========================================================================
-
--- | Eyring fit: @t = A · T^(-1) · exp(Ea / (k_B · T)) · exp(B · S)@
--- (温度 T と 1 ストレス変数 S)
-data EyringFit = EyringFit
-  { efA      :: !Double
-  , efEa     :: !Double
-  , efB      :: !Double  -- ストレス係数
-  , efLogLik :: !Double
-  , efN      :: !Int
-  } deriving (Show)
-
--- | Eyring モデルの fit。
---
--- モデル: @t · T = A · exp(Ea / (k_B · T)) · exp(B · S)@
--- 等価に: @log t = log A − log T + Ea/(k_B · T) + B · S@
---
--- 入力: @[(temperature_K, stress, [lifetimes])]@。 各 (T, S) 組合せで複数寿命可。
--- 解法: y = log t + log T を (1/T, S) の 2 変量 OLS で fit (intercept 含む)。
---   β0 = log A、 β1 = Ea / k_B、 β2 = B
-fitEyring :: [(Double, Double, [Double])] -> Either Text EyringFit
-fitEyring input = do
-  () <- if null input then Left "fitEyring: empty input" else Right ()
-  let pairs =
-        [ (t, s, life)
-        | (t, s, lives) <- input
-        , life <- lives
-        ]
-  () <- if null pairs
-          then Left "fitEyring: no lifetime observations"
-          else Right ()
-  () <- if any (\(t, _, l) -> t <= 0 || l <= 0) pairs
-          then Left "fitEyring: temperatures and lifetimes must be > 0"
-          else Right ()
-  let distinctTS = nubByPair [ (t, s) | (t, s, _) <- pairs ]
-  () <- if length distinctTS < 3
-          then Left "fitEyring: need at least 3 distinct (T, S) combinations"
-          else Right ()
-  let xRows = [ [1, 1 / t, s] | (t, s, _) <- pairs ]
-      ys    = [ log l + log t | (t, _, l) <- pairs ]
-      xMat  = LA.fromLists xRows :: LA.Matrix Double
-      yVec  = LA.fromList ys     :: LA.Vector Double
-      -- normal equations: β = (XᵀX)⁻¹ Xᵀy
-      xt    = LA.tr xMat
-      xtx   = xt LA.<> xMat
-      xty   = xt LA.#> yVec
-  betaList <- case LA.linearSolve xtx (LA.asColumn xty) of
-    Just m  -> Right (LA.toList (LA.flatten m))
-    Nothing -> Left "fitEyring: design matrix is singular (collinear T/S?)"
-  case betaList of
-    [b0, b1, b2] -> do
-      let n      = length pairs
-          a      = exp b0
-          ea     = b1 * kBoltzmann
-          bCoef  = b2
-          yHat   = LA.toList (xMat LA.#> LA.fromList [b0, b1, b2])
-          sse    = sum [ (y - yh) ** 2 | (y, yh) <- zip ys yHat ]
-          dof    = max 1 (n - 3)
-          sigma2 = sse / fromIntegral dof
-          ll     = -0.5 * fromIntegral n * (log (2 * pi * sigma2) + 1)
-      Right EyringFit
-        { efA      = a
-        , efEa     = ea
-        , efB      = bCoef
-        , efLogLik = ll
-        , efN      = n
-        }
-    _ -> Left "fitEyring: linearSolve returned unexpected length"
-
--- | (T, S) ペアの重複除去。
-nubByPair :: [(Double, Double)] -> [(Double, Double)]
-nubByPair = go []
-  where
-    go acc []     = reverse acc
-    go acc (p:ps) | p `elem` acc = go acc ps
-                  | otherwise    = go (p : acc) ps
-
--- ===========================================================================
--- Inverse Power Law モデル (Phase 2.6)
--- ===========================================================================
-
--- | Inverse Power Law fit: @t = A · S^(-n)@
-data InversePowerFit = InversePowerFit
-  { ipfA      :: !Double
-  , ipfN      :: !Double  -- パワー指数
-  , ipfLogLik :: !Double
-  , ipfNobs   :: !Int
-  } deriving (Show)
-
--- | Inverse Power Law モデルの fit。
---
--- モデル: @t = A · S^(-n)@
--- log 変換: @log t = log A − n · log S@
---
--- 入力: @[(stress, [lifetimes])]@。 stress > 0、 lifetime > 0 必須。
--- 解法: y = log t を log S の単変量 OLS で fit。 傾き = -n。
-fitInversePower :: [(Double, [Double])] -> Either Text InversePowerFit
-fitInversePower input = do
-  () <- if null input then Left "fitInversePower: empty input" else Right ()
-  let pairs =
-        [ (s, life)
-        | (s, lives) <- input
-        , life <- lives
-        ]
-  () <- if null pairs
-          then Left "fitInversePower: no lifetime observations"
-          else Right ()
-  () <- if any (\(s, l) -> s <= 0 || l <= 0) pairs
-          then Left "fitInversePower: stress and lifetimes must be > 0"
-          else Right ()
-  let distinctS = length (nubByDouble (map fst pairs))
-  () <- if distinctS < 2
-          then Left "fitInversePower: need at least 2 distinct stress levels"
-          else Right ()
-  let xs = map (\(s, _) -> log s) pairs
-      ys = map (\(_, l) -> log l) pairs
-      n  = length pairs
-      meanX = sum xs / fromIntegral n
-      meanY = sum ys / fromIntegral n
-      sxx = sum [ (x - meanX) ** 2 | x <- xs ]
-      sxy = sum [ (x - meanX) * (y - meanY) | (x, y) <- zip xs ys ]
-  () <- if sxx <= 0
-          then Left "fitInversePower: zero variance in log S"
-          else Right ()
-  let slope  = sxy / sxx           -- = -n
-      b0     = meanY - slope * meanX
-      a      = exp b0
-      nExp   = - slope
-      yHat   = [ b0 + slope * x | x <- xs ]
-      sse    = sum [ (y - yh) ** 2 | (y, yh) <- zip ys yHat ]
-      sigma2 = if n > 2 then sse / fromIntegral (n - 2) else sse / fromIntegral n
-      ll     = -0.5 * fromIntegral n * (log (2 * pi * sigma2) + 1)
-  Right InversePowerFit
-    { ipfA      = a
-    , ipfN      = nExp
-    , ipfLogLik = ll
-    , ipfNobs   = n
-    }
diff --git a/src/Hanalyze/Model/ReliabilityBlockDiagram.hs b/src/Hanalyze/Model/ReliabilityBlockDiagram.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/ReliabilityBlockDiagram.hs
+++ /dev/null
@@ -1,88 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
-
--- |
--- Module      : Hanalyze.Model.ReliabilityBlockDiagram
--- Description : 信頼性ブロック図 (RBD) の直列/並列/k-out-of-n 再帰合成による系全体信頼度計算
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Reliability Block Diagram (RBD).
---
--- Computes system reliability from a structural composition of components
--- with known individual reliabilities. The three primitive combinators
--- are the textbook ones (e.g. O'Connor & Kleyner, /Practical Reliability
--- Engineering/):
---
---   * Series (every block must work):
---       @R = ∏ Rᵢ@
---   * Parallel (any block working suffices):
---       @R = 1 − ∏ (1 − Rᵢ)@
---   * k-out-of-n (at least @k@ of @n@ blocks must work):
---       @R = Σ_{i = k}^{n} P(exactly i succeed)@
---       computed by Poisson-binomial DP — works with heterogeneous block
---       reliabilities (the binomial closed form is the homogeneous
---       special case).
---
--- Blocks can be arbitrarily nested. Failure independence between blocks
--- is assumed (the standard RBD assumption).
---
--- @
--- import Hanalyze.Model.ReliabilityBlockDiagram
---
--- -- Two-out-of-three redundancy of three series strings:
--- let sys = KofN 2 [ Series [Leaf 0.95, Leaf 0.99]
---                  , Series [Leaf 0.95, Leaf 0.99]
---                  , Series [Leaf 0.95, Leaf 0.99] ]
---     r   = reliabilityOf sys
--- @
---
--- == Implemented
---
---   * 'RBDBlock' — composable tree of components.
---   * 'reliabilityOf' — recursive evaluation.
-module Hanalyze.Model.ReliabilityBlockDiagram
-  ( RBDBlock (..)
-  , reliabilityOf
-  ) where
-
--- ---------------------------------------------------------------------------
--- Types
--- ---------------------------------------------------------------------------
-
--- | A block in a reliability diagram. @Leaf p@ is a single component with
--- reliability @p ∈ [0, 1]@; the other constructors compose sub-blocks.
-data RBDBlock
-  = Leaf     !Double
-  | Series   ![RBDBlock]
-  | Parallel ![RBDBlock]
-  | KofN     !Int ![RBDBlock]
-  deriving (Show, Eq)
-
--- ---------------------------------------------------------------------------
--- Evaluation
--- ---------------------------------------------------------------------------
-
--- | System reliability of a block, in @[0, 1]@. Component reliabilities
--- are assumed independent (the standard RBD assumption).
-reliabilityOf :: RBDBlock -> Double
-reliabilityOf (Leaf p)         = p
-reliabilityOf (Series bs)      = product (map reliabilityOf bs)
-reliabilityOf (Parallel bs)    = 1 - product [ 1 - reliabilityOf b | b <- bs ]
-reliabilityOf (KofN k bs)
-  | k <= 0           = 1                   -- always satisfied
-  | k > length bs    = 0                   -- impossible
-  | otherwise        =
-      let ps     = map reliabilityOf bs
-          n      = length ps
-          -- Poisson-binomial DP: pmf!!i = P(exactly i blocks work).
-          pmf    = foldr step [1.0] ps
-            where
-              step pi acc =
-                -- acc = pmf of current partial product (length = current j + 1).
-                let len = length acc
-                in [ let aPrev = if i - 1 >= 0    then acc !! (i - 1) else 0
-                         aHere = if i     <  len then acc !! i         else 0
-                     in pi * aPrev + (1 - pi) * aHere
-                   | i <- [0 .. len] ]
-      in sum (drop k pmf)
diff --git a/src/Hanalyze/Model/Robust.hs b/src/Hanalyze/Model/Robust.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Robust.hs
+++ /dev/null
@@ -1,238 +0,0 @@
--- |
--- Module      : Hanalyze.Model.Robust
--- Description : IRLS による Huber / Tukey biweight ロバスト回帰 (M-estimator)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Robust regression M-estimators via IRLS (Phase 31-A5)。
---
--- 外れ値を含むデータに対する線形回帰。 OLS の二乗損失を bounded influence
--- 関数 (Huber / Tukey biweight) に置き換え、 Iteratively Reweighted Least
--- Squares で β を求める。 JMP "Fit Model > Personality: Robust Fit"、
--- R `MASS::rlm` 相当。
---
--- ## アルゴリズム
---
--- 1. β を OLS で初期化
--- 2. 残差 @r_i = y_i - x_i^T β@ を計算
--- 3. ロバストスケール推定 @σ̂ = MAD(r) / 0.6745@
--- 4. 影響関数から重み @w_i@ を計算 ('huberWeight' / 'tukeyWeight')
--- 5. 加重 LS で β を更新: @β ← (X^T W X)^{-1} X^T W y@
--- 6. 収束まで 2-5 を繰り返す
---
--- ## 推定子の選択
---
--- - **Huber** (@k=1.345@、 95% 効率): 線形 + 線形クリップ、 滑らか、 標準
--- - **Tukey biweight** (@c=4.685@、 95% 効率): 完全棄却閾値付き、 外れ値の
---   影響を 0 に落とす、 だが多峰目的関数 (OLS 初期化が重要)
---
--- Reference:
---   Huber (1964) "Robust estimation of a location parameter".
---   Tukey (1977) biweight、 Rousseeuw-Leroy (1987) 教科書。
-module Hanalyze.Model.Robust
-  ( RobustEstimator (..)
-  , RobustFit (..)
-  , defaultHuberK
-  , defaultTukeyC
-  , fitRobustLM
-  , huberWeight
-  , tukeyWeight
-  , psiFn
-  , psiDerivFn
-  , robustCovBeta
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import           Data.List             (sort)
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
--- | M-estimator の選択。 LTS (Least Trimmed Squares) は非凸組合せ最適化なので
--- 別 Phase 候補 (phase-NN-regression-advanced.md §RR3 参照)。
-data RobustEstimator
-  = Huber !Double  -- ^ @k@ (= 1.345 で 95% 効率、 = 'defaultHuberK')
-  | Tukey !Double  -- ^ @c@ (= 4.685 で 95% 効率、 = 'defaultTukeyC')
-  deriving (Show, Eq)
-
-data RobustFit = RobustFit
-  { rfCoef       :: !(LA.Vector Double)   -- ^ 係数 β̂
-  , rfScale      :: !Double                -- ^ ロバストスケール σ̂ (MAD-based)
-  , rfWeights    :: !(LA.Vector Double)   -- ^ 最終 IRLS 重み (≤ 1)
-  , rfFitted     :: !(LA.Vector Double)   -- ^ ŷ = Xβ̂
-  , rfResiduals  :: !(LA.Vector Double)   -- ^ y - ŷ
-  , rfIterations :: !Int                   -- ^ IRLS 反復回数
-  , rfConverged  :: !Bool                  -- ^ tol 内収束したか
-  , rfEstimator  :: !RobustEstimator       -- ^ 使用した estimator
-  } deriving (Show)
-
--- | Huber の標準値 (95% Gaussian 効率): @k = 1.345@
-defaultHuberK :: Double
-defaultHuberK = 1.345
-
--- | Tukey biweight の標準値 (95% Gaussian 効率): @c = 4.685@
-defaultTukeyC :: Double
-defaultTukeyC = 4.685
-
--- ---------------------------------------------------------------------------
--- 重み関数 (= ψ(u)/u where ψ is the influence function)
--- ---------------------------------------------------------------------------
-
--- | Huber 重み: @w(u) = 1@ if @|u| ≤ k@、 @k/|u|@ otherwise。
--- ここで @u = r / σ@ (標準化残差)。
-huberWeight :: Double -> Double -> Double
-huberWeight k u
-  | absU <= k = 1
-  | absU == 0 = 1
-  | otherwise = k / absU
-  where absU = abs u
-
--- | Tukey biweight 重み: @w(u) = (1 - (u/c)²)²@ if @|u| ≤ c@、 @0@ otherwise。
-tukeyWeight :: Double -> Double -> Double
-tukeyWeight c u
-  | absU >= c = 0
-  | otherwise = let t = u / c
-                    s = 1 - t * t
-                in s * s
-  where absU = abs u
-
--- ---------------------------------------------------------------------------
--- 影響関数 ψ とその導関数 ψ' (M 推定量の漸近共分散に使う)
--- ψ(u) = w(u)·u (重み × 標準化残差)。
--- ---------------------------------------------------------------------------
-
--- | 影響関数 @ψ(u) = w(u)·u@ (= 標準化残差に重みを掛けたスコア)。
---   Huber: @u@ (|u|≤k) / @k·sign u@ (それ以外)。 Tukey: @u(1-(u/c)²)²@ (|u|≤c) / 0。
-psiFn :: RobustEstimator -> Double -> Double
-psiFn (Huber k) u = huberWeight k u * u
-psiFn (Tukey c) u = tukeyWeight c u * u
-
--- | ψ の導関数 @ψ'(u)@ (M 推定量サンドイッチ分散の分母項)。
---   Huber: @1@ (|u|≤k) / @0@。 Tukey: @(1-(u/c)²)(1-5(u/c)²)@ (|u|≤c) / 0。
-psiDerivFn :: RobustEstimator -> Double -> Double
-psiDerivFn (Huber k) u = if abs u <= k then 1 else 0
-psiDerivFn (Tukey c) u
-  | abs u >= c = 0
-  | otherwise  = let t2 = (u / c) * (u / c)
-                 in (1 - t2) * (1 - 5 * t2)
-
--- ---------------------------------------------------------------------------
--- M 推定量の漸近共分散 (サンドイッチ・statsmodels RLM cov="H1")
--- ---------------------------------------------------------------------------
-
--- | M 推定量 β̂ の漸近共分散行列。 statsmodels @RLM@ 既定 (cov="H1") に一致:
---
--- @
--- u_i   = r_i / σ̂                       (標準化残差)
--- m     = mean ψ'(u_i)
--- K     = 1 + (p\/n)·Var(ψ')\/m²         (自由度補正)
--- cov   = K²·(σ̂²·Σψ(u_i)²\/(n−p))\/m² · (XᵀX)⁻¹
--- @
---
--- SE は @sqrt (diag cov)@、 β̂±z·SE が Wald 信頼区間 (RLM は正規分布で z)。
-robustCovBeta
-  :: RobustEstimator       -- ^ 使用した estimator (ψ/ψ' を決める)。
-  -> Double                -- ^ ロバストスケール σ̂ ('rfScale')。
-  -> LA.Vector Double      -- ^ 残差 r = y − ŷ ('rfResiduals')。
-  -> LA.Matrix Double      -- ^ 設計行列 X (intercept 列付き)。
-  -> LA.Matrix Double      -- ^ β̂ の共分散 (p × p)。
-robustCovBeta est scale resid x =
-  let n      = LA.rows x
-      p      = LA.cols x
-      u      = LA.cmap (/ scale) resid
-      pderiv = LA.cmap (psiDerivFn est) u
-      m      = meanV pderiv
-      varpp  = meanV (LA.cmap (\v -> (v - m) * (v - m)) pderiv)   -- 母分散 (ddof=0)
-      kcorr  = 1 + (fromIntegral p / fromIntegral n) * varpp / (m * m)
-      sspsi  = LA.sumElements (LA.cmap (\v -> let pv = psiFn est v in pv * pv) u)
-      xtxInv = LA.inv (LA.tr x LA.<> x)
-      factor = kcorr * kcorr
-               * (sspsi * scale * scale / fromIntegral (n - p)) / (m * m)
-  in LA.scale factor xtxInv
-  where
-    meanV v = LA.sumElements v / fromIntegral (LA.size v)
-
--- ---------------------------------------------------------------------------
--- IRLS
--- ---------------------------------------------------------------------------
-
--- | M-estimator IRLS で線形回帰を fit。
---
--- @X@ は @n × p@ (intercept 列は呼び出し側で付加)、 @y@ は長さ @n@。
--- @maxIter@ デフォルト 50、 @tol@ デフォルト 1e-6。
-fitRobustLM
-  :: RobustEstimator
-  -> LA.Matrix Double      -- ^ X
-  -> LA.Vector Double      -- ^ y
-  -> Int                   -- ^ max IRLS iterations
-  -> Double                -- ^ tolerance on @|Δβ|₂@
-  -> RobustFit
-fitRobustLM est x y maxIter tol =
-  let -- 初期 β: OLS
-      beta0 = LA.flatten (x LA.<\> LA.asColumn y)
-      step beta =
-        let yHat   = x LA.#> beta
-            resid  = y - yHat
-            sigma  = madScale resid
-            sigma' = if sigma < 1e-12 then 1e-12 else sigma
-            uVec   = LA.cmap (/ sigma') resid
-            wVec   = case est of
-                       Huber k -> LA.cmap (huberWeight k) uVec
-                       Tukey c -> LA.cmap (tukeyWeight c) uVec
-            -- 加重 LS: β ← (X^T W X)^{-1} X^T W y
-            wDiag  = wVec
-            xtWx   = LA.tr x LA.<> (x * LA.asColumn wDiag)
-            xtWy   = LA.tr x LA.#> (wDiag * y)
-            betaN  = LA.flatten (xtWx LA.<\> LA.asColumn xtWy)
-        in (betaN, sigma', wVec)
-      loop !k !beta
-        | k >= maxIter = (beta, k, False)
-        | otherwise    =
-            let (betaN, _, _) = step beta
-                diff = LA.norm_2 (betaN - beta)
-            in if diff < tol
-                 then (betaN, k + 1, True)
-                 else loop (k + 1) betaN
-      (betaFinal, iters, converged) = loop 0 beta0
-      yHatF  = x LA.#> betaFinal
-      residF = y - yHatF
-      sigmaF = max 1e-12 (madScale residF)
-      uF     = LA.cmap (/ sigmaF) residF
-      wF     = case est of
-                 Huber k -> LA.cmap (huberWeight k) uF
-                 Tukey c -> LA.cmap (tukeyWeight c) uF
-  in RobustFit
-       { rfCoef       = betaFinal
-       , rfScale      = sigmaF
-       , rfWeights    = wF
-       , rfFitted     = yHatF
-       , rfResiduals  = residF
-       , rfIterations = iters
-       , rfConverged  = converged
-       , rfEstimator  = est
-       }
-
--- ---------------------------------------------------------------------------
--- ロバストスケール (Median Absolute Deviation)
--- ---------------------------------------------------------------------------
-
--- | MAD ベースのロバストスケール推定:
--- @σ̂ = median(|r_i - median(r)|) / 0.6745@ (Gaussian 整合性)。
--- | ロバストスケール σ̂ = median(|r|) / Φ⁻¹(0.75)。 残差 r は intercept で中心化済
--- ゆえ **中心 0** で MAD を取る (= statsmodels RLM の @mad(resid, center=0)@ と一致。
--- median 中心化は二重中心化になり scale が過小になる)。 定数は Φ⁻¹(0.75)=0.674489…。
-madScale :: LA.Vector Double -> Double
-madScale v =
-  let dev = map abs (LA.toList v)       -- 中心 0 (statsmodels RLM 準拠)
-      mad = medianList dev
-  in mad / 0.6744897501960817
-
-medianList :: [Double] -> Double
-medianList [] = 0
-medianList xs =
-  let s = sort xs
-      n = length s
-  in if odd n
-       then s !! (n `div` 2)
-       else 0.5 * (s !! (n `div` 2 - 1) + s !! (n `div` 2))
diff --git a/src/Hanalyze/Model/SVM.hs b/src/Hanalyze/Model/SVM.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/SVM.hs
+++ /dev/null
@@ -1,302 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-
--- |
--- Module      : Hanalyze.Model.SVM
--- Description : SMO ソルバによる双対形カーネル SVM (C-SVC)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- カーネル SVM (双対形・SMO ソルバ) — Phase 75.11 / 共有 Kernel 化 Phase 75.15。
---
--- 双対 C-SVC (hinge 損失) を SMO (Platt 1998) で解き、 共有カーネル語彙
--- ('Hanalyze.Model.Kernel': Linear/Poly/RBF/Matern52/Periodic) と
--- **スパースな真のサポートベクタ** (α>0 の点) を提供する。 既定カーネルは Linear で、
--- 線形 SVM が必要なら kernel=Linear・非線形は RBF/Poly を選ぶ (R `e1071::svm` の kernel= 流)。
---
--- カーネルハイパラは 'KernelParams' (ℓ/σ_f²/period) を持つ。 GP の観測ノイズ σ_n² は
--- SVM には不要なので 'GPParams' でなく 'KernelParams' のみに依存する (Phase 75.18)。
---
--- 双対問題: max_α  Σα_i − ½ ΣΣ α_i α_j y_i y_j K(x_i,x_j)
---           s.t.   0 ≤ α_i ≤ C,  Σ α_i y_i = 0
---
--- SMO は 2 変数 (α_i, α_j) ずつ解析更新する。 第 1 変数 = KKT 違反点、 第 2 変数 =
--- |E_i − E_j| 最大 (Platt の 2nd heuristic)。 **乱数不使用ゆえ純粋・決定的** (簡易 SMO の
--- ランダム j 選択は使わない)。 予測は Σ_{SV} α_i y_i K(x_i, x) + b (SV のみで決まる)。
---
--- カーネル評価は 'kEvalMV'(距離カーネルは ‖a−b‖²、 内積カーネル Linear/Poly は a·b、
--- Poly の γ は 'kpLengthScale' から γ=1/(2ℓ²)・Linear の倍率は σ_f²)で共有する。
-module Hanalyze.Model.SVM
-  ( SVMConfig (..)
-  , defaultSVM
-  , SVM (..)
-  , SVMMulti (..)
-  , fitSVM
-  , fitSVMMulti
-  , predictSVMScore
-  , predictSVM
-  , predictSVMMulti
-  , numSupportVectors
-    -- * 自動最適化 (k-fold CV グリッド探索・config に畳む)
-  , SVMHyper (..)
-  , SVMTuneGrid (..)
-  , defaultSVMTuneGrid
-  , tuneSVM
-  ) where
-
-import qualified Data.Vector           as V
-import qualified Data.Vector.Unboxed   as VU
-import qualified Numeric.LinearAlgebra as LA
-import           Data.Text             (Text)
-import           Data.List             (nub, sort, maximumBy)
-import           Data.Ord              (comparing)
-import           Control.Monad.ST      (runST)
-import qualified System.Random.MWC     as MWC
-import           Hanalyze.Stat.CV (Fold, kFold)
-import           Hanalyze.Model.Kernel (Kernel (..), KernelParams (..), defaultKernelParams, kEvalMV)
-
--- ===========================================================================
--- カーネル (共有 'Kernel' + 'KernelParams' を使う)
--- ===========================================================================
-
--- | Gram 行列 K (n×n)。 K_ij = kEvalMV ker params (row i) (row j)。
-kGram :: Kernel -> KernelParams -> LA.Matrix Double -> LA.Matrix Double
-kGram ker p x =
-  let rv = V.fromList (LA.toRows x)   -- boxed Vector of 行ベクトル (O(1) 添字)
-      n  = V.length rv
-  in LA.build (n, n) (\i j -> kEvalMV ker p (rv V.! round i) (rv V.! round j))
-  -- NB: LA.build の i,j は Double。 round で Int 添字に戻す (整数値ゆえ安全)。
-
--- ===========================================================================
--- 設定 / モデル
--- ===========================================================================
-
-data SVMConfig = SVMConfig
-  { svmC         :: !Double        -- ^ 正則化 C (0 ≤ α ≤ C)。
-  , svmKernel    :: !Kernel        -- ^ 共有カーネル (既定 'Linear')。
-  , svmParams    :: !KernelParams  -- ^ カーネルハイパラ (ℓ→γ=1/2ℓ²、 σ_f²=Linear 倍率)。
-  , svmTol       :: !Double    -- ^ KKT 許容 (E の許容)。
-  , svmMaxPasses :: !Int       -- ^ 変化が無いパスの連続上限 (収束判定)。
-  , svmMaxIter   :: !Int       -- ^ 総パス数の上限 (安全弁)。
-  , svmHyper     :: !SVMHyper  -- ^ ハイパラの決め方 (固定 or CV グリッド探索)。 GP の
-                               --   'HyperStrategy' と同型: 調整は config に畳み動詞は @svmCls@ 一本。
-  } deriving (Show)
-
-defaultSVM :: SVMConfig
-defaultSVM = SVMConfig
-  { svmC = 1.0, svmKernel = Linear, svmParams = defaultKernelParams
-  , svmTol = 1e-3, svmMaxPasses = 5, svmMaxIter = 1000
-  , svmHyper = SVMFixed }
-
--- | 学習済カーネル SVM。 **α>0 のサポートベクタのみ**保持 (スパース)。
-data SVM = SVM
-  { svmSVx    :: !(LA.Matrix Double)  -- ^ サポートベクタ (n_sv × d)。
-  , svmSVy    :: !(VU.Vector Double)  -- ^ その符号ラベル ±1。
-  , svmSVa    :: !(VU.Vector Double)  -- ^ 双対係数 α (>0)。
-  , svmB      :: !Double              -- ^ バイアス。
-  , svmKern   :: !Kernel              -- ^ 共有カーネル。
-  , svmKParams :: !KernelParams       -- ^ カーネルハイパラ (予測時に再利用)。
-  } deriving (Show)
-
--- | サポートベクタ数 (= α>0 の点数)。
-numSupportVectors :: SVM -> Int
-numSupportVectors = LA.rows . svmSVx
-
--- ===========================================================================
--- SMO (双対・2 クラス {0,1} → ±1)
--- ===========================================================================
-
--- | 2 クラス C-SVC を SMO で学習 (y ∈ {0,1})。 決定的 (乱数不使用)。
-fitSVM :: SVMConfig -> LA.Matrix Double -> VU.Vector Int -> SVM
-fitSVM cfg x yInt =
-  let !n    = LA.rows x
-      ys    = VU.generate n (\i -> if yInt VU.! i == 0 then -1 else 1) :: VU.Vector Double
-      gram  = kGram (svmKernel cfg) (svmParams cfg) x
-      cC    = svmC cfg
-      tol   = svmTol cfg
-      kij i j = gram `LA.atIndex` (i, j)
-      -- 決定関数 f(i) = Σ_j α_j y_j K_ij + b
-      decision al b i = b + sum [ al VU.! j * ys VU.! j * kij i j | j <- [0 .. n - 1] ]
-      -- 1 パス: 全 i を走査し KKT 違反点を見つけ第 2 変数を選んで更新。
-      onePass (!al0, !b0) =
-        let step (al, b, changed) i =
-              let ei = decision al b i - ys VU.! i
-                  ai = al VU.! i; yi = ys VU.! i
-                  viol = (yi * ei < negate tol && ai < cC) || (yi * ei > tol && ai > 0)
-              in if not viol then (al, b, changed)
-                 else
-                   -- 第 2 変数 j = |E_i − E_j| 最大 (j /= i)。
-                   let es = [ (j, decision al b j - ys VU.! j) | j <- [0 .. n - 1], j /= i ]
-                       (j, ej) = maximumBy (comparing (\(_, e) -> abs (ei - e))) es
-                       aj = al VU.! j; yj = ys VU.! j
-                       (lo, hi) = if yi /= yj
-                                    then (max 0 (aj - ai), min cC (cC + aj - ai))
-                                    else (max 0 (ai + aj - cC), min cC (ai + aj))
-                       eta = 2 * kij i j - kij i i - kij j j
-                   in if lo >= hi || eta >= 0 then (al, b, changed)
-                      else
-                        let ajNew0 = aj - yj * (ei - ej) / eta
-                            ajNew  = min hi (max lo ajNew0)
-                        in if abs (ajNew - aj) < 1e-5 then (al, b, changed)
-                           else
-                             let aiNew = ai + yi * yj * (aj - ajNew)
-                                 al'   = al VU.// [(i, aiNew), (j, ajNew)]
-                                 b1 = b - ei - yi * (aiNew - ai) * kij i i
-                                        - yj * (ajNew - aj) * kij i j
-                                 b2 = b - ej - yi * (aiNew - ai) * kij i j
-                                        - yj * (ajNew - aj) * kij j j
-                                 bNew | aiNew > 0 && aiNew < cC = b1
-                                      | ajNew > 0 && ajNew < cC = b2
-                                      | otherwise               = (b1 + b2) / 2
-                             in (al', bNew, changed + 1)
-        in foldl step (al0, b0, 0 :: Int) [0 .. n - 1]
-      -- パスを回す: 変化無しが maxPasses 連続 or maxIter 到達で停止。
-      loop !al !b !passes !iter
-        | passes >= svmMaxPasses cfg || iter >= svmMaxIter cfg = (al, b)
-        | otherwise =
-            let (al', b', changed) = onePass (al, b)
-            in if changed == 0 then loop al' b' (passes + 1) (iter + 1)
-                               else loop al' b' 0 (iter + 1)
-      (alphaF, bF) = loop (VU.replicate n 0) 0 0 0
-      -- α>0 のみ保持 (スパース SV)。
-      svIdx = [ i | i <- [0 .. n - 1], alphaF VU.! i > 1e-8 ]
-      svX   = LA.fromRows [ LA.toRows x !! i | i <- svIdx ]
-      svY   = VU.fromList [ ys VU.! i | i <- svIdx ]
-      svA   = VU.fromList [ alphaF VU.! i | i <- svIdx ]
-  in SVM { svmSVx = svX, svmSVy = svY, svmSVa = svA
-               , svmB = bF, svmKern = svmKernel cfg
-               , svmKParams = svmParams cfg }
-
--- | 決定値 f(x) = Σ_{SV} α_i y_i K(x_i, x) + b (各行)。
-predictSVMScore :: SVM -> LA.Matrix Double -> VU.Vector Double
-predictSVMScore m x =
-  let svRows = LA.toRows (svmSVx m)
-      nsv    = length svRows
-      ker    = svmKern m
-      kp     = svmKParams m
-      score xr = svmB m
-        + sum [ svmSVa m VU.! s * svmSVy m VU.! s * kEvalMV ker kp (svRows !! s) xr
-              | s <- [0 .. nsv - 1] ]
-  in VU.fromList (map score (LA.toRows x))
-
--- | 予測ラベル {0,1} (score ≥ 0 → 1)。
-predictSVM :: SVM -> LA.Matrix Double -> VU.Vector Int
-predictSVM m x = VU.map (\s -> if s >= 0 then 1 else 0) (predictSVMScore m x)
-
--- ===========================================================================
--- 多クラス (one-vs-rest)
--- ===========================================================================
-
-data SVMMulti = SVMMulti
-  { svmmClasses    :: ![Int]
-  , svmmBinaries   :: ![SVM]   -- ^ クラス順に 1-vs-rest。
-  , svmmClassNames :: ![Text]  -- ^ クラス名 (df|-> が levels 注入・空=数値表示)。
-  } deriving (Show)
-
--- | 多クラス C-SVC (one-vs-rest・各 binary は 'fitSVM'・決定的)。
-fitSVMMulti :: SVMConfig -> LA.Matrix Double -> VU.Vector Int -> SVMMulti
-fitSVMMulti cfg x y =
-  let classes = sort (nub (VU.toList y))
-      bins = [ fitSVM cfg x (VU.map (\yi -> if yi == c then 1 else 0) y)
-             | c <- classes ]
-  in SVMMulti { svmmClasses = classes, svmmBinaries = bins, svmmClassNames = [] }
-
--- | 各クラスの score 最大で分類。
-predictSVMMulti :: SVMMulti -> LA.Matrix Double -> VU.Vector Int
-predictSVMMulti m x =
-  let classes = svmmClasses m
-      scores  = [ VU.toList (predictSVMScore b x) | b <- svmmBinaries m ]
-      n       = LA.rows x
-      pick i  = let col = [ (classes !! k, scores !! k !! i) | k <- [0 .. length classes - 1] ]
-                in fst (maximumBy (comparing snd) col)
-  in VU.fromList [ pick i | i <- [0 .. n - 1] ]
-
--- ===========================================================================
--- 自動最適化 (k-fold CV グリッド探索)
---
--- SVM は確率モデルでないため GP の周辺尤度最適化は使えない。 代わりに
--- **k-fold 交差検証の accuracy を最大化**する格子探索 (sklearn `GridSearchCV` /
--- R `e1071::tune.svm` 相当)。 SMO は乱数不使用・fold 分割も固定 seed の
--- 'Hanalyze.Stat.CV.kFold' を 'runST' で回すため **完全に決定的**。
--- ===========================================================================
-
--- | ハイパラの決め方 (GP の 'HyperStrategy' と同型)。 固定値をそのまま使うか、
---   CV グリッドを探索して最良を選ぶか。 'SVMConfig' の @svmHyper@ に持たせ、 動詞 @svmCls@ が
---   これを見て分岐する (別動詞 @svmClsTuned@ は作らない)。
-data SVMHyper
-  = SVMFixed              -- ^ 'SVMConfig' の C/kernel/params をそのまま使う。
-  | SVMTuneCV SVMTuneGrid -- ^ グリッドを k-fold CV で探索し最良ハイパラで再学習。
-  deriving (Show)
-
--- | SVM ハイパラ探索グリッド。 候補は C × kernel × ℓ の直積。
--- 'Linear' カーネルは ℓ を使わないので ℓ 軸は無視する (重複評価を避ける)。
-data SVMTuneGrid = SVMTuneGrid
-  { svmtCs      :: ![Double]   -- ^ 正則化 C 候補 (0 < C)。
-  , svmtKernels :: ![Kernel]   -- ^ カーネル候補。
-  , svmtLengths :: ![Double]   -- ^ 長さスケール ℓ 候補 (距離カーネル/Poly の γ=1/2ℓ²)。
-  , svmtFolds   :: !Int        -- ^ CV fold 数 k (2 以上)。
-  } deriving (Show)
-
--- | 既定グリッド: C ∈ {0.1,1,10,100} × RBF × ℓ ∈ {0.25,0.5,1,2,4}・5-fold。
-defaultSVMTuneGrid :: SVMTuneGrid
-defaultSVMTuneGrid = SVMTuneGrid
-  { svmtCs      = [0.1, 1, 10, 100]
-  , svmtKernels = [RBF]
-  , svmtLengths = [0.25, 0.5, 1, 2, 4]
-  , svmtFolds   = 5
-  }
-
--- | グリッドの 1 点に対応する 'SVMConfig' を作る (base から C/kernel/ℓ を差し替え)。
-tuneCandidate :: SVMConfig -> Double -> Kernel -> Double -> SVMConfig
-tuneCandidate base c ker l =
-  base { svmC = c, svmKernel = ker
-       , svmParams = (svmParams base) { kpLengthScale = l } }
-
--- | グリッドの全候補 'SVMConfig' (Linear は ℓ 軸を畳む)。
-tuneCandidates :: SVMConfig -> SVMTuneGrid -> [SVMConfig]
-tuneCandidates base grid =
-  [ tuneCandidate base c ker l
-  | c   <- svmtCs grid
-  , ker <- svmtKernels grid
-  , l   <- lengthsFor ker ]
-  where
-    lengthsFor Linear = take 1 (svmtLengths grid ++ [1.0])  -- ℓ 無関係 → 1 点
-    lengthsFor _      = svmtLengths grid
-
--- | 行添字リストで行列の行とラベルを抜き出す。
-sliceRows :: V.Vector (LA.Vector Double) -> VU.Vector Int -> [Int]
-          -> (LA.Matrix Double, VU.Vector Int)
-sliceRows rows y idx =
-  ( LA.fromRows [ rows V.! i | i <- idx ]
-  , VU.fromList [ y VU.! i | i <- idx ] )
-
--- | 1 候補の平均 CV accuracy。 各 fold で train に学習し test の正解率を測る。
-cvAccuracy :: SVMConfig -> [Fold]
-           -> V.Vector (LA.Vector Double) -> VU.Vector Int -> Double
-cvAccuracy cfg folds rows y =
-  let accs = [ foldAcc tr te | (tr, te) <- folds, not (null te) ]
-      foldAcc trIdx teIdx =
-        let (xTr, yTr) = sliceRows rows y trIdx
-            (xTe, yTe) = sliceRows rows y teIdx
-            model = fitSVMMulti cfg xTr yTr
-            pred  = predictSVMMulti model xTe
-            nTe   = VU.length yTe
-            ok    = length [ () | i <- [0 .. nTe - 1], pred VU.! i == yTe VU.! i ]
-        in fromIntegral ok / fromIntegral nTe
-  in if null accs then 0 else sum accs / fromIntegral (length accs)
-
--- | k-fold CV で SVM のハイパラ (C × kernel × ℓ) を調律する。 CV accuracy を
--- 最大化する 'SVMConfig' と、 その平均 CV accuracy を返す。 **決定的** (固定 seed の
--- fold 分割・SMO は乱数不使用)。 sklearn `GridSearchCV` / R `tune.svm` 相当。
-tuneSVM :: SVMConfig -> SVMTuneGrid -> LA.Matrix Double -> VU.Vector Int
-        -> (SVMConfig, Double)
-tuneSVM base grid x y =
-  let n     = LA.rows x
-      rows  = V.fromList (LA.toRows x)
-      k     = max 2 (min (svmtFolds grid) n)
-      -- 固定 seed の k-fold (決定的・再現可能)。
-      folds = runST $ do
-                gen <- MWC.initialize (V.singleton 42)
-                kFold k n gen
-      scored = [ (cfg, cvAccuracy cfg folds rows y)
-               | cfg <- tuneCandidates base grid ]
-  in maximumBy (comparing snd) scored
diff --git a/src/Hanalyze/Model/Spline.hs b/src/Hanalyze/Model/Spline.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Spline.hs
+++ /dev/null
@@ -1,230 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
--- |
--- Module      : Hanalyze.Model.Spline
--- Description : B-spline / 自然三次スプライン回帰 (Cox-de Boor 基底 + LM フィット)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- B-spline and natural cubic-spline regression.
---
--- Builds a design matrix @B@ from spline basis functions and solves
--- ordinary least squares for the coefficients @β@:
---
--- @
--- y_i = Σ_j β_j B_j(x_i) + ε_i
--- @
---
---   * 'bsplineBasis'       — degree-@k@ B-spline basis via the Cox-de Boor
---     recursion.
---   * 'naturalSplineBasis' — natural cubic spline (linear outside the
---     boundary).
---   * 'fitSpline'          — fit using the basis matrix + LM.
---   * 'predictSpline'      — predict at new @x@ values.
-module Hanalyze.Model.Spline
-  ( SplineKind (..)
-  , SplineFit (..)
-  , SplineFitMulti (..)
-  , bsplineBasis
-  , naturalSplineBasis
-  , fitSpline
-  , fitSplineMulti
-  , predictSpline
-  , predictSplineMulti
-  , equalSpacedKnots
-  , quantileKnots
-  ) where
-
-import qualified Data.Vector as V
-import qualified Numeric.LinearAlgebra as LA
-import Data.List (sort)
-import Hanalyze.Model.Core (FitResult (..))
-import Hanalyze.Model.LM (fitLM)
-
--- | Spline kind.
-data SplineKind
-  = BSpline Int    -- ^ B-spline of degree @k@ (3 = cubic is typical).
-  | NaturalCubic   -- ^ Natural cubic spline.
-  deriving (Show, Eq)
-
--- | Spline fit result, with everything needed to reproduce predictions.
-data SplineFit = SplineFit
-  { sfKind   :: SplineKind
-  , sfKnots  :: [Double]         -- ^ Interior knots (boundaries included).
-  , sfBeta   :: LA.Vector Double -- ^ Basis-coefficient vector.
-  , sfResult :: FitResult        -- ^ Underlying linear-model fit.
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- B-spline basis (Cox-de Boor recursion)
--- ---------------------------------------------------------------------------
-
--- | Evaluate every B-spline basis function at a single point.
---
--- Inputs: degree @k@, extended knot sequence @t@ (length
--- @n_basis + k + 1@), and the evaluation point @x@. Returns
--- @[B_0(x), B_1(x), ..., B_{n_basis-1}(x)]@.
-bsplineEval :: Int -> [Double] -> Double -> [Double]
-bsplineEval k tKnots x =
-  let nBasis = length tKnots - k - 1
-      -- Order 0 (= k=0): 1 if x in [t_i, t_{i+1}), else 0
-      -- 端点処理: 右端 x == hi は **hi で終わる最後の正幅区間** [ti, hi) に含める。
-      -- clamped ノットは hi を k+1 回重複させるため、 単純に「最後の区間 index を右閉」
-      -- にすると退化区間 [hi, hi] を選んでしまい、 高次 Cox-de Boor 再帰で d2=0 となって
-      -- 基底が全ゼロ化する (= partition of unity 崩壊。 計測で確認: x=hi で sum=0)。
-      hiKnot = last tKnots
-      order0 i =
-        let ti  = tKnots !! i
-            ti1 = tKnots !! (i + 1)
-            atRightEnd = x >= ti1 && ti1 == hiKnot && ti < ti1
-        in if (x >= ti && x < ti1) || atRightEnd
-             then 1.0 else 0.0
-      -- 高次: Cox-de Boor
-      go p prev =
-        let n_p = length prev - 1   -- prev の長さは n + p
-        in [ let ti   = tKnots !! i
-                 tipk = tKnots !! (i + p)
-                 ti1  = tKnots !! (i + 1)
-                 ti1pk = tKnots !! (i + p + 1)
-                 d1   = tipk - ti
-                 d2   = ti1pk - ti1
-                 a    = if d1 == 0 then 0
-                          else (x - ti) / d1 * (prev !! i)
-                 b    = if d2 == 0 then 0
-                          else (ti1pk - x) / d2 * (prev !! (i + 1))
-             in a + b
-           | i <- [0 .. n_p - 1] ]
-      step p prev | p > k     = prev
-                  | otherwise = step (p + 1) (go p prev)
-      ord0 = [order0 i | i <- [0 .. length tKnots - 2]]
-  in take nBasis (step 1 ord0)
-
--- | B-spline basis matrix.
---
--- Inputs:
---
---   * @k@        — degree (3 typical).
---   * @intKnots@ — interior knots (boundaries included; assumed sorted).
---   * @xs@       — evaluation points.
---
--- The output matrix has shape @n × n_basis@ where
--- @n_basis = length intKnots + k - 1@. The extended knot sequence is
--- built by replicating each boundary @k+1@ times (clamped B-spline).
-bsplineBasis :: Int -> [Double] -> V.Vector Double -> LA.Matrix Double
-bsplineBasis k intKnots xs =
-  let knots = sort intKnots
-      lo    = head knots
-      hi    = last knots
-      tExt  = replicate (k + 1) lo
-              ++ tail (init knots)        -- 内部ノット
-              ++ replicate (k + 1) hi
-      -- 上で tExt の長さは (k+1) + (length knots - 2) + (k+1) = length knots + 2k
-      -- n_basis = length knots + 2k - k - 1 = length knots + k - 1
-      rows  = [ bsplineEval k tExt x | x <- V.toList xs ]
-  in LA.fromLists rows
-
--- ---------------------------------------------------------------------------
--- Natural cubic spline basis
--- ---------------------------------------------------------------------------
-
--- | Natural cubic-spline basis (zero second derivative at the
--- boundaries; linear outside the boundary).
---
--- ノット K1 < K2 < ... < KN に対して、N 個の基底関数:
---   N_1(x) = 1
---   N_2(x) = x
---   N_{k+2}(x) = d_k(x) - d_{N-1}(x)  for k = 1..N-2
--- where
---   d_k(x) = [(x - K_k)_+^3 - (x - K_N)_+^3] / (K_N - K_k)
---
--- 出力: 行列 (n × N)。
-naturalSplineBasis :: [Double] -> V.Vector Double -> LA.Matrix Double
-naturalSplineBasis knots xs =
-  let ks = sort knots
-      n  = length ks
-      kN = last ks
-      kNm1 = ks !! (n - 2)
-      pos3 v = if v <= 0 then 0 else v ^ (3 :: Int)
-      d k x =
-        let kk = ks !! k
-        in (pos3 (x - kk) - pos3 (x - kN)) / (kN - kk)
-      basis x =
-        [1.0, x] ++
-        [ d k x - d (n - 2) x | k <- [0 .. n - 3] ]
-  in LA.fromLists [basis xv | xv <- V.toList xs]
-
--- ---------------------------------------------------------------------------
--- Fit / predict
--- ---------------------------------------------------------------------------
-
--- | Single-output spline regression. Delegates to 'fitSplineMulti' by
--- promoting @y@ to a one-column matrix.
-fitSpline :: SplineKind -> [Double] -> V.Vector Double -> V.Vector Double -> SplineFit
-fitSpline kind knots xs ys =
-  let yMat = LA.asColumn (LA.fromList (V.toList ys))
-      mf   = fitSplineMulti kind knots xs yMat
-      beta = LA.flatten (smfBeta mf LA.¿ [0])
-  in SplineFit kind knots beta (smfResult mf)
-
--- | Predict at new @x@ values from a 'SplineFit'.
-predictSpline :: SplineFit -> V.Vector Double -> V.Vector Double
-predictSpline fit xsNew =
-  let dm = case sfKind fit of
-        BSpline k     -> bsplineBasis k (sfKnots fit) xsNew
-        NaturalCubic  -> naturalSplineBasis (sfKnots fit) xsNew
-      yPred = dm LA.#> sfBeta fit
-  in V.fromList (LA.toList yPred)
-
--- | Multi-output spline regression: fit @q@ outputs jointly on the same
--- @x@ grid. Internally a basis matrix plus a multi-output LM.
-data SplineFitMulti = SplineFitMulti
-  { smfKind   :: SplineKind
-  , smfKnots  :: [Double]
-  , smfBeta   :: LA.Matrix Double  -- ^ Basis coefficients (@basis_dim × q@).
-  , smfResult :: FitResult
-  } deriving (Show)
-
--- | Fit a multi-output spline. @Y@ has shape @n × q@; columns share the
--- basis but are otherwise fit independently.
-fitSplineMulti :: SplineKind
-               -> [Double]            -- ^ Knots.
-               -> V.Vector Double     -- ^ Inputs @xs@ (length @n@).
-               -> LA.Matrix Double    -- ^ Response @Y@ (@n × q@).
-               -> SplineFitMulti
-fitSplineMulti kind knots xs ys =
-  let dm = case kind of
-        BSpline k     -> bsplineBasis k knots xs
-        NaturalCubic  -> naturalSplineBasis knots xs
-      r  = fitLM dm ys
-  in SplineFitMulti kind knots (coefficients r) r
-
--- | Predict @Ŷ@ at new inputs from a 'SplineFitMulti'.
-predictSplineMulti :: SplineFitMulti -> V.Vector Double -> LA.Matrix Double
-predictSplineMulti fit xsNew =
-  let dm = case smfKind fit of
-        BSpline k     -> bsplineBasis k (smfKnots fit) xsNew
-        NaturalCubic  -> naturalSplineBasis (smfKnots fit) xsNew
-  in dm LA.<> smfBeta fit
-
--- ---------------------------------------------------------------------------
--- Knot helpers
--- ---------------------------------------------------------------------------
-
--- | Equal-spaced knots (both endpoints included, @n@ points total).
-equalSpacedKnots :: Int -> Double -> Double -> [Double]
-equalSpacedKnots n lo hi
-  | n < 2     = [lo, hi]
-  | otherwise = [lo + fromIntegral i * (hi - lo) / fromIntegral (n - 1)
-                | i <- [0 .. n - 1]]
-
--- | Quantile-based knots (boundaries at min/max, interior knots at
--- evenly-spaced sample quantiles).
-quantileKnots :: Int -> V.Vector Double -> [Double]
-quantileKnots n xs
-  | n < 2     = [V.minimum xs, V.maximum xs]
-  | otherwise =
-      let sorted = sort (V.toList xs)
-          m      = length sorted
-          qAt p  = sorted !! min (m - 1) (max 0 (floor (p * fromIntegral m) :: Int))
-          ps     = [fromIntegral i / fromIntegral (n - 1) | i <- [0 .. n - 1] :: [Int]]
-      in map qAt ps
diff --git a/src/Hanalyze/Model/StateSpace.hs b/src/Hanalyze/Model/StateSpace.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/StateSpace.hs
+++ /dev/null
@@ -1,133 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
-
--- |
--- Module      : Hanalyze.Model.StateSpace
--- Description : 線形ガウス状態空間モデルの Kalman Filter / RTS Smoother
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- 線形ガウス状態空間モデル (Linear Gaussian State Space Model) +
--- Kalman Filter / RTS Smoother。
---
--- モデル:
---
--- @
--- x_t = F x_{t-1} + w_t,   w_t ~ N(0, Q)
--- y_t = H x_t     + v_t,   v_t ~ N(0, R)
--- @
---
--- * 'kalmanFilter' は前向きフィルタリングで filtered mean / cov を計算し、
---   同時に innovation 系列の対数尤度 (= モデル尤度) を返す。
--- * 'kalmanSmoother' は RTS (Rauch-Tung-Striebel) で smoothed mean / cov を
---   後ろ向きに計算。 入力に既にフィルタ済の 'KalmanResult' を渡す。
---
--- すべて hmatrix Vector / Matrix で実装 (list 化禁止)。
-module Hanalyze.Model.StateSpace
-  ( StateSpaceModel (..)
-  , KalmanResult (..)
-  , kalmanFilter
-  , kalmanSmoother
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
-data StateSpaceModel = StateSpaceModel
-  { ssF  :: !(LA.Matrix Double)   -- ^ 状態遷移行列 F (n_x × n_x)
-  , ssH  :: !(LA.Matrix Double)   -- ^ 観測行列 H (n_y × n_x)
-  , ssQ  :: !(LA.Matrix Double)   -- ^ プロセスノイズ共分散 Q (n_x × n_x)
-  , ssR  :: !(LA.Matrix Double)   -- ^ 観測ノイズ共分散 R (n_y × n_y)
-  , ssX0 :: !(LA.Vector Double)   -- ^ 初期状態 (n_x)
-  , ssP0 :: !(LA.Matrix Double)   -- ^ 初期共分散 (n_x × n_x)
-  } deriving (Show)
-
-data KalmanResult = KalmanResult
-  { krFilteredMean :: ![LA.Vector Double]
-  , krFilteredCov  :: ![LA.Matrix Double]
-  , krSmoothedMean :: ![LA.Vector Double]
-    -- ^ 'kalmanFilter' のみ呼んだ場合は空。 'kalmanSmoother' を通すと埋まる。
-  , krSmoothedCov  :: ![LA.Matrix Double]
-  , krLogLik       :: !Double      -- ^ Σ log p(y_t | y_{1:t-1})
-  } deriving (Show)
-
--- ===========================================================================
--- Kalman Filter (forward pass)
--- ===========================================================================
-
--- | 観測系列 ys (各列が 1 時点の観測ベクトル) からフィルタリング。
---   ys の行 = 観測次元 n_y、 列 = 時点数 T。
-kalmanFilter :: StateSpaceModel -> LA.Matrix Double -> KalmanResult
-kalmanFilter ssm ys =
-  let nY = LA.rows ys
-      _  = nY :: Int
-      tT = LA.cols ys
-      f  = ssF ssm
-      h  = ssH ssm
-      q  = ssQ ssm
-      r  = ssR ssm
-      step (x, p, accM, accP, ll) t =
-        let yt   = LA.flatten (ys LA.¿ [t])
-            -- predict
-            xPred = f LA.#> x
-            pPred = f LA.<> p LA.<> LA.tr f + q
-            -- update
-            yPred = h LA.#> xPred
-            sInn  = h LA.<> pPred LA.<> LA.tr h + r
-            -- guard against singular S
-            sInv  = LA.inv sInn
-            gain  = pPred LA.<> LA.tr h LA.<> sInv
-            inn   = yt - yPred
-            xNew  = xPred + gain LA.#> inn
-            pNew  = pPred - gain LA.<> h LA.<> pPred
-            -- log-likelihood contribution
-            nY_   = fromIntegral (LA.size inn) :: Double
-            detS  = LA.det sInn
-            quad  = inn `LA.dot` (sInv LA.#> inn)
-            lt    = -0.5 * (nY_ * log (2 * pi) + log (max 1e-300 detS) + quad)
-        in (xNew, pNew, accM ++ [xNew], accP ++ [pNew], ll + lt)
-      (_, _, ms, ps, llTotal) =
-        foldl step (ssX0 ssm, ssP0 ssm, [], [], 0) [0 .. tT - 1]
-  in KalmanResult
-       { krFilteredMean = ms
-       , krFilteredCov  = ps
-       , krSmoothedMean = []
-       , krSmoothedCov  = []
-       , krLogLik       = llTotal
-       }
-
--- ===========================================================================
--- RTS Smoother (backward pass)
--- ===========================================================================
-
--- | RTS smoother。 'kalmanFilter' の出力を受け取り smoothed * を埋めて返す。
-kalmanSmoother :: StateSpaceModel -> KalmanResult -> KalmanResult
-kalmanSmoother ssm kr =
-  let f  = ssF ssm
-      q  = ssQ ssm
-      ms = krFilteredMean kr
-      ps = krFilteredCov  kr
-      tT = length ms
-      -- 末尾は filtered と smoothed が同じ
-      mTLast = last ms
-      pTLast = last ps
-      -- 後ろから前へ走査
-      step (smMs, smPs) i =
-        let mFilt = ms !! i
-            pFilt = ps !! i
-            mPred = f LA.#> mFilt
-            pPred = f LA.<> pFilt LA.<> LA.tr f + q
-            mNext = head smMs
-            pNext = head smPs
-            g     = pFilt LA.<> LA.tr f LA.<> LA.inv pPred
-            mNew  = mFilt + g LA.#> (mNext - mPred)
-            pNew  = pFilt + g LA.<> (pNext - pPred) LA.<> LA.tr g
-        in (mNew : smMs, pNew : smPs)
-      (smMsFinal, smPsFinal) =
-        foldl step ([mTLast], [pTLast]) (reverse [0 .. tT - 2])
-  in kr { krSmoothedMean = smMsFinal
-        , krSmoothedCov  = smPsFinal
-        }
diff --git a/src/Hanalyze/Model/Survival.hs b/src/Hanalyze/Model/Survival.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Survival.hs
+++ /dev/null
@@ -1,431 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
-
--- |
--- Module      : Hanalyze.Model.Survival
--- Description : 打ち切りを伴う生存時間解析 (Kaplan-Meier / Nelson-Aalen / log-rank / Cox PH)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Survival analysis.
---
--- Time-to-event analysis under right censoring. Implements:
---
---   * 'kaplanMeier' — non-parametric survival function estimator.
---   * 'nelsonAalen' — non-parametric cumulative hazard estimator.
---   * 'logRankTest' — compare survival between groups.
---   * 'coxPH' — Cox proportional hazards regression.
---
--- == Convention
---
--- A "survival" sample is @(time, event)@ where @time@ is duration and
--- @event ∈ {0, 1}@: @1@ = event observed (death, failure, etc.),
--- @0@ = censored (still alive at study end / dropout). All functions
--- accept the convention via @SurvSample@ records.
-module Hanalyze.Model.Survival
-  ( -- * Common types
-    SurvSample (..)
-  , Event (..)
-    -- * Non-parametric estimators
-  , KMResult (..)
-  , kaplanMeier
-  , NAResult (..)
-  , nelsonAalen
-    -- * Hypothesis tests
-  , LogRankResult (..)
-  , logRankTest
-    -- * Cox proportional hazards
-  , CoxFit (..)
-  , coxPH
-  , coxBaselineHazard
-  ) where
-
-import qualified Numeric.LinearAlgebra            as LA
-import qualified Statistics.Distribution          as SD
-import qualified Statistics.Distribution.ChiSquared as ChiSq
-import qualified Data.Vector                      as V
-import qualified Data.Vector.Unboxed              as VU
-import qualified Data.Vector.Storable             as VS
-import           Data.List                        (sort, sortBy, group)
-import           Data.Ord                         (comparing)
-
--- ---------------------------------------------------------------------------
--- Common types
--- ---------------------------------------------------------------------------
-
--- | Event indicator.
-data Event = Censored | Observed deriving (Show, Eq, Ord)
-
--- | A single observation: @(time, event)@.
-data SurvSample = SurvSample
-  { ssTime  :: !Double
-  , ssEvent :: !Event
-  } deriving (Show, Eq)
-
--- ---------------------------------------------------------------------------
--- Kaplan-Meier
--- ---------------------------------------------------------------------------
-
--- | Kaplan-Meier survival function estimator.
-data KMResult = KMResult
-  { kmrTimes      :: ![Double]   -- ^ Distinct event times.
-  , kmrSurvival   :: ![Double]   -- ^ Ŝ(t) at each event time.
-  , kmrAtRisk     :: ![Int]      -- ^ Number at risk just before t_i.
-  , kmrEvents     :: ![Int]      -- ^ Number of events at t_i.
-  , kmrCensored   :: ![Int]      -- ^ Number censored at t_i.
-  } deriving (Show)
-
--- | Compute the Kaplan-Meier estimator.
---
--- @Ŝ(t_i) = Π_{j ≤ i} (1 − d_j / n_j)@ where @d_j@ is events at @t_j@
--- and @n_j@ is the number at risk just before @t_j@.
---
--- B9c: rewritten with a single sorted-vector pass + linear run-length
--- grouping (no @[s | s <- ss, ssTime s == t]@ filter for each time,
--- which was @O(n × distinct_times)@). On the n=2000 bench this drops
--- KM from ~33 ms to a few ms.
-kaplanMeier :: [SurvSample] -> KMResult
-kaplanMeier samples =
-  let !sorted = sortBy (comparing ssTime) samples
-      !n0     = length sorted
-      groups  = runLengthGroups sorted
-      -- 累積生存は **先頭から** 積む: Ŝ(tᵢ) = ∏_{j ≤ i} (1 − dⱼ/nⱼ)。
-      -- (旧実装は rest を先に再帰して右から積んでおり、 最終時点の (1−dⱼ/nⱼ)=0 が
-      --  全時点を 0 に潰す逆順バグだった。 計測で確認・修正。)
-      go _    _     [] = ([], [], [], [], [])
-      go !nAt !sAcc ((t, dj, cj) : rest) =
-        let !sFactor = if nAt > 0
-                         then 1 - fromIntegral dj / fromIntegral nAt
-                         else 1
-            !sNew = sAcc * sFactor
-            (ts, ss, ns, ds, cs) = go (nAt - dj - cj) sNew rest
-        in (t : ts, sNew : ss, nAt : ns, dj : ds, cj : cs)
-      (ts, ss, ns, ds, cs) = go n0 1.0 groups
-  in KMResult ts ss ns ds cs
-
--- | Walk a list pre-sorted by 'ssTime' and return per-distinct-time
--- @(time, num_events, num_censored)@ tuples.
-runLengthGroups :: [SurvSample] -> [(Double, Int, Int)]
-runLengthGroups []     = []
-runLengthGroups (x:xs) = go (ssTime x) (countOf x) xs
-  where
-    countOf s = case ssEvent s of
-                  Observed -> (1 :: Int, 0 :: Int)
-                  Censored -> (0, 1)
-    go !t (!d, !c) [] = [(t, d, c)]
-    go !t (!d, !c) (s:rest)
-      | ssTime s == t =
-          let (di, ci) = countOf s
-          in go t (d + di, c + ci) rest
-      | otherwise =
-          let (di, ci) = countOf s
-          in (t, d, c) : go (ssTime s) (di, ci) rest
-
--- | Backwards-compatible export of the old @groupByTime@ API. Builds
--- on the new run-length walk for performance.
-groupByTime :: [SurvSample] -> [(Double, [SurvSample], [SurvSample])]
-groupByTime samples =
-  let !sorted = sortBy (comparing ssTime) samples
-      walk []     = []
-      walk (s:rest) = collect (ssTime s) [s] rest
-      collect t acc [] = [emit t acc]
-      collect t acc (x:xs)
-        | ssTime x == t = collect t (x:acc) xs
-        | otherwise     = emit t acc : collect (ssTime x) [x] xs
-      emit t bucket =
-        let (evs, cns) = splitByEvent bucket
-        in (t, evs, cns)
-      splitByEvent = foldr step ([], [])
-        where step s (es, cs) = case ssEvent s of
-                Observed -> (s : es, cs)
-                Censored -> (es,    s : cs)
-  in walk sorted
-
--- ---------------------------------------------------------------------------
--- Nelson-Aalen
--- ---------------------------------------------------------------------------
-
--- | Nelson-Aalen cumulative hazard estimator.
-data NAResult = NAResult
-  { narTimes      :: ![Double]
-  , narCumHazard  :: ![Double]   -- ^ Ĥ(t) = Σ_j d_j / n_j.
-  , narAtRisk     :: ![Int]
-  , narEvents     :: ![Int]
-  } deriving (Show)
-
--- | Compute the Nelson-Aalen estimator.
-nelsonAalen :: [SurvSample] -> NAResult
-nelsonAalen samples =
-  let km = kaplanMeier samples
-      ts = kmrTimes km
-      ns = kmrAtRisk km
-      ds = kmrEvents km
-      hazardIncrements = [fromIntegral d / fromIntegral n | (n, d) <- zip ns ds]
-      cumH = scanl1 (+) hazardIncrements
-  in NAResult ts cumH ns ds
-
--- ---------------------------------------------------------------------------
--- Log-rank test
--- ---------------------------------------------------------------------------
-
--- | Log-rank test result.
-data LogRankResult = LogRankResult
-  { lrChi2    :: !Double
-  , lrDf      :: !Int
-  , lrPValue  :: !Double
-  , lrGroupSizes :: ![Int]
-  } deriving (Show)
-
--- | Log-rank test for comparing survival across @k@ groups.
---
--- Tests @H_0: S_1(t) = S_2(t) = ⋯ = S_k(t)@ for all @t@. Asymptotic
--- chi-square approximation with @k − 1@ degrees of freedom.
-logRankTest :: [[SurvSample]] -> LogRankResult
-logRankTest groups =
-  let k = length groups
-      ns = map length groups
-      -- Pool all samples with group labels.
-      labelled = concat
-        [ [(g, s) | s <- ss] | (g, ss) <- zip [0 :: Int ..] groups ]
-      sorted = sortBy (comparing (ssTime . snd)) labelled
-      times = map head (group (map (ssTime . snd) sorted))
-      -- For each time t_j, compute observed events O_{ij} per group i
-      -- and expected events E_{ij} = (n_{ij} / n_j) × d_j, where
-      -- n_{ij} = at risk in group i, n_j = total at risk, d_j = total events.
-      go _      _    [] acc = acc
-      go nAtRiskBy nAtRiskTotal (t : tRest) acc =
-        let -- Events / censored at this time, by group.
-            atTime = [s | s <- sorted, ssTime (snd s) == t]
-            eventsByGrp = [ length [() | (g, s) <- atTime,
-                                          g == i, ssEvent s == Observed]
-                          | i <- [0 .. k - 1] ]
-            censoredByGrp = [ length [() | (g, s) <- atTime,
-                                            g == i, ssEvent s == Censored]
-                            | i <- [0 .. k - 1] ]
-            dTotal = sum eventsByGrp
-            cTotal = sum censoredByGrp
-            -- Expected events per group at this time.
-            expected = [ if nAtRiskTotal > 0
-                           then fromIntegral nij * fromIntegral dTotal
-                                / fromIntegral nAtRiskTotal
-                           else 0
-                       | nij <- nAtRiskBy ]
-            -- Variance contribution to each group's (O - E):
-            -- v_{ij} = n_{ij}(n_j - n_{ij}) d_j (n_j - d_j) / (n_j² (n_j - 1))
-            varContrib =
-              if nAtRiskTotal > 1 && dTotal > 0
-                then [ let nij = fromIntegral nij_i :: Double
-                           nj  = fromIntegral nAtRiskTotal :: Double
-                           dj  = fromIntegral dTotal :: Double
-                       in nij * (nj - nij) * dj * (nj - dj)
-                          / (nj * nj * (nj - 1))
-                     | nij_i <- nAtRiskBy ]
-                else replicate k 0
-            (oeAcc, varAcc) = acc
-            oeNew = zipWith3 (\o e prev -> prev + (fromIntegral o - e))
-                             eventsByGrp expected oeAcc
-            varNew = zipWith (+) varAcc varContrib
-            -- Update at-risk counts (subtract events + censored).
-            nAtRiskBy' = zipWith3 (\nrij ej cj -> nrij - ej - cj)
-                                  nAtRiskBy eventsByGrp censoredByGrp
-        in go nAtRiskBy' (nAtRiskTotal - dTotal - cTotal) tRest (oeNew, varNew)
-      (oeFinal, varFinal) = go ns (sum ns) times
-                                  (replicate k 0, replicate k 0)
-      -- Test statistic: (O - E)² / Var summed (approx for k=2);
-      -- for general k, use first (k-1) components.
-      chi2 =
-        if k == 2
-          then case (oeFinal, varFinal) of
-                 ([o1, _], [v1, _]) | v1 > 0 -> o1 * o1 / v1
-                 _ -> 0
-          else
-            -- General case: sum of squared standardised (O - E).
-            sum [ if v > 0 then o * o / v else 0
-                | (o, v) <- zip oeFinal varFinal ]
-      df = k - 1
-      pVal = SD.complCumulative (ChiSq.chiSquared df) chi2
-  in LogRankResult
-       { lrChi2    = chi2
-       , lrDf      = df
-       , lrPValue  = pVal
-       , lrGroupSizes = ns
-       }
-
--- ---------------------------------------------------------------------------
--- Cox proportional hazards
--- ---------------------------------------------------------------------------
-
--- | Cox PH model fit.
-data CoxFit = CoxFit
-  { coxBeta    :: !(LA.Vector Double)   -- ^ Coefficients.
-  , coxSE      :: !(LA.Vector Double)   -- ^ Standard errors.
-  , coxLogLik  :: !Double                -- ^ Log partial likelihood.
-  , coxIters   :: !Int                   -- ^ Newton iterations.
-  } deriving (Show)
-
--- | Fit Cox proportional hazards by maximising the partial likelihood
--- via Newton-Raphson.
---
--- Partial likelihood (ties handled by Breslow approximation):
---
--- @L(β) = Π_i exp(β·x_i) / Σ_{j ∈ R(t_i)} exp(β·x_j)@
---
--- where @R(t_i)@ is the risk set at time @t_i@.
-coxPH
-  :: [LA.Vector Double]   -- ^ Covariates per sample.
-  -> [SurvSample]         -- ^ Times and events.
-  -> CoxFit
---
--- B9c: list operations (@scanr1@, @!!@, list comprehensions over
--- 'LA.Vector') replaced with @VS@/@V@-vector reverse cumulative sums
--- and a precomputed boxed 'V.Vector' of risk-set rows. The score and
--- gradient now run in @O(n p)@ per call (no per-index list traversal).
--- Hessian remains numerical for now (algorithmic Hessian is a future
--- improvement) but each finite-difference call is now cheap.
-coxPH xs samples =
-  let !n = length xs
-      !p = if n == 0 then 0 else LA.size (head xs)
-      !indexed       = zip xs samples
-      !sortedByTime  = sortBy (comparing (ssTime . snd)) indexed
-      -- Event indices as an unboxed Vector for fast iteration.
-      !eventIdxsV    = VU.fromList
-        [ i | (i, (_, s)) <- zip [0 :: Int ..] sortedByTime
-            , ssEvent s == Observed ]
-      !xsArr  = LA.fromRows (map fst sortedByTime)
-      !xsRows = V.fromList (LA.toRows xsArr)        -- O(1) indexing
-
-      -- Score vector at β: X β. Storable for VS.scanr1.
-      scoresV beta = LA.flatten (xsArr LA.<> LA.asColumn beta) :: VS.Vector Double
-
-      -- Reverse cumulative sum on Storable: out[i] = Σ_{j≥i} v[j].
-      revCumSum :: VS.Vector Double -> VS.Vector Double
-      revCumSum = VS.fromList . scanr1 (+) . VS.toList
-      -- (Acceptable: VS.toList -> scanr1 -> VS.fromList is O(n) and
-      -- runs once per gradAndHess; the dominant cost is the BLAS GEMV
-      -- and per-row work below.)
-
-      -- log-partial-likelihood at β.
-      logLik beta =
-        let scs    = scoresV beta
-            !expS  = VS.map exp scs
-            !cumE  = revCumSum expS
-            walk acc k
-              | k >= VU.length eventIdxsV = acc
-              | otherwise =
-                  let !i = VU.unsafeIndex eventIdxsV k
-                      !s = VS.unsafeIndex scs i
-                      !c = VS.unsafeIndex cumE i
-                  in walk (acc + s - log c) (k + 1)
-        in walk (0 :: Double) 0
-
-      -- Gradient of log partial likelihood w.r.t. β.
-      gradAt beta =
-        let scs   = scoresV beta
-            !expS = VS.map exp scs
-            !cumE = revCumSum expS
-            -- Weighted X: rows scaled by exp(score). Then row-wise
-            -- reverse cumulative sum (per column) gives Σ_{j≥i} e_j x_j.
-            !weightedRows = V.zipWith
-              (\x e -> LA.scale e x) xsRows
-              (V.fromList (VS.toList expS))
-            -- Reverse cumulative sum of vectors:
-            !cumWeighted = revCumSumVecV (LA.konst 0 p) weightedRows
-            walk acc k
-              | k >= VU.length eventIdxsV = acc
-              | otherwise =
-                  let !i  = VU.unsafeIndex eventIdxsV k
-                      !ri = xsRows V.! i
-                      !ci = VS.unsafeIndex cumE i
-                      !wi = cumWeighted V.! i
-                      !contrib = ri - LA.scale (1 / ci) wi
-                  in walk (acc + contrib) (k + 1)
-        in walk (LA.konst 0 p) 0
-
-      maxIter = 25 :: Int
-      tol     = 1e-6
-      h       = 1e-5
-
-      -- Numerical Hessian column i (central difference of grad).
-      hessCol betaList i =
-        let bp = LA.fromList [if k == i then v + h else v
-                             | (k, v) <- zip [0::Int ..] betaList]
-            bm = LA.fromList [if k == i then v - h else v
-                             | (k, v) <- zip [0::Int ..] betaList]
-        in LA.scale (1 / (2 * h)) (gradAt bp - gradAt bm)
-
-      step beta =
-        let !g       = gradAt beta
-            !bL      = LA.toList beta
-            !hessian = LA.fromRows [hessCol bL i | i <- [0 .. p - 1]]
-            !negH    = LA.scale (-1) hessian
-            !delta   = negH LA.<\> g
-            !betaNew = beta + delta
-            !converged = LA.norm_2 delta < tol
-        in (betaNew, converged)
-
-      loop !i beta
-        | i >= maxIter = (beta, i)
-        | otherwise =
-            let (beta', conv) = step beta
-            in if conv then (beta', i + 1)
-                       else loop (i + 1) beta'
-
-      (!betaFinal, !iters) = loop 0 (LA.konst 0 p)
-
-      -- Final Hessian for SEs.
-      !bFL       = LA.toList betaFinal
-      !hessFinal = LA.fromRows [hessCol bFL i | i <- [0 .. p - 1]]
-      !negHFinal = LA.scale (-1) hessFinal
-      !seVec     = case maybeInverse negHFinal of
-                     Just inv -> LA.cmap sqrt (LA.takeDiag inv)
-                     Nothing  -> LA.konst (1/0) p
-  in CoxFit
-       { coxBeta   = betaFinal
-       , coxSE     = seVec
-       , coxLogLik = logLik betaFinal
-       , coxIters  = iters
-       }
-
--- | Reverse cumulative sum over a boxed Vector of 'LA.Vector Double':
--- @out[i] = Σ_{j≥i} v[j]@. Returns a Vector of the same length.
--- Uses 'scanr' once (O(n p)) — total cost dominated by BLAS-bound
--- vector additions.
-revCumSumVecV :: LA.Vector Double
-              -> V.Vector (LA.Vector Double)
-              -> V.Vector (LA.Vector Double)
-revCumSumVecV zeroV vs =
-  -- scanr produces length n+1 with a trailing zero seed; drop it.
-  let !suf = scanr (+) zeroV (V.toList vs)
-  in V.fromList (init suf)
-
--- | Baseline cumulative hazard (Breslow estimator).
-coxBaselineHazard
-  :: CoxFit
-  -> [LA.Vector Double]
-  -> [SurvSample]
-  -> [(Double, Double)]         -- ^ @(t_i, Ĥ_0(t_i))@.
-coxBaselineHazard fit xs samples =
-  let beta    = coxBeta fit
-      indexed = zip xs samples
-      sortedByTime = sortBy (comparing (ssTime . snd)) indexed
-      times = sort (map (ssTime . snd) sortedByTime)
-      uniqueTs = map head (group times)
-      atRiskAt t =
-        [ x | (x, s) <- sortedByTime, ssTime s >= t ]
-      eventsAt t =
-        length [() | (_, s) <- sortedByTime, ssTime s == t,
-                                              ssEvent s == Observed]
-      hazardIncrements t =
-        let denom = sum [ exp (LA.dot beta x) | x <- atRiskAt t ]
-            d     = eventsAt t
-        in if denom > 0 then fromIntegral d / denom else 0
-      hi = map hazardIncrements uniqueTs
-      cumH = scanl1 (+) hi
-  in zip uniqueTs cumH
-
--- | Try to compute the inverse of a matrix; returns Nothing if singular.
-maybeInverse :: LA.Matrix Double -> Maybe (LA.Matrix Double)
-maybeInverse m =
-  case LA.rank m of
-    r | r == LA.rows m -> Just (LA.inv m)
-      | otherwise       -> Nothing
diff --git a/src/Hanalyze/Model/TimeSeries.hs b/src/Hanalyze/Model/TimeSeries.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/TimeSeries.hs
+++ /dev/null
@@ -1,482 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
-
--- |
--- Module      : Hanalyze.Model.TimeSeries
--- Description : AR/MA/ARIMA・指数平滑・STL 分解を含む時系列モデリング一式
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Time-series modelling.
---
--- @
--- import Hanalyze.Model.TimeSeries
---
--- let acf = autocorrelation 20 ys
---     fit = fitAR 2 ys                          -- AR(2) by Yule-Walker
---     fc  = forecastAR fit ys 10                -- 10-step ahead
---
--- let hw  = holtWinters HWAdditive 12 ys
---     fc2 = hwForecast hw 24
--- @
---
--- == Implemented
---
---   * 'autocorrelation' / 'partialAutocorrelation' (sample ACF / PACF)
---   * 'fitAR' / 'forecastAR' (autoregressive AR(p) via Yule-Walker)
---   * 'fitMA' / 'forecastMA' (moving-average MA(q) via innovations)
---   * 'differencing' / 'inverseDifferencing' (helpers for ARIMA d)
---   * 'fitARIMA' / 'forecastARIMA' (ARIMA(p, d, q))
---   * 'simpleExpSmoothing' (single exp smoothing)
---   * 'holtWinters' (triple exp smoothing, additive / multiplicative)
---   * 'movingAverage' (centred / trailing)
---   * 'stlDecompose' (STL — seasonal-trend decomposition, simplified)
-module Hanalyze.Model.TimeSeries
-  ( -- * ACF / PACF
-    autocorrelation
-  , partialAutocorrelation
-    -- * AR
-  , ARFit (..)
-  , fitAR
-  , forecastAR
-    -- * MA
-  , MAFit (..)
-  , fitMA
-  , forecastMA
-    -- * ARIMA
-  , ARIMAFit (..)
-  , fitARIMA
-  , forecastARIMA
-  , differencing
-  , inverseDifferencing
-    -- * Exponential smoothing
-  , simpleExpSmoothing
-  , HWMode (..)
-  , HWFit (..)
-  , holtWinters
-  , hwForecast
-    -- * Helpers
-  , movingAverage
-  , stlDecompose
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-
--- ---------------------------------------------------------------------------
--- ACF / PACF
--- ---------------------------------------------------------------------------
-
--- | Sample autocorrelation function up to @maxLag@. Lag 0 is always
--- @1.0@. Computed as @r_k = c_k / c_0@ with biased autocovariance:
--- @c_k = (1/n) Σ_{t=0..n-k-1} (y_t - ȳ)(y_{t+k} - ȳ)@.
-autocorrelation
-  :: Int               -- ^ Maximum lag.
-  -> LA.Vector Double
-  -> LA.Vector Double
-autocorrelation maxLag y =
-  let n     = LA.size y
-      ybar  = LA.sumElements y / fromIntegral n
-      ydev  = y - LA.scalar ybar
-      c0    = LA.dot ydev ydev / fromIntegral n
-      cAt k = sum [ LA.atIndex ydev t * LA.atIndex ydev (t + k)
-                  | t <- [0 .. n - k - 1] ]
-              / fromIntegral n
-      rs    = [ if c0 == 0 then 0 else cAt k / c0
-              | k <- [0 .. maxLag] ]
-  in LA.fromList rs
-
--- | Sample partial autocorrelation function up to @maxLag@ via direct
--- AR-fit: PACF[k] = last AR coefficient when fitting AR(k) by
--- Yule-Walker. Conceptually equivalent to the Durbin-Levinson
--- recursion but easier to implement correctly.
-partialAutocorrelation
-  :: Int
-  -> LA.Vector Double
-  -> LA.Vector Double
-partialAutocorrelation maxLag y =
-  let pacfAt 0 = 1
-      pacfAt k =
-        let fit = fitAR k y
-            phi = arPhi fit
-        in if LA.size phi == 0 then 0
-             else LA.atIndex phi (k - 1)
-  in LA.fromList [pacfAt k | k <- [0 .. maxLag]]
-
--- ---------------------------------------------------------------------------
--- AR (autoregressive)
--- ---------------------------------------------------------------------------
-
--- | Fitted AR(p) model.
-data ARFit = ARFit
-  { arOrder    :: !Int          -- ^ p
-  , arPhi      :: !(LA.Vector Double)  -- ^ AR coefficients (length p)
-  , arIntercept :: !Double      -- ^ μ (mean)
-  , arResidVar :: !Double       -- ^ Innovation variance.
-  } deriving (Show)
-
--- | Fit an AR(p) model by the Yule-Walker equations.
--- Solves @R φ = r@ where @R@ is the @p × p@ Toeplitz matrix of
--- autocovariances and @r = (γ_1, …, γ_p)@.
-fitAR :: Int -> LA.Vector Double -> ARFit
-fitAR p y =
-  let n     = LA.size y
-      ybar  = LA.sumElements y / fromIntegral n
-      yC    = y - LA.scalar ybar
-      gamma k = LA.dot (LA.subVector 0 (n - k) yC)
-                       (LA.subVector k (n - k) yC) / fromIntegral n
-      rhs   = LA.fromList [gamma k | k <- [1 .. p]]
-      mat   = LA.fromLists
-                [[gamma (abs (i - j)) | j <- [0 .. p - 1]]
-                                      | i <- [0 .. p - 1]]
-      phi   = mat LA.<\> rhs
-      -- Innovation variance via Yule-Walker:
-      -- σ² = γ_0 - Σ φ_i γ_i
-      innovVar = gamma 0 - LA.dot phi rhs
-  in ARFit
-       { arOrder     = p
-       , arPhi       = phi
-       , arIntercept = ybar
-       , arResidVar  = max 0 innovVar
-       }
-
--- | Forecast @h@ steps ahead from a fitted AR model and the most
--- recent observations (in chronological order).
-forecastAR
-  :: ARFit
-  -> LA.Vector Double  -- ^ History (must be ≥ p).
-  -> Int               -- ^ Horizon h.
-  -> LA.Vector Double
-forecastAR fit hist h =
-  let p     = arOrder fit
-      mu    = arIntercept fit
-      phi   = arPhi fit
-      lastP = LA.toList (LA.subVector (LA.size hist - p) p hist)
-      go _ acc 0 = reverse acc
-      go window acc k =
-        let dev    = zipWith (-) window (replicate p mu)
-            yHat   = mu + LA.dot phi (LA.fromList dev)
-            window' = drop 1 window ++ [yHat]
-        in go window' (yHat : acc) (k - 1)
-  in LA.fromList (go lastP [] h)
-
--- ---------------------------------------------------------------------------
--- MA (moving average)
--- ---------------------------------------------------------------------------
-
--- | Fitted MA(q) model.
-data MAFit = MAFit
-  { maOrder    :: !Int
-  , maTheta    :: !(LA.Vector Double)  -- ^ MA coefficients (length q)
-  , maIntercept :: !Double
-  , maResidVar :: !Double
-  , maResiduals :: !(LA.Vector Double)  -- ^ Innovation series.
-  } deriving (Show)
-
--- | Fit an MA(q) model via the innovations algorithm (Brockwell-Davis
--- 1991, §5.2). Returns the estimated θ_i and innovation series.
-fitMA :: Int -> LA.Vector Double -> MAFit
-fitMA q y =
-  let n     = LA.size y
-      ybar  = LA.sumElements y / fromIntegral n
-      yC    = y - LA.scalar ybar
-      gamma k = LA.dot (LA.subVector 0 (n - k) yC)
-                       (LA.subVector k (n - k) yC) / fromIntegral n
-      -- Innovations algorithm: recursion
-      -- v_n = γ_0
-      -- θ_{n,n-k} = (γ_{n-k} - Σ_{j=0}^{k-1} θ_{n,n-j} θ_{k,k-j} v_j) / v_k
-      -- v_n = γ_0 - Σ_{j=0}^{n-1} θ_{n,n-j}² v_j
-      --
-      -- We compute up to lag q.
-      theta = LA.konst 0 q :: LA.Vector Double
-      _ = theta
-      -- Simplified approximation: use sample autocovariances directly
-      -- to estimate θ via least squares (Hannan-Rissanen 1982).
-      -- This is less accurate than full Innovations but simpler.
-      thetaSimple = LA.fromList [ gamma k / max 1e-15 (gamma 0)
-                                | k <- [1 .. q] ]
-      -- Compute residuals: e_t = y_t - μ - Σ θ_i e_{t-i}
-      residuals = computeMAResiduals (LA.toList yC) (LA.toList thetaSimple)
-      sigma2 = sum [r * r | r <- residuals] / fromIntegral n
-  in MAFit
-       { maOrder     = q
-       , maTheta     = thetaSimple
-       , maIntercept = ybar
-       , maResidVar  = sigma2
-       , maResiduals = LA.fromList residuals
-       }
-  where
-    computeMAResiduals :: [Double] -> [Double] -> [Double]
-    computeMAResiduals ys thetas =
-      let go acc []     = reverse acc
-          go acc (yi:ys') =
-            let q' = length thetas
-                eHist = take q' acc  -- recent residuals
-                pad   = replicate (q' - length eHist) 0
-                ePadded = pad ++ eHist
-                yHat  = sum (zipWith (*) thetas (reverse ePadded))
-                eNew  = yi - yHat
-            in go (eNew : acc) ys'
-      in go [] ys
-
--- | Forecast h steps from MA(q). Beyond q steps, the forecast equals
--- the mean (innovations are zero in expectation).
-forecastMA :: MAFit -> Int -> LA.Vector Double
-forecastMA fit h =
-  let q     = maOrder fit
-      theta = LA.toList (maTheta fit)
-      mu    = maIntercept fit
-      eHist = LA.toList (maResiduals fit)
-      eRecent = take q (reverse eHist)
-      go k
-        | k > q || k > h = []
-        | otherwise =
-            let pad = replicate (q - length eRecent) 0
-                eP  = pad ++ eRecent
-                yhat = mu + sum (zipWith (*) theta (drop (k - 1) (reverse eP)))
-            in yhat : go (k + 1)
-      truncated = take h (go 1 ++ repeat mu)
-  in LA.fromList truncated
-
--- ---------------------------------------------------------------------------
--- ARIMA
--- ---------------------------------------------------------------------------
-
--- | Fitted ARIMA(p, d, q) model.
-data ARIMAFit = ARIMAFit
-  { arimaP   :: !Int
-  , arimaD   :: !Int
-  , arimaQ   :: !Int
-  , arimaAR  :: !ARFit
-  , arimaMA  :: !MAFit
-  , arimaOrigSeries :: !(LA.Vector Double)
-  } deriving (Show)
-
--- | Fit ARIMA(p, d, q): difference d times, then fit AR(p) + MA(q) on
--- the differenced series. Uses two-stage estimation (AR first, then
--- MA on residuals).
-fitARIMA :: Int -> Int -> Int -> LA.Vector Double -> ARIMAFit
-fitARIMA p d q y =
-  let yDiff = iterate differencing y !! d
-      arFit = fitAR p yDiff
-      arResid = computeARResiduals arFit yDiff
-      maFit = fitMA q arResid
-  in ARIMAFit
-       { arimaP   = p
-       , arimaD   = d
-       , arimaQ   = q
-       , arimaAR  = arFit
-       , arimaMA  = maFit
-       , arimaOrigSeries = y
-       }
-
-computeARResiduals :: ARFit -> LA.Vector Double -> LA.Vector Double
-computeARResiduals fit y =
-  let p   = arOrder fit
-      mu  = arIntercept fit
-      phi = LA.toList (arPhi fit)
-      n   = LA.size y
-      ys  = LA.toList y
-      go i
-        | i < p = 0
-        | otherwise =
-            let dev = [ys !! (i - k - 1) - mu | k <- [0 .. p - 1]]
-                yHat = mu + sum (zipWith (*) phi dev)
-            in (ys !! i) - yHat
-      residuals = [go i | i <- [0 .. n - 1]]
-  in LA.fromList residuals
-
--- | Forecast h steps from a fitted ARIMA model.
-forecastARIMA :: ARIMAFit -> Int -> LA.Vector Double
-forecastARIMA fit h =
-  let _origY = arimaOrigSeries fit
-      d      = arimaD fit
-      diff_d = iterate differencing _origY !! d
-      arFc   = forecastAR (arimaAR fit) diff_d h
-      maFc   = forecastMA (arimaMA fit) h
-      combined = arFc + maFc - LA.scalar (arIntercept (arimaAR fit))
-      -- Inverse-difference d times.
-      lastObs = take d (reverse (LA.toList _origY))
-      _ = lastObs
-  in iterate (inverseDifferencing _origY) combined !! d
-
--- | First-difference: @y'_t = y_t - y_{t-1}@. Output length = n - 1.
-differencing :: LA.Vector Double -> LA.Vector Double
-differencing y =
-  let n = LA.size y
-  in if n < 2 then LA.fromList []
-       else LA.subVector 1 (n - 1) y - LA.subVector 0 (n - 1) y
-
--- | Inverse first-difference given the last observation of the
--- original series. Output length = n + 1 (prepends the seed).
--- Simplified: cumulative sum prepended by 0.
-inverseDifferencing
-  :: LA.Vector Double  -- ^ Original (for last value reference).
-  -> LA.Vector Double  -- ^ Differenced forecast.
-  -> LA.Vector Double
-inverseDifferencing origY diff =
-  let lastY = LA.atIndex origY (LA.size origY - 1)
-      cumS  = scanl (+) lastY (LA.toList diff)
-  in LA.fromList (drop 1 cumS)
-
--- ---------------------------------------------------------------------------
--- Exponential smoothing
--- ---------------------------------------------------------------------------
-
--- | Simple exponential smoothing (single, no trend / seasonality).
--- @s_t = α y_t + (1 − α) s_{t−1}@. Returns the smoothed series.
-simpleExpSmoothing
-  :: Double            -- ^ α ∈ (0, 1).
-  -> LA.Vector Double
-  -> LA.Vector Double
-simpleExpSmoothing alpha y =
-  let ys = LA.toList y
-      go _    []     = []
-      go prev (yi:rest) =
-        let sNew = alpha * yi + (1 - alpha) * prev
-        in sNew : go sNew rest
-      s0 = case ys of { (y0:_) -> y0; [] -> 0 }
-  in LA.fromList (go s0 ys)
-
--- | Holt-Winters mode (additive vs multiplicative seasonality).
-data HWMode = HWAdditive | HWMultiplicative deriving (Show, Eq)
-
--- | Fitted Holt-Winters (triple exponential smoothing).
-data HWFit = HWFit
-  { hwMode   :: !HWMode
-  , hwPeriod :: !Int
-  , hwAlpha  :: !Double
-  , hwBeta   :: !Double
-  , hwGamma  :: !Double
-  , hwLevel  :: !Double          -- ^ Final level component.
-  , hwTrend  :: !Double          -- ^ Final trend component.
-  , hwSeasonal :: ![Double]      -- ^ Final seasonal indices (length period).
-  , hwFitted :: !(LA.Vector Double)
-  } deriving (Show)
-
--- | Fit Holt-Winters (additive seasonal). Picks default smoothing
--- parameters @α = β = γ = 0.3@; for production use, optimise these.
-holtWinters
-  :: HWMode            -- ^ Additive or multiplicative.
-  -> Int               -- ^ Seasonal period (e.g. 12 for monthly).
-  -> LA.Vector Double  -- ^ Time series.
-  -> HWFit
-holtWinters mode period y =
-  let alpha = 0.3 :: Double
-      beta  = 0.1 :: Double
-      gamma = 0.1 :: Double
-      ys    = LA.toList y
-      -- Initialise from first 'period' observations.
-      initLevel = sum (take period ys) / fromIntegral period
-      initTrend = (sum (take period (drop period ys))
-                  - sum (take period ys))
-                  / fromIntegral (period * period)
-      initSeas = case mode of
-        HWAdditive       ->
-          [ ys !! i - initLevel | i <- [0 .. period - 1] ]
-        HWMultiplicative ->
-          [ ys !! i / max 1e-15 initLevel | i <- [0 .. period - 1] ]
-      -- Iterate.
-      go !lvl !trd !seas !fitted [] = (lvl, trd, seas, reverse fitted)
-      go !lvl !trd !seas !fitted (yi:rest) =
-        let p     = period
-            sIdx  = length fitted `mod` p
-            sCur  = seas !! sIdx
-            (lvlNew, trdNew, sNew, fHat) = case mode of
-              HWAdditive ->
-                let l' = alpha * (yi - sCur) + (1 - alpha) * (lvl + trd)
-                    t' = beta  * (l' - lvl) + (1 - beta)  * trd
-                    s' = gamma * (yi - l') + (1 - gamma) * sCur
-                    fh = lvl + trd + sCur
-                in (l', t', s', fh)
-              HWMultiplicative ->
-                let l' = alpha * (yi / max 1e-15 sCur) + (1 - alpha) * (lvl + trd)
-                    t' = beta  * (l' - lvl) + (1 - beta)  * trd
-                    s' = gamma * (yi / max 1e-15 l') + (1 - gamma) * sCur
-                    fh = (lvl + trd) * sCur
-                in (l', t', s', fh)
-            seas' = updateAt sIdx sNew seas
-        in go lvlNew trdNew seas' (fHat : fitted) rest
-      (finalLvl, finalTrd, finalSeas, fits) =
-        go initLevel initTrend initSeas [] ys
-  in HWFit
-       { hwMode     = mode
-       , hwPeriod   = period
-       , hwAlpha    = alpha
-       , hwBeta     = beta
-       , hwGamma    = gamma
-       , hwLevel    = finalLvl
-       , hwTrend    = finalTrd
-       , hwSeasonal = finalSeas
-       , hwFitted   = LA.fromList fits
-       }
-
--- | Forecast @h@ steps ahead from a fitted Holt-Winters model.
-hwForecast :: HWFit -> Int -> LA.Vector Double
-hwForecast fit h =
-  let lvl   = hwLevel fit
-      trd   = hwTrend fit
-      seas  = hwSeasonal fit
-      p     = hwPeriod fit
-      mode  = hwMode fit
-      go k
-        | k > h = []
-        | otherwise =
-            let sIdx = (k - 1) `mod` p
-                fc   = case mode of
-                  HWAdditive       -> lvl + fromIntegral k * trd + seas !! sIdx
-                  HWMultiplicative -> (lvl + fromIntegral k * trd) * seas !! sIdx
-            in fc : go (k + 1)
-  in LA.fromList (go 1)
-
--- ---------------------------------------------------------------------------
--- Helpers
--- ---------------------------------------------------------------------------
-
--- | Centred moving average with window @w@ (odd recommended). Values
--- near the edges have NaN.
-movingAverage :: Int -> LA.Vector Double -> LA.Vector Double
-movingAverage w y =
-  let n     = LA.size y
-      half  = w `div` 2
-      avg i
-        | i - half < 0 || i + half >= n = 0/0
-        | otherwise = sum [LA.atIndex y (i + j) | j <- [-half .. half]]
-                      / fromIntegral w
-  in LA.fromList [avg i | i <- [0 .. n - 1]]
-
--- | Simplified STL decomposition (loess-free version): subtract a
--- centred moving-average trend, then estimate seasonality as the
--- mean per phase.
-stlDecompose
-  :: Int               -- ^ Period.
-  -> LA.Vector Double
-  -> (LA.Vector Double, LA.Vector Double, LA.Vector Double)
-       -- ^ (trend, seasonal, residual).
-stlDecompose period y =
-  let n     = LA.size y
-      trend = movingAverage period y
-      detrended = LA.fromList
-        [ if isNaN (LA.atIndex trend i) then 0
-            else LA.atIndex y i - LA.atIndex trend i
-        | i <- [0 .. n - 1] ]
-      -- Per-phase mean over non-NaN cells.
-      phaseMeans =
-        [ let maxJ = (n - 1 - i) `div` period
-              xs = [LA.atIndex detrended (i + j * period)
-                   | j <- [0 .. maxJ], i + j * period < n]
-              valid = filter (not . isNaN) xs
-          in if null valid then 0 else sum valid / fromIntegral (length valid)
-        | i <- [0 .. period - 1] ]
-      -- Centre seasonal indices around 0.
-      seasMean = sum phaseMeans / fromIntegral period
-      seasonal = LA.fromList
-        [ phaseMeans !! (i `mod` period) - seasMean | i <- [0 .. n - 1] ]
-      residual = y - trend - seasonal
-  in (trend, seasonal, residual)
-
--- | Update list element at index.
-updateAt :: Int -> a -> [a] -> [a]
-updateAt _ _ []     = []
-updateAt 0 v (_:xs) = v : xs
-updateAt i v (x:xs) = x : updateAt (i - 1) v xs
-
diff --git a/src/Hanalyze/Model/VAR.hs b/src/Hanalyze/Model/VAR.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/VAR.hs
+++ /dev/null
@@ -1,134 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
-
--- |
--- Module      : Hanalyze.Model.VAR
--- Description : 多変量自己回帰 VAR(p) モデルの方程式別 OLS 推定と予測
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- VAR(p) — Vector AutoRegressive model.
---
--- Multivariate generalization of AR(p): for a @K@-dimensional series
--- @yₜ ∈ ℝᴷ@,
---
--- @
---   yₜ = c + A₁·yₜ₋₁ + A₂·yₜ₋₂ + … + Aₚ·yₜ₋ₚ + εₜ
--- @
---
--- where each @Aₗ@ is a @K × K@ coefficient matrix and @c@ is a length-@K@
--- intercept. Estimation is by equation-by-equation OLS, which is the
--- maximum-likelihood estimator for VAR under Gaussian innovations (the
--- stacked system has the same regressors in every equation, so SUR
--- collapses to OLS — Lütkepohl 2005 §3.2).
---
--- @
--- import Hanalyze.Model.VAR
---
--- let fit = fitVAR 2 yMat              -- VAR(2) on n × K series
---     fc  = forecastVAR fit yMat 10    -- 10-step ahead
--- @
---
--- == Implemented
---
---   * 'fitVAR' (equation-by-equation OLS, joint estimation)
---   * 'forecastVAR' (deterministic point forecast, h steps)
-module Hanalyze.Model.VAR
-  ( VARFit (..)
-  , fitVAR
-  , forecastVAR
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-
--- ---------------------------------------------------------------------------
--- Types
--- ---------------------------------------------------------------------------
-
--- | Fitted VAR(p) model.
-data VARFit = VARFit
-  { varP         :: !Int              -- ^ Lag order @p@.
-  , varK         :: !Int              -- ^ Series dimensionality @K@.
-  , varConst     :: !(LA.Vector Double) -- ^ Intercept @c@ (length @K@).
-  , varCoefs     :: ![LA.Matrix Double] -- ^ @[A₁, …, Aₚ]@, each @K × K@.
-  , varResiduals :: !(LA.Matrix Double) -- ^ Residuals, @(n − p) × K@.
-  , varSigma     :: !(LA.Matrix Double) -- ^ Residual covariance @K × K@.
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- Fitting
--- ---------------------------------------------------------------------------
-
--- | Fit a VAR(@p@) model to an @n × K@ series @Y@ by equation-by-equation
--- OLS. The first @p@ rows are consumed as the initial lag window;
--- @n − p@ effective observations are used. Requires @n > p · K + 1@.
-fitVAR :: Int -> LA.Matrix Double -> VARFit
-fitVAR p y =
-  let n     = LA.rows y
-      k     = LA.cols y
-      neff  = n - p
-      -- Design matrix Z: each row t = [1, y_{t-1}, y_{t-2}, …, y_{t-p}]
-      -- (1 + p·K columns), for t = p, p+1, …, n-1.
-      buildRow t =
-        1.0 : concat [ LA.toList (LA.flatten (y LA.? [t - l]))
-                     | l <- [1 .. p] ]
-      zRows = [ buildRow t | t <- [p .. n - 1] ]
-      z     = LA.fromLists zRows                -- (neff × (1 + p·K))
-      yLag  = y LA.?? (LA.Drop p, LA.All)       -- (neff × K)
-      -- OLS: B = (Zᵀ Z)⁻¹ Zᵀ Y. Use linearSolveLS (least squares) for
-      -- numerical stability.
-      bMat  = LA.linearSolveLS z yLag           -- ((1 + p·K) × K)
-      cVec  = LA.flatten (bMat LA.? [0])        -- intercept (K,)
-      coefs =
-        [ LA.tr (bMat LA.?? ( LA.Pos (LA.idxs [ 1 + (l - 1) * k + j
-                                              | j <- [0 .. k - 1] ])
-                            , LA.All ))
-        | l <- [1 .. p] ]
-        -- Each block row of B is K rows giving Aₗᵀ; transpose for K × K Aₗ.
-      yhat  = z LA.<> bMat
-      resid = yLag - yhat
-      sigma = (LA.tr resid LA.<> resid)
-              / fromIntegral (max 1 (neff - (1 + p * k)))
-  in VARFit
-       { varP         = p
-       , varK         = k
-       , varConst     = cVec
-       , varCoefs     = coefs
-       , varResiduals = resid
-       , varSigma     = sigma
-       }
-
--- ---------------------------------------------------------------------------
--- Forecasting
--- ---------------------------------------------------------------------------
-
--- | Deterministic @h@-step-ahead point forecast (ε set to zero):
---
--- @
---   ŷ_{T+k} = c + Σₗ Aₗ · ŷ_{T+k-ℓ}
--- @
---
--- where @ŷ_{T+j} = y_{T+j}@ for @j ≤ 0@. The full input series @y@ is
--- accepted to supply the last @p@ rows used as initial history.
-forecastVAR :: VARFit -> LA.Matrix Double -> Int -> LA.Matrix Double
-forecastVAR fit y h
-  | h <= 0    = LA.fromLists []
-  | otherwise =
-      let p    = varP fit
-          n    = LA.rows y
-          -- Initial history: last p rows of y, as a [Vector Double] list
-          -- with index 0 = y_{T-1}, index 1 = y_{T-2}, …, index p-1 = y_{T-p}.
-          hist0 = [ LA.flatten (y LA.? [n - 1 - i]) | i <- [0 .. p - 1] ]
-          step !hist =
-            let !pred_ =
-                  varConst fit
-                  + foldr1 (+)
-                      [ (varCoefs fit !! (l - 1)) LA.#> (hist !! (l - 1))
-                      | l <- [1 .. p] ]
-            in (pred_, pred_ : init hist)
-          go !k !hist acc
-            | k > h     = reverse acc
-            | otherwise =
-                let (yk, hist') = step hist
-                in go (k + 1) hist' (yk : acc)
-      in LA.fromRows (go 1 hist0 [])
diff --git a/src/Hanalyze/Model/Weibull.hs b/src/Hanalyze/Model/Weibull.hs
deleted file mode 100644
--- a/src/Hanalyze/Model/Weibull.hs
+++ /dev/null
@@ -1,306 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
-
--- |
--- Module      : Hanalyze.Model.Weibull
--- Description : Weibull 分布の最尤推定・B_x 寿命・Wald 標準誤差 (信頼性/故障時間解析の中核)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Weibull 分布の最尤推定 + B_x 寿命 + Wald SE。
---
--- 信頼性 / 故障時間解析の中核。 半導体 / 材料分野の加速試験データ解析に使う。
--- 加速モデル (Arrhenius / Eyring / Inverse Power Law) は
--- @Hanalyze.Model.Reliability@ で別途扱う。
---
--- Weibull(k, λ) の確率密度 / 生存関数:
---
--- > f(x) = (k/λ) (x/λ)^(k-1) exp(-(x/λ)^k)        for x > 0
--- > S(x) = exp(-(x/λ)^k)
---
--- 形状 k と尺度 λ は両方とも正。 k < 1 は故障率低下 (初期不良)、 k = 1 は
--- 指数分布、 k > 1 は故障率上昇 (摩耗故障)。
-module Hanalyze.Model.Weibull
-  ( -- * 結果型
-    WeibullFit (..)
-    -- * MLE fit
-  , fitWeibullMLE
-  , fitWeibullCensored
-    -- * 派生量
-  , bxLife
-  , bxLifeCI
-  , weibullParameterSE
-  , weibullParameterCovariance
-    -- * 数値ユーティリティ
-  , quantileNormal
-  ) where
-
-import           Data.Text     (Text)
-import           Data.Vector   (Vector)
-import qualified Data.Vector   as V
-
--- ===========================================================================
--- 型定義
--- ===========================================================================
-
--- | Weibull MLE 結果。
-data WeibullFit = WeibullFit
-  { wfShape   :: !Double           -- ^ k (形状パラメータ、 > 0)
-  , wfScale   :: !Double           -- ^ λ (尺度パラメータ、 > 0)
-  , wfLogLik  :: !Double           -- ^ 対数尤度の MLE 値
-  , wfN       :: !Int              -- ^ 観測総数 (打ち切り含む)
-  , wfRObs    :: !Int              -- ^ 観測 failure 数 (打ち切り除く)
-  , wfFisher  :: !(Double, Double, Double)
-    -- ^ Fisher 情報行列 2x2 を上三角 (I_kk, I_kλ, I_λλ) で保持。
-    --   Wald SE 計算で逆行列を取る。
-  } deriving (Show)
-
--- ===========================================================================
--- 内部ヘルパ
--- ===========================================================================
-
--- | 観測値リストの sanity check (全て正で非空)。
-validatePositive :: Vector Double -> Either Text ()
-validatePositive xs
-  | V.null xs           = Left "fitWeibull: empty observation series"
-  | V.any (<= 0) xs     = Left "fitWeibull: all observations must be positive"
-  | otherwise           = Right ()
-
--- | A(k) = Σ x_i^k log x_i (failures のみ加算する版は censored 用)。
-weightedLog :: Double -> Vector Double -> Double
-weightedLog k xs = V.sum (V.map (\x -> x ** k * log x) xs)
-
--- | B(k) = Σ x_i^k。 censored 含む場合は加算範囲を呼び出し側で制御する。
-sumPow :: Double -> Vector Double -> Double
-sumPow k xs = V.sum (V.map (** k) xs)
-
--- | g(k) = A(k)/B(k) − (1/r)·Σ_{failures} log x − 1/k = 0
---   r = failure 数。 単調増加なので bisection で root を取れる。
-scoreG :: Double -> Vector Double -> Vector Double -> Int -> Double
-scoreG k allXs failuresXs r =
-  let bk = sumPow k allXs
-      ak = weightedLog k allXs
-      meanLogFail = V.sum (V.map log failuresXs) / fromIntegral r
-  in ak / bk - meanLogFail - 1 / k
-
--- | 単調増加関数の root を bisection で。 区間 [lo, hi] で g(lo) < 0 < g(hi) を仮定。
-bisect
-  :: (Double -> Double)  -- 単調増加 g
-  -> Double              -- lo
-  -> Double              -- hi
-  -> Double              -- 許容誤差
-  -> Int                 -- 最大反復
-  -> Either Text Double
-bisect g lo0 hi0 tol maxIter = go lo0 hi0 0
-  where
-    go !lo !hi !i
-      | i >= maxIter             = Left "Weibull MLE: bisection did not converge"
-      | (hi - lo) < tol          = Right ((lo + hi) / 2)
-      | otherwise =
-          let mid = (lo + hi) / 2
-              gm  = g mid
-          in if gm > 0
-               then go lo mid (i + 1)
-               else go mid hi (i + 1)
-
--- | 区間を「拡張 + 縮小」 でブラケットを取る。
---   関数 g は単調増加。 g(start_lo) ≥ 0 や g(start_hi) ≤ 0 の場合は範囲を広げる。
-findBracket
-  :: (Double -> Double)
-  -> Double  -- 初期 lo (>0)
-  -> Double  -- 初期 hi
-  -> Int     -- 最大拡張回数
-  -> Either Text (Double, Double)
-findBracket g lo0 hi0 maxExp = go lo0 hi0 0
-  where
-    go !lo !hi !i
-      | i >= maxExp = Left "Weibull MLE: failed to bracket root"
-      | otherwise =
-          let glo = g lo
-              ghi = g hi
-          in if glo <= 0 && ghi >= 0
-               then Right (lo, hi)
-               else if glo > 0  -- root より大きすぎる
-                      then go (lo / 4) hi (i + 1)
-                      else if ghi < 0  -- root より小さすぎる
-                             then go lo (hi * 4) (i + 1)
-                             else Right (lo, hi)
-
--- | 全観測 failure 仮定で MLE を解く中核ロジック。
---   xs (failure 時間) + xsAll (全観測; censored 含む) を分けるのは Phase 2.3 用。
-solveWeibull
-  :: Vector Double  -- failures (時間)
-  -> Vector Double  -- 全観測 (失敗 + 打ち切り)
-  -> Int            -- failure 数 r
-  -> Either Text WeibullFit
-solveWeibull failuresXs allXs r = do
-  let g k = scoreG k allXs failuresXs r
-  (lo, hi) <- findBracket g 0.1 10.0 30
-  k        <- bisect g lo hi 1e-10 200
-  let bk     = sumPow k allXs
-      lam    = (bk / fromIntegral r) ** (1 / k)
-      -- log-likelihood at MLE (failures contribution + censored survival)
-      n      = V.length allXs
-      sumLogFailures = V.sum (V.map log failuresXs)
-      sumScaled = V.sum (V.map (\x -> (x / lam) ** k) allXs)
-      ll     = fromIntegral r * (log k - k * log lam)
-             + (k - 1) * sumLogFailures
-             - sumScaled
-      -- 観測 Fisher 情報 (uncensored 公式; censored ではバイアスあり)
-      -- I_kk ≈ r / k^2 + Σ (x/λ)^k (log(x/λ))^2
-      -- I_λλ ≈ k^2 · (Σ (x/λ)^k) / λ^2 − r k / λ^2  ... 簡素化:
-      -- 厳密 expected information を Phase 2.4 で詰める。 ここでは
-      -- observed information (負 Hessian) の対角成分を返す。
-      iKK   = fromIntegral r / (k * k)
-            + V.sum (V.map (\x -> (x / lam) ** k * (log (x / lam))**2) allXs)
-      iLL   = (k * k / (lam * lam)) * V.sum (V.map (\x -> (x / lam) ** k) allXs)
-            - fromIntegral r * k / (lam * lam) + 2 * k * fromIntegral r / (lam * lam)
-            -- 教科書: I_λλ = r·k² / λ²  (uncensored at MLE は Σ (x/λ)^k = r)
-            -- censored の場合は上の Σ がそのまま入る。
-      iKL   = V.sum (V.map (\x -> (x / lam) ** k * log (x / lam)) allXs)
-            * (k / lam)
-            - fromIntegral r / lam
-  pure WeibullFit
-    { wfShape   = k
-    , wfScale   = lam
-    , wfLogLik  = ll
-    , wfN       = n
-    , wfRObs    = r
-    , wfFisher  = (iKK, iKL, iLL)
-    }
-
--- ===========================================================================
--- 公開関数
--- ===========================================================================
-
--- | Weibull MLE (打ち切り無し)。
---
--- 入力: 全て観測済の故障時間 (> 0)。
--- 解法: score equation @1/k = A(k)/B(k) − (1/n)·Σ log x@ を 1D bisection で
---       解き、 λ = (Σ x^k / n)^(1/k)。
-fitWeibullMLE :: Vector Double -> Either Text WeibullFit
-fitWeibullMLE xs = do
-  _ <- validatePositive xs
-  if V.length xs < 2
-    then Left "fitWeibullMLE: need at least 2 observations"
-    else
-      let logs = V.map log xs
-          maxL = V.maximum logs
-          meanL = V.sum logs / fromIntegral (V.length xs)
-      in if abs (maxL - meanL) < 1e-12
-           then Left "fitWeibullMLE: data is constant (degenerate)"
-           else solveWeibull xs xs (V.length xs)
-
--- | Weibull MLE (右打ち切り対応)。
---
--- 第 2 引数の @True@ = failure observed、 @False@ = right-censored。
--- 同じ score equation @1/k = A_all(k)/B_all(k) − (1/r)·Σ_{δ=1} log x@ を解くが、
--- @A@, @B@ は 全観測 (failure + 打ち切り) で加算し、 log-sum は failure のみ。
--- @r@ は failure 数。
-fitWeibullCensored :: Vector Double -> Vector Bool -> Either Text WeibullFit
-fitWeibullCensored xs deltas = do
-  _ <- validatePositive xs
-  if V.length xs /= V.length deltas
-    then Left "fitWeibullCensored: times and delta indicators differ in length"
-    else
-      let failuresXs = V.ifilter (\i _ -> deltas V.! i) xs
-          r = V.length failuresXs
-      in if r < 2
-           then Left "fitWeibullCensored: need at least 2 observed failures"
-           else
-             let logsFail = V.map log failuresXs
-                 maxL  = V.maximum logsFail
-                 meanL = V.sum logsFail / fromIntegral r
-             in if abs (maxL - meanL) < 1e-12
-                  then Left "fitWeibullCensored: failure data is constant (degenerate)"
-                  else solveWeibull failuresXs xs r
-
--- | B_p 寿命: F^{-1}(p) = λ · (−ln(1−p))^(1/k)。
---
--- 典型用途: @bxLife 0.10 fit@ → B_10 (10%故障時間)、
---           @bxLife 0.50 fit@ → B_50 (中央寿命)。
-bxLife :: Double -> WeibullFit -> Double
-bxLife p _ | p <= 0 || p >= 1 = error "bxLife: probability must be in (0, 1)"
-bxLife p fit =
-  let k   = wfShape fit
-      lam = wfScale fit
-  in lam * (- log (1 - p)) ** (1 / k)
-
--- | (k_SE, λ_SE) — Fisher 情報行列の逆行列の対角の平方根。
---
--- 2x2 逆行列: var(k) = I_λλ / det、 var(λ) = I_kk / det、 det = I_kk·I_λλ − I_kλ²
-weibullParameterSE :: WeibullFit -> (Double, Double)
-weibullParameterSE fit =
-  let (vK, _, vL) = weibullParameterCovariance fit
-  in (sqrt (max 0 vK), sqrt (max 0 vL))
-
--- | (Var(k), Cov(k, λ), Var(λ))。 Fisher 情報行列の 2x2 逆行列。
---   非正定値の場合は (0, 0, 0) を返す (canvas 側で警告するための signal)。
-weibullParameterCovariance :: WeibullFit -> (Double, Double, Double)
-weibullParameterCovariance fit =
-  let (iKK, iKL, iLL) = wfFisher fit
-      det = iKK * iLL - iKL * iKL
-  in if det <= 0
-       then (0, 0, 0)
-       else (iLL / det, -iKL / det, iKK / det)
-
--- | B_p 寿命の Wald 信頼区間 (delta method)。
---
--- @bxLifeCI p α fit@ で「故障時間が確率 p に達する時刻」 の
--- 信頼度 @1 − α@ 信頼区間 (例: α = 0.05 で 95% CI) を返す。
---
--- delta method:
---
--- > Var(B_p) ≈ (∂B_p/∂k)² Var(k) + (∂B_p/∂λ)² Var(λ) + 2 (∂B_p/∂k)(∂B_p/∂λ) Cov(k,λ)
--- > ∂B_p/∂λ = B_p / λ
--- > ∂B_p/∂k = −B_p · log(−log(1−p)) / k²
---
--- 戻り値: @(estimate, lower, upper)@。 lower は max(0, ...) で 0 にクリップ
--- (寿命は非負)。 共分散が非正定値で SE 計算不能の場合は @(estimate, estimate, estimate)@。
---
--- 注: α は両側で考えるので 95% CI なら z = 1.96 を内部使用。
-bxLifeCI :: Double -> Double -> WeibullFit -> (Double, Double, Double)
-bxLifeCI p alpha fit =
-  let bp     = bxLife p fit
-      k      = wfShape fit
-      lam    = wfScale fit
-      (vK, cKL, vL) = weibullParameterCovariance fit
-      logArg = log (- log (1 - p))
-      dbdL   = bp / lam
-      dbdK   = - bp * logArg / (k * k)
-      varBp  = dbdK * dbdK * vK + dbdL * dbdL * vL + 2 * dbdK * dbdL * cKL
-      seBp   = if varBp > 0 then sqrt varBp else 0
-      z      = quantileNormal (1 - alpha / 2)
-      lo     = max 0 (bp - z * seBp)
-      hi     = bp + z * seBp
-  in (bp, lo, hi)
-
--- | 標準正規分布の分位点 (近似)。 95% CI で z = 1.959964…。
---   Acklam 高精度近似 (12 桁) を採用。
-quantileNormal :: Double -> Double
-quantileNormal q
-  | q <= 0 || q >= 1 = error "quantileNormal: q must be in (0, 1)"
-  | q < pLow = let qn = sqrt (-2 * log q) in
-      (((((cN1 * qn + cN2) * qn + cN3) * qn + cN4) * qn + cN5) * qn + cN6)
-      / ((((dN1 * qn + dN2) * qn + dN3) * qn + dN4) * qn + 1)
-  | q <= pHigh = let qn = q - 0.5; r = qn * qn in
-      ((((((aN1 * r + aN2) * r + aN3) * r + aN4) * r + aN5) * r + aN6) * qn)
-      / (((((bN1 * r + bN2) * r + bN3) * r + bN4) * r + bN5) * r + 1)
-  | otherwise = let qn = sqrt (-2 * log (1 - q)) in
-      negate $
-      (((((cN1 * qn + cN2) * qn + cN3) * qn + cN4) * qn + cN5) * qn + cN6)
-      / ((((dN1 * qn + dN2) * qn + dN3) * qn + dN4) * qn + 1)
-  where
-    pLow  = 0.02425
-    pHigh = 1 - pLow
-    aN1 = -3.969683028665376e1; aN2 =  2.209460984245205e2
-    aN3 = -2.759285104469687e2; aN4 =  1.383577518672690e2
-    aN5 = -3.066479806614716e1; aN6 =  2.506628277459239e0
-    bN1 = -5.447609879822406e1; bN2 =  1.615858368580409e2
-    bN3 = -1.556989798598866e2; bN4 =  6.680131188771972e1
-    bN5 = -1.328068155288572e1
-    cN1 = -7.784894002430293e-3; cN2 = -3.223964580411365e-1
-    cN3 = -2.400758277161838e0;  cN4 = -2.549732539343734e0
-    cN5 =  4.374664141464968e0;  cN6 =  2.938163982698783e0
-    dN1 =  7.784695709041462e-3; dN2 =  3.224671290700398e-1
-    dN3 =  2.445134137142996e0;  dN4 =  3.754408661907416e0
diff --git a/src/Hanalyze/Model/Wrappers.hs b/src/Hanalyze/Model/Wrappers.hs
--- a/src/Hanalyze/Model/Wrappers.hs
+++ b/src/Hanalyze/Model/Wrappers.hs
@@ -11,14 +11,27 @@
 -- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
 -- License     : BSD-3-Clause
 --
--- 描画ラッパ型 + smart constructor の plot 非依存層。
+-- [日本語]: 描画ラッパ型 + smart constructor の plot 非依存層。
 --
--- 'Hanalyze.Plot' (cabal flag @plot-integration@ 配下) が描画
--- ('VisualSpec' 化・'Plottable' instance) を担うのに対し、 本モジュールは
--- そこから **hgg に依存しない** 部分 (フィット済みモデルを束ねた
+-- 別パッケージ @hanalyze-plot@ の 'Hanalyze.Plot'
+-- (@cabal build --project-file=cabal.project.plot@ で build) が描画
+-- (@VisualSpec@ 化・@Plottable@ instance) を担うのに対し、 本モジュールは
+-- そこから __hgg に依存しない__ 部分 (フィット済みモデルを束ねた
 -- 描画ラッパ型と、 それを組み立てる smart constructor) のみを切り出したもの。
--- 非ゲート (常時 build) なので 'Hgg.Plot.*' を一切 import しない。
+-- 非ゲート (常時 build) なので 'Graphics.Hgg.*' を一切 import しない。
 -- 'Hanalyze.Plot' は本モジュールを import し従来の名前で再 export する。
+--
+-- [English]: Plot-independent layer of rendering wrapper types + smart
+-- constructors.
+--
+-- Whereas 'Hanalyze.Plot' (in the separate @hanalyze-plot@
+-- package, built via @cabal build --project-file=cabal.project.plot@)
+-- handles rendering (turning results into a @VisualSpec@, the @Plottable@
+-- instances), this module carves out only the __hgg-independent__
+-- part: the rendering wrapper types that bundle a fitted model,
+-- and the smart constructors that assemble them. It's ungated (always
+-- built), so it never imports 'Graphics.Hgg.*'. 'Hanalyze.Plot'
+-- imports this module and re-exports under the traditional names.
 module Hanalyze.Model.Wrappers
   ( -- * 多変量 effect plot のラッパ + smart ctor
     AlongSpec (..)
@@ -152,15 +165,20 @@
 -- 多変量 effect plot の along / HoldAgg
 -- ===========================================================================
 
--- | 多変量 effect plot で「動かす変数」 (along)。 'statModelMulti' の必須引数。
--- 型で単/多変量を分離し along 忘れをコンパイル時に弾く (§3 確定設計)。
+-- | [日本語]: 多変量 effect plot で「動かす変数」 (along)。 @statModelMulti@ の必須引数。
+--   型で単/多変量を分離し along 忘れをコンパイル時に弾く (§3 確定設計)。
+--   [English]: The "variable to vary" (along) in a multivariate effect plot;
+--   a required argument of @statModelMulti@. Separating single\/multivariate
+--   by type catches a forgotten along at compile time (confirmed design in
+--   §3).
 newtype AlongSpec = AlongSpec Text
 
--- | along 変数を指定する。 @statModelMulti m (along \"x1\")@。
+-- | [日本語]: along 変数を指定する。 @statModelMulti m (along \"x1\")@。
+--   [English]: Specifies the along variable. @statModelMulti m (along \"x1\")@.
 along :: Text -> AlongSpec
 along = AlongSpec
 
--- | 多変量 effect で along 以外の説明変数をどう固定するか (既定 'Mean')。
+-- | [日本語]: 多変量 effect で along 以外の説明変数をどう固定するか (既定 'Mean')。
 --
 --   * 'Mean' \/ 'Median' = 連続変数の集約 (factor 列は自動で 'Mode' に振替)。
 --   * 'Mode' = 最頻 (連続は丸め最頻、 factor は最頻水準)。
@@ -168,6 +186,20 @@
 --   * 'Marginalize' = 固定せず観測分布で周辺化 (PDP\/AME。 全観測行 × grid で重く、
 --     band は提供しない = 曲線のみ)。
 --   * 'Fixed' = 明示指定 (部分指定可。 指定の無い変数は 'Mean')。
+--   [English]: How to hold the explanatory variables other than along fixed
+--   in a multivariate effect (default 'Mean').
+--
+--   * 'Mean' \/ 'Median' = aggregation of continuous variables (factor
+--     columns automatically switch to 'Mode').
+--   * 'Mode' = most frequent (rounded mode for continuous, most frequent
+--     level for factors).
+--   * 'Reference' = the factor's reference level (first in ascending order;
+--     continuous switches to 'Mean').
+--   * 'Marginalize' = not fixed, marginalized over the observed distribution
+--     (PDP\/AME; heavy — all observed rows × grid — and provides no band,
+--     curve only).
+--   * 'Fixed' = explicit values (partial specification allowed; unspecified
+--     variables use 'Mean').
 data HoldAgg
   = Mean
   | Median
@@ -181,20 +213,37 @@
 -- 帯モード / 予測区間セレクタ
 -- ===========================================================================
 
--- | 帯モードセレクタ (Phase 70.F)。 出す帯を 1 つの値で選ぶ ('bandMode' で指定):
+-- | [日本語]: 帯モードセレクタ。 出す帯を 1 つの値で選ぶ (@bandMode@ で指定):
 --
 --   * @BandOff@  = 帯なし (曲線のみ)。
---   * @BandCI@   = 信頼区間のみ (平均 E[y|x] の不確実性・**既定**)。
+--   * @BandCI@   = 信頼区間のみ (平均 E[y|x] の不確実性・__既定__)。
 --   * @BandPI@   = 予測区間のみ (新規観測 1 点が入る区間・σ̂² を含むぶん広い)。
 --   * @BandCIPI@ = CI + PI を入れ子で重ねる (外=PI 薄・内=CI 濃・ファンチャート)。
 --
 -- PI 非提供モデル (Robust\/GAM\/Quantile\/非 Gaussian GLM\/effect plot) では
--- @BandPI@\/@BandCIPI@ は CI へフォールバックする ('svGridPI' = 'Nothing')。
+-- @BandPI@\/@BandCIPI@ は CI へフォールバックする (@svGridPI@ = 'Nothing')。
+--   [English]: Band mode selector. Choose which band to draw with a single
+--   value (specified via @bandMode@):
+--
+--   * @BandOff@  = no band (curve only).
+--   * @BandCI@   = confidence interval only (uncertainty of the mean
+--     E[y|x] — __default__).
+--   * @BandPI@   = prediction interval only (interval containing one new
+--     observation; wider since it includes σ̂²).
+--   * @BandCIPI@ = CI + PI overlaid nested (outer = PI, faint; inner = CI,
+--     bold; a "fan chart").
+--
+-- For models that don't provide a PI (Robust\/GAM\/Quantile\/non-Gaussian
+-- GLM\/effect plot), @BandPI@\/@BandCIPI@ fall back to the CI
+-- (@svGridPI@ = 'Nothing').
 data BandMode = BandOff | BandCI | BandPI | BandCIPI
   deriving (Eq, Show)
 
--- | 帯の算出法 (Phase 70.H・'piMethod' で指定)。 @PIClosedForm@ = 閉形式 (既定)、
+-- | [日本語]: 帯の算出法 (@piMethod@ で指定)。 @PIClosedForm@ = 閉形式 (既定)、
 --   @PIBootstrap seed draws@ = case-resampling ブートストラップ (seed 決定的・draws 回)。
+--   [English]: How the band is computed (specified via @piMethod@).
+--   @PIClosedForm@ = closed form (default); @PIBootstrap seed draws@ =
+--   case-resampling bootstrap (deterministic seed, draws repetitions).
 data PIMethod = PIClosedForm | PIBootstrap !Word32 !Int
   deriving (Eq, Show)
 
@@ -202,12 +251,13 @@
 -- 応答曲面オプション
 -- ===========================================================================
 
--- | 'surfaceOf' / 'surfaceGrid' のオプション。
+-- | [日本語]: @surfaceOf@ / @surfaceGrid@ のオプション。
+--   [English]: Options for @surfaceOf@ \/ @surfaceGrid@.
 data SurfaceOpts = SurfaceOpts
-  { soN      :: Int                     -- ^ 各軸の grid 点数 (既定 40)。
-  , soHoldAt :: HoldAgg                 -- ^ 他変数の固定方式 (既定 'Mean')。
-  , soXRange :: Maybe (Double, Double)  -- ^ v1 範囲 (既定 = 観測 min\/max)。
-  , soYRange :: Maybe (Double, Double)  -- ^ v2 範囲 (既定 = 観測 min\/max)。
+  { soN      :: Int                     -- ^ [日本語]: 各軸の grid 点数 (既定 40)。 [English]: Grid points per axis (default 40).
+  , soHoldAt :: HoldAgg                 -- ^ [日本語]: 他変数の固定方式 (既定 'Mean')。 [English]: How other variables are held fixed (default 'Mean').
+  , soXRange :: Maybe (Double, Double)  -- ^ [日本語]: v1 範囲 (既定 = 観測 min\/max)。 [English]: Range of v1 (default = observed min\/max).
+  , soYRange :: Maybe (Double, Double)  -- ^ [日本語]: v2 範囲 (既定 = 観測 min\/max)。 [English]: Range of v2 (default = observed min\/max).
   }
 
 defaultSurfaceOpts :: SurfaceOpts
@@ -218,20 +268,27 @@
 -- 多変量モデル型 (effect plot 用、 新規 fit)
 -- ===========================================================================
 
--- | formula + 'DataFrame' で fit した多変量線形モデル (effect plot 用)。
+-- | [日本語]: formula + @DataFrame@ で fit した多変量線形モデル (effect plot 用)。
+--   [English]: A multivariate linear model fit with a formula + @DataFrame@
+--   (for effect plots).
 data MultiLMModel = MultiLMModel
-  { mlmFormula :: Formula           -- ^ R\/独自 formula (評価点設計行列の組み立てに保持)。
-  , mlmFrame   :: ModelFrame        -- ^ 訓練 frame (along range + 他変数集約元)。
-  , mlmDesign  :: LA.Matrix Double  -- ^ 訓練設計行列 X ('confidenceBandAt' の分散核)。
-  , mlmResult  :: FitResult         -- ^ OLS 結果。
+  { mlmFormula :: Formula           -- ^ [日本語]: R\/独自 formula (評価点設計行列の組み立てに保持)。 [English]: R-style\/custom formula (kept for building the evaluation-point design matrix).
+  , mlmFrame   :: ModelFrame        -- ^ [日本語]: 訓練 frame (along range + 他変数集約元)。 [English]: Training frame (source of the along range + aggregation of other variables).
+  , mlmDesign  :: LA.Matrix Double  -- ^ [日本語]: 訓練設計行列 X (@confidenceBandAt@ の分散核)。 [English]: Training design matrix X (variance kernel for @confidenceBandAt@).
+  , mlmResult  :: FitResult         -- ^ [日本語]: OLS 結果。 [English]: OLS result.
   }
 
--- | formula 文字列 (例 @\"y ~ x1 + x2 + x3\"@) と 'DataFrame' から多変量 LM を組む。
+-- | [日本語]: formula 文字列 (例 @\"y ~ x1 + x2 + x3\"@) と @DataFrame@ から多変量 LM を組む。
+--   [English]: Builds a multivariate LM from a formula string (e.g.
+--   @\"y ~ x1 + x2 + x3\"@) and a @DataFrame@.
 multiLMModel :: Text -> DX.DataFrame -> Either String MultiLMModel
 multiLMModel fml df = parseModel fml >>= \f -> multiLMModelF f df
 
--- | 既に組み上げた 'Formula' (parse 済 or 'additiveFormula' で直接合成) から多変量 LM を
---   組む。 重回帰 spec ('lmMulti') が parse を経ずに使う経路 (Phase 70.D)。
+-- | [日本語]: 既に組み上げた 'Formula' (parse 済 or 'additiveFormula' で直接合成) から多変量 LM を
+--   組む。 重回帰 spec (@lmMulti@) が parse を経ずに使う経路。
+--   [English]: Builds a multivariate LM from an already-assembled 'Formula'
+--   (either parsed or directly composed via 'additiveFormula'). The path
+--   used by the multiple-regression spec (@lmMulti@) that skips parsing.
 multiLMModelF :: Formula -> DX.DataFrame -> Either String MultiLMModel
 multiLMModelF f df = do
   mf         <- modelFrame f df
@@ -239,22 +296,28 @@
   (res, _)   <- fitLMF f df
   Right MultiLMModel { mlmFormula = f, mlmFrame = mf, mlmDesign = x, mlmResult = res }
 
--- | formula + 'DataFrame' で fit した多変量 GLM (effect plot 用)。 帯は μ スケールで非対称。
+-- | [日本語]: formula + @DataFrame@ で fit した多変量 GLM (effect plot 用)。 帯は μ スケールで非対称。
+--   [English]: A multivariate GLM fit with a formula + @DataFrame@ (for
+--   effect plots). The band is asymmetric on the μ scale.
 data MultiGLMModel = MultiGLMModel
-  { mglmFormula :: Formula           -- ^ formula (評価点設計行列に保持)。
-  , mglmFrame   :: ModelFrame        -- ^ 訓練 frame。
-  , mglmResult  :: FitResult         -- ^ 'fitGLMFull' の結果 (β\/μ̂)。
-  , mglmSigma   :: LA.Matrix Double  -- ^ 逆 Fisher 情報 Σ (CI 用)。
-  , mglmFamily  :: Family            -- ^ 分布族。
-  , mglmLink    :: LinkFn            -- ^ リンク関数。
+  { mglmFormula :: Formula           -- ^ [日本語]: formula (評価点設計行列に保持)。 [English]: Formula (kept for the evaluation-point design matrix).
+  , mglmFrame   :: ModelFrame        -- ^ [日本語]: 訓練 frame。 [English]: Training frame.
+  , mglmResult  :: FitResult         -- ^ [日本語]: 'fitGLMFull' の結果 (β\/μ̂)。 [English]: Result of 'fitGLMFull' (β\/μ̂).
+  , mglmSigma   :: LA.Matrix Double  -- ^ [日本語]: 逆 Fisher 情報 Σ (CI 用)。 [English]: Inverse Fisher information Σ (for the CI).
+  , mglmFamily  :: Family            -- ^ [日本語]: 分布族。 [English]: Distribution family.
+  , mglmLink    :: LinkFn            -- ^ [日本語]: リンク関数。 [English]: Link function.
   }
 
--- | family\/link + formula 文字列 + 'DataFrame' から多変量 GLM を組む。
+-- | [日本語]: family\/link + formula 文字列 + @DataFrame@ から多変量 GLM を組む。
+--   [English]: Builds a multivariate GLM from a family\/link + formula
+--   string + @DataFrame@.
 multiGLMModel :: Family -> LinkFn -> Text -> DX.DataFrame -> Either String MultiGLMModel
 multiGLMModel family link fml df =
   parseModel fml >>= \f -> multiGLMModelF family link f df
 
--- | 既に組み上げた 'Formula' から多変量 GLM を組む (重回帰 spec 'glmMulti' 用・Phase 70.D)。
+-- | [日本語]: 既に組み上げた 'Formula' から多変量 GLM を組む (重回帰 spec @glmMulti@ 用)。
+--   [English]: Builds a multivariate GLM from an already-assembled 'Formula'
+--   (for the multiple-regression spec @glmMulti@).
 multiGLMModelF :: Family -> LinkFn -> Formula -> DX.DataFrame -> Either String MultiGLMModel
 multiGLMModelF family link f df = do
   mf     <- modelFrame f df
@@ -274,9 +337,14 @@
 -- (= @parseModel "y ~ x1 + … + xp"@ と同一 AST。 パラメータ名 @_p0.._pp@ も同じ規約)。
 -- ===========================================================================
 
--- | 応答列 @y@ と説明変数列名 @[x1,…,xp]@ から加法線形 'Formula' を直接合成する。
+-- | [日本語]: 応答列 @y@ と説明変数列名 @[x1,…,xp]@ から加法線形 'Formula' を直接合成する。
 --   RHS = @_p0 + _p1*x1 + … + _pp*xp@ (切片 + 各変数の主効果)。 数値列前提
---   (factor / 交互作用 / 変換が要るなら formula 版 'lmF' を使う)。
+--   (factor / 交互作用 / 変換が要るなら formula 版 @lmF@ を使う)。
+--   [English]: Directly composes an additive linear 'Formula' from a
+--   response column @y@ and explanatory variable column names
+--   @[x1,…,xp]@. RHS = @_p0 + _p1*x1 + … + _pp*xp@ (intercept + main effect
+--   of each variable). Assumes numeric columns (use the formula version
+--   @lmF@ if factors \/ interactions \/ transforms are needed).
 additiveFormula :: Text -> [Text] -> Formula
 additiveFormula y xs = Formula
   { formResponse = y
@@ -286,16 +354,20 @@
                            [1 :: Int ..] xs) }
   where tshowInt = T.pack . show
 
--- | formula + 'DataFrame' で fit した多変量ロバスト回帰 (effect plot 用)。
+-- | [日本語]: formula + @DataFrame@ で fit した多変量ロバスト回帰 (effect plot 用)。
+--   [English]: A multivariate robust regression fit with a formula +
+--   @DataFrame@ (for effect plots).
 data MultiRobustModel = MultiRobustModel
-  { mrmEstimator :: RobustEstimator   -- ^ Huber k or Tukey c。
-  , mrmFormula   :: Formula           -- ^ 評価点設計行列の組み立てに保持。
-  , mrmFrame     :: ModelFrame        -- ^ 訓練 frame (along range + 他変数集約元)。
-  , mrmDesign    :: LA.Matrix Double  -- ^ 訓練設計行列 X (サンドイッチ共分散の核)。
-  , mrmFit       :: RobustFit         -- ^ 'fitRobustLM' の結果 (β̂ / 重み / スケール)。
+  { mrmEstimator :: RobustEstimator   -- ^ [日本語]: Huber k or Tukey c。 [English]: Huber k or Tukey c.
+  , mrmFormula   :: Formula           -- ^ [日本語]: 評価点設計行列の組み立てに保持。 [English]: Kept for building the evaluation-point design matrix.
+  , mrmFrame     :: ModelFrame        -- ^ [日本語]: 訓練 frame (along range + 他変数集約元)。 [English]: Training frame (source of the along range + aggregation of other variables).
+  , mrmDesign    :: LA.Matrix Double  -- ^ [日本語]: 訓練設計行列 X (サンドイッチ共分散の核)。 [English]: Training design matrix X (kernel of the sandwich covariance).
+  , mrmFit       :: RobustFit         -- ^ [日本語]: 'fitRobustLM' の結果 (β̂ / 重み / スケール)。 [English]: Result of 'fitRobustLM' (β̂ \/ weights \/ scale).
   }
 
--- | 'Formula' + 'DataFrame' から多変量ロバスト回帰を組む (重回帰 spec 'robustMulti' 用)。
+-- | [日本語]: 'Formula' + @DataFrame@ から多変量ロバスト回帰を組む (重回帰 spec @robustMulti@ 用)。
+--   [English]: Builds a multivariate robust regression from a 'Formula' +
+--   @DataFrame@ (for the multiple-regression spec @robustMulti@).
 multiRobustModelF :: RobustEstimator -> Formula -> DX.DataFrame
                   -> Either String MultiRobustModel
 multiRobustModelF est f df = do
@@ -307,19 +379,27 @@
   Right MultiRobustModel { mrmEstimator = est, mrmFormula = f, mrmFrame = mf
                          , mrmDesign = x, mrmFit = fit }
 
--- | PLS の effect plot 用ラッパ (Phase 70.B2)。 'PLSFit' は列名/'ModelFrame' を
--- 持たないので、 'statModelMulti' (along/holdAt/byVar) を効かせるために訓練 frame と
+-- | [日本語]: PLS の effect plot 用ラッパ。 'PLSFit' は列名/'ModelFrame' を
+-- 持たないので、 @statModelMulti@ (along/holdAt/byVar) を効かせるために訓練 frame と
 -- 列順・出力選択を保持する。 'MultiLMModel' と同型 (frame-carrying wrapper)。
+--   [English]: Effect-plot wrapper for PLS. Since 'PLSFit' doesn't carry
+--   column names\/a 'ModelFrame', this holds the training frame, column
+--   order, and output selection so that @statModelMulti@
+--   (along\/holdAt\/byVar) can be used. Same shape as 'MultiLMModel' (a
+--   frame-carrying wrapper).
 data PLSModel = PLSModel
-  { plsmFit    :: !PLSFit       -- ^ 学習済 PLS。
-  , plsmFrame  :: !ModelFrame   -- ^ 訓練 frame (X 列 = 'RoleContinuous'・along range/hold の元)。
-  , plsmXNames :: ![Text]       -- ^ X 列名 ('predictPLS' へ渡す列順)。
-  , plsmYNames :: ![Text]       -- ^ Y 出力名 (出力セレクタ 'selectOutput' 用)。
-  , plsmOutIdx :: !Int          -- ^ effect plot に描く Y 出力列 index (既定 0)。
+  { plsmFit    :: !PLSFit       -- ^ [日本語]: 学習済 PLS。 [English]: The trained PLS.
+  , plsmFrame  :: !ModelFrame   -- ^ [日本語]: 訓練 frame (X 列 = 'RoleContinuous'・along range/hold の元)。 [English]: Training frame (X columns = 'RoleContinuous'; source of the along range\/hold).
+  , plsmXNames :: ![Text]       -- ^ [日本語]: X 列名 ('predictPLS' へ渡す列順)。 [English]: X column names (order passed to 'predictPLS').
+  , plsmYNames :: ![Text]       -- ^ [日本語]: Y 出力名 (出力セレクタ 'selectOutput' 用)。 [English]: Y output names (for the output selector 'selectOutput').
+  , plsmOutIdx :: !Int          -- ^ [日本語]: effect plot に描く Y 出力列 index (既定 0)。 [English]: Y output column index to plot in the effect plot (default 0).
   }
 
--- | 列名指定で PLS effect plot 用モデルを組む。 @plsModel cfg xcols ycols df@。
+-- | [日本語]: 列名指定で PLS effect plot 用モデルを組む。 @plsModel cfg xcols ycols df@。
 --   学習は 'fitPLS'、 frame は X 列を 'RoleContinuous' として手組み (応答ダミー)。
+--   [English]: Builds a model for PLS effect plots by specifying column
+--   names. @plsModel cfg xcols ycols df@. Trained via 'fitPLS'; the frame is
+--   hand-assembled with X columns as 'RoleContinuous' (dummy response).
 plsModel :: ColumnSource d
          => PLSConfig -> [Text] -> [Text] -> d -> Either String PLSModel
 plsModel cfg xcols ycols d = do
@@ -335,8 +415,12 @@
   Right PLSModel { plsmFit = fit, plsmFrame = mf, plsmXNames = xcols
                  , plsmYNames = ycols, plsmOutIdx = 0 }
 
--- | 描く Y 出力列を名前で選ぶ (多出力 PLS 用・既定は第 0 出力)。
+-- | [日本語]: 描く Y 出力列を名前で選ぶ (多出力 PLS 用・既定は第 0 出力)。
 --   @statModelMulti (selectOutput \"y2\" m) (along \"x1\")@。 名前が無ければ無変更。
+--   [English]: Selects, by name, which Y output column to plot (for
+--   multi-output PLS; default is output 0).
+--   @statModelMulti (selectOutput \"y2\" m) (along \"x1\")@. Leaves unchanged
+--   if the name isn't found.
 selectOutput :: Text -> PLSModel -> PLSModel
 selectOutput yname m =
   case lookup yname (zip (plsmYNames m) [0 ..]) of
@@ -347,32 +431,40 @@
 -- 線形モデル (描画可能)
 -- ===========================================================================
 
--- | X を束ねた描画可能な単回帰モデル。
+-- | [日本語]: X を束ねた描画可能な単回帰モデル。
+--   [English]: A plottable simple regression model bundling X.
 data LMModel = LMModel
-  { lmDesign :: LA.Matrix Double  -- ^ 設計行列 X @n × p@ (intercept 列含む)。
-  , lmResult :: FitResult         -- ^ 'fitLMVec' の結果 (β / ŷ / 残差 / R²)。
-  , lmXraw   :: LA.Vector Double  -- ^ 散布図 x 軸の生 predictor @n@ (単回帰の x)。
+  { lmDesign :: LA.Matrix Double  -- ^ [日本語]: 設計行列 X @n × p@ (intercept 列含む)。 [English]: Design matrix X, @n × p@ (including the intercept column).
+  , lmResult :: FitResult         -- ^ [日本語]: 'fitLMVec' の結果 (β / ŷ / 残差 / R²)。 [English]: Result of 'fitLMVec' (β \/ ŷ \/ residuals \/ R²).
+  , lmXraw   :: LA.Vector Double  -- ^ [日本語]: 散布図 x 軸の生 predictor @n@ (単回帰の x)。 [English]: Raw predictor for the scatter plot's x axis, length @n@ (the x of the simple regression).
   }
 
--- | 単回帰 @(x, y)@ から 'LMModel' を組む。 設計行列は @[1, x]@、 fit は 'fitLMVec'。
+-- | [日本語]: 単回帰 @(x, y)@ から 'LMModel' を組む。 設計行列は @[1, x]@、 fit は 'fitLMVec'。
+--   [English]: Builds an 'LMModel' from simple regression @(x, y)@. The
+--   design matrix is @[1, x]@, fit via 'fitLMVec'.
 lmModel :: LA.Vector Double -> LA.Vector Double -> LMModel
 lmModel xs ys =
   let dm  = designMatrix (V.fromList (LA.toList xs))  -- designMatrix は boxed Vector 入力
       res = fitLMVec dm ys
   in LMModel { lmDesign = dm, lmResult = res, lmXraw = xs }
 
--- | X と family/link を束ねた描画可能な単回帰 GLM。
+-- | [日本語]: X と family/link を束ねた描画可能な単回帰 GLM。
+--   [English]: A plottable simple-regression GLM bundling X and the
+--   family\/link.
 data GLMModel = GLMModel
-  { glmDesign :: LA.Matrix Double  -- ^ 設計行列 X @n × p@ (intercept 列含む)。
-  , glmResult :: FitResult         -- ^ 'fitGLMFull' の結果 (β / μ̂ / 残差)。
-  , glmSigma  :: LA.Matrix Double  -- ^ 逆 Fisher 情報 Σ=(XᵀWX)⁻¹ (CI 用)。
-  , glmFamily :: Family            -- ^ 分布族 (帯の意味付けに保持)。
-  , glmLink   :: LinkFn            -- ^ リンク関数 (μ スケールへの逆変換に必要)。
-  , glmXraw   :: LA.Vector Double  -- ^ 散布図 x 軸の生 predictor @n@ (単回帰の x)。
+  { glmDesign :: LA.Matrix Double  -- ^ [日本語]: 設計行列 X @n × p@ (intercept 列含む)。 [English]: Design matrix X, @n × p@ (including the intercept column).
+  , glmResult :: FitResult         -- ^ [日本語]: 'fitGLMFull' の結果 (β / μ̂ / 残差)。 [English]: Result of 'fitGLMFull' (β \/ μ̂ \/ residuals).
+  , glmSigma  :: LA.Matrix Double  -- ^ [日本語]: 逆 Fisher 情報 Σ=(XᵀWX)⁻¹ (CI 用)。 [English]: Inverse Fisher information Σ=(XᵀWX)⁻¹ (for the CI).
+  , glmFamily :: Family            -- ^ [日本語]: 分布族 (帯の意味付けに保持)。 [English]: Distribution family (kept for interpreting the band).
+  , glmLink   :: LinkFn            -- ^ [日本語]: リンク関数 (μ スケールへの逆変換に必要)。 [English]: Link function (needed for the inverse transform to the μ scale).
+  , glmXraw   :: LA.Vector Double  -- ^ [日本語]: 散布図 x 軸の生 predictor @n@ (単回帰の x)。 [English]: Raw predictor for the scatter plot's x axis, length @n@ (the x of the simple regression).
   }
 
--- | 単回帰 @(x, y)@ と family/link から 'GLMModel' を組む。 設計行列は @[1, x]@、
+-- | [日本語]: 単回帰 @(x, y)@ と family/link から 'GLMModel' を組む。 設計行列は @[1, x]@、
 -- fit は 'fitGLMFull' (FitResult と逆 Fisher 情報 Σ の両方を返す)。
+--   [English]: Builds a 'GLMModel' from simple regression @(x, y)@ and a
+--   family\/link. The design matrix is @[1, x]@, fit via 'fitGLMFull'
+--   (which returns both the FitResult and the inverse Fisher information Σ).
 glmModel :: Family -> LinkFn -> LA.Vector Double -> LA.Vector Double -> GLMModel
 glmModel family link xs ys =
   let dm           = designMatrix (V.fromList (LA.toList xs))
@@ -380,21 +472,29 @@
   in GLMModel { glmDesign = dm, glmResult = res, glmSigma = sigma
               , glmFamily = family, glmLink = link, glmXraw = xs }
 
--- | X (生 predictor) を束ねた描画可能なスプライン回帰モデル。
+-- | [日本語]: X (生 predictor) を束ねた描画可能なスプライン回帰モデル。
 --
--- 'SplineFit' は基底行列を保持しないので、 'confidenceBand' / 散布図用に生 x を別途
+-- 'SplineFit' は基底行列を保持しないので、 @confidenceBand@ / 散布図用に生 x を別途
 -- 束ねる (= LMModel と同型の「描画可能なモデル」 化)。
+--   [English]: A plottable spline regression model bundling X (the raw
+--   predictor).
+--
+-- Since 'SplineFit' doesn't keep the basis matrix, the raw x is bundled
+-- separately for @confidenceBand@ \/ the scatter plot (turning it into a
+-- "plottable model" of the same shape as LMModel).
 data SplineModel = SplineModel
-  { splFit  :: SplineFit          -- ^ 'fitSpline' の結果 (basis 係数 + 線形核)。
-  , splXraw :: LA.Vector Double   -- ^ 散布図 x 軸の生 predictor @n@。
+  { splFit  :: SplineFit          -- ^ [日本語]: 'fitSpline' の結果 (basis 係数 + 線形核)。 [English]: Result of 'fitSpline' (basis coefficients + linear kernel).
+  , splXraw :: LA.Vector Double   -- ^ [日本語]: 散布図 x 軸の生 predictor @n@。 [English]: Raw predictor for the scatter plot's x axis, length @n@.
   }
 
--- | @(x, y)@ と spline 種別・ノットから 'SplineModel' を組む。
+-- | [日本語]: @(x, y)@ と spline 種別・ノットから 'SplineModel' を組む。
+--   [English]: Builds a 'SplineModel' from @(x, y)@ and the spline
+--   kind\/knots.
 splineModel
-  :: SplineKind        -- ^ B-spline (次数) or 自然 3 次スプライン。
-  -> [Double]          -- ^ 内部ノット (境界含む)。
-  -> LA.Vector Double  -- ^ 説明変数 x。
-  -> LA.Vector Double  -- ^ 応答 y。
+  :: SplineKind        -- ^ [日本語]: B-spline (次数) or 自然 3 次スプライン。 [English]: B-spline (with degree) or natural cubic spline.
+  -> [Double]          -- ^ [日本語]: 内部ノット (境界含む)。 [English]: Interior knots (including boundaries).
+  -> LA.Vector Double  -- ^ [日本語]: 説明変数 x。 [English]: Explanatory variable x.
+  -> LA.Vector Double  -- ^ [日本語]: 応答 y。 [English]: Response y.
   -> SplineModel
 splineModel kind knots xs ys =
   let xsV = V.fromList (LA.toList xs)
@@ -402,19 +502,23 @@
       fit = fitSpline kind knots xsV ysV
   in SplineModel { splFit = fit, splXraw = xs }
 
--- | X (単一 predictor の生 x) を束ねた描画可能な単変量 GAM。
+-- | [日本語]: X (単一 predictor の生 x) を束ねた描画可能な単変量 GAM。
+--   [English]: A plottable univariate GAM bundling X (the raw single
+--   predictor).
 data GAMModel = GAMModel
-  { gamFit  :: GAMFit             -- ^ 'fitGAM' の結果 (基底係数 + fitted)。
-  , gamXraw :: LA.Vector Double   -- ^ 散布図 x 軸の生 predictor @n@ (単変量の x)。
+  { gamFit  :: GAMFit             -- ^ [日本語]: 'fitGAM' の結果 (基底係数 + fitted)。 [English]: Result of 'fitGAM' (basis coefficients + fitted values).
+  , gamXraw :: LA.Vector Double   -- ^ [日本語]: 散布図 x 軸の生 predictor @n@ (単変量の x)。 [English]: Raw predictor for the scatter plot's x axis, length @n@ (the univariate x).
   }
 
--- | 単変量 @(x, y)@ から 'GAMModel' を組む。 内部で 1 特徴の 'fitGAM' を呼ぶ。
+-- | [日本語]: 単変量 @(x, y)@ から 'GAMModel' を組む。 内部で 1 特徴の 'fitGAM' を呼ぶ。
+--   [English]: Builds a 'GAMModel' from a univariate @(x, y)@. Internally
+--   calls the single-feature 'fitGAM'.
 gamModel
-  :: Int               -- ^ B-spline 次数 (3 = cubic 推奨)。
-  -> Int               -- ^ 内部ノット数 (例 5)。
-  -> Double            -- ^ ridge 罰則 λ (0 で無効)。
-  -> LA.Vector Double  -- ^ 説明変数 x。
-  -> LA.Vector Double  -- ^ 応答 y。
+  :: Int               -- ^ [日本語]: B-spline 次数 (3 = cubic 推奨)。 [English]: B-spline degree (3 = cubic, recommended).
+  -> Int               -- ^ [日本語]: 内部ノット数 (例 5)。 [English]: Number of interior knots (e.g. 5).
+  -> Double            -- ^ [日本語]: ridge 罰則 λ (0 で無効)。 [English]: Ridge penalty λ (0 disables it).
+  -> LA.Vector Double  -- ^ [日本語]: 説明変数 x。 [English]: Explanatory variable x.
+  -> LA.Vector Double  -- ^ [日本語]: 応答 y。 [English]: Response y.
   -> GAMModel
 gamModel degree nKnots lambda xs ys =
   let xsV = V.fromList (LA.toList xs)
@@ -422,70 +526,90 @@
       fit = fitGAM degree nKnots lambda [xsV] ysV
   in GAMModel { gamFit = fit, gamXraw = xs }
 
--- | df|-> 由来の (多予測子) GAM。 第1予測子を描画軸にする。
+-- | [日本語]: df|-> 由来の (多予測子) GAM。 第1予測子を描画軸にする。
+--   [English]: A (multi-predictor) GAM originating from @df |->@. The first
+--   predictor is used as the plotting axis.
 data GAMModelN = GAMModelN
-  { gamNFit   :: GAMFit              -- ^ 'fitGAMAuto' の結果。
-  , gamNXraws :: [LA.Vector Double]  -- ^ 予測子ごとの訓練 x (列名順)。
-  , gamNNames :: [Text]             -- ^ 予測子名 (列名順)。
+  { gamNFit   :: GAMFit              -- ^ [日本語]: @fitGAMAuto@ の結果。 [English]: Result of @fitGAMAuto@.
+  , gamNXraws :: [LA.Vector Double]  -- ^ [日本語]: 予測子ごとの訓練 x (列名順)。 [English]: Training x per predictor (in column-name order).
+  , gamNNames :: [Text]             -- ^ [日本語]: 予測子名 (列名順)。 [English]: Predictor names (in column-name order).
   }
 
--- | X (生 predictor) を束ねた描画可能な単回帰ロバストモデル。
+-- | [日本語]: X (生 predictor) を束ねた描画可能な単回帰ロバストモデル。
+--   [English]: A plottable simple-regression robust model bundling X (the
+--   raw predictor).
 data RobustModel = RobustModel
-  { rmFit  :: RobustFit           -- ^ 'fitRobustLM' の結果 (β̂ / ŷ / 重み)。
-  , rmXraw :: LA.Vector Double    -- ^ 散布図 x 軸の生 predictor @n@ (単回帰の x)。
+  { rmFit  :: RobustFit           -- ^ [日本語]: 'fitRobustLM' の結果 (β̂ / ŷ / 重み)。 [English]: Result of 'fitRobustLM' (β̂ \/ ŷ \/ weights).
+  , rmXraw :: LA.Vector Double    -- ^ [日本語]: 散布図 x 軸の生 predictor @n@ (単回帰の x)。 [English]: Raw predictor for the scatter plot's x axis, length @n@ (the x of the simple regression).
   }
 
--- | 単回帰 @(x, y)@ と estimator から 'RobustModel' を組む。 設計行列は @[1, x]@、
+-- | [日本語]: 単回帰 @(x, y)@ と estimator から 'RobustModel' を組む。 設計行列は @[1, x]@、
 -- fit は 'fitRobustLM' (max 50 iter / tol 1e-6)。
+--   [English]: Builds a 'RobustModel' from simple regression @(x, y)@ and an
+--   estimator. The design matrix is @[1, x]@, fit via 'fitRobustLM' (max 50
+--   iterations \/ tolerance 1e-6).
 robustModel
-  :: RobustEstimator   -- ^ Huber k or Tukey c。
-  -> LA.Vector Double  -- ^ 説明変数 x。
-  -> LA.Vector Double  -- ^ 応答 y。
+  :: RobustEstimator   -- ^ [日本語]: Huber k or Tukey c。 [English]: Huber k or Tukey c.
+  -> LA.Vector Double  -- ^ [日本語]: 説明変数 x。 [English]: Explanatory variable x.
+  -> LA.Vector Double  -- ^ [日本語]: 応答 y。 [English]: Response y.
   -> RobustModel
 robustModel est xs ys =
   let dm  = designMatrix (V.fromList (LA.toList xs))
       fit = fitRobustLM est dm ys 50 1e-6
   in RobustModel { rmFit = fit, rmXraw = xs }
 
--- | X と複数 τ の fit を束ねた描画可能な分位点回帰モデル。
+-- | [日本語]: X と複数 τ の fit を束ねた描画可能な分位点回帰モデル。
+--   [English]: A plottable quantile regression model bundling X and fits for
+--   multiple τ.
 data QuantileModel = QuantileModel
-  { qmFits :: [(Double, QRFit)]   -- ^ (τ, その fit) の並び (τ 昇順を推奨)。
-  , qmXraw :: LA.Vector Double    -- ^ 散布図 x 軸の生 predictor @n@ (単回帰の x)。
+  { qmFits :: [(Double, QRFit)]   -- ^ [日本語]: (τ, その fit) の並び (τ 昇順を推奨)。 [English]: List of (τ, its fit) pairs (ascending τ order recommended).
+  , qmXraw :: LA.Vector Double    -- ^ [日本語]: 散布図 x 軸の生 predictor @n@ (単回帰の x)。 [English]: Raw predictor for the scatter plot's x axis, length @n@ (the x of the simple regression).
   }
 
--- | 単回帰 @(x, y)@ と分位水準 τ のリストから 'QuantileModel' を組む。 設計行列は @[1, x]@、
+-- | [日本語]: 単回帰 @(x, y)@ と分位水準 τ のリストから 'QuantileModel' を組む。 設計行列は @[1, x]@、
 -- 各 τ を 'fitQuantile' で fit。
+--   [English]: Builds a 'QuantileModel' from simple regression @(x, y)@ and
+--   a list of quantile levels τ. The design matrix is @[1, x]@; each τ is
+--   fit via 'fitQuantile'.
 quantileModel
-  :: [Double]          -- ^ 分位水準 τ ∈ (0,1) のリスト (例 [0.1, 0.5, 0.9])。
-  -> LA.Vector Double  -- ^ 説明変数 x。
-  -> LA.Vector Double  -- ^ 応答 y。
+  :: [Double]          -- ^ [日本語]: 分位水準 τ ∈ (0,1) のリスト (例 [0.1, 0.5, 0.9])。 [English]: List of quantile levels τ ∈ (0,1) (e.g. [0.1, 0.5, 0.9]).
+  -> LA.Vector Double  -- ^ [日本語]: 説明変数 x。 [English]: Explanatory variable x.
+  -> LA.Vector Double  -- ^ [日本語]: 応答 y。 [English]: Response y.
   -> QuantileModel
 quantileModel taus xs ys =
   let dm   = designMatrix (V.fromList (LA.toList xs))
       fits = [ (t, fitQuantile t dm ys) | t <- taus ]
   in QuantileModel { qmFits = fits, qmXraw = xs }
 
--- | 多変量 (重回帰) 分位点回帰の結果。 設計行列 @[1, x₁..xₚ]@ に各 τ で 'fitQuantile' を
+-- | [日本語]: 多変量 (重回帰) 分位点回帰の結果。 設計行列 @[1, x₁..xₚ]@ に各 τ で 'fitQuantile' を
 --   当てた fit 群を保持する。 係数は @qfBeta@ ('mqmFits' の各 'QRFit') で取り出す
---   (分位点回帰は SE を持たないため 'coefSummary' は非対応・単変量 'quantile' と一貫)。
+--   (分位点回帰は SE を持たないため @coefSummary@ は非対応・単変量 @quantile@ と一貫)。
+--   [English]: Result of multivariate (multiple-regression) quantile
+--   regression. Holds the fits obtained by applying 'fitQuantile' at each τ
+--   to the design matrix @[1, x₁..xₚ]@. Coefficients are extracted via
+--   @qfBeta@ (each 'QRFit' in 'mqmFits') — since quantile regression has no
+--   SE, @coefSummary@ isn't supported, consistent with the univariate
+--   @quantile@.
 data MultiQuantileModel = MultiQuantileModel
-  { mqmTaus  :: ![Double]            -- ^ 分位水準 τ の並び。
-  , mqmNames :: ![Text]             -- ^ 予測子名 (intercept を除く・設計行列の 2 列目以降と対応)。
-  , mqmFits  :: ![(Double, QRFit)]   -- ^ 各 τ の fit (係数 'qfBeta' = @[β₀, β₁, …, βₚ]@)。
-  , mqmX     :: !(LA.Matrix Double)  -- ^ 設計行列 @[1, x₁, …, xₚ]@ (effect plot の評価元)。
+  { mqmTaus  :: ![Double]            -- ^ [日本語]: 分位水準 τ の並び。 [English]: List of quantile levels τ.
+  , mqmNames :: ![Text]             -- ^ [日本語]: 予測子名 (intercept を除く・設計行列の 2 列目以降と対応)。 [English]: Predictor names (excluding the intercept; correspond to columns 2 onward of the design matrix).
+  , mqmFits  :: ![(Double, QRFit)]   -- ^ [日本語]: 各 τ の fit (係数 'qfBeta' = @[β₀, β₁, …, βₚ]@)。 [English]: Fit for each τ (coefficients 'qfBeta' = @[β₀, β₁, …, βₚ]@).
+  , mqmX     :: !(LA.Matrix Double)  -- ^ [日本語]: 設計行列 @[1, x₁, …, xₚ]@ (effect plot の評価元)。 [English]: Design matrix @[1, x₁, …, xₚ]@ (source for effect-plot evaluation).
   }
 
 -- ===========================================================================
 -- MCMC チェーン (描画可能)
 -- ===========================================================================
 
--- | 1 パラメータを選んだ描画可能な MCMC チェーン。
+-- | [日本語]: 1 パラメータを選んだ描画可能な MCMC チェーン。
+--   [English]: A plottable MCMC chain with one parameter selected.
 data ChainModel = ChainModel
-  { cmChain :: Chain   -- ^ サンプラ出力 (post-burn-in)。
-  , cmParam :: Text    -- ^ 描画対象のパラメータ名。
+  { cmChain :: Chain   -- ^ [日本語]: サンプラ出力 (post-burn-in)。 [English]: Sampler output (post-burn-in).
+  , cmParam :: Text    -- ^ [日本語]: 描画対象のパラメータ名。 [English]: Name of the parameter to plot.
   }
 
--- | パラメータ名と 'Chain' から 'ChainModel' を組む。
+-- | [日本語]: パラメータ名と 'Chain' から 'ChainModel' を組む。
+--   [English]: Builds a 'ChainModel' from a parameter name and a 'Chain'.
 chainModel :: Text -> Chain -> ChainModel
 chainModel name ch = ChainModel { cmChain = ch, cmParam = name }
 
@@ -493,28 +617,49 @@
 -- HBM (ベイズ確率プログラム) の学習 — Phase 49 A1
 -- ===========================================================================
 
--- | HBM 学習の設定。 NUTS の chain 数 / 本サンプル数 / warmup を保持する
+-- | [日本語]: HBM 学習の設定。 NUTS の chain 数 / 本サンプル数 / warmup を保持する
 -- (brms 既定 = 4 chains × 1000 draws + 1000 warmup に相当)。 'hbmSeed' は
 -- 純粋化 (将来の ST 版 @hbmModelPure seed …@) の継ぎ目として今から署名に持つ。
+--   [English]: Configuration for HBM training. Holds NUTS's chain count \/
+--   sample count \/ warmup (equivalent to brms's default: 4 chains × 1000
+--   draws + 1000 warmup). 'hbmSeed' is carried in the signature now as a
+--   seam for future purification (a future ST version,
+--   @hbmModelPure seed …@).
 data HBMConfig = HBMConfig
-  { hbmChains    :: !Int            -- ^ chain 数 (既定 4)。
-  , hbmSamples   :: !Int            -- ^ 本サンプル数 = post-warmup draws (既定 1000)。
-  , hbmWarmup    :: !Int            -- ^ warmup / burn-in (既定 1000)。
-  , hbmSeed      :: !(Maybe Word32) -- ^ 乱数シード (現状は IO 内で消費・将来 ST の継ぎ目)。
-  , hbmAdaptMass :: !Bool           -- ^ 対角質量行列の適応 (既定 True・brms/PyMC 同様)。
+  { hbmChains    :: !Int            -- ^ [日本語]: chain 数 (既定 4)。 [English]: Number of chains (default 4).
+  , hbmSamples   :: !Int            -- ^ [日本語]: 本サンプル数 = post-warmup draws (既定 1000)。 [English]: Number of samples = post-warmup draws (default 1000).
+  , hbmWarmup    :: !Int            -- ^ [日本語]: warmup / burn-in (既定 1000)。 [English]: Warmup \/ burn-in (default 1000).
+  , hbmSeed      :: !(Maybe Word32) -- ^ [日本語]: 乱数シード (現状は IO 内で消費・将来 ST の継ぎ目)。 [English]: Random seed (currently consumed inside IO; a future seam for ST).
+  , hbmAdaptMass :: !Bool           -- ^ [日本語]: 対角質量行列の適応 (既定 True・brms/PyMC 同様)。
                                      --   a/b と s のようにスケールが大きく異なる posterior で
                                      --   収束 (特に scale param) に必須。 OFF だと s が未収束に
-                                     --   なりやすい (Phase 52.A12 で計測確認)。
+                                     --   なりやすい (計測で確認済)。
+                                     --   [English]: Diagonal mass matrix adaptation (default True,
+                                     --   as in brms\/PyMC). Essential for convergence (especially
+                                     --   of the scale param) in posteriors where scales differ
+                                     --   greatly, such as a\/b vs. s. When OFF, s tends not to
+                                     --   converge (confirmed by measurement).
   , hbmWarmupInitMaxDepth :: !(Maybe Int)
-                                     -- ^ Phase 96 A5: 'nutsWarmupInitMaxDepth' の pass-through
+                                     -- ^ [日本語]: 'nutsWarmupInitMaxDepth' の pass-through
                                      --   (opt-in・既定 'Nothing' = 無効)。 質量行列の初回更新前
                                      --   (M=I 期間) の tree depth 上限。 warmup 初期の ε 鋸歯で
                                      --   deep tree を掘る浪費 (05-mh 実測で warmup evals が
                                      --   nutpie 比 1.92×) を 'Just' 6 等で抑制する。 参照実装に
                                      --   無いヒューリスティックゆえ既定 OFF (NUTS.hs 側と同判断)。
+                                     --   [English]: A pass-through for 'nutsWarmupInitMaxDepth'
+                                     --   (opt-in; default 'Nothing' = disabled). An upper bound on
+                                     --   tree depth during the period before the mass matrix's
+                                     --   first update (the M=I period). Suppresses the waste of
+                                     --   digging deep trees during early-warmup ε sawtoothing
+                                     --   (measured on 05-mh: warmup evals were 1.92× nutpie's)
+                                     --   with something like 'Just' 6. Since this heuristic isn't
+                                     --   in the reference implementation, it defaults to OFF
+                                     --   (matching the decision on the NUTS.hs side).
   } deriving (Show, Eq)
 
--- | 既定の HBM 設定: 4 chains × 1000 draws + 1000 warmup (brms 既定相当)。 質量行列適応 ON。
+-- | [日本語]: 既定の HBM 設定: 4 chains × 1000 draws + 1000 warmup (brms 既定相当)。 質量行列適応 ON。
+--   [English]: Default HBM configuration: 4 chains × 1000 draws + 1000
+--   warmup (equivalent to brms's default). Mass matrix adaptation ON.
 defaultHBM :: HBMConfig
 defaultHBM = HBMConfig
   { hbmChains    = 4
@@ -525,39 +670,67 @@
   , hbmWarmupInitMaxDepth = Nothing
   }
 
--- | 学習済 HBM モデル。 data placeholder を bind したモデル本体 ('hbmModelSpec')、
+-- | [日本語]: 学習済 HBM モデル。 data placeholder を bind したモデル本体 (@hbmModelSpec@)、
 -- posterior draws ('hbmChainsR' = chain 群)、 bind 済みデータ ('hbmData') を保持する。
--- ★ 抽出子 ('epred' 等) はここから純粋に図を組む (= @df |>>@ と整合)。
+-- ★ 抽出子 (@epred@ 等) はここから純粋に図を組む (= @df |>>@ と整合)。
+--   [English]: A trained HBM model. Holds the model body with data
+--   placeholders bound (@hbmModelSpec@), the posterior draws ('hbmChainsR' =
+--   the chains), and the bound data ('hbmData').
+--   Note: extractors (e.g. @epred@) build plots purely from this (consistent
+--   with @df |>>@).
 data HBMModel = HBMModel
-  { hbmModelSpec :: ModelP ()          -- ^ data を bind 済みのモデル (epred 評価でも使う)。
-  , hbmChainsR   :: ![Chain]           -- ^ posterior draws (chain 群)。
-  , hbmData      :: ![(Text, [Double])] -- ^ bind 済みデータ列 (列名 → 値)。
+  { hbmModelSpec :: ModelP ()          -- ^ [日本語]: data を bind 済みのモデル (epred 評価でも使う)。 [English]: The model with data already bound (also used for epred evaluation).
+  , hbmChainsR   :: ![Chain]           -- ^ [日本語]: posterior draws (chain 群)。 [English]: Posterior draws (the chains).
+  , hbmData      :: ![(Text, [Double])] -- ^ [日本語]: bind 済みデータ列 (列名 → 値)。 [English]: Bound data columns (column name → values).
   , hbmFactorLevels :: ![(Text, [Text])]
-    -- ^ Phase 60.3: 'dataNamedIx' slot に Text factor 列を bind した場合の
+    -- ^ [日本語]: @dataNamedIx@ slot に Text factor 列を bind した場合の
     --   sort 順 levels (slot 名 → levels)。 コード i = levels !! i で、
     --   indexed パラメータ (b0_2 等) がどの群かを引ける。 数値列 bind は空。
+    --   [English]: When a Text factor column is bound to a @dataNamedIx@
+    --   slot, this holds the sorted levels (slot name → levels). Code i =
+    --   levels !! i lets you look up which group an indexed parameter
+    --   (e.g. b0_2) belongs to. Empty for numeric-column binds.
   }
 
--- | 列名→値の組をモデル中の data placeholder に順に 'withData' で bind する。
+-- | [日本語]: 列名→値の組をモデル中の data placeholder に順に 'withData' で bind する。
 -- 明示再帰 (foldr ではなく) なのは、 'ModelP' が rank-2 多相 ('forall a.') ゆえ
 -- ImpredicativeTypes 下の foldr では accumulator が単相化してしまうため。
+--   [English]: Binds column-name → value pairs in order to the model's data
+--   placeholders via 'withData'. Explicit recursion (rather than foldr) is
+--   used because 'ModelP' is rank-2 polymorphic ('forall a.'), and under
+--   ImpredicativeTypes a foldr would cause the accumulator to be
+--   monomorphized.
 bindCols :: [(Text, [Double])] -> ModelP r -> ModelP r
 bindCols []             m = m
 bindCols ((n, vs):rest) m = bindCols rest (withData n vs m)
 
--- | 'bindCols' の 'DataIx' 版 (Phase 60.3): 列名→index 列を 'withDataIx' で bind。
+-- | [日本語]: 'bindCols' の @DataIx@ 版: 列名→index 列を 'withDataIx' で bind。
+--   [English]: The @DataIx@ version of 'bindCols': binds column-name →
+--   index-column pairs via 'withDataIx'.
 bindIxCols :: [(Text, [Int])] -> ModelP r -> ModelP r
 bindIxCols []             m = m
 bindIxCols ((n, is):rest) m = bindIxCols rest (withDataIx n is m)
 
--- | 確率プログラム 'ModelP' を NUTS で学習し 'HBMModel' にする (MCMC ゆえ IO)。
+-- | [日本語]: 確率プログラム 'ModelP' を NUTS で学習し 'HBMModel' にする (MCMC ゆえ IO)。
 --
--- 列名で 'withData' を畳み込み、 モデル中の placeholder ('dataNamed' / observe の
+-- 列名で 'withData' を畳み込み、 モデル中の placeholder (@dataNamed@ / observe の
 -- 参照名) を df 由来の実データに差し替える (PyMC @set_data@ 同型)。 chain は既存
 -- 'nutsChains' が並列実行 (実 OS スレッド並列には @-threaded +RTS -N@ が要る)。
 --
 -- 当面の入口は @[(Text,[Double])]@ (列名→値)。 'ColumnSource' 一般化
 -- (Map/DataFrame/assoc 疎結合) は別 Phase (データ API)。
+--   [English]: Trains a probabilistic program 'ModelP' via NUTS into an
+--   'HBMModel' (IO, since this is MCMC).
+--
+-- Folds 'withData' over the column names, replacing the model's
+-- placeholders (the reference names of @dataNamed@ \/ observe) with real
+-- data sourced from the df (the same shape as PyMC's @set_data@). Chains
+-- are run in parallel via the existing 'nutsChains' (actual OS-thread
+-- parallelism requires @-threaded +RTS -N@).
+--
+-- The entry point for now is @[(Text,[Double])]@ (column name → values).
+-- Generalizing 'ColumnSource' (loosely coupled Map\/DataFrame\/assoc) is a
+-- separate Phase (the data API).
 hbmModel :: HBMConfig -> ModelP () -> [(Text, [Double])] -> IO HBMModel
 hbmModel cfg model dat = do
   let bound :: ModelP ()
@@ -575,9 +748,14 @@
     , hbmFactorLevels = []
     }
 
--- | NUTS の初期点 (制約空間)。 positive 制約 (σ 等) を 0 で初期化すると @log 0 = -∞@ で
--- 初手から全 proposal が divergence する (実測 2026-06-05)。 'getTransforms' で制約を検出し
+-- | [日本語]: NUTS の初期点 (制約空間)。 positive 制約 (σ 等) を 0 で初期化すると @log 0 = -∞@ で
+-- 初手から全 proposal が divergence する (実測 2026-06-05)。 @getTransforms@ で制約を検出し
 -- PositiveT→1 / UnitIntervalT→0.5 / 他→0 で初期化する。
+--   [English]: NUTS's initial point (in constrained space). Initializing a
+--   positive-constrained parameter (e.g. σ) at 0 causes @log 0 = -∞@, making
+--   every proposal diverge from the very first step (measured 2026-06-05).
+--   Detects constraints via @getTransforms@ and initializes: PositiveT→1,
+--   UnitIntervalT→0.5, others→0.
 hbmInitPoint :: ModelP () -> Map.Map Text Double
 hbmInitPoint bound = Map.map initFor (getTransforms bound)
   where
@@ -585,8 +763,11 @@
     initFor UnitIntervalT  = 0.5
     initFor UnconstrainedT = 0.0
 
--- | 'HBMConfig' → 'NUTSConfig'。 NUTS は @total = burnIn + iterations@ を回し iterations 本
+-- | [日本語]: 'HBMConfig' → 'NUTSConfig'。 NUTS は @total = burnIn + iterations@ を回し iterations 本
 -- だけ保持するので、 iterations に本サンプル数・burnIn に warmup を割り当てる。
+--   [English]: 'HBMConfig' → 'NUTSConfig'. NUTS runs
+--   @total = burnIn + iterations@ and keeps only iterations of them, so the
+--   sample count is assigned to iterations and warmup to burnIn.
 hbmNutsConfig :: HBMConfig -> NUTSConfig
 hbmNutsConfig cfg = defaultNUTSConfig
   { nutsIterations = hbmSamples cfg
@@ -600,15 +781,24 @@
   -- 明示的に効かせたい呼び出し側が個別に nutsInitJitter を上げる。
   }
 
--- | 純粋・決定的な HBM 学習 (Phase 50.4)。 'hbmModel' の ST/seed 版で IO を持たない。
--- 'nutsChainsPure' (chain 横断を spark 並列・seed で再現可能) を使う。 @hbmSeed@ が
+-- | [日本語]: 純粋・決定的な HBM 学習。 'hbmModel' の ST/seed 版で IO を持たない。
+-- @nutsChainsPure@ (chain 横断を spark 並列・seed で再現可能) を使う。 @hbmSeed@ が
 -- 'Nothing' のときは固定既定 seed (42) を用いる (純粋・決定的を保証する設計判断)。
+--   [English]: Pure, deterministic HBM training. An ST\/seed version of
+--   'hbmModel' with no IO. Uses @nutsChainsPure@ (spark-parallel across
+--   chains, reproducible via seed). When @hbmSeed@ is 'Nothing', a fixed
+--   default seed (42) is used (a design decision to guarantee pure,
+--   deterministic behavior).
 hbmModelPure :: HBMConfig -> ModelP () -> [(Text, [Double])] -> HBMModel
 hbmModelPure cfg model dat = hbmModelPureWith cfg model dat [] []
 
--- | 'hbmModelPure' の拡張形 (Phase 60.3): 'DataIx' slot の index 列と
+-- | [日本語]: 'hbmModelPure' の拡張形: @DataIx@ slot の index 列と
 -- Text factor levels も bind する。 'df |-> hbm' ('Fit' instance) が
--- 'resolveIxSlots' で解決した結果を渡す主経路。
+-- @resolveIxSlots@ で解決した結果を渡す主経路。
+--   [English]: An extended form of 'hbmModelPure': also binds @DataIx@
+--   slots' index columns and Text factor levels. The main path used when
+--   @df |-> hbm@ (the 'Fit' instance) passes results resolved by
+--   @resolveIxSlots@.
 hbmModelPureWith :: HBMConfig -> ModelP () -> [(Text, [Double])]
                  -> [(Text, [Int])] -> [(Text, [Text])] -> HBMModel
 hbmModelPureWith cfg model dat ixDat levels =
@@ -625,15 +815,23 @@
        , hbmFactorLevels = levels
        }
 
--- | 'hbmModelPure' の IO 版 (Phase 61.3): stderr に進捗を表示しながら学習する。
+-- | [日本語]: 'hbmModelPure' の IO 版: stderr に進捗を表示しながら学習する。
 -- bind + seed 規約は 'hbmModelPureWith' と同一・chain ごとの seed は
--- 'chainSeeds' 共有 ('nutsChainsStream') ゆえ、 結果は同 cfg の
--- 'hbmModelPure' と**ビット一致**する (test-plot で固定)。
+-- @chainSeeds@ 共有 ('nutsChainsStream') ゆえ、 結果は同 cfg の
+-- 'hbmModelPure' と__ビット一致__する (test-plot で固定)。
+--   [English]: The IO version of 'hbmModelPure': trains while showing
+--   progress on stderr. The bind + seed convention is identical to
+--   'hbmModelPureWith'; since per-chain seeds are shared via @chainSeeds@
+--   ('nutsChainsStream'), the result is __bit-identical__ to 'hbmModelPure'
+--   with the same cfg (pinned down in test-plot).
 hbmModelIO :: HBMConfig -> ModelP () -> [(Text, [Double])] -> IO HBMModel
 hbmModelIO cfg model dat = hbmModelIOWith cfg model dat [] []
 
--- | 'hbmModelPureWith' の IO + 進捗表示版 (Phase 61.3)。 '(|->!)' の
--- HBM 経路 ('fitIO') が 'resolveIxSlots' の解決結果を渡す主経路。
+-- | [日本語]: 'hbmModelPureWith' の IO + 進捗表示版。 @(|->!)@ の
+-- HBM 経路 ('fitIO') が @resolveIxSlots@ の解決結果を渡す主経路。
+--   [English]: The IO + progress-display version of 'hbmModelPureWith'. The
+--   main path used when the HBM path of @(|->!)@ ('fitIO') passes results
+--   resolved by @resolveIxSlots@.
 hbmModelIOWith :: HBMConfig -> ModelP () -> [(Text, [Double])]
                -> [(Text, [Int])] -> [(Text, [Text])] -> IO HBMModel
 hbmModelIOWith cfg model dat ixDat levels = do
@@ -657,16 +855,23 @@
 -- HBM 事後要約 (Phase 103)
 -- ===========================================================================
 
--- | 要約対象のパラメタ名 (latent 宣言順 → deterministic 宣言順の連結)。
+-- | [日本語]: 要約対象のパラメタ名 (latent 宣言順 → deterministic 宣言順の連結)。
 -- deterministic 派生量を既定で含めるのは PyMC/arviz の @az.summary@ が
--- Deterministic を含むのと同型 (Phase 103 A1 確定)。
+-- Deterministic を含むのと同型。
+--   [English]: Parameter names to summarize (declaration order of latent
+--   variables followed by declaration order of deterministic ones).
+--   Including deterministic derived quantities by default matches how
+--   PyMC\/arviz's @az.summary@ includes Deterministic.
 hbmSummaryNames :: HBMModel -> [Text]
 hbmSummaryNames m = sampleNames spec ++ deterministicNames spec
   where spec :: ModelP ()
         spec = hbmModelSpec m
 
--- | deterministic 派生量を注入済みの chain 群。派生量が無いモデルでは
+-- | [日本語]: deterministic 派生量を注入済みの chain 群。派生量が無いモデルでは
 -- augment (全 draw の再評価) を省いて素の chain を返す。
+--   [English]: The chains with deterministic derived quantities injected.
+--   For models with no derived quantities, skips the augment step
+--   (re-evaluating every draw) and returns the plain chains.
 hbmAugmentedChains :: HBMModel -> [Chain]
 hbmAugmentedChains m
   | null (deterministicNames spec) = hbmChainsR m
@@ -674,18 +879,25 @@
   where spec :: ModelP ()
         spec = hbmModelSpec m
 
--- | 学習済 HBM の事後要約表 (@az.summary@ 相当・純粋)。
+-- | [日本語]: 学習済 HBM の事後要約表 (@az.summary@ 相当・純粋)。
 -- mean / sd / HDI / ess_bulk (+ multi-chain 時 r_hat) を latent +
 -- deterministic の全パラメタについて返す。
+--   [English]: Posterior summary table of a trained HBM (equivalent to
+--   @az.summary@; pure). Returns mean \/ sd \/ HDI \/ ess_bulk (+ r_hat for
+--   multi-chain) for all latent and deterministic parameters.
 hbmSummary :: HBMModel -> [SummaryRow]
 hbmSummary m = posteriorSummary (hbmSummaryNames m) (hbmAugmentedChains m)
 
--- | 'hbmSummary' をコンソール表として表示する。
+-- | [日本語]: 'hbmSummary' をコンソール表として表示する。
+--   [English]: Displays 'hbmSummary' as a console table.
 printHBMSummary :: HBMModel -> IO ()
 printHBMSummary m = printPosteriorSummary (hbmSummaryNames m) (hbmAugmentedChains m)
 
--- | 'hbmSummary' の DataFrame 化。列 = param / mean / sd / hdi_lo / hdi_hi /
+-- | [日本語]: 'hbmSummary' の DataFrame 化。列 = param / mean / sd / hdi_lo / hdi_hi /
 -- ess_bulk (+ multi-chain 時のみ r_hat = 'printPosteriorSummary' の列規約と同じ)。
+--   [English]: Turns 'hbmSummary' into a DataFrame. Columns = param \/ mean \/
+--   sd \/ hdi_lo \/ hdi_hi \/ ess_bulk (+ r_hat only for multi-chain — the
+--   same column convention as 'printPosteriorSummary').
 hbmSummaryDf :: HBMModel -> DX.DataFrame
 hbmSummaryDf m =
   let rows  = hbmSummary m
@@ -702,9 +914,14 @@
               | multi ]
   in DX.fromNamedColumns (base ++ rh)
 
--- | 事後 draw の DataFrame 化 (1 パラメタ = 1 列・全 chain を chain 順に連結、
+-- | [日本語]: 事後 draw の DataFrame 化 (1 パラメタ = 1 列・全 chain を chain 順に連結、
 -- deterministic 派生量込み)。'Hanalyze.Data.Wrangle' の
 -- @summarise@ / @groupBy@ 等で自由集計する入口。
+--   [English]: Turns the posterior draws into a DataFrame (one parameter =
+--   one column, all chains concatenated in chain order, including
+--   deterministic derived quantities). An entry point for free-form
+--   aggregation via 'Hanalyze.Data.Wrangle''s @summarise@ \/
+--   @groupBy@ etc.
 hbmDrawsDf :: HBMModel -> DX.DataFrame
 hbmDrawsDf m =
   let chains = hbmAugmentedChains m
@@ -716,18 +933,22 @@
 -- 時系列予測 (描画可能)
 -- ===========================================================================
 
--- | 履歴系列と AR fit・予測地平を束ねた描画可能な時系列予測モデル。
+-- | [日本語]: 履歴系列と AR fit・予測地平を束ねた描画可能な時系列予測モデル。
+--   [English]: A plottable time-series forecast model bundling the history
+--   series, the AR fit, and the forecast horizon.
 data ForecastModel = ForecastModel
-  { fmFit     :: ARFit             -- ^ 'fitAR' の結果。
-  , fmHistory :: LA.Vector Double  -- ^ 観測系列 (時系列順)。
-  , fmHorizon :: Int               -- ^ 予測地平 h。
+  { fmFit     :: ARFit             -- ^ [日本語]: 'fitAR' の結果。 [English]: Result of 'fitAR'.
+  , fmHistory :: LA.Vector Double  -- ^ [日本語]: 観測系列 (時系列順)。 [English]: Observed series (in time order).
+  , fmHorizon :: Int               -- ^ [日本語]: 予測地平 h。 [English]: Forecast horizon h.
   }
 
--- | 系列・AR 次数・地平から 'ForecastModel' を組む ('fitAR' で fit)。
+-- | [日本語]: 系列・AR 次数・地平から 'ForecastModel' を組む ('fitAR' で fit)。
+--   [English]: Builds a 'ForecastModel' from the series, AR order, and
+--   horizon (fit via 'fitAR').
 forecastModel
-  :: Int               -- ^ AR 次数 p。
-  -> Int               -- ^ 予測地平 h。
-  -> LA.Vector Double  -- ^ 観測系列 (時系列順)。
+  :: Int               -- ^ [日本語]: AR 次数 p。 [English]: AR order p.
+  -> Int               -- ^ [日本語]: 予測地平 h。 [English]: Forecast horizon h.
+  -> LA.Vector Double  -- ^ [日本語]: 観測系列 (時系列順)。 [English]: Observed series (in time order).
   -> ForecastModel
 forecastModel order horizon series =
   ForecastModel { fmFit = fitAR order series, fmHistory = series
@@ -737,57 +958,103 @@
 -- データ源 → モデル を当てはめる統一型クラス。
 -- ===========================================================================
 
--- | データ源 → モデル を当てはめる統一型クラス。
+-- | [日本語]: データ源 → モデル を当てはめる統一型クラス。
 --
--- 'fitWith' / '(|->)' は **pure だが total ではない** (列欠落・parse 失敗は
+-- @fitWith@ / @(|->)@ は __pure だが total ではない__ (列欠落・parse 失敗は
 -- 'error')。 検証パイプライン用に total な 'fitEither' を併設する
--- (既定の 'fitWith' は 'fitEither' を 'error' で潰したもの)。
+-- (既定の @fitWith@ は 'fitEither' を 'error' で潰したもの)。
+--   [English]: A unified type class for fitting a data source → a model.
+--
+-- @fitWith@ \/ @(|->)@ are __pure but not total__ (missing columns \/ parse
+-- failures raise 'error'). A total 'fitEither' is provided alongside for
+-- validation pipelines (the default @fitWith@ is 'fitEither' collapsed via
+-- 'error').
 class Fit spec where
-  -- | この spec を当てはめた結果のモデル型。
+  -- | [日本語]: この spec を当てはめた結果のモデル型。
+  --   [English]: The resulting model type when this spec is fitted.
   type Fitted spec
-  -- | 当てはめ (pure・失敗は 'error')。 既定実装は 'fitEither' 経由。
+  -- | [日本語]: 当てはめ (pure・失敗は 'error')。 既定実装は 'fitEither' 経由。
+  --   [English]: Fit (pure; failure raises 'error'). The default
+  --   implementation goes through 'fitEither'.
   fitWith   :: ColumnSource d => spec -> d -> Fitted spec
   fitWith spec d = either error id (fitEither spec d)
-  -- | 当てはめ (total・失敗は 'Left')。
+  -- | [日本語]: 当てはめ (total・失敗は 'Left')。
+  --   [English]: Fit (total; failure is 'Left').
   fitEither :: ColumnSource d => spec -> d -> Either String (Fitted spec)
-  -- | 当てはめ (IO・進捗表示など副作用つき学習・Phase 61.4)。 既定 =
-  -- @pure . fitWith@ で純粋 spec は挙動不変 (失敗の error 意味論も '(|->)'
-  -- と同じ)。 学習が重い spec ('HBMSpec') だけ override して進捗を出す。
+  -- | [日本語]: 当てはめ (IO・進捗表示など副作用つき学習)。 既定 =
+  -- @pure . fitWith@ で純粋 spec は挙動不変 (失敗の error 意味論も @(|->)@
+  -- と同じ)。 学習が重い spec (@HBMSpec@) だけ override して進捗を出す。
+  --   [English]: Fit (IO; training with side effects such as progress
+  --   display). Default = @pure . fitWith@, so pure specs are unchanged in
+  --   behavior (the error semantics of failure also match @(|->)@). Only
+  --   specs with heavy training (@HBMSpec@) override this to show progress.
   fitIO     :: ColumnSource d => spec -> d -> IO (Fitted spec)
   fitIO spec d = pure (fitWith spec d)
-  -- | 透過標準化ラッパ ('standardized' / 'standardizedY') が **標準化対象とする
-  -- 予測子列名** (Phase 70.3 項目 C)。 既定は @[]@ = 「被せる意味の無い spec」
-  -- であり、 'standardized' を付けても 'fitEither' が 'Left' で誤用を弾く。
+  -- | [日本語]: 透過標準化ラッパ (@standardized@ / @standardizedY@) が __標準化対象とする予測子列名__
+  -- 既定は @[]@ = 「被せる意味の無い spec」
+  -- であり、 @standardized@ を付けても 'fitEither' が 'Left' で誤用を弾く。
   -- 距離ベース (kNN) や線形 (整形目的) の spec だけが実列名を返す。 内部標準化済
-  -- ('GPSpec' \/ 'RegSpec' \/ 'PCASpec' \/ 'PLSSpec') と木系は二重標準化\/無意味回避で
+  -- (@GPSpec@ \/ @RegSpec@ \/ @PCASpec@ \/ @PLSSpec@) と木系は二重標準化\/無意味回避で
   -- 既定 @[]@ のまま (= ラッパ拒否)。
+  --   [English]: The __predictor column names to standardize__ for the
+  --   transparent standardization wrapper (@standardized@ \/
+  --   @standardizedY@). The default is @[]@ ("a spec for which wrapping is
+  --   meaningless"), so even if @standardized@ is applied, 'fitEither'
+  --   rejects the misuse with 'Left'. Only distance-based (kNN) or linear
+  --   (for shaping purposes) specs return real column names. Specs that are
+  --   already internally standardized (@GPSpec@ \/ @RegSpec@ \/ @PCASpec@ \/
+  --   @PLSSpec@) and tree-based ones stay at the default @[]@ to avoid
+  --   double standardization \/ meaninglessness (i.e. the wrapper is
+  --   rejected).
   predictorCols :: spec -> [Text]
   predictorCols _ = []
-  -- | 透過標準化ラッパが y も標準化する ('standardizedY') 際の**応答列名**
-  -- (Phase 70.3 項目 C)。 既定 'Nothing'。 **連続応答の回帰 spec のみ** @Just@ を返す。
+  -- | [日本語]: 透過標準化ラッパが y も標準化する (@standardizedY@) 際の__応答列名__
+  -- 既定 'Nothing'。 __連続応答の回帰 spec のみ__ @Just@ を返す。
   -- 分類 (クラスラベル) や family\/link でスケールが拘束される GLM は標準化が不正ゆえ
-  -- 'Nothing' のまま (= 'standardizedY' を付けると 'fitEither' が 'Left')。
+  -- 'Nothing' のまま (= @standardizedY@ を付けると 'fitEither' が 'Left')。
+  --   [English]: The __response column name__ used when the transparent
+  --   standardization wrapper also standardizes y (@standardizedY@).
+  --   Default 'Nothing'. __Only regression specs with a continuous response__
+  --   return @Just@. Classification (class labels) and GLMs
+  --   whose scale is constrained by the family\/link keep 'Nothing', since
+  --   standardizing them is invalid (i.e. applying @standardizedY@ makes
+  --   'fitEither' return 'Left').
   responseCol :: spec -> Maybe Text
   responseCol _ = Nothing
 
--- | 因果探索 (LiNGAM) の高レベル @df |->@ 結果ラッパ (Phase 77)。 各 LiNGAM fit 型は
---   変数名を持たないため、 学習した fit @a@ に**変数名** (@df |->@ が渡した列名) を添える。
---   'Plottable' (@Hanalyze.Plot.ML@) が @lfNames@ を DAG ノード名に使う
+-- | [日本語]: 因果探索 (LiNGAM) の高レベル @df |->@ 結果ラッパ。 各 LiNGAM fit 型は
+--   変数名を持たないため、 学習した fit @a@ に__変数名__ (@df |->@ が渡した列名) を添える。
+--   @Plottable@ (@Hanalyze.Plot.ML@) が @lfNames@ を DAG ノード名に使う
 --   (無ければ @x0..@ フォールバック)。 侵襲的な per-fit-型 names フィールド追加を避ける汎用ラッパ。
+--   [English]: A high-level @df |->@ result wrapper for causal discovery
+--   (LiNGAM). Since each LiNGAM fit type carries no variable names, this
+--   attaches the __variable names__ (the column names passed by @df |->@)
+--   to the trained fit @a@. @Plottable@ (@Hanalyze.Plot.ML@) uses
+--   @lfNames@ as DAG node names (falling back to @x0..@ if absent). A
+--   generic wrapper that avoids invasively adding a names field to each
+--   per-fit type.
 data LiNGAMFitted a = LiNGAMFitted
-  { lfFit   :: !a        -- ^ 各 variant の fit 結果 ('DirectLiNGAMFit' 等)
-  , lfNames :: ![Text]   -- ^ 変数名 (行列の列順 = fit の変数 index 順)
+  { lfFit   :: !a        -- ^ [日本語]: 各 variant の fit 結果 (@DirectLiNGAMFit@ 等) [English]: The fit result of each variant (e.g. @DirectLiNGAMFit@).
+  , lfNames :: ![Text]   -- ^ [日本語]: 変数名 (行列の列順 = fit の変数 index 順) [English]: Variable names (matrix column order = fit's variable index order).
   } deriving (Show)
 
--- | 列名で数値列を引き 'LA.Vector' 化 (無ければ 'Left')。 二変量近道の素経路。
+-- | [日本語]: 列名で数値列を引き 'LA.Vector' 化 (無ければ 'Left')。 二変量近道の素経路。
+--   [English]: Looks up a numeric column by name and turns it into an
+--   'LA.Vector' (or 'Left' if absent). The plain path for the bivariate
+--   shortcut.
 reqColV :: ColumnSource d => Text -> d -> Either String (LA.Vector Double)
 reqColV n d = case lookupCol n d of
   Just xs -> Right (LA.fromList xs)
   Nothing -> Left ("ColumnSource: 列が見つかりません: " <> T.unpack n)
 
--- | 複数の列名から @n × p@ 行列を組む (各列名 = 1 変数 = 行列の 1 列・行=標本)。
---   行列入力モデル (PCA \/ PLS \/ …) を列名 spec で高レベル化する際の素経路
---   (Phase 70.A)。 列が 1 つも無い / 長さ不揃いは 'Left'。
+-- | [日本語]: 複数の列名から @n × p@ 行列を組む (各列名 = 1 変数 = 行列の 1 列・行=標本)。
+--   行列入力モデル (PCA \/ PLS \/ …) を列名 spec で高レベル化する際の素経路。
+--   列が 1 つも無い / 長さ不揃いは 'Left'。
+--   [English]: Builds an @n × p@ matrix from multiple column names (each
+--   column name = 1 variable = 1 matrix column; rows = samples). The plain
+--   path used when giving matrix-input models (PCA \/ PLS \/ …) a
+--   high-level column-name spec. 'Left' if there are no columns at all \/
+--   the lengths don't match.
 reqColsM :: ColumnSource d => [Text] -> d -> Either String (LA.Matrix Double)
 reqColsM [] _ = Left "ColumnSource: 列名が空です (1 列以上必要)"
 reqColsM ns d = do
@@ -801,100 +1068,135 @@
 -- WLS / 透過標準化 / 群別フィット の結果型
 -- ===========================================================================
 
--- | WLS の結果。 内側 'LMModel' は **√w スケール設計行列** ('lmDesign'=X_w) と
---   その OLS 結果 ('lmResult')、 **元の x** ('lmXraw') を保持する (grid 経路で正しい
---   WLS CI を出すための容れ物)。 weighted R² 算出用に**重み** と**元 y** も保持する。
+-- | [日本語]: WLS の結果。 内側 'LMModel' は __√w スケール設計行列__ ('lmDesign'=X_w) と
+--   その OLS 結果 ('lmResult')、 __元の x__ ('lmXraw') を保持する (grid 経路で正しい
+--   WLS CI を出すための容れ物)。 weighted R² 算出用に__重み__ と__元 y__ も保持する。
+--   [English]: Result of WLS. The inner 'LMModel' holds the
+--   __√w-scaled design matrix__ ('lmDesign'=X_w), its OLS result
+--   ('lmResult'), and the __original x__ ('lmXraw') — a container for
+--   producing a correct WLS CI on the grid path. Also holds the
+--   __weights__ and __original y__ for computing weighted R².
 data WeightedLMModel = WeightedLMModel
-  { wlmInner   :: !LMModel    -- ^ √w スケール設計・OLS 結果・元 x。
-  , wlmWeights :: ![Double]   -- ^ 重み w (元の行順)。
-  , wlmY       :: ![Double]   -- ^ 元の応答 y (weighted R² 算出用)。
+  { wlmInner   :: !LMModel    -- ^ [日本語]: √w スケール設計・OLS 結果・元 x。 [English]: √w-scaled design, OLS result, original x.
+  , wlmWeights :: ![Double]   -- ^ [日本語]: 重み w (元の行順)。 [English]: Weights w (in original row order).
+  , wlmY       :: ![Double]   -- ^ [日本語]: 元の応答 y (weighted R² 算出用)。 [English]: Original response y (for computing weighted R²).
   }
 
--- | 透過標準化の結果。 内側モデル (標準化空間で学習) と逆変換に要る (μ,σ) を保持する。
---   'SingleVarModel' / 'Plottable' instance (Phase 70.3 C2) がこれを使い元スケール軸で描く。
+-- | [日本語]: 透過標準化の結果。 内側モデル (標準化空間で学習) と逆変換に要る (μ,σ) を保持する。
+--   @SingleVarModel@ / @Plottable@ instance がこれを使い元スケール軸で描く。
+--   [English]: Result of transparent standardization. Holds the inner model
+--   (trained in standardized space) and the (μ,σ) needed for the inverse
+--   transform. The @SingleVarModel@ \/ @Plottable@ instance uses this to
+--   plot on the original-scale axes.
 data StandardizedModel m = StandardizedModel
-  { smInner :: !m                            -- ^ 標準化空間で fit した内側モデル。
-  , smXStd  :: !Standardizer                 -- ^ 予測子列の (μ,σ)。'Stat.Standardize'。
-  , smYStd  :: !(Maybe (Double, Double))     -- ^ 応答 y の (μ,σ)。'standardizedY' 時のみ。
-  , smTrain :: !(Maybe ([Double], [Double])) -- ^ 元スケール訓練 (x,y)。単変量散布図用 (予測子 1 列時のみ)。
+  { smInner :: !m                            -- ^ [日本語]: 標準化空間で fit した内側モデル。 [English]: Inner model fit in standardized space.
+  , smXStd  :: !Standardizer                 -- ^ [日本語]: 予測子列の (μ,σ)。'Stat.Standardize'。 [English]: (μ,σ) of the predictor columns. See 'Stat.Standardize'.
+  , smYStd  :: !(Maybe (Double, Double))     -- ^ [日本語]: 応答 y の (μ,σ)。@standardizedY@ 時のみ。 [English]: (μ,σ) of the response y. Only when @standardizedY@ is used.
+  , smTrain :: !(Maybe ([Double], [Double])) -- ^ [日本語]: 元スケール訓練 (x,y)。単変量散布図用 (予測子 1 列時のみ)。 [English]: Original-scale training (x,y). For the univariate scatter plot (only when there is a single predictor column).
   }
 
--- | 群別フィットの結果。 各群ラベル → その群の 'Fitted spec' を保持する
---   **実結果型** ('HBMModel' 同族・'ModelSpec' ではない)。 'groupModels' で取り出す。
+-- | [日本語]: 群別フィットの結果。 各群ラベル → その群の 'Fitted spec' を保持する
+--   __実結果型__ ('HBMModel' 同族・@ModelSpec@ ではない)。 @groupModels@ で取り出す。
+--   [English]: Result of a per-group fit. Holds each group label →  its
+--   group's 'Fitted spec' as an __actual result type__ (in the same family
+--   as 'HBMModel', not a @ModelSpec@). Extracted via @groupModels@.
 newtype GroupedFit spec = GroupedFit { gfGroups :: [(Text, Fitted spec)] }
 
 -- ===========================================================================
 -- カーネル回帰 (描画可能)
 -- ===========================================================================
 
--- | カーネル回帰の 4 象限。 @seed@/@D@ は RFF 近似コンストラクタにだけ載る。
--- KRR 象限は 'Krr'/'KrrRff' (Kernel Ridge Regression・線形罰則回帰の 'Ridge' と区別)。
+-- | [日本語]: カーネル回帰の 4 象限。 @seed@/@D@ は RFF 近似コンストラクタにだけ載る。
+-- KRR 象限は 'Krr'/'KrrRff' (Kernel Ridge Regression・線形罰則回帰の @Ridge@ と区別)。
+--   [English]: The 4 quadrants of kernel regression. @seed@\/@D@ appear only
+--   on the RFF-approximation constructors. The KRR quadrants are
+--   'Krr'\/'KrrRff' (Kernel Ridge Regression, distinct from linear
+--   penalized regression's @Ridge@).
 data GPMethod
-  = Gp                       -- ^ 厳密 GP   (分布あり・事後分散→帯)。
-  | Krr                      -- ^ 厳密 KRR  (点・KRR ≡ GP 事後平均)。
-  | GpRff  !Int !Word32      -- ^ RFF 近似 GP  (@D@ 特徴次元, seed)。
-  | KrrRff !Int !Word32      -- ^ RFF 近似 KRR (@D@ 特徴次元, seed)。
+  = Gp                       -- ^ [日本語]: 厳密 GP   (分布あり・事後分散→帯)。 [English]: Exact GP (has a distribution; posterior variance → band).
+  | Krr                      -- ^ [日本語]: 厳密 KRR  (点・KRR ≡ GP 事後平均)。 [English]: Exact KRR (point estimate; KRR ≡ GP posterior mean).
+  | GpRff  !Int !Word32      -- ^ [日本語]: RFF 近似 GP  (@D@ 特徴次元, seed)。 [English]: RFF-approximated GP (@D@ feature dimension, seed).
+  | KrrRff !Int !Word32      -- ^ [日本語]: RFF 近似 KRR (@D@ 特徴次元, seed)。 [English]: RFF-approximated KRR (@D@ feature dimension, seed).
   deriving (Eq, Show)
 
--- | ハイパラの「決め方 + (固定時のみ) 値」を一箇所に集約 (役割重複ゆえ別フィールドは持たない)。
+-- | [日本語]: ハイパラの「決め方 + (固定時のみ) 値」を一箇所に集約 (役割重複ゆえ別フィールドは持たない)。
+--   [English]: Gathers the hyperparameter's "how it's decided + (only when
+--   fixed) value" into one place (no separate field, since that would
+--   duplicate the role).
 data HyperStrategy
-  = FixedHyper GPParams   -- ^ 固定: この 'GPParams' を使う (最適化しない)。
-  | AutoMarginalLik       -- ^ 周辺尤度で自動 ('GP.optimizeGP'・初期値はデータ駆動)。
-  | AutoCV                -- ^ LOOCV (PRESS) で自動 ('GP.autoCVHyperGP'・初期値は同上)。
+  = FixedHyper GPParams   -- ^ [日本語]: 固定: この 'GPParams' を使う (最適化しない)。 [English]: Fixed: use this 'GPParams' (no optimization).
+  | AutoMarginalLik       -- ^ [日本語]: 周辺尤度で自動 ('GP.optimizeGP'・初期値はデータ駆動)。 [English]: Automatic via marginal likelihood ('GP.optimizeGP'; data-driven initial values).
+  | AutoCV                -- ^ [日本語]: LOOCV (PRESS) で自動 ('GP.autoCVHyperGP'・初期値は同上)。 [English]: Automatic via LOOCV (PRESS) ('GP.autoCVHyperGP'; same initial values).
 
--- | 当てはめ済の統合カーネル回帰モデル。 象限 + 解決済ハイパラ + 予測子を保持する。
+-- | [日本語]: 当てはめ済の統合カーネル回帰モデル。 象限 + 解決済ハイパラ + 予測子を保持する。
 -- 予測子 'gprPredict' は @grid x → (μ̂, Maybe 事後分散)@: 分布あり象限 (Gp/GpRff) は
--- @Just 分散@、 点象限 (Ridge/RidgeRff) は @Nothing@ (= 帯なし)。 'SingleVarModel' /
--- 'Plottable' instance は E2。
+-- @Just 分散@、 点象限 (Ridge/RidgeRff) は @Nothing@ (= 帯なし)。 @SingleVarModel@ /
+-- @Plottable@ instance は E2。
+--   [English]: A fitted unified kernel regression model. Holds the
+--   quadrant + resolved hyperparameters + predictor. The predictor
+--   'gprPredict' takes @grid x → (μ̂, Maybe posterior variance)@: quadrants
+--   with a distribution (Gp\/GpRff) give @Just variance@, point quadrants
+--   (Ridge\/RidgeRff) give @Nothing@ (= no band). The @SingleVarModel@ \/
+--   @Plottable@ instance is E2.
 data GPRegModel = GPRegModel
-  { gprMethod  :: !GPMethod                                  -- ^ Periodic フォールバック後の象限。
-  , gprKernel  :: !Kernel                                    -- ^ カーネル種 (Periodic は不変)。
-  , gprParams  :: !GPParams                                  -- ^ 解決済ハイパラ。
-  , gprXraw    :: !(LA.Vector Double)                        -- ^ 訓練 x (svRange / 散布図)。
-  , gprY       :: !(LA.Vector Double)                        -- ^ 訓練 y。
-  , gprPredict :: !([Double] -> ([Double], Maybe [Double]))  -- ^ grid x → (μ̂, Maybe 事後分散)。
+  { gprMethod  :: !GPMethod                                  -- ^ [日本語]: Periodic フォールバック後の象限。 [English]: The quadrant after any Periodic fallback.
+  , gprKernel  :: !Kernel                                    -- ^ [日本語]: カーネル種 (Periodic は不変)。 [English]: Kernel kind (unchanged for Periodic).
+  , gprParams  :: !GPParams                                  -- ^ [日本語]: 解決済ハイパラ。 [English]: Resolved hyperparameters.
+  , gprXraw    :: !(LA.Vector Double)                        -- ^ [日本語]: 訓練 x (svRange / 散布図)。 [English]: Training x (svRange \/ scatter plot).
+  , gprY       :: !(LA.Vector Double)                        -- ^ [日本語]: 訓練 y。 [English]: Training y.
+  , gprPredict :: !([Double] -> ([Double], Maybe [Double]))  -- ^ [日本語]: grid x → (μ̂, Maybe 事後分散)。 [English]: grid x → (μ̂, Maybe posterior variance).
   }
 
--- | 当てはめ済の多変量カーネル回帰モデル。 予測子 'gprnPredict' は評価行列 (@m × p@) を
+-- | [日本語]: 当てはめ済の多変量カーネル回帰モデル。 予測子 'gprnPredict' は評価行列 (@m × p@) を
 -- 取り @(μ̂, Maybe 事後分散)@ を返す (分布あり象限のみ Just)。
+--   [English]: A fitted multivariate kernel regression model. The predictor
+--   'gprnPredict' takes an evaluation matrix (@m × p@) and returns
+--   @(μ̂, Maybe posterior variance)@ (Just only for quadrants with a
+--   distribution).
 data GPRegModelN = GPRegModelN
   { gprnMethod  :: !GPMethod
   , gprnKernel  :: !Kernel
   , gprnParams  :: !GPParams
-  , gprnXraws   :: ![LA.Vector Double]                           -- ^ 予測子ごとの訓練 x (列名順)。
-  , gprnNames   :: ![Text]                                       -- ^ 予測子名 (列名順)。
-  , gprnYraw    :: !(LA.Vector Double)                           -- ^ 訓練応答 y (profiler 実測点の重ね用)。
-  , gprnPredict :: !(LA.Matrix Double -> ([Double], Maybe [Double])) -- ^ testX (m×p) → (μ̂, Maybe 分散)。
+  , gprnXraws   :: ![LA.Vector Double]                           -- ^ [日本語]: 予測子ごとの訓練 x (列名順)。 [English]: Training x per predictor (in column-name order).
+  , gprnNames   :: ![Text]                                       -- ^ [日本語]: 予測子名 (列名順)。 [English]: Predictor names (in column-name order).
+  , gprnYraw    :: !(LA.Vector Double)                           -- ^ [日本語]: 訓練応答 y (profiler 実測点の重ね用)。 [English]: Training response y (for overlaying the profiler's observed points).
+  , gprnPredict :: !(LA.Matrix Double -> ([Double], Maybe [Double])) -- ^ [日本語]: testX (m×p) → (μ̂, Maybe 分散)。 [English]: testX (m×p) → (μ̂, Maybe variance).
   }
 
 -- ===========================================================================
 -- 罰則付き回帰 (描画可能)
 -- ===========================================================================
 
--- | 罰則の種類 (実装済み全 7 種)。 追加パラメータも型に載せる。
+-- | [日本語]: 罰則の種類 (実装済み全 7 種)。 追加パラメータも型に載せる。
+--   [English]: Kinds of penalty (all 7 implemented kinds). Extra parameters
+--   are carried in the type too.
 data RegMethod
-  = Ridge                      -- ^ L2。
-  | Lasso                      -- ^ L1。
-  | ElasticNet    !Double      -- ^ α = L1 比 (0..1)。
-  | MCP           !Double      -- ^ γ concavity (推奨 ≥3)。
-  | SCAD          !Double      -- ^ a (推奨 3.7)。
-  | AdaptiveLasso !Double      -- ^ OLS pilot weight 指数 γ。
-  | GroupLasso    ![Int]       -- ^ 各列の群 ID (列名順・長さ = 列数)。
+  = Ridge                      -- ^ [日本語]: L2。 [English]: L2.
+  | Lasso                      -- ^ [日本語]: L1。 [English]: L1.
+  | ElasticNet    !Double      -- ^ [日本語]: α = L1 比 (0..1)。 [English]: α = L1 ratio (0..1).
+  | MCP           !Double      -- ^ [日本語]: γ concavity (推奨 ≥3)。 [English]: γ concavity (recommended ≥3).
+  | SCAD          !Double      -- ^ [日本語]: a (推奨 3.7)。 [English]: a (recommended 3.7).
+  | AdaptiveLasso !Double      -- ^ [日本語]: OLS pilot weight 指数 γ。 [English]: OLS pilot weight exponent γ.
+  | GroupLasso    ![Int]       -- ^ [日本語]: 各列の群 ID (列名順・長さ = 列数)。 [English]: Group ID for each column (in column-name order, length = column count).
   deriving (Eq, Show)
 
--- | 当てはめ済の罰則回帰モデル。 係数は **元スケール** (intercept + 特徴ごと)。
+-- | [日本語]: 当てはめ済の罰則回帰モデル。 係数は __元スケール__ (intercept + 特徴ごと)。
+--   [English]: A fitted penalized regression model. Coefficients are on the
+--   __original scale__ (intercept + per feature).
 data RegModel = RegModel
   { rmgMethod    :: !RegMethod
-  , rmgNames     :: ![Text]                       -- ^ 説明変数名 (列順)。
-  , rmgLambda    :: !Double                        -- ^ 選択された λ。
-  , rmgIntercept :: !Double                        -- ^ β₀ (元スケール)。
-  , rmgCoefs     :: ![Double]                      -- ^ β (元スケール・特徴ごと・長さ = 列数)。
-  , rmgFitStd    :: !RegFit                        -- ^ 標準化空間の fit (診断用)。
-  , rmgCVPath    :: !(Maybe ([Double], [Double]))  -- ^ (λ grid, CV/LOOCV スコア)・自動選択時のみ。
-  , rmgXraw      :: !(LA.Matrix Double)            -- ^ 生設計行列 (特徴のみ・intercept 列なし)。bootstrap refit 用。
-  , rmgYraw      :: !(LA.Vector Double)            -- ^ 生応答 y。bootstrap refit 用。
+  , rmgNames     :: ![Text]                       -- ^ [日本語]: 説明変数名 (列順)。 [English]: Explanatory variable names (in column order).
+  , rmgLambda    :: !Double                        -- ^ [日本語]: 選択された λ。 [English]: The selected λ.
+  , rmgIntercept :: !Double                        -- ^ [日本語]: β₀ (元スケール)。 [English]: β₀ (original scale).
+  , rmgCoefs     :: ![Double]                      -- ^ [日本語]: β (元スケール・特徴ごと・長さ = 列数)。 [English]: β (original scale, per feature, length = column count).
+  , rmgFitStd    :: !RegFit                        -- ^ [日本語]: 標準化空間の fit (診断用)。 [English]: The fit in standardized space (for diagnostics).
+  , rmgCVPath    :: !(Maybe ([Double], [Double]))  -- ^ [日本語]: (λ grid, CV/LOOCV スコア)・自動選択時のみ。 [English]: (λ grid, CV\/LOOCV score); only when auto-selected.
+  , rmgXraw      :: !(LA.Matrix Double)            -- ^ [日本語]: 生設計行列 (特徴のみ・intercept 列なし)。bootstrap refit 用。 [English]: Raw design matrix (features only, no intercept column). For bootstrap refit.
+  , rmgYraw      :: !(LA.Vector Double)            -- ^ [日本語]: 生応答 y。bootstrap refit 用。 [English]: Raw response y. For bootstrap refit.
   }
 
--- | 新規データ (各行が p 次元の特徴ベクトル) での予測 @ŷ = β₀ + Σ βⱼ xⱼ@。
+-- | [日本語]: 新規データ (各行が p 次元の特徴ベクトル) での予測 @ŷ = β₀ + Σ βⱼ xⱼ@。
+--   [English]: Prediction on new data (each row a p-dimensional feature
+--   vector), @ŷ = β₀ + Σ βⱼ xⱼ@.
 regPredict :: RegModel -> [[Double]] -> [Double]
 regPredict m rows = [ rmgIntercept m + sum (zipWith (*) (rmgCoefs m) r) | r <- rows ]
diff --git a/src/Hanalyze/Optim/Acquisition.hs b/src/Hanalyze/Optim/Acquisition.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/Acquisition.hs
+++ /dev/null
@@ -1,174 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.Acquisition
--- Description : ベイズ最適化の獲得関数 (単一目的 EI/UCB/PI, 多目的 EHVI/ParEGO)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Acquisition functions for Bayesian Optimization.
---
--- Single-objective:
---
---   * EI  — Expected Improvement (Mockus 1978).
---   * UCB — Upper Confidence Bound.
---   * PI  — Probability of Improvement.
---
--- Multi-objective:
---
---   * EHVI   — Expected Hypervolume Improvement.
---   * ParEGO — Tchebycheff scalarization + EI.
-{-# LANGUAGE StrictData #-}
-{-# LANGUAGE OverloadedStrings #-}
-module Hanalyze.Optim.Acquisition
-  ( ei
-  , ucb
-  , pi_
-    -- * Multi-objective
-  , parEGO
-  , ehvi2D
-  ) where
-
-import Statistics.Distribution     (cumulative, density)
-import Statistics.Distribution.Normal (standard)
-
--- ---------------------------------------------------------------------------
--- 単一目的 acquisition 関数
--- ---------------------------------------------------------------------------
-
--- | Expected Improvement (minimization, with exploration parameter @ξ@).
---
--- @
--- EI(x) = E[max(y_best − y(x), 0)]
---       = (y_best − μ) Φ(z) + σ φ(z)
--- where z = (y_best − μ − ξ) / σ
--- @
-ei :: Double               -- ^ Current best @y_best@ (minimum so far).
-   -> Double               -- ^ Exploration trade-off @ξ@ (0.01 typical).
-   -> (Double, Double)     -- ^ Predictive @(μ, σ)@.
-   -> Double
-ei yBest xi (mu, sigma)
-  | sigma <= 0 = 0
-  | otherwise =
-      let z   = (yBest - mu - xi) / sigma
-          phi = density standard z
-          cdf = cumulative standard z
-      in (yBest - mu - xi) * cdf + sigma * phi
-
--- | Lower Confidence Bound for minimization (sometimes called UCB).
---
--- @LCB(x) = μ − β σ@. Large @β@ encourages exploration (prefers large
--- @σ@); small @β@ encourages exploitation (prefers small @μ@).
-ucb :: Double -> (Double, Double) -> Double
-ucb beta (mu, sigma) = mu - beta * sigma
-
--- | Probability of Improvement.
---
--- @PI(x) = P(y(x) < y_best − ξ) = Φ((y_best − μ − ξ) / σ)@.
-pi_ :: Double -> Double -> (Double, Double) -> Double
-pi_ yBest xi (mu, sigma)
-  | sigma <= 0 = 0
-  | otherwise =
-      let z = (yBest - mu - xi) / sigma
-      in cumulative standard z
-
--- ---------------------------------------------------------------------------
--- 多目的 acquisition
--- ---------------------------------------------------------------------------
-
--- | ParEGO (Knowles 2006): Tchebycheff scalarization + EI.
---
--- Each iteration draws a random weight vector @w@ and computes EI on the
--- scalarized objective:
---
--- @
--- y_scalar(x) = max_j (w_j (y_j(x) − z*_j)) + ρ Σ_j w_j (y_j(x) − z*_j)
--- @
-parEGO :: [Double]              -- ^ Weights @w@ (non-negative, sum to 1).
-       -> [Double]              -- ^ Ideal point @z*@ (per-objective minima).
-       -> Double                -- ^ ParEGO @ρ@ (≈ 0.05).
-       -> Double                -- ^ Best scalarized value so far @y_best@.
-       -> [(Double, Double)]    -- ^ Per-objective predictive @(μ_j, σ_j)@.
-       -> Double                -- ^ Scalarized EI value (to be maximized).
-parEGO weights ideal rho yBest preds =
-  let -- scalarized μ: max_j (w_j (μ_j - z*_j)) + rho Σ ...
-      diffs    = zipWith3 (\w mu zStar -> w * (mu - zStar)) weights (map fst preds) ideal
-      muScalar = maximum diffs + rho * sum diffs
-      -- scalarized σ: 簡易合算 (上界)
-      sigSqs   = zipWith (\w (_, sg) -> (w * sg) ^ (2 :: Int)) weights preds
-      sigScalar = sqrt (sum sigSqs)
-  in ei yBest 0.01 (muScalar, sigScalar)
-
--- | Expected Hypervolume Improvement (2-objective only).
---
--- Computes the expected hypervolume gained by adding a candidate point
--- @(μ, σ)@ to the current Pareto front. The full EHVI integral is
--- expensive, so this implementation uses a Monte Carlo approximation.
-ehvi2D :: [Double]                  -- ^ Reference point @r@ (2D).
-       -> [[Double]]                -- ^ Current front (each point @[y1, y2]@).
-       -> [(Double, Double)]        -- ^ Per-objective predictive @(μ, σ)@.
-       -> Int                       -- ^ Number of Monte Carlo samples.
-       -> Double
-ehvi2D _ref _front _preds 0 = 0
-ehvi2D ref front preds nSamples =
-  let -- 現在 HV
-      currentHV = hv2DSimple ref front
-      -- MC: 新点 y_new = (μ_1 + σ_1 z_1, μ_2 + σ_2 z_2) で z ~ N(0, 1)
-      sample i =
-        let z1 = qnorm ((fromIntegral i + 0.5) / fromIntegral nSamples)
-            z2 = qnorm ((fromIntegral i + 0.13) / fromIntegral nSamples)
-            (m1, s1) = head preds
-            (m2, s2) = preds !! 1
-            yNew = [m1 + s1 * z1, m2 + s2 * z2]
-            newFront = pareto2D (yNew : front)
-            newHV = hv2DSimple ref newFront
-        in max 0 (newHV - currentHV)
-      improvements = [sample i | i <- [0 .. nSamples - 1]]
-  in sum improvements / fromIntegral nSamples
-
--- 2D simplified HV
-hv2DSimple :: [Double] -> [[Double]] -> Double
-hv2DSimple [rx, ry] front =
-  let valid  = [p | p <- front, head p < rx, p !! 1 < ry]
-      sorted = sortByFst valid
-      go _   [] acc = acc
-      go yPrev (p:ps) acc =
-        let xCur = head p
-            yCur = p !! 1
-        in if yCur >= yPrev
-             then go yPrev ps acc
-             else go yCur ps (acc + (rx - xCur) * (yPrev - yCur))
-  in go ry sorted 0
-hv2DSimple _ _ = 0
-
--- 2D Pareto front 抽出
-pareto2D :: [[Double]] -> [[Double]]
-pareto2D pts =
-  [p | (i, p) <- indexed,
-       not (any (\(j, q) -> j /= i && allLE q p && anyLT q p) indexed) ]
-  where
-    indexed = zip [0 :: Int ..] pts
-    allLE a b = and (zipWith (<=) a b)
-    anyLT a b = or (zipWith (<) a b)
-
-sortByFst :: [[Double]] -> [[Double]]
-sortByFst = qs
-  where
-    qs []     = []
-    qs (p:xs) = qs [x | x <- xs, head x <= head p]
-                ++ [p]
-                ++ qs [x | x <- xs, head x > head p]
-
--- 標準正規分布の逆関数 (簡易、Beasley-Springer/Moro)
-qnorm :: Double -> Double
-qnorm p
-  | p <= 0    = -1/0
-  | p >= 1    =  1/0
-  | otherwise =
-      -- 近似 (誤差 < 4.5e-4 in central, やや悪化 in tails)
-      let t = if p < 0.5 then sqrt (-2 * log p)
-                          else sqrt (-2 * log (1 - p))
-          c0 = 2.515517; c1 = 0.802853; c2 = 0.010328
-          d1 = 1.432788; d2 = 0.189269; d3 = 0.001308
-          num = c0 + c1 * t + c2 * t * t
-          den = 1 + d1 * t + d2 * t * t + d3 * t * t * t
-          x   = t - num / den
-      in if p < 0.5 then -x else x
diff --git a/src/Hanalyze/Optim/Adam.hs b/src/Hanalyze/Optim/Adam.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/Adam.hs
+++ /dev/null
@@ -1,136 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.Adam
--- Description : Adam 一次勾配法オプティマイザ (Kingma & Ba 2014)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Adam first-order optimizer (Kingma & Ba 2014).
---
--- A general-purpose gradient-based optimizer used for ELBO maximization,
--- neural-network training, acquisition-function optimization, and similar
--- tasks. Originally embedded in @Hanalyze.Stat.VI@; extracted here as a shared
--- foundation.
---
--- 使い方:
---
--- @
--- let cfg = defaultAdamConfig { adamLearningRate = 0.01, adamIterations = 1000 }
---     gradFn x = ...                            -- 勾配 (上昇方向)
---     (xFinal, history) = runAdam cfg gradFn x0
--- @
---
--- 'adamStep' 単体は 1 ステップだけ進める低レベル API で、`Hanalyze.Stat.VI` などが
--- 内部で利用する。
-{-# LANGUAGE OverloadedStrings #-}
-module Hanalyze.Optim.Adam
-  ( -- * 設定
-    AdamConfig (..)
-  , defaultAdamConfig
-    -- * Single-step update (low-level)
-  , adamStep
-    -- * High-level loop
-  , runAdam
-  , runAdamMaximize
-  , runAdamMinimize
-  ) where
-
-import Control.DeepSeq (force)
-import Data.IORef
-import Control.Monad (forM_)
-import System.IO.Unsafe (unsafePerformIO)
-
--- | Adam configuration.
-data AdamConfig = AdamConfig
-  { adamIterations   :: Int     -- ^ Number of iterations.
-  , adamLearningRate :: Double  -- ^ Learning rate @α@.
-  , adamBeta1        :: Double  -- ^ First-moment decay (default 0.9).
-  , adamBeta2        :: Double  -- ^ Second-moment decay (default 0.999).
-  , adamEpsilon      :: Double  -- ^ Numerical stabilizer (default 1e-8).
-  } deriving (Show)
-
--- | Default Adam configuration: 1000 iterations, @α = 0.01@,
--- @β₁ = 0.9@, @β₂ = 0.999@, @ε = 1e-8@.
-defaultAdamConfig :: AdamConfig
-defaultAdamConfig = AdamConfig
-  { adamIterations   = 1000
-  , adamLearningRate = 0.01
-  , adamBeta1        = 0.9
-  , adamBeta2        = 0.999
-  , adamEpsilon      = 1e-8
-  }
-
--- | Single Adam update.
---
--- Arguments:
---
---   * @β1@, @β2@, @ε@, @α@ — Adam hyperparameters.
---   * @t@ — iteration count (1-based; needed for bias correction).
---   * @m1@, @m2@ — previous first- and second-moment estimates.
---   * @g@ — current gradient.
---
--- Returns @(m1', m2', dx)@: the updated moments and the step direction
--- (in the @+gradient@ direction). Callers do @x ← x + dx@ for ascent or
--- @x ← x − dx@ for descent.
-adamStep
-  :: Double -> Double -> Double -> Double -> Int
-  -> [Double] -> [Double] -> [Double]
-  -> ([Double], [Double], [Double])
-adamStep b1 b2 eps alpha t m1 m2 g =
-  let m1' = zipWith (\m gi -> b1 * m + (1 - b1) * gi)      m1 g
-      m2' = zipWith (\v gi -> b2 * v + (1 - b2) * gi * gi)  m2 g
-      mH  = map (/ (1 - b1 ^ t)) m1'
-      vH  = map (/ (1 - b2 ^ t)) m2'
-      dx  = zipWith (\m_ v -> alpha * m_ / (sqrt v + eps))   mH vH
-  in (m1', m2', dx)
-
--- | Gradient-ascent loop. @gradFn@ returns the gradient of the objective.
--- The update @x ← x + Δx@ moves in the @+gradient@ direction, so pass the
--- gradient of the quantity to maximize.
---
--- Returns @(x_final, x_history)@; the per-iteration trajectory is kept
--- for debugging and visualization.
-runAdamMaximize :: AdamConfig
-                -> ([Double] -> [Double])  -- ^ Gradient function.
-                -> [Double]                -- ^ Initial point.
-                -> ([Double], [[Double]])
-runAdamMaximize cfg gradFn x0 = unsafePerformIO $ do
-  let n = length x0
-  xRef  <- newIORef x0
-  m1Ref <- newIORef (replicate n 0.0)
-  m2Ref <- newIORef (replicate n 0.0)
-  histRef <- newIORef []
-  forM_ [1 .. adamIterations cfg] $ \t -> do
-    x  <- readIORef xRef
-    m1 <- readIORef m1Ref
-    m2 <- readIORef m2Ref
-    let g            = gradFn x
-        (m1', m2', dx) = adamStep
-                          (adamBeta1 cfg) (adamBeta2 cfg) (adamEpsilon cfg)
-                          (adamLearningRate cfg) t m1 m2 g
-        x'           = zipWith (+) x dx
-    -- Phase Q3 (2026-05-14): force lists before storing in IORef. Without
-    -- this each iter writes a thunk that reads the previous IORef contents
-    -- and chains a fresh @zipWith@ on top — after T iters the chain holds
-    -- O(T) closures. See Stat.VI for the same fix and BenchMemVI numbers.
-    let !x''  = force x'
-        !m1'' = force m1'
-        !m2'' = force m2'
-    writeIORef xRef x''
-    writeIORef m1Ref m1''
-    writeIORef m2Ref m2''
-    modifyIORef' histRef (x'' :)
-  xF   <- readIORef xRef
-  hist <- fmap reverse (readIORef histRef)
-  return (xF, hist)
-
--- | Gradient-descent variant: negates @gradFn@ and delegates to
--- 'runAdamMaximize'.
-runAdamMinimize :: AdamConfig -> ([Double] -> [Double]) -> [Double]
-                -> ([Double], [[Double]])
-runAdamMinimize cfg gradFn x0 =
-  runAdamMaximize cfg (map negate . gradFn) x0
-
--- | Alias for 'runAdamMaximize' (the default convention is ascent).
-runAdam :: AdamConfig -> ([Double] -> [Double]) -> [Double]
-        -> ([Double], [[Double]])
-runAdam = runAdamMaximize
diff --git a/src/Hanalyze/Optim/BayesOpt.hs b/src/Hanalyze/Optim/BayesOpt.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/BayesOpt.hs
+++ /dev/null
@@ -1,786 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.BayesOpt
--- Description : ベイズ最適化ループ (GP フィット + 獲得関数最大化)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Bayesian Optimization loop.
---
--- Single-objective procedure:
---
---   1. Evaluate initial points (Latin hypercube or random).
---   2. Fit a Gaussian process to the observations.
---   3. Maximize an acquisition function to choose the next @x@.
---   4. Evaluate @x@ and append to the observed sequence.
---   5. Repeat steps 2-4 for @T@ iterations.
-{-# LANGUAGE StrictData #-}
-{-# LANGUAGE OverloadedStrings #-}
-module Hanalyze.Optim.BayesOpt
-  ( BayesOptConfig (..)
-  , defaultBayesOptConfig
-  , BOIterEvent (..)
-  , bayesOpt
-  , bayesOptWithCallback
-  , bayesOptND
-  , bayesOptScalarMO
-  , bayesOptMOWithNSGA
-    -- * GP HP optimization helpers
-  , optimizeGPMVRestart
-  , optimizeHPMultiRestart
-  ) where
-
-import Control.Exception (SomeException, try, evaluate)
-import Control.Monad (forM, replicateM)
-import Data.List (minimumBy, maximumBy, sortBy)
-import Data.Ord (comparing)
-import System.IO.Unsafe (unsafePerformIO)
-import System.Random.MWC (GenIO, uniform)
-
-import Hanalyze.Model.GP (Kernel (..), GPModel (..), GPResult (..), GPParams (..),
-                 gpKernelParams,
-                 fitGP, optimizeGP, initParamsFromData,
-                 GPResultMV (..), fitGPMV, optimizeGPMV,
-                 logMarginalLikelihoodMV,
-                 buildKernelMatrixMV, noiseKernelMV)
-import qualified Hanalyze.Stat.Cholesky    as Chol
-import qualified Hanalyze.Stat.KernelDist  as KD
-import Hanalyze.Optim.Acquisition (ei, ucb, pi_, parEGO)
-import Hanalyze.Optim.NSGA       (NSGAConfig (..), defaultNSGAConfig,
-                         Solution (..), nsga2)
-import Hanalyze.Optim.Common     (Bounds)
-import qualified Hanalyze.Optim.LineSearch as LS
-import qualified Hanalyze.Optim.LBFGS      as LBFGS
-import qualified Hanalyze.Optim.Common     as OC
-import qualified Numeric.LinearAlgebra as LA
-import qualified Hanalyze.Stat.QuasiRandom      as QR
-import qualified Hanalyze.Stat.Standardize      as Std
-import Statistics.Distribution        (cumulative, density)
-import Statistics.Distribution.Normal (standard)
-
--- | Bayesian Optimization configuration.
-data BayesOptConfig = BayesOptConfig
-  { boIterations :: Int        -- ^ Evaluation budget (excluding initial points).
-  , boInitPoints :: Int        -- ^ Number of initial sample points.
-  , boKernel     :: Kernel     -- ^ GP kernel.
-  , boUCBBeta    :: Double     -- ^ @β@ for UCB.
-  , boGridSize   :: Int        -- ^ Inner-optimization grid density (1D).
-  } deriving (Show)
-
--- | Default configuration: 30 iterations, 5 initial points,
--- **Matérn 5/2 kernel**, @β = 2.0@ for UCB, grid size 200 for 1D
--- inner optimization.
---
--- Matérn 5/2 is the recommended default for general-purpose BO
--- (matches scikit-optimize's defaults). RBF is too smooth for many
--- real-world objective surfaces; Matérn captures the @C²@ regularity
--- typical of engineering / black-box functions and is what the BO
--- literature converged on.
-defaultBayesOptConfig :: BayesOptConfig
-defaultBayesOptConfig = BayesOptConfig
-  { boIterations = 30
-  , boInitPoints = 5
-  , boKernel     = Matern52
-  , boUCBBeta    = 2.0
-  , boGridSize   = 200
-  }
-
--- | Single-objective Bayesian Optimization (1D simplified entry point).
---
--- Returns @(observations, best)@: the full @(x, y)@ history and the best
--- @(x*, y*)@.
--- | Phase 21 で追加。 BO の各 iteration 末端で発火するイベント。
-data BOIterEvent = BOIterEvent
-  { boeIter        :: !Int               -- ^ 0-based iteration index
-  , boeProposedX   :: !Double             -- ^ acquisition が選んだ新点
-  , boeProposedY   :: !Double             -- ^ そこでの f 値
-  , boeCurrentBest :: !(Double, Double)   -- ^ (x*, y*) これまで
-  } deriving (Show)
-
-bayesOpt :: BayesOptConfig
-         -> (Double -> IO Double)   -- ^ Objective (1D, minimized).
-         -> (Double, Double)        -- ^ Search bounds.
-         -> GenIO
-         -> IO ([(Double, Double)], (Double, Double))
-bayesOpt cfg f bounds gen =
-  bayesOptWithCallback cfg f bounds gen (\_ -> pure ())
-
--- | Phase 21 で追加。 BO iteration ごとに 'BOIterEvent' を渡す callback 付き版。
--- 既存 'bayesOpt' は no-op callback の wrapper として保持される。
-bayesOptWithCallback
-  :: BayesOptConfig
-  -> (Double -> IO Double)
-  -> (Double, Double)
-  -> GenIO
-  -> (BOIterEvent -> IO ())
-  -> IO ([(Double, Double)], (Double, Double))
-bayesOptWithCallback cfg f (lo, hi) gen onIter = do
-  -- 初期点 (uniform random, 簡易)
-  initX <- replicateM (boInitPoints cfg) (do
-              u <- uniform gen :: IO Double
-              return (lo + u * (hi - lo)))
-  initY <- mapM f initX
-  let history0 = zip initX initY
-      totalIter = boIterations cfg
-
-  -- BO ループ
-  -- 内側 acquisition 最大化は **Brent 法** (1D 単峰超線形収束)。
-  -- 旧 grid (boGridSize 点) は seeding として併用、Brent の bracket を作る。
-  let loop t hist
-        | t == 0 = return hist
-        | otherwise = do
-            let xs = map fst hist
-                ys = map snd hist
-                yBest = minimum ys
-                p0 = initParamsFromData xs ys
-                pOpt = optimizeGP (boKernel cfg) xs ys p0
-                model = GPModel (boKernel cfg) pOpt
-
-                -- 1 点での負 EI (Brent は最小化、引数は [Double] で受ける)
-                -- Cholesky / SVD 失敗時はペナルティ +1e30 を返す。
-                -- gpMean / gpUpper は遅延フィールドなので evaluate で強制してから返す。
-                negEI [x] = unsafePerformIO $ do
-                  let computed = do
-                        let res = fitGP model xs ys [x]
-                            mu  = head (gpMean res)
-                            sg  = (head (gpUpper res) - mu) / 2
-                        _ <- evaluate mu
-                        _ <- evaluate sg
-                        pure (negate (ei yBest 0.01 (mu, sg)))
-                  r <- try computed :: IO (Either SomeException Double)
-                  case r of
-                    Left _  -> pure 1e30
-                    Right v -> pure v
-                negEI _   = error "negEI: 1D"
-
-                -- 粗グリッドで bracket を作る
-                gridN = max 16 (boGridSize cfg `div` 4)
-                grid  = [lo + fromIntegral i * (hi - lo)
-                              / fromIntegral (gridN - 1)
-                        | i <- [0 .. gridN - 1]]
-                gridV = [(x, negEI [x]) | x <- grid]
-                bestG = minimumBy (comparing snd) gridV
-                bestX = fst bestG
-                idxBest = case [i | (i, (gx, _)) <- zip [0::Int ..] gridV, gx == bestX] of
-                            (k:_) -> k; [] -> 0
-                ax = fst (gridV !! max 0 (idxBest - 1))
-                bx = fst (gridV !! min (gridN - 1) (idxBest + 1))
-                -- Brent で局所最大 (= 負の最小)
-                bRes = LS.brent (LS.defaultBrentConfig { LS.bcMaxIter = 80
-                                                       , LS.bcTol    = 1e-7 })
-                                negEI (min ax bx) (max ax bx)
-                xNext = head (OC.orBest bRes)
-
-            yNext <- f xNext
-            let newHist = hist ++ [(xNext, yNext)]
-                bestPair = head [pair | pair@(_, y) <- newHist
-                                      , y == minimum (map snd newHist)]
-                iterIdx = totalIter - t   -- 0-based
-            onIter BOIterEvent
-              { boeIter        = iterIdx
-              , boeProposedX   = xNext
-              , boeProposedY   = yNext
-              , boeCurrentBest = bestPair
-              }
-            loop (t - 1) newHist
-
-  finalHist <- loop totalIter history0
-  let bestPair = head [pair | pair@(_, y) <- finalHist
-                            , y == minimum (map snd finalHist)]
-  return (finalHist, bestPair)
-
--- ---------------------------------------------------------------------------
--- GP HP optimization with multiple random restarts
--- ---------------------------------------------------------------------------
-
--- | Optimize a GP's hyperparameters with multiple random restarts and
--- pick the best (highest marginal likelihood). One restart corresponds
--- to a single 'optimizeGPMV' call from a perturbed initial point.
---
--- Critical for BO performance: the marginal-likelihood surface is
--- multi-modal, so a single fixed init is not robust. scikit-optimize
--- defaults to @n_restarts_optimizer = 0@ (= 1 fit) but its kernel has
--- the prior baked in; for our wider search we use 5 restarts.
-optimizeGPMVRestart
-  :: Int                       -- ^ Number of restarts.
-  -> Kernel
-  -> LA.Matrix Double          -- ^ Training X (n × p).
-  -> LA.Vector Double          -- ^ Training y (length n).
-  -> GenIO
-  -> IO GPParams
-optimizeGPMVRestart n kern x y gen = do
-  let p0base = initParamsFromData (concat (LA.toLists x)) (LA.toList y)
-  -- generate n random initial points: log-spaced perturbation of p0base
-  -- to cover several orders of magnitude.
-  let scaleVar = sqrt . max 1e-6
-  inits <- forM [1 .. n] $ \_ -> do
-    u1 <- uniform gen :: IO Double
-    u2 <- uniform gen :: IO Double
-    u3 <- uniform gen :: IO Double
-    -- log-uniform multipliers in [0.1, 10]
-    let m1 = exp ((u1 - 0.5) * 2 * log 10)
-        m2 = exp ((u2 - 0.5) * 2 * log 10)
-        m3 = exp ((u3 - 0.5) * 2 * log 10)
-    pure $ p0base
-      { gpLengthScale = max 1e-3 (gpLengthScale p0base * m1)
-      , gpSignalVar   = max 1e-6 (scaleVar (gpSignalVar p0base) * m2)
-      , gpNoiseVar    = max 1e-6 (gpNoiseVar p0base * m3)
-      }
-  let runOne p0 = do
-        let pOpt = optimizeGPMV kern x y p0
-            ll   = logMarginalLikelihoodMV x y kern pOpt
-        pure (pOpt, ll)
-  results <- mapM runOne inits
-  let (best, _) = head [ r | r@(_, ll) <- results
-                           , ll == maximum (map snd results) ]
-  pure best
-
--- | N-dimensional single-objective Bayesian Optimization.
--- 内側 acquisition 最大化を **L-BFGS multi-start** で行う:
--- bounds 範囲内で nStarts 個の初期点を一様乱数で生成、各点から L-BFGS で
--- 負 EI を最小化、最良点を採用。
-bayesOptND :: BayesOptConfig
-           -> Int                         -- ^ multi-start 数 (典型 5-20)
-           -> ([Double] -> IO Double)     -- ^ 目的関数 (N 次元、最小化)
-           -> Bounds                      -- ^ 各次元 (lo, hi)
-           -> GenIO
-           -> IO ([([Double], Double)], ([Double], Double))
-bayesOptND cfg nStarts f bounds gen = do
-  let dim = length bounds
-      kern = boKernel cfg
-      -- Initial design: low-discrepancy Halton sequence (better
-      -- coverage of the box than iid uniform random for the small @n@
-      -- typical of BO initial designs).
-      initX = QR.haltonSequenceIn (boInitPoints cfg) bounds
-      sampleX = forM bounds $ \(lo, hi) -> do
-        u <- uniform gen :: IO Double
-        return (lo + u * (hi - lo))
-  initY <- mapM f initX
-  let history0 = zip initX initY
-
-  -- BO2: per-dim X scaling — map every dim to [0, 1] using its (lo, hi)
-  -- bound. After this, a single isotropic ℓ in the GP equates to per-dim
-  -- length scales = ℓ × (hi - lo) in the original space, i.e. ARD with
-  -- weights tied to the box width. skopt's "transform=normalize"
-  -- preprocessing achieves the same effect.
-  let scaleX :: [Double] -> [Double]
-      scaleX xs = [ if hi > lo then (v - lo) / (hi - lo) else v
-                  | ((lo, hi), v) <- zip bounds xs ]
-      unitBounds = replicate dim (0, 1)
-  -- Phase B (GP-Hedge, Hoffman 2011): maintain online "gains" for
-  -- {EI, LCB, PI}. Each iteration each acquisition proposes its best
-  -- candidate via L-BFGS multi-start; one is selected by softmax over
-  -- gains, evaluated, and gains are updated using the GP's predicted
-  -- μ at every proposal (lower μ = higher reward for minimisation).
-  -- This protects against any single acquisition's pathological
-  -- behaviour on a given problem (e.g. EI's exploitation bias on
-  -- multi-modal Branin).
-  let hedgeEta = 1.0 :: Double
-      pickAcq gains gen0 = do
-        let m   = maximum gains
-            ws  = map (\g -> exp (hedgeEta * (g - m))) gains
-            tot = sum ws
-            ps  = map (/ tot) ws
-        u <- uniform gen0 :: IO Double
-        let cum = scanl1 (+) ps
-        pure (length (takeWhile (< u) cum))
-  let loop t hist gains
-        | t == 0 = return hist
-        | otherwise = do
-            let xss     = map fst hist
-                ys      = map snd hist
-                -- BO2: scale X to [0,1]^d for the GP only (history is
-                -- still kept in raw units for f).
-                xssScl  = map scaleX xss
-                xMat    = LA.fromLists xssScl
-                yVec0   = LA.fromList ys
-                -- BO1: z-score y so HP optimization is scale-free
-                -- (skopt normalize_y=True equivalent). Both GP fitting
-                -- and EI run in normalized space; the next-x choice is
-                -- scale-equivariant.
-                stdr    = Std.fitStandardizer (LA.asColumn yVec0)
-                yVec    = LA.flatten
-                            (Std.applyStandardizer stdr (LA.asColumn yVec0))
-                yBest   = LA.minElement yVec
-                -- After BO2 scaling, X lives on [0, 1]^d. The natural ℓ
-                -- grows as √d (mean pairwise distance scales that way),
-                -- so start L-BFGS from ℓ = 0.25 √d to keep correlations
-                -- meaningful as input dimension grows.
-                --
-                -- Phase A (true ARD): the per-dim ℓ_d API is implemented
-                -- in 'Hanalyze.Model.GP.GPParams.gpLengthScales' but disabled in
-                -- the BO loop because with only ~30 evaluations the
-                -- per-dim L-BFGS over-fits noise and underperforms
-                -- isotropic on both Branin and Hartmann6. Future tuning
-                -- (e.g. tighter ℓ_d prior, isotropic-warm-start) can
-                -- re-enable it by setting 'gpLengthScales = Just v'.
-                p0Base  = initParamsFromData (concat xssScl) (LA.toList yVec)
-                ell0    = 0.25 * sqrt (fromIntegral dim)
-                p0      = p0Base { gpLengthScale = ell0 }
-                pOpt    = optimizeGPMV kern xMat yVec p0
-                params  = pOpt
-                -- BO core fix: precompute Cholesky factor (R) and
-                -- α = Ky⁻¹ y ONCE per BO iteration. The negEI callback
-                -- reuses them via 'predictFast' below; this replaces the
-                -- old fitGPMV-per-call which factorised Ky on every
-                -- L-BFGS step (O(n³) wasted per evaluation).
-                kyMat   = noiseKernelMV kern params xMat
-                rChol   = case Chol.cholFactor kyMat of
-                            Just r  -> r
-                            Nothing ->
-                              -- Jitter and try again.
-                              let n     = LA.rows xMat
-                                  kyJ   = kyMat
-                                          + LA.scale 1e-4 (LA.ident n)
-                              in case Chol.cholFactor kyJ of
-                                   Just r  -> r
-                                   Nothing -> error "BO: chol failed"
-                alpha   = LA.flatten
-                            (Chol.cholSolveWithFactor rChol
-                              (LA.asColumn yVec))
-                sf      = gpSignalVar params
-
-                -- Predict (μ, σ, k_star, vstar) at a single x via the
-                -- cached factor. vstar = Ky⁻¹ k_star is reused for both
-                -- the variance and its gradient.
-                predictAt xVec =
-                  let xScl    = LA.fromList (scaleX xVec)
-                      xRow    = LA.asRow xScl
-                      kStarV  = LA.flatten
-                                 (buildKernelMatrixMV kern (gpKernelParams params) xRow xMat)
-                      mu      = LA.dot kStarV alpha
-                      vstar   = LA.flatten
-                                 (Chol.cholSolveWithFactor rChol
-                                   (LA.asColumn kStarV))
-                      varV    = max 0 (sf - LA.dot kStarV vstar)
-                  in (mu, sqrt varV, kStarV, vstar)
-
-                predictMuSig xVec = let (m, s, _, _) = predictAt xVec in (m, s)
-
-                -- Batch predict (μ, σ) at m candidate rows simultaneously.
-                -- Single GEMM for K_*, single triangular solve for V,
-                -- elementwise σ². Replaces m sequential predicts (m
-                -- BLAS-dispatch overheads) with O(1) BLAS calls.
-                predictBatchScaled
-                  :: LA.Matrix Double  -- ^ Scaled X candidates (m × p)
-                  -> (LA.Vector Double, LA.Vector Double)
-                predictBatchScaled xCand =
-                  let kStar = buildKernelMatrixMV kern (gpKernelParams params) xCand xMat  -- m × n
-                      mus   = kStar LA.#> alpha                            -- m
-                      vMat  = Chol.cholSolveWithFactor rChol (LA.tr kStar) -- n × m
-                      -- F1: diag(kStar · vMat) without forming m×m.
-                      kStarDotV = KD.diagAB kStar vMat
-                      sigmas = LA.cmap (\v -> sqrt (max 0 (sf - v))) kStarDotV
-                  in (mus, sigmas)
-
-                -- Phase C (BO4 analytic gradient): per-input partial
-                -- derivatives of μ and σ w.r.t. x. Avoids the 2(p+1)
-                -- function-call overhead of central differences inside
-                -- the inner L-BFGS. Periodic kernel falls back to the
-                -- numeric path (gradient unsupported).
-                --
-                -- diffs[i, d] = scaleX(x)_d − xMat[i, d]
-                -- factor_i = ∂k_i/∂(diffs_i,d) / diffs_i,d  (kernel-specific)
-                -- ∂μ/∂x_scaled_d = (factor ⊙ α)ᵀ · diffs[:, d]
-                -- ∂σ/∂x_scaled_d = −(1/σ) · (factor ⊙ vstar)ᵀ · diffs[:, d]
-                -- Chain back to raw x_d via 1/(hi - lo) factor (BO2).
-                gradMuSig xVec =
-                  let xScl    = LA.fromList (scaleX xVec)
-                      diffs   = LA.fromRows
-                                  [ xScl - xRow | xRow <- LA.toRows xMat ]
-                      sqd     = LA.fromList
-                                  [ d `LA.dot` d | d <- LA.toRows diffs ]
-                      l       = gpLengthScale params
-                      l2      = l * l
-                      kStarV  = LA.flatten
-                                  (buildKernelMatrixMV kern (gpKernelParams params)
-                                     (LA.asRow xScl) xMat)
-                      factor  = case kern of
-                                  RBF      ->
-                                    LA.scale (-1 / l2) kStarV
-                                  Matern52 ->
-                                    let r = LA.cmap (\s ->
-                                              sqrt (max 0 s) * sqrt 5 / l) sqd
-                                        ef = LA.cmap exp (LA.scale (-1) r)
-                                        c  = LA.scale (-5 / (3 * l2))
-                                                (sf `LA.scale`
-                                                  (ef * (LA.cmap (1 +) r)))
-                                    in c
-                                  _ ->
-                                    LA.konst 0 (LA.size kStarV)  -- Periodic/Linear/Poly: numeric fallback
-                      vstar   = LA.flatten
-                                  (Chol.cholSolveWithFactor rChol
-                                    (LA.asColumn kStarV))
-                      mu      = LA.dot kStarV alpha
-                      varV    = max 0 (sf - LA.dot kStarV vstar)
-                      sg      = sqrt varV
-                      -- ∇μ in scaled coordinates: diffsᵀ · (α ⊙ factor)
-                      gradMuS  = LA.tr diffs LA.#> (alpha * factor)
-                      -- ∇σ in scaled coordinates: −(1/σ) · diffsᵀ · (vstar ⊙ factor)
-                      gradSgS
-                        | sg < 1e-12 = LA.konst 0 (LA.cols xMat)
-                        | otherwise  = LA.scale (-1 / sg)
-                                         (LA.tr diffs LA.#> (vstar * factor))
-                      -- Chain back through scaleX: ∂scaledX/∂x = 1/(hi-lo)
-                      invSpan = LA.fromList
-                                  [ if hi > lo then 1 / (hi - lo) else 1
-                                  | (lo, hi) <- bounds ]
-                      gradMu  = LA.toList (gradMuS * invSpan)
-                      gradSg  = LA.toList (gradSgS * invSpan)
-                  in (mu, sg, gradMu, gradSg)
-
-                -- Build (negAcq, gradNegAcq) pair for each acquisition.
-                -- ∂EI/∂(μ,σ) = (-Φ(z), φ(z)) so ∇EI = -Φ(z) ∇μ + φ(z) ∇σ.
-                -- ∂PI/∂(μ,σ) = (-φ(z)/σ, -z·φ(z)/σ) so
-                --   ∇PI = -φ(z)/σ · ∇μ - z·φ(z)/σ · ∇σ.
-                -- LCB is linear: ∇LCB = ∇μ − β ∇σ.
-                wrapAcqGrad
-                  :: ((Double, Double) -> Double)        -- acq value
-                  -> ((Double, Double) -> (Double, Double)) -- (∂/∂μ, ∂/∂σ) of acq
-                  -> ([Double] -> Double, [Double] -> [Double])
-                wrapAcqGrad acqFn dAcq =
-                  let fn xVec = unsafePerformIO $ do
-                        r <- try (evaluate
-                                   (negate (acqFn (let (m, s) = predictMuSig xVec
-                                                   in (m, s)))))
-                              :: IO (Either SomeException Double)
-                        case r of { Left _ -> pure 1e30; Right v -> pure v }
-                      gn xVec = unsafePerformIO $ do
-                        r <- try (evaluate
-                                   (let (mu, sg, gMu, gSg) = gradMuSig xVec
-                                        (dM, dS) = dAcq (mu, sg)
-                                    in [ - (dM * gm + dS * gs)
-                                       | (gm, gs) <- zip gMu gSg ]))
-                              :: IO (Either SomeException [Double])
-                        case r of
-                          Left _  -> pure (replicate (length xVec) 0)
-                          Right v -> pure v
-                  in (fn, gn)
-
-                eiGrad (mu, sg)
-                  | sg <= 1e-12 = (0, 0)
-                  | otherwise   =
-                      let z   = (yBest - mu - 0.01) / sg
-                          phi = density standard z
-                          cdf = cumulative standard z
-                      in (-cdf, phi)
-                piGrad (mu, sg)
-                  | sg <= 1e-12 = (0, 0)
-                  | otherwise   =
-                      let z   = (yBest - mu - 0.01) / sg
-                          phi = density standard z
-                      in (-phi / sg, -z * phi / sg)
-                lcbGrad _      = (1, -2.0)  -- ∂(μ - 2σ)/∂μ = 1, ∂/∂σ = -2
-
-                (negEI,  gNegEI)  = wrapAcqGrad (ei yBest 0.01)         eiGrad
-                (negPI,  gNegPI)  = wrapAcqGrad (pi_ yBest 0.01)        piGrad
-                -- For LCB we want to minimise μ - βσ. Wrap as the value
-                -- itself (acq = -LCB), so negate(acq) = LCB.
-                (negLCB, gNegLCB) =
-                  wrapAcqGrad (negate . ucb 2.0)
-                              (\ms -> let (a, b) = lcbGrad ms in (-a, -b))
-                _ = unitBounds
-
-            -- Inner acquisition optimization: original 20 Halton starts
-            -- (kept for diversity; preselection via batch eval was tried
-            -- in D2 but consistently regressed Hartmann6 — even with
-            -- diversity injection — to a -1.83 local mode that the broad
-            -- Halton scan avoids). Maxiter is reduced from 100 → 50 as
-            -- a speed compromise (Branin and Hartmann6 still solid).
-            haltonStarts <- pure (QR.haltonSequenceIn nStarts bounds)
-            starts <- forM haltonStarts $ \xs ->
-              forM (zip bounds xs) $ \((lo, hi), v) -> do
-                u <- uniform gen :: IO Double
-                let span_ = hi - lo
-                    jit   = (u - 0.5) * 0.05 * span_
-                pure (max lo (min hi (v + jit)))
-            let useAnalytic = case kern of
-                                RBF      -> True
-                                Matern52 -> True
-                                _        -> False   -- Periodic/Linear/Poly は数値勾配
-                runMSG objFn gradFn = mapM (\x0 ->
-                  LBFGS.runLBFGSWith
-                    (LBFGS.defaultLBFGSConfig
-                       { LBFGS.lbStop = OC.defaultStopCriteria
-                                          { OC.stMaxIter = 50 } })
-                    objFn gradFn x0) starts
-                runMS objFn = mapM (\x0 ->
-                  LBFGS.runLBFGSNumeric
-                    (LBFGS.defaultLBFGSConfig
-                       { LBFGS.lbStop = OC.defaultStopCriteria
-                                          { OC.stMaxIter = 50 } })
-                    objFn x0) starts
-                pickXNext rs =
-                  let best     = minimumBy (comparing OC.orValue) rs
-                      xRaw     = OC.orBest best
-                  in zipWith (\(lo, hi) v -> max lo (min hi v)) bounds xRaw
-            xEI  <- pickXNext <$> if useAnalytic
-                                    then runMSG negEI  gNegEI
-                                    else runMS  negEI
-            xLCB <- pickXNext <$> if useAnalytic
-                                    then runMSG negLCB gNegLCB
-                                    else runMS  negLCB
-            xPI  <- pickXNext <$> if useAnalytic
-                                    then runMSG negPI  gNegPI
-                                    else runMS  negPI
-            let candidates = [xEI, xLCB, xPI]
-            -- GP-Hedge selection.
-            k <- pickAcq gains gen
-            let kSafe   = max 0 (min 2 k)
-                xNext   = candidates !! kSafe
-            yNext <- f xNext
-            -- Update gains: reward = -μ at each candidate (we want low μ).
-            let mus     = map (fst . predictMuSig) candidates
-                gains'  = zipWith (\g m -> g - m) gains mus
-            loop (t - 1) (hist ++ [(xNext, yNext)]) gains'
-
-  finalHist <- loop (boIterations cfg) history0 [0, 0, 0]
-  let bestPair = minimumBy (comparing snd) finalHist
-  return (finalHist, bestPair)
-
--- ---------------------------------------------------------------------------
--- Phase E1: bounded multi-restart HP optimisation
--- ---------------------------------------------------------------------------
-
--- | Bounded multi-restart kernel HP optimization for use inside the BO
--- loop. Mirrors skopt's @cook_estimator@ + @n_restarts_optimizer=2@:
--- runs L-BFGS-B from @n@ random log-uniform inits in
--- @log ℓ ∈ [log 0.01, log 100]@, picks the maximum-LML solution.
---
--- Compared to a single-init 'optimizeGPMV' this is significantly more
--- robust on multi-modal log-marginal-likelihood surfaces (Branin, where
--- the 3 global mins demand a sharp ℓ but the LML basin near a broad ℓ
--- is also locally optimal).
---
--- The first init is the user-provided @p0@; subsequent inits are
--- log-uniform perturbations of @p0@ over [0.01, 100].
-optimizeHPMultiRestart
-  :: Int                       -- ^ Total restarts (≥ 1)
-  -> Kernel
-  -> LA.Matrix Double          -- ^ Training X (n × p)
-  -> LA.Vector Double          -- ^ Training y (length n)
-  -> GPParams                  -- ^ Initial guess (first restart)
-  -> GPParams
-optimizeHPMultiRestart nRestarts kern trainX y p0 =
-  let pdim   = LA.cols trainX
-      isARD  = case gpLengthScales p0 of
-                 Just v | LA.size v == pdim && pdim > 0 -> True
-                 _                                       -> False
-      -- log-space bounds: skopt の length_scale_bounds=(0.01, 100)
-      logLo  = log 0.01
-      logHi  = log 100
-      -- σ_f² and σ_n² の bounds は緩めに (kernel HP より広い)
-      logVarLo = log 1e-6
-      logVarHi = log 1e6
-      -- LBFGS bounds for HP vector
-      hpBounds
-        | isARD     = replicate pdim (logLo, logHi)
-                      ++ [(logVarLo, logVarHi), (logVarLo, logVarHi)]
-        | otherwise = [(logLo, logHi), (logVarLo, logVarHi)
-                                     , (logVarLo, logVarHi)]
-      -- Pack/unpack between [Double] (LBFGS state) and GPParams
-      paramsToVec p
-        | isARD     = let Just v = gpLengthScales p
-                          ls = LA.toList v
-                      in map log ls
-                         ++ [log (gpSignalVar p), log (gpNoiseVar p)]
-        | otherwise = [ log (gpLengthScale p)
-                      , log (gpSignalVar  p)
-                      , log (gpNoiseVar   p) ]
-      vecToParams u
-        | isARD     =
-            let lsV = LA.fromList (map exp (take pdim u))
-            in p0
-                 { gpLengthScales = Just lsV
-                 , gpSignalVar    = exp (u !! pdim)
-                 , gpNoiseVar     = exp (u !! (pdim + 1))
-                 }
-        | otherwise = p0
-            { gpLengthScale = exp (u !! 0)
-            , gpSignalVar   = exp (u !! 1)
-            , gpNoiseVar    = exp (u !! 2)
-            }
-      -- Negative LML to minimise (LBFGS minimises by default).
-      negLML u = - logMarginalLikelihoodMV trainX y kern (vecToParams u)
-      -- Build restart inits: keep σ_f²/σ_n² at p0, vary ℓ over a few
-      -- fixed log-spaced points (Branin needs sharp ℓ near 0.1, others
-      -- benefit from broad ℓ near 1-10).
-      p0Vec     = paramsToVec p0
-      sigfLog   = p0Vec !! pdim     -- (paramsToVec layout) for ARD
-      signLog   = p0Vec !! (pdim + 1)
-      sigfLogIso = p0Vec !! 1
-      signLogIso = p0Vec !! 2
-      ellGrid   = take (max 0 (nRestarts - 1)) [log 0.1, log 1.0, log 10.0]
-      mkInit ll
-        | isARD     = replicate pdim ll ++ [sigfLog, signLog]
-        | otherwise = [ll, sigfLogIso, signLogIso]
-      inits = p0Vec : map mkInit ellGrid
-      cfg = LBFGS.defaultLBFGSConfig
-              { LBFGS.lbStop   = OC.defaultStopCriteria
-                                   { OC.stMaxIter = 50, OC.stTolFun = 1e-7 }
-              , LBFGS.lbBounds = Just hpBounds
-              }
-      runOne u0 = unsafePerformIO $ LBFGS.runLBFGSNumeric cfg negLML u0
-      results = map runOne inits
-      -- Pick the lowest-negLML result (= highest LML)
-      best = minimumBy (comparing OC.orValue) results
-  in vecToParams (OC.orBest best)
-
--- | Multi-objective BO using **scalarization** (ParEGO-style).
--- 各反復で random 重み w で Tchebycheff scalarize し、単目的 BO の 1 ステップ
--- (L-BFGS multi-start で acquisition 最大化) を実行する。
--- NSGA 版より高速、acquisition 計算コストが軽い問題に向く。
-bayesOptScalarMO :: Int                                -- iter
-                 -> Int                                -- nInit
-                 -> Int                                -- nStarts (multi-start)
-                 -> Kernel
-                 -> ([Double] -> IO [Double])
-                 -> Bounds
-                 -> GenIO
-                 -> IO [([Double], [Double])]
-bayesOptScalarMO nIter nInit nStarts kern f bounds gen = do
-  initX <- replicateM nInit (forM bounds $ \(lo, hi) -> do
-              u <- uniform gen :: IO Double
-              return (lo + u * (hi - lo)))
-  initY <- mapM f initX
-  let history0 = zip initX initY
-
-      step hist = do
-        let xss   = map fst hist
-            ysAll = map snd hist
-            qDim  = length (head ysAll)
-            xsFlat = map head xss             -- 1D 入力前提の簡易版
-            ysCol j = [y !! j | y <- ysAll]
-        -- random scalarization weight
-        wsRaw <- replicateM qDim (uniform gen :: IO Double)
-        let wSum = sum wsRaw
-            ws   = map (/ wSum) wsRaw
-            -- 各目的の GP fit (1D 入力)
-            modelFor j =
-              let trainY = ysCol j
-                  p0 = initParamsFromData xsFlat trainY
-                  pOpt = optimizeGP kern xsFlat trainY p0
-              in GPModel kern pOpt
-            models = [(modelFor j, ysCol j) | j <- [0 .. qDim - 1]]
-            -- Tchebycheff: max_j w_j (μ_j - z*_j) — z*_j は最良観測
-            zStars = [minimum (ysCol j) | j <- [0 .. qDim - 1]]
-            scalarLcb xVec = unsafePerformIO $ do
-              let xkey = head xVec
-                  computeOne j = do
-                    let (m, ty) = models !! j
-                        r = fitGP m xsFlat ty [xkey]
-                        mu = head (gpMean r)
-                        sg = (head (gpUpper r) - mu) / 2
-                        lcb = mu - 2.0 * sg
-                    _ <- evaluate mu; _ <- evaluate sg
-                    pure ((ws !! j) * (lcb - (zStars !! j)))
-                  safe j = do
-                    res <- try (computeOne j) :: IO (Either SomeException Double)
-                    case res of { Left _ -> pure 1e30; Right v -> pure v }
-              perJ <- mapM safe [0 .. qDim - 1]
-              pure (maximum perJ)
-        -- L-BFGS multi-start で scalarLcb 最小化
-        starts <- replicateM nStarts (forM bounds $ \(lo, hi) -> do
-                    u <- uniform gen :: IO Double
-                    return (lo + u * (hi - lo)))
-        results <- mapM (\x0 ->
-          LBFGS.runLBFGSNumeric
-            (LBFGS.defaultLBFGSConfig
-               { LBFGS.lbStop = OC.defaultStopCriteria { OC.stMaxIter = 60 } })
-            scalarLcb x0) starts
-        let best = minimumBy (comparing OC.orValue) results
-            xNextRaw = OC.orBest best
-            xNext = zipWith (\(lo, hi) v -> max lo (min hi v)) bounds xNextRaw
-        yNext <- f xNext
-        return (hist ++ [(xNext, yNext)])
-
-      loop t h
-        | t == 0 = return h
-        | otherwise = step h >>= loop (t - 1)
-
-  loop nIter history0
-
-argmax :: Ord a => [a] -> Int
-argmax xs = snd (maximum (zip xs [0..]))
-
--- ---------------------------------------------------------------------------
--- 多目的 BO with NSGA-II (Phase V4)
--- ---------------------------------------------------------------------------
-
--- | Multi-objective BO using NSGA-II to optimize the acquisition function.
---
--- Internally fits a @MultiGP@ to obtain per-objective @(μ, σ)@, then
--- runs NSGA-II to find the Pareto front in @(μ_1, μ_2, ...)@ space; one
--- point from that front is chosen and evaluated.
---
--- A deliberately simple implementation; an EHVI-based variant is left
--- for future extension.
-bayesOptMOWithNSGA
-  :: Int                                -- ^ Number of BO iterations.
-  -> Int                                -- ^ Number of initial samples.
-  -> Kernel
-  -> ([Double] -> IO [Double])          -- ^ Multi-objective function.
-  -> Bounds
-  -> GenIO
-  -> IO [([Double], [Double])]          -- ^ Sequence of @(x, y)@ pairs.
-bayesOptMOWithNSGA nIter nInit kern f bounds gen = do
-  -- 初期点
-  initX <- replicateM nInit (do
-              vs <- forM bounds $ \(lo, hi) -> do
-                u <- uniform gen :: IO Double
-                return (lo + u * (hi - lo))
-              return vs)
-  initY <- mapM f initX
-  let history0 = zip initX initY
-
-  let loop t hist
-        | t == 0 = return hist
-        | otherwise = do
-            -- 各目的に GP を fit (1D 入力前提の簡易版)
-            -- 多次元入力の場合は MultiGP を別途準備
-            -- ここでは bounds の最初の次元のみ使う簡易動作
-            let xsFlat = map head (map fst hist)  -- 1D 入力前提
-                ysAll = map snd hist
-                qDim  = length (head ysAll)
-                ysCol j = [y !! j | y <- ysAll]
-
-            -- 各目的 j の GP モデルを fit
-            let modelFor j =
-                  let trainY = ysCol j
-                      p0 = initParamsFromData xsFlat trainY
-                      pOpt = optimizeGP kern xsFlat trainY p0
-                  in GPModel kern pOpt
-
-                models = [modelFor j | j <- [0 .. qDim - 1]]
-
-                -- NSGA-II で Pareto front を探索 (acquisition surface 上)
-                -- 各目的: μ - β σ (LCB) を最小化
-                acqObjective xVec =
-                  [ unsafePerformIO $ do
-                      let computed = do
-                            let trainY = ysCol j
-                                m = models !! j
-                                gpRes = fitGP m xsFlat trainY [head xVec]
-                                mu = head (gpMean gpRes)
-                                sg = (head (gpUpper gpRes) - mu) / 2
-                            _ <- evaluate mu; _ <- evaluate sg
-                            pure (ucbToMin mu sg)
-                      r <- try computed :: IO (Either SomeException Double)
-                      case r of { Left _ -> pure 1e30; Right v -> pure v }
-                  | j <- [0 .. qDim - 1] ]
-
-                ucbToMin :: Double -> Double -> Double
-                ucbToMin mu sigma = mu - 2.0 * sigma   -- LCB
-
-            -- NSGA-II で Pareto front を 1 ステップ探索
-            front <- nsga2 (defaultNSGAConfig { nsgaPopSize = 30
-                                             , nsgaGenerations = 30 })
-                          acqObjective bounds gen
-
-            -- front から random 選択
-            idx <- uniform gen :: IO Double
-            let i = floor (idx * fromIntegral (length front))
-                xNext = solDecision (front !! min i (length front - 1))
-            yNext <- f xNext
-            loop (t - 1) (hist ++ [(xNext, yNext)])
-
-  loop nIter history0
diff --git a/src/Hanalyze/Optim/CMAES.hs b/src/Hanalyze/Optim/CMAES.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/CMAES.hs
+++ /dev/null
@@ -1,196 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.CMAES
--- Description : CMA-ES 簡易版 (対角共分散のみ) — 非凸連続最適化
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- CMA-ES (Covariance Matrix Adaptation Evolution Strategy) — Hansen 2001.
---
--- The de-facto state of the art for non-convex continuous optimization.
--- This module implements a **simplified single-stage** version of the
--- @(μ/μ_w, λ)@-rank-μ + rank-1 update.
---
--- Spec (simplified):
---
--- * Each generation samples @λ@ vectors @z_k ~ N(0, I)@ and forms
---   @x_k = m + σ B z_k@ (diagonal covariance only; @B = diag(d)@, full
---   rank @C@ is omitted).
--- * The top @μ@ samples (weights @w@) update the mean @m ← Σ w_i x_i@.
--- * @σ@ is multiplicatively updated with a 1/5-rule-like rule
---   (no path cumulation). Sufficient for problems up to Rastrigin 5D.
---
--- For the full-rank tutorial CMA-ES (Hansen 2016), see 'Hanalyze.Optim.CMAESFull'.
-{-# LANGUAGE StrictData #-}
-module Hanalyze.Optim.CMAES
-  ( CMAESConfig (..)
-  , defaultCMAESConfig
-  , runCMAES
-  , runCMAESWith
-  ) where
-
-import Data.List (sortBy)
-import Data.Ord (comparing)
-import qualified System.Random.MWC as MWC
-import qualified System.Random.MWC.Distributions as MWCD
-import Control.Monad (replicateM, forM)
-import Control.Exception (SomeException, try, evaluate)
-import Hanalyze.Optim.Common
-import qualified Hanalyze.Optim.LBFGS as LB
-
--- | Configuration for the simplified diagonal CMA-ES.
-data CMAESConfig = CMAESConfig
-  { cmStop    :: !StopCriteria
-  , cmSigma0  :: !Double          -- ^ Initial step size @σ@.
-  , cmLambda  :: !(Maybe Int)     -- ^ Population size @λ@ (defaults to
-                                  --   @4 + ⌊3 ln D⌋@ when 'Nothing').
-  , cmDir     :: !Direction
-  , cmBounds  :: !(Maybe Bounds)  -- ^ Optional box constraints. When set,
-                                  --   each sampled point is reflected
-                                  --   back into the bounds via
-                                  --   'clipToBounds'.
-  , cmPolish  :: !Bool
-    -- ^ When 'True' (default), run a final L-BFGS-B (numeric gradient)
-    --   refinement on @x_best@ at termination. Mirrors scipy's
-    --   @differential_evolution(polish=True)@ pattern. Brings smooth
-    --   landscapes to near-machine precision after CMA-ES localised
-    --   the basin.
-  } deriving (Show, Eq)
-
--- | Default configuration: 200 iterations, @σ₀ = 0.5@, default @λ@,
--- minimization, no bounds.
-defaultCMAESConfig :: CMAESConfig
-defaultCMAESConfig = CMAESConfig
-  { cmStop   = defaultStopCriteria { stMaxIter = 200, stTolFun = 1e-10 }
-  , cmSigma0 = 0.5
-  , cmLambda = Nothing
-  , cmDir    = Minimize
-  , cmBounds = Nothing
-  , cmPolish = True
-  }
-
--- | Run simplified CMA-ES with the default configuration.
-runCMAES :: ([Double] -> Double)
-         -> [Double]              -- ^ Initial mean @m₀@.
-         -> MWC.GenIO
-         -> IO OptimResult
-runCMAES = runCMAESWith defaultCMAESConfig
-
--- | Run simplified CMA-ES with a user-specified configuration.
-runCMAESWith :: CMAESConfig
-             -> ([Double] -> Double)
-             -> [Double]
-             -> MWC.GenIO
-             -> IO OptimResult
-runCMAESWith cfg fUser m0 gen = do
-  let f      = flipFor (cmDir cfg) fUser
-      d      = length m0
-      lam    = case cmLambda cfg of
-                 Just l  -> l
-                 Nothing -> 4 + floor (3 * log (fromIntegral d) :: Double)
-      mu     = lam `div` 2
-      -- 重み: ln(μ + 0.5) - ln(i)、正規化
-      wsRaw  = [ log (fromIntegral mu + 0.5) - log (fromIntegral i)
-               | i <- [1 .. mu] ]
-      wsSum  = sum wsRaw
-      ws     = map (/ wsSum) wsRaw
-      -- 初期分散 (対角) = 1
-      diag0  = replicate d 1.0
-  res <- loop cfg f gen 0 m0 (cmSigma0 cfg) diag0 ws lam mu (f m0) [f m0]
-  -- Optional final L-BFGS-B polish (scipy parity).
-  if cmPolish cfg
-    then do
-      let polCfg = LB.defaultLBFGSConfig
-                     { LB.lbStop   = defaultStopCriteria
-                                       { stMaxIter = 100
-                                       , stTolFun  = 1e-12
-                                       , stTolX    = 1e-12 }
-                     , LB.lbBounds = cmBounds cfg
-                     , LB.lbDir    = cmDir cfg
-                     }
-      ePol <- try (LB.runLBFGSNumeric polCfg fUser (orBest res))
-                :: IO (Either SomeException OptimResult)
-      case ePol of
-        Left _ -> pure res
-        Right polRes ->
-          let xC = case cmBounds cfg of
-                     Nothing -> orBest polRes
-                     Just bs -> clipToBounds bs (orBest polRes)
-          in do
-            evC <- try (evaluate (fUser xC)) :: IO (Either SomeException Double)
-            case evC of
-              Right vC ->
-                let better = case cmDir cfg of
-                               Minimize -> vC < orValue res
-                               Maximize -> vC > orValue res
-                in pure $ if better
-                            then res { orBest = xC, orValue = vC }
-                            else res
-              Left _   -> pure res
-    else pure res
-
--- | 反復本体。
-loop :: CMAESConfig
-     -> ([Double] -> Double)
-     -> MWC.GenIO
-     -> Int
-     -> [Double]                 -- m (現平均)
-     -> Double                    -- σ
-     -> [Double]                  -- 対角 D (Cholesky)
-     -> [Double]                  -- weights w (length μ)
-     -> Int -> Int                -- λ, μ
-     -> Double                    -- 現 best 値
-     -> [Double]                  -- history (新しい先頭)
-     -> IO OptimResult
-loop cfg f gen iter m sigma diag ws lam mu bestV hist
-  | iter >= stMaxIter (cmStop cfg) = mkResult cfg m bestV hist iter False
-  | sigma < 1e-14 = mkResult cfg m bestV hist iter True
-  | otherwise = do
-      -- λ 個サンプル
-      samples <- replicateM lam $ do
-        z <- replicateM (length m) (MWCD.standard gen)
-        let xRaw = zipWith3 (\mi di zi -> mi + sigma * di * zi) m diag z
-            x    = case cmBounds cfg of
-                     Nothing -> xRaw
-                     Just bs -> clipToBounds bs xRaw
-        return (x, z, f x)
-      let sorted   = sortBy (comparing (\(_, _, v) -> v)) samples
-          topMu    = take mu sorted
-          xs'      = map (\(x, _, _) -> x) topMu
-          zs'      = map (\(_, z, _) -> z) topMu
-          fs'      = map (\(_, _, v) -> v) topMu
-          -- 平均更新: m ← Σ w_i x_i
-          mNew     = avgWeighted ws xs'
-          -- 簡易ステップ更新: 集団 best が改善した割合で σ を増減
-          newBestV = head fs'
-          improve  = newBestV < bestV
-          sigmaN   = if improve then sigma * 1.05 else sigma * 0.95
-          -- 対角分散の rank-μ 更新 (極簡易): w_i z_i² の重み付き平均で更新
-          var      = [ max 1e-12 (sum (zipWith (\w zi -> w * (zs' !! 0 !! 0) ^ (2::Int)) ws zs')) | _ <- m ]
-          -- 上の var はバグ気味なので、ちゃんと書き直す
-          varDiag  = [ max 1e-12 $ sum (zipWith (\w (zi:_) -> w * zi^(2::Int)) ws (transposeZs zs' j))
-                     | j <- [0 .. length m - 1] ]
-          diagN    = zipWith (\d0 v -> d0 * 0.7 + sqrt v * 0.3) diag varDiag
-          bestN    = min bestV newBestV
-          histN    = bestN : hist
-          _ = var  -- 未使用置きの抑制
-      if abs (bestV - newBestV) < stTolFun (cmStop cfg) && iter > 10
-        then mkResult cfg mNew bestN histN (iter + 1) True
-        else loop cfg f gen (iter + 1) mNew sigmaN diagN ws lam mu bestN histN
-  where
-    transposeZs :: [[Double]] -> Int -> [[Double]]
-    transposeZs zss j = [ [zs !! j] | zs <- zss ]
-
--- | 重み付きベクトル平均。
-avgWeighted :: [Double] -> [[Double]] -> [Double]
-avgWeighted ws xs =
-  let dim = length (head xs)
-  in [ sum (zipWith (\w x -> w * (x !! j)) ws xs) | j <- [0 .. dim - 1] ]
-
-mkResult :: CMAESConfig -> [Double] -> Double -> [Double]
-         -> Int -> Bool -> IO OptimResult
-mkResult cfg m bestV hist iter conv =
-  let vUser = case cmDir cfg of { Minimize -> bestV; Maximize -> negate bestV }
-      hU    = case cmDir cfg of
-                Minimize -> reverse hist
-                Maximize -> map negate (reverse hist)
-  in pure $ OptimResult m vUser hU iter conv
diff --git a/src/Hanalyze/Optim/CMAESFull.hs b/src/Hanalyze/Optim/CMAESFull.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/CMAESFull.hs
+++ /dev/null
@@ -1,210 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.CMAESFull
--- Description : フルランク CMA-ES (Hansen 2016 チュートリアル準拠)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Full-rank CMA-ES (Hansen 2016 tutorial, complete edition).
---
--- The companion module @Hanalyze.Optim.CMAES@ is a simplified diagonal variant.
--- This module implements:
---
--- * Rank-1 + rank-μ updates of the full covariance matrix @C@.
--- * Evolution-path cumulation for both @p_σ@ and @p_c@.
--- * Eigendecomposition of @C@ to recover @B, D@ (recomputed periodically
---   to reduce cost).
--- * Cumulative Step-size Adaptation (CSA) for the step size @σ@.
--- * The Heaviside helper @h_σ@ that suppresses @C@ updates after large
---   jumps.
---
--- Hyperparameters use the standard values from Hansen (2016).
-{-# LANGUAGE StrictData #-}
-module Hanalyze.Optim.CMAESFull
-  ( CMAESFConfig (..)
-  , defaultCMAESFConfig
-  , runCMAESFull
-  , runCMAESFullWith
-  ) where
-
-import Data.List (sortBy)
-import Data.Ord (comparing)
-import qualified System.Random.MWC as MWC
-import qualified System.Random.MWC.Distributions as MWCD
-import qualified Numeric.LinearAlgebra as LA
-import Control.Monad (replicateM, forM)
-import Hanalyze.Optim.Common
-
--- | Configuration for full-rank CMA-ES.
-data CMAESFConfig = CMAESFConfig
-  { cmfStop    :: !StopCriteria
-  , cmfSigma0  :: !Double          -- ^ Initial step size @σ@.
-  , cmfLambda  :: !(Maybe Int)     -- ^ Population size @λ@ (defaults to
-                                   --   @4 + ⌊3 ln n⌋@ when 'Nothing').
-  , cmfDir     :: !Direction
-  , cmfBounds  :: !(Maybe Bounds)  -- ^ Optional box constraints. Each
-                                   --   sampled @x@ is reflected with
-                                   --   'clipToBounds' /before/ being
-                                   --   evaluated; @y = (x-m)/σ@ is left
-                                   --   untouched so the covariance
-                                   --   update is not distorted.
-  } deriving (Show, Eq)
-
--- | Default configuration: 200 iterations, @σ₀ = 0.5@, default @λ@,
--- minimization, no bounds.
-defaultCMAESFConfig :: CMAESFConfig
-defaultCMAESFConfig = CMAESFConfig
-  { cmfStop   = defaultStopCriteria { stMaxIter = 200, stTolFun = 1e-12 }
-  , cmfSigma0 = 0.5
-  , cmfLambda = Nothing
-  , cmfDir    = Minimize
-  , cmfBounds = Nothing
-  }
-
--- | Run full-rank CMA-ES with the default configuration.
-runCMAESFull :: ([Double] -> Double)
-             -> [Double]              -- ^ Initial mean @m₀@.
-             -> MWC.GenIO
-             -> IO OptimResult
-runCMAESFull = runCMAESFullWith defaultCMAESFConfig
-
--- | Run full-rank CMA-ES with a user-specified configuration.
-runCMAESFullWith :: CMAESFConfig
-                 -> ([Double] -> Double)
-                 -> [Double]
-                 -> MWC.GenIO
-                 -> IO OptimResult
-runCMAESFullWith cfg fUser m0 gen = do
-  let f      = flipFor (cmfDir cfg) fUser
-      n      = length m0
-      nD     = fromIntegral n :: Double
-      lam    = case cmfLambda cfg of
-                 Just l  -> l
-                 Nothing -> 4 + floor (3 * log nD :: Double)
-      mu     = lam `div` 2
-
-      -- 重み (log(μ+1) - log(i))
-      wsRaw  = [ log (fromIntegral mu + 1.0) - log (fromIntegral i)
-               | i <- [1 .. mu] ]
-      wsSum  = sum wsRaw
-      ws     = map (/ wsSum) wsRaw
-      muEff  = 1 / sum [w*w | w <- ws]
-
-      -- 標準パラメータ (Hansen 2016 Eq. (49)-(58))
-      cs     = (muEff + 2) / (nD + muEff + 5)
-      ds     = 1 + 2 * max 0 (sqrt ((muEff - 1) / (nD + 1)) - 1) + cs
-      cc     = (4 + muEff / nD) / (nD + 4 + 2 * muEff / nD)
-      c1     = 2 / ((nD + 1.3)^(2::Int) + muEff)
-      cmuRaw = 2 * (muEff - 2 + 1 / muEff) / ((nD + 2)^(2::Int) + muEff)
-      cmu    = min (1 - c1) cmuRaw
-      eN     = sqrt nD * (1 - 1/(4*nD) + 1/(21*nD*nD))
-
-      m0v    = LA.fromList m0
-      cm0    = LA.ident n :: LA.Matrix Double
-      ps0    = LA.konst 0 n
-      pc0    = LA.konst 0 n
-      f0     = f m0
-      params = CMAESParams n nD lam mu ws muEff cs ds cc c1 cmu eN
-  loop cfg f gen 0 params m0v (cmfSigma0 cfg) cm0 ps0 pc0 f0 [f0]
-
-data CMAESParams = CMAESParams
-  { pN      :: !Int
-  , pNd     :: !Double
-  , pLam    :: !Int
-  , pMu     :: !Int
-  , pWs     :: ![Double]
-  , pMuEff  :: !Double
-  , pCs     :: !Double
-  , pDs     :: !Double
-  , pCc     :: !Double
-  , pC1     :: !Double
-  , pCmu    :: !Double
-  , pEN     :: !Double
-  }
-
--- | 反復本体。
-loop :: CMAESFConfig
-     -> ([Double] -> Double)
-     -> MWC.GenIO
-     -> Int
-     -> CMAESParams
-     -> LA.Vector Double           -- m
-     -> Double                      -- σ
-     -> LA.Matrix Double            -- C
-     -> LA.Vector Double            -- p_σ
-     -> LA.Vector Double            -- p_c
-     -> Double                      -- best f
-     -> [Double]                    -- history
-     -> IO OptimResult
-loop cfg f gen iter p m sigma c psig pc bestV hist
-  | iter >= stMaxIter (cmfStop cfg) = mkRes cfg m bestV hist iter False
-  | sigma < 1e-16 = mkRes cfg m bestV hist iter True
-  | otherwise = do
-      -- 共分散の固有分解 C = B D² Bᵀ
-      let (eigs, bMat) = LA.eigSH (LA.sym c)
-          dDiag = LA.cmap (\v -> sqrt (max 1e-16 v)) eigs   -- D
-          bd    = bMat LA.<> LA.diag dDiag                  -- B·D (n × n)
-          -- C^{-1/2} = B · diag(1/d) · Bᵀ (path 更新で使う)
-          dInv  = LA.cmap (\d -> 1 / max 1e-16 d) dDiag
-          cInvSqrt = bMat LA.<> LA.diag dInv LA.<> LA.tr bMat
-          n     = pN p
-          lam   = pLam p
-      -- λ 個サンプル
-      samples <- replicateM lam $ do
-        z <- LA.fromList <$> replicateM n (MWCD.standard gen)
-        let y    = bd LA.#> z
-            xRaw = m + LA.scale sigma y
-            xEval = case cmfBounds cfg of
-                      Nothing -> xRaw
-                      Just bs -> LA.fromList (clipToBounds bs (LA.toList xRaw))
-            fx   = f (LA.toList xEval)
-        return (xEval, y, fx)
-      let sortedAll = sortBy (comparing (\(_,_,v) -> v)) samples
-          topMu = take (pMu p) sortedAll
-          ys    = [ y | (_, y, _) <- topMu ]
-          fs    = [ v | (_, _, v) <- topMu ]
-          newBest = minimum fs
-          -- ⟨y⟩_w = Σ w_i y_i
-          yMean = LA.fromList
-                    [ sum [ (pWs p !! i) * (LA.toList (ys !! i) !! j)
-                          | i <- [0 .. pMu p - 1] ]
-                    | j <- [0 .. n - 1] ]
-          -- 平均更新: m ← m + σ · yMean
-          mNew = m + LA.scale sigma yMean
-          -- p_σ 更新
-          psNew = LA.scale (1 - pCs p) psig +
-                  LA.scale (sqrt (pCs p * (2 - pCs p) * pMuEff p))
-                           (cInvSqrt LA.#> yMean)
-          psNorm = LA.norm_2 psNew
-          -- σ 更新 (CSA)
-          sigmaN = sigma * exp ((pCs p / pDs p) * (psNorm / pEN p - 1))
-          -- h_σ (Heaviside): big jumps を抑制
-          gen1   = fromIntegral (iter + 1) :: Double
-          chiBound = (1.4 + 2 / (pNd p + 1)) * pEN p
-          hSig = if psNorm / sqrt (1 - (1 - pCs p) ** (2 * gen1)) < chiBound
-                 then 1 else 0 :: Double
-          -- p_c 更新
-          pcNew = LA.scale (1 - pCc p) pc +
-                  LA.scale (hSig * sqrt (pCc p * (2 - pCc p) * pMuEff p)) yMean
-          -- C 更新 (rank-1 + rank-μ)
-          ppT  = LA.outer pcNew pcNew
-          deltaH = (1 - hSig) * pCc p * (2 - pCc p)
-          rankMu = sum [ LA.scale (pWs p !! i)
-                                  (LA.outer (ys !! i) (ys !! i))
-                       | i <- [0 .. pMu p - 1] ]
-          cNew = LA.scale (1 - pC1 p - pCmu p) c
-                 + LA.scale (pC1 p) (ppT + LA.scale deltaH c)
-                 + LA.scale (pCmu p) rankMu
-          bestN  = min bestV newBest
-          histN  = bestN : hist
-      if abs (bestV - newBest) < stTolFun (cmfStop cfg) && iter > 10
-        then mkRes cfg mNew bestN histN (iter + 1) True
-        else loop cfg f gen (iter + 1) p mNew sigmaN cNew psNew pcNew bestN histN
-
-mkRes :: CMAESFConfig -> LA.Vector Double -> Double -> [Double]
-      -> Int -> Bool -> IO OptimResult
-mkRes cfg mV bestV hist iter conv =
-  let vUser = case cmfDir cfg of { Minimize -> bestV; Maximize -> negate bestV }
-      hU    = case cmfDir cfg of
-                Minimize -> reverse hist
-                Maximize -> map negate (reverse hist)
-  in pure $ OptimResult (LA.toList mV) vUser hU iter conv
diff --git a/src/Hanalyze/Optim/Common.hs b/src/Hanalyze/Optim/Common.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/Common.hs
+++ /dev/null
@@ -1,139 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.Common
--- Description : 単一目的最適化アルゴリズム群が共有する基盤型・既定値
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Common foundation for the single-objective optimization algorithms.
---
--- Provides the shared types and defaults used by every single-objective
--- optimizer (@Hanalyze.Optim.NelderMead@, @Hanalyze.Optim.LBFGS@, @Hanalyze.Optim.LineSearch@,
--- @Hanalyze.Optim.DifferentialEvolution@, @Hanalyze.Optim.CMAES@, @Hanalyze.Optim.CMAESFull@,
--- @Hanalyze.Optim.SimulatedAnnealing@, @Hanalyze.Optim.ParticleSwarm@), plus the unified
--- 'Bounds' type for box constraints.
---
--- Each optimizer's runner has the same shape:
---
--- @
--- runX :: XConfig -> ([Double] -> Double) -> [Double] -> IO OptimResult
--- @
---
--- (Deterministic algorithms also return @IO@ for uniformity. A pure-only
--- variant can be exported separately when needed.)
-{-# LANGUAGE StrictData #-}
-module Hanalyze.Optim.Common
-  ( OptimResult (..)
-  , StopCriteria (..)
-  , defaultStopCriteria
-  , Direction (..)
-  , flipFor
-    -- * Box constraints (search range)
-  , Bounds
-  , clipToBounds
-  , projectToBounds
-  , sampleUniformIn
-  , boundsPenalty
-  , inBounds
-  ) where
-
-import Control.Monad (forM)
-import qualified System.Random.MWC as MWC
-
--- | Optimization direction.
-data Direction = Minimize | Maximize deriving (Show, Eq)
-
--- | Stopping criteria shared by every optimizer.
-data StopCriteria = StopCriteria
-  { stMaxIter :: !Int     -- ^ Maximum number of iterations.
-  , stTolFun  :: !Double  -- ^ Convergence on @|Δf| < tol@.
-  , stTolX    :: !Double  -- ^ Convergence on @‖Δx‖∞ < tol@ (or simplex
-                          --   size for Nelder-Mead).
-  } deriving (Show, Eq)
-
--- | Standard generic stopping criteria. Sufficient for the bundled
--- benchmarks.
-defaultStopCriteria :: StopCriteria
-defaultStopCriteria = StopCriteria
-  { stMaxIter = 1000
-  , stTolFun  = 1e-8
-  , stTolX    = 1e-10
-  }
-
--- | Optimization result.
-data OptimResult = OptimResult
-  { orBest      :: ![Double]   -- ^ Best point @x*@.
-  , orValue     :: !Double     -- ^ Best value @f(x*)@ (internally minimized).
-  , orHistory   :: ![Double]   -- ^ Per-iteration best-value trace (up to
-                               --   @stMaxIter + 1@ entries).
-  , orIters     :: !Int        -- ^ Actual number of iterations executed.
-  , orConverged :: !Bool       -- ^ True if stopped on tolerance criteria.
-  } deriving (Show, Eq)
-
--- | Toggle between the user's 'Direction' and the internal-always-minimize
--- representation. Each optimizer applies this at entry and reverses the
--- value sign at exit.
---
--- > flipFor Maximize f x = -(f x)
--- > flipFor Minimize f x =   f x
-flipFor :: Direction -> ([Double] -> Double) -> ([Double] -> Double)
-flipFor Minimize f = f
-flipFor Maximize f = negate . f
-{-# INLINE flipFor #-}
-
--- ---------------------------------------------------------------------------
--- Box constraints (各次元の上下限)
--- ---------------------------------------------------------------------------
-
--- | Per-dimension @(lower, upper)@ list.
-type Bounds = [(Double, Double)]
-
--- | Reflect each coordinate back into its range when outside. Excessive
--- excursions are clamped to the range width.
-clipToBounds :: Bounds -> [Double] -> [Double]
-clipToBounds bs xs = zipWith reflect bs xs
-  where
-    reflect (lo, hi) x
-      | x < lo    = let d = lo - x in lo + min d (hi - lo)
-      | x > hi    = let d = x - hi in hi - min d (hi - lo)
-      | otherwise = x
-
--- | Plain clipping: pin out-of-range coordinates to the boundary value.
---
--- >>> projectToBounds [(0,1),(0,1)] [-0.5, 1.5]
--- [0.0,1.0]
-projectToBounds :: Bounds -> [Double] -> [Double]
-projectToBounds bs xs =
-  zipWith (\(lo, hi) x -> max lo (min hi x)) bs xs
-
--- | Sample a single point uniformly within the bounds (shared
--- initialization for DE / PSO / SA / NSGA).
-sampleUniformIn :: Bounds -> MWC.GenIO -> IO [Double]
-sampleUniformIn bs gen = forM bs $ \(lo, hi) -> MWC.uniformR (lo, hi) gen
-
--- | Soft penalty for out-of-range coordinates, intended to be added to
--- the objective in L-BFGS / Nelder-Mead. Returns @0@ inside the bounds
--- and @k Σ_i d_i²@ outside (with @k = 10^6@).
---
--- @
--- objWithPenalty xs = f xs + boundsPenalty (Just bs) xs
--- @
-boundsPenalty :: Maybe Bounds -> [Double] -> Double
-boundsPenalty Nothing   _  = 0
-boundsPenalty (Just bs) xs =
-  let k = 1e6 :: Double
-      dists = zipWith dist bs xs
-  in k * sum [d * d | d <- dists]
-  where
-    dist (lo, hi) x
-      | x < lo    = lo - x
-      | x > hi    = x - hi
-      | otherwise = 0
-
--- | True when every coordinate lies inside the bounds.
---
--- >>> inBounds [(0,1),(0,1)] [0.5, 0.5]
--- True
--- >>> inBounds [(0,1),(0,1)] [0.5, 1.5]
--- False
-inBounds :: Bounds -> [Double] -> Bool
-inBounds bs xs = all (\((lo, hi), x) -> x >= lo && x <= hi) (zip bs xs)
diff --git a/src/Hanalyze/Optim/Constrained.hs b/src/Hanalyze/Optim/Constrained.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/Constrained.hs
+++ /dev/null
@@ -1,182 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.Constrained
--- Description : 拡張ラグランジュ法による制約付き最適化
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Constrained optimization via the **Augmented Lagrangian** method.
---
--- Internalizes equality constraints @g_i(x) = 0@ and inequality constraints
--- @h_j(x) ≤ 0@ via Lagrange multipliers + a quadratic penalty, exposing an
--- outer loop that calls an existing unconstrained solver (typically
--- @Hanalyze.Optim.LBFGS@) on each subproblem.
---
--- Augmented Lagrangian:
---
--- @
--- L_A(x, λ, μ, ρ) = f(x)
---                 + Σ_i λ_i g_i(x) + (ρ/2) Σ_i g_i(x)²
---                 + Σ_j (1/(2ρ)) [max(0, μ_j + ρ h_j(x))² - μ_j²]
--- @
---
--- Each outer iteration:
---
---   1. Minimize @L_A@ in @x@ with the inner solver (L-BFGS or Nelder-Mead).
---   2. Update multipliers: @λ ← λ + ρ g(x*)@, @μ ← max(0, μ + ρ h(x*))@.
---   3. Grow the penalty @ρ@ if the constraint violation did not improve.
---
--- Reference: Nocedal & Wright, /Numerical Optimization/, Ch. 17.
-module Hanalyze.Optim.Constrained
-  ( ConstrainedConfig (..)
-  , ConstraintSet (..)
-  , defaultConstrainedConfig
-  , runAugmentedLagrangian
-  , penaltyMethod
-  , boxToIneq
-  ) where
-
-import qualified Hanalyze.Optim.LBFGS  as LBFGS
-import qualified Hanalyze.Optim.Common as OC
-
--- | A set of constraints.
---
--- Equality constraints:   @g_i(x) = 0@.
--- Inequality constraints: @h_j(x) ≤ 0@.
-data ConstraintSet = ConstraintSet
-  { csEq   :: ![[Double] -> Double]   -- ^ Equality constraints @g_i@
-                                      --   (the satisfying value is 0).
-  , csIneq :: ![[Double] -> Double]   -- ^ Inequality constraints @h_j ≤ 0@.
-  }
-
--- | Augmented Lagrangian configuration.
-data ConstrainedConfig = ConstrainedConfig
-  { ccOuterIter :: !Int                -- ^ Outer iterations (10–30 typical).
-  , ccRho0      :: !Double             -- ^ Initial penalty coefficient @ρ₀@.
-  , ccRhoGrowth :: !Double             -- ^ Growth rate for @ρ@ (2.0–10.0 typical).
-  , ccTolViol   :: !Double             -- ^ Constraint-violation tolerance.
-  , ccInnerStop :: !OC.StopCriteria    -- ^ Stop criteria for the inner L-BFGS solver.
-  } deriving (Show, Eq)
-
--- | Default configuration: 20 outer iterations, @ρ₀ = 1.0@, growth 5.0,
--- violation tolerance 1e-6, inner solver capped at 200 iterations.
-defaultConstrainedConfig :: ConstrainedConfig
-defaultConstrainedConfig = ConstrainedConfig
-  { ccOuterIter = 20
-  , ccRho0      = 1.0
-  , ccRhoGrowth = 5.0
-  , ccTolViol   = 1e-6
-  , ccInnerStop = OC.defaultStopCriteria { OC.stMaxIter = 200 }
-  }
-
--- | Solve a constrained problem via the Augmented Lagrangian method.
---
--- Returns @(inner solver result, constraint-violation norm)@.
-runAugmentedLagrangian
-  :: ConstrainedConfig
-  -> ([Double] -> Double)        -- ^ Objective (minimized).
-  -> ConstraintSet
-  -> [Double]                     -- ^ Initial point.
-  -> IO (OC.OptimResult, Double)  -- ^ Inner L-BFGS result and violation norm.
-runAugmentedLagrangian cfg f cs x0 = do
-  let neq    = length (csEq cs)
-      nineq  = length (csIneq cs)
-      lam0   = replicate neq   0
-      mu0    = replicate nineq 0
-      rho0   = ccRho0 cfg
-  go 0 x0 lam0 mu0 rho0
-  where
-    go iter x lam mu rho
-      | iter >= ccOuterIter cfg = do
-          r <- innerSolve x lam mu rho
-          return (r, viol (OC.orBest r))
-      | otherwise = do
-          r <- innerSolve x lam mu rho
-          let xNew = OC.orBest r
-              vNorm = viol xNew
-          if vNorm < ccTolViol cfg
-            then return (r, vNorm)
-            else do
-              -- 乗数更新
-              let lamN = zipWith (\l g_i -> l + rho * g_i) lam
-                                 [g xNew | g <- csEq cs]
-                  muN  = zipWith (\m h_j -> max 0 (m + rho * h_j)) mu
-                                 [h xNew | h <- csIneq cs]
-                  rhoN = rho * ccRhoGrowth cfg
-              go (iter + 1) xNew lamN muN rhoN
-
-    -- 拡張 Lagrangian を内側で最小化
-    innerSolve x lam mu rho = do
-      let lagrangian xs =
-            let fx     = f xs
-                eqVals = [g xs | g <- csEq cs]
-                inVals = [h xs | h <- csIneq cs]
-                eqTerm = sum (zipWith (*) lam eqVals)
-                       + (rho / 2) * sum [v * v | v <- eqVals]
-                inTerm = sum [ let z = max 0 (m + rho * v)
-                               in (z * z - m * m) / (2 * rho)
-                             | (m, v) <- zip mu inVals ]
-            in fx + eqTerm + inTerm
-          lcfg = LBFGS.defaultLBFGSConfig { LBFGS.lbStop = ccInnerStop cfg }
-      LBFGS.runLBFGSNumeric lcfg lagrangian x
-
-    -- 制約違反ノルム ||g||² + Σ max(0, h)²
-    viol xs =
-      let eqV = sum [(g xs)^(2::Int) | g <- csEq cs]
-          ineqV = sum [(max 0 (h xs))^(2::Int) | h <- csIneq cs]
-      in sqrt (eqV + ineqV)
-
--- | Expand box constraints (@lo_i ≤ x_i ≤ hi_i@) into two inequality
--- constraints (@≤ 0@) per dimension.
---
--- For each dimension @i@ this emits @lo_i - x_i ≤ 0@ (lower bound) and
--- @x_i - hi_i ≤ 0@ (upper bound). The returned list has length
--- @2 × length bs@.
---
--- @
--- let cs = ConstraintSet { csEq = []
---                        , csIneq = boxToIneq bs ++ otherIneq }
--- (r, viol) <- runAugmentedLagrangian defaultConstrainedConfig f cs x0
--- @
-boxToIneq :: OC.Bounds -> [[Double] -> Double]
-boxToIneq bs = concat
-  [ [ \xs -> lo - (xs !! i)
-    , \xs -> (xs !! i) - hi ]
-  | (i, (lo, hi)) <- zip [0 ..] bs ]
-
--- | The simpler **penalty method** — a stripped-down Augmented Lagrangian
--- that omits the multiplier updates and only grows the penalty. Easy to
--- implement and lightweight, but prone to ill-conditioning.
-penaltyMethod
-  :: ConstrainedConfig
-  -> ([Double] -> Double)
-  -> ConstraintSet
-  -> [Double]
-  -> IO (OC.OptimResult, Double)
-penaltyMethod cfg f cs x0 = do
-  go 0 x0 (ccRho0 cfg)
-  where
-    go iter x rho
-      | iter >= ccOuterIter cfg = do
-          r <- innerSolve x rho
-          return (r, viol (OC.orBest r))
-      | otherwise = do
-          r <- innerSolve x rho
-          let xNew = OC.orBest r
-              vNorm = viol xNew
-          if vNorm < ccTolViol cfg
-            then return (r, vNorm)
-            else go (iter + 1) xNew (rho * ccRhoGrowth cfg)
-
-    innerSolve x rho = do
-      let penalty xs =
-            let fx     = f xs
-                eqV    = sum [(g xs)^(2::Int) | g <- csEq cs]
-                ineqV  = sum [(max 0 (h xs))^(2::Int) | h <- csIneq cs]
-            in fx + (rho / 2) * (eqV + ineqV)
-          lcfg = LBFGS.defaultLBFGSConfig { LBFGS.lbStop = ccInnerStop cfg }
-      LBFGS.runLBFGSNumeric lcfg penalty x
-
-    viol xs =
-      let eqV = sum [(g xs)^(2::Int) | g <- csEq cs]
-          ineqV = sum [(max 0 (h xs))^(2::Int) | h <- csIneq cs]
-      in sqrt (eqV + ineqV)
diff --git a/src/Hanalyze/Optim/Desirability.hs b/src/Hanalyze/Optim/Desirability.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/Desirability.hs
+++ /dev/null
@@ -1,62 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.Desirability
--- Description : Desirability 関数 (Derringer & Suich 1980) による多目的スカラー化
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Desirability functions (Derringer & Suich 1980).
---
--- A classical scalarization for multi-objective optimization. Each response
--- @y_j@ is mapped to a per-response desirability @d_j ∈ [0, 1]@, and the
--- overall desirability is the geometric mean:
---
--- @
--- D = (Π d_j)^(1/q)
--- @
---
--- The @x@ that maximizes @D@ is a point that satisfies all responses
--- reasonably well.
-{-# LANGUAGE OverloadedStrings #-}
-module Hanalyze.Optim.Desirability
-  ( DesirabilityType (..)
-  , individualDesirability
-  , overallDesirability
-  ) where
-
--- | The three desirability shapes.
-data DesirabilityType
-  = Maximize  Double Double          -- ^ Maximize: thresholds @low@ (→ 0) and @high@ (→ 1).
-  | Minimize  Double Double          -- ^ Minimize: thresholds @high@ (→ 0) and @low@ (→ 1).
-  | Target    Double Double Double   -- ^ Target value @t@ with allowed range @[low, high]@.
-  deriving (Show, Eq)
-
--- | Compute the individual desirability @d_j(y)@.
-individualDesirability :: DesirabilityType -> Double -> Double
-individualDesirability dt y = case dt of
-  Maximize lo hi
-    | y <= lo   -> 0
-    | y >= hi   -> 1
-    | otherwise -> (y - lo) / (hi - lo)
-  Minimize hi lo
-    | y >= hi   -> 0
-    | y <= lo   -> 1
-    | otherwise -> (hi - y) / (hi - lo)
-  Target t lo hi
-    | y == t                  -> 1
-    | y < lo || y > hi        -> 0
-    | y < t                   -> (y - lo) / (t - lo)
-    | otherwise               -> (hi - y) / (hi - t)
-
--- | Overall desirability @D = (Π d_j)^(1/q)@.
---
--- Any single zero collapses @D@ to zero — out-of-range responses are
--- strongly penalized.
-overallDesirability :: [DesirabilityType] -> [Double] -> Double
-overallDesirability dts ys
-  | length dts /= length ys = 0
-  | null ys                 = 0
-  | otherwise =
-      let ds = zipWith individualDesirability dts ys
-          q  = fromIntegral (length ds) :: Double
-      in if any (<= 0) ds then 0
-           else (product ds) ** (1 / q)
diff --git a/src/Hanalyze/Optim/DifferentialEvolution.hs b/src/Hanalyze/Optim/DifferentialEvolution.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/DifferentialEvolution.hs
+++ /dev/null
@@ -1,252 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.DifferentialEvolution
--- Description : Differential Evolution (DE/rand/1/bin) — Storn & Price 1997
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Differential Evolution (DE/rand/1/bin) — Storn & Price 1997.
---
--- A gradient-free, global, simple-to-implement and empirically robust
--- evolutionary algorithm. Best suited to continuous non-convex problems,
--- typically effective in the 5-30 dimensional regime.
---
--- Algorithm (DE/rand/1/bin) — each generation, for every individual @i@:
---
---   1. Pick three distinct indices @a, b, c@ from the population (all
---      different from @i@).
---   2. Mutation: @v = a + F * (b - c)@ with mutation factor @F ∈ [0.5, 0.8]@
---      typical.
---   3. Binomial crossover: @u_j = v_j@ with probability @CR ∈ [0.7, 0.9]@,
---      otherwise @x_j@; at least one dimension is forced from @v@.
---   4. Selection: replace @x_i ← u@ if @f(u) ≤ f(x_i)@.
---
--- Cost: @N@ function evaluations per generation (population size). Easily
--- parallelizable, but this implementation is sequential.
-{-# LANGUAGE StrictData #-}
-module Hanalyze.Optim.DifferentialEvolution
-  ( DEConfig (..)
-  , DEStrategy (..)
-  , defaultDEConfig
-  , runDE
-  , runDEWith
-  ) where
-
-import Data.List (minimumBy)
-import Data.Ord (comparing)
-import qualified System.Random.MWC as MWC
-import qualified System.Random.MWC.Distributions as MWCD
-import Control.Monad (forM, forM_)
-import Data.IORef
-import Control.Exception (SomeException, try, evaluate)
-import Hanalyze.Optim.Common
-import qualified Hanalyze.Optim.LBFGS as LB
-
--- | DE strategy.
---
---   * 'ClassicRand1Bin' — DE/rand/1/bin with fixed @F@ / @CR@ from
---     'deF' / 'deCR' (the original Storn-Price 1997 formulation).
---   * 'JDE' — self-adaptive DE (Brest et al. 2006). Each individual
---     carries its own @F_i@ and @CR_i@; before each trial each is
---     re-sampled with probability @τ@ (defaults @τ_F = τ_CR = 0.1@):
---
---       @F_i  ←  F_l + r₁ · (F_u − F_l)@        (r₁ ~ U(0, 1))
---       @CR_i ←  r₂@                            (r₂ ~ U(0, 1))
---
---     where @F_l, F_u = 0.1, 0.9@. The new @(F_i, CR_i)@ are kept iff
---     the trial is accepted. Removes the manual @F@/@CR@ tuning that
---     classic DE is sensitive to.
-data DEStrategy
-  = ClassicRand1Bin
-  | JDE
-  deriving (Show, Eq)
-
--- | DE configuration.
---
--- @F@ (mutation factor) and @CR@ (crossover rate) defaults are typical
--- values. The population size should be roughly @5×D@ to @10×D@.
-data DEConfig = DEConfig
-  { deStop      :: !StopCriteria
-  , dePopSize   :: !Int        -- ^ Population size @N@ (5×D – 10×D typical).
-  , deF         :: !Double     -- ^ Mutation factor @F@ (initial value when 'JDE').
-  , deCR        :: !Double     -- ^ Crossover probability @CR@ (initial value when 'JDE').
-  , deBounds    :: !Bounds     -- ^ Per-dimension @(lo, hi)@; used for both
-                               --   initialization and boundary reflection.
-  , deStrategy  :: !DEStrategy -- ^ Trial-generation strategy.
-  , deDir       :: !Direction
-  , dePolish    :: !Bool
-    -- ^ When 'True' (default), run a final L-BFGS-B (numeric gradient)
-    --   refinement on @x_best@ at termination. Mirrors scipy's
-    --   @differential_evolution(polish=True)@. Brings smooth landscapes
-    --   (Sphere, Levy etc.) to near-machine precision after DE has
-    --   localised the basin.
-  } deriving (Show, Eq)
-
--- | Default configuration: 200 iterations, population @max(20, 10×D)@,
--- @F = 0.5@, @CR = 0.9@, **'JDE' self-adaptive** strategy, minimization.
---
--- 'JDE' is the recommended default because the classic @F = 0.7@ /
--- @CR = 0.9@ is brittle on diverse problem types (Sphere, Rastrigin
--- and Rosenbrock all want different settings). Switch to
--- 'ClassicRand1Bin' to recover the previous behaviour.
-defaultDEConfig :: [(Double, Double)] -> DEConfig
-defaultDEConfig bs = DEConfig
-  { deStop     = defaultStopCriteria { stMaxIter = 200 }
-  , dePopSize  = max 20 (10 * length bs)
-  , deF        = 0.5
-  , deCR       = 0.9
-  , deBounds   = bs
-  , deStrategy = JDE
-  , deDir      = Minimize
-  , dePolish   = True
-  }
-
--- | Run DE with the default configuration built from @bounds@.
-runDE :: [(Double, Double)]            -- ^ Per-dimension bounds.
-      -> ([Double] -> Double)          -- ^ Objective.
-      -> MWC.GenIO
-      -> IO OptimResult
-runDE bounds f gen = runDEWith (defaultDEConfig bounds) f gen
-
--- | Run DE with a user-supplied configuration.
-runDEWith :: DEConfig
-          -> ([Double] -> Double)
-          -> MWC.GenIO
-          -> IO OptimResult
-runDEWith cfg fUser gen = do
-  let f      = flipFor (deDir cfg) fUser
-      n      = dePopSize cfg
-  -- 初期集団: 各次元 (lo, hi) 一様乱数。
-  -- 各個体に (F_i, CR_i) を持たせる (Classic では未使用、jDE では更新)。
-  pop0 <- forM [1 .. n] $ \_ -> sampleUniformIn (deBounds cfg) gen
-  let fPop0 = map f pop0
-      pop0' = [ (x, fx, deF cfg, deCR cfg) | (x, fx) <- zip pop0 fPop0 ]
-  popRef  <- newIORef pop0'
-  histRef <- newIORef [minimum fPop0]
-  iterRef <- newIORef 0
-  convRef <- newIORef False
-  let stop = deStop cfg
-      maxI = stMaxIter stop
-
-  let loop = do
-        i <- readIORef iterRef
-        if i >= maxI
-          then return ()
-          else do
-            pop <- readIORef popRef
-            let fs     = map (\(_, ff, _, _) -> ff) pop
-                bestF  = minimum fs
-                worstF = maximum fs
-            if abs (worstF - bestF) < stTolFun stop
-              then writeIORef convRef True
-              else do
-                pop' <- stepDE cfg f gen pop
-                writeIORef popRef pop'
-                let bestF' = minimum (map (\(_, ff, _, _) -> ff) pop')
-                modifyIORef histRef (bestF' :)
-                writeIORef iterRef (i + 1)
-                loop
-  loop
-  popFinal <- readIORef popRef
-  iters    <- readIORef iterRef
-  conv     <- readIORef convRef
-  histR    <- readIORef histRef
-  let (xb, vb, _, _) = minimumBy (comparing (\(_, ff, _, _) -> ff)) popFinal
-  -- Optional final L-BFGS-B polish on x_best (scipy parity).
-  -- Numeric gradient because the user's f is opaque. Bounds stay
-  -- within deBounds. If polish improves, replace; otherwise keep.
-  (xPol, vPol) <-
-    if dePolish cfg
-      then do
-        let polCfg = LB.defaultLBFGSConfig
-                       { LB.lbStop   = defaultStopCriteria
-                                         { stMaxIter = 100
-                                         , stTolFun  = 1e-12
-                                         , stTolX    = 1e-12 }
-                       , LB.lbBounds = Just (deBounds cfg)
-                       }
-        -- Polish can fail (numeric grad → linearSolveSVDR etc. for
-        -- objectives that internally invert near-singular matrices).
-        -- Catch any exception and fall back to the unpolished best.
-        eR <- try (LB.runLBFGSNumeric polCfg f xb) :: IO (Either SomeException OptimResult)
-        case eR of
-          Left _  -> pure (xb, vb)
-          Right r ->
-            let xR = clipToBounds (deBounds cfg) (orBest r)
-            in do
-              evR <- try (evaluate (f xR)) :: IO (Either SomeException Double)
-              case evR of
-                Right vR | vR < vb -> pure (xR, vR)
-                _                  -> pure (xb, vb)
-      else pure (xb, vb)
-  let vUser    = case deDir cfg of { Minimize -> vPol; Maximize -> negate vPol }
-      histUser = case deDir cfg of
-                   Minimize -> reverse histR
-                   Maximize -> map negate (reverse histR)
-  return $ OptimResult xPol vUser histUser iters conv
-
--- | jDE re-sampling probabilities (Brest 2006 standard values).
-jdeTau :: Double
-jdeTau = 0.1
-
-jdeFLo, jdeFHi :: Double
-jdeFLo = 0.1
-jdeFHi = 0.9
-
--- | 1 世代の更新。'DEStrategy' によって @F_i@/@CR_i@ の扱いが分かれる:
---
---   * 'ClassicRand1Bin': @F_i = deF cfg@, @CR_i = deCR cfg@ (固定)。
---   * 'JDE'            : 各 trial 前に確率 'jdeTau' で再サンプリング、
---     trial が採用された場合のみ新値を保持。
-stepDE :: DEConfig
-       -> ([Double] -> Double)
-       -> MWC.GenIO
-       -> [([Double], Double, Double, Double)]
-       -> IO [([Double], Double, Double, Double)]
-stepDE cfg f gen pop = do
-  let n   = length pop
-      d   = length (deBounds cfg)
-      bs  = deBounds cfg
-  newPop <- forM [0 .. n - 1] $ \i -> do
-    let (xi, fi, fOld, crOld) = pop !! i
-    -- jDE: confirm or refresh F_i / CR_i for this trial
-    (fTrial, crTrial) <- case deStrategy cfg of
-      ClassicRand1Bin -> return (deF cfg, deCR cfg)
-      JDE             -> do
-        u1 <- MWC.uniformR (0, 1) gen :: IO Double
-        u2 <- MWC.uniformR (0, 1) gen :: IO Double
-        u3 <- MWC.uniformR (0, 1) gen :: IO Double
-        u4 <- MWC.uniformR (0, 1) gen :: IO Double
-        let f'  = if u1 < jdeTau then jdeFLo + u2 * (jdeFHi - jdeFLo) else fOld
-            cr' = if u3 < jdeTau then u4 else crOld
-        return (f', cr')
-    -- mutation 用に i と異なる 3 個体をランダム選択
-    [a, b, c] <- pickThree n i gen
-    let xa = let (x, _, _, _) = pop !! a in x
-        xb' = let (x, _, _, _) = pop !! b in x
-        xc' = let (x, _, _, _) = pop !! c in x
-        v   = zipWith3 (\xai xbi xci -> xai + fTrial * (xbi - xci)) xa xb' xc'
-        v'  = clipToBounds bs v
-    -- crossover (binomial)
-    jRand <- MWC.uniformR (0, d - 1) gen
-    u <- forM (zip3 [0..] xi v') $ \(j, xj, vj) -> do
-      r <- MWC.uniformR (0, 1) gen
-      return $ if (r :: Double) < crTrial || j == jRand then vj else xj
-    let fu = f u
-    if fu <= fi
-      then return (u,  fu, fTrial, crTrial)
-      else return (xi, fi, fOld,   crOld)
-  return newPop
-
--- | i と異なる 3 つの相異なるインデックスを集団 [0, n) から選ぶ。
-pickThree :: Int -> Int -> MWC.GenIO -> IO [Int]
-pickThree n i gen = do
-  let pickOne avoid = do
-        k <- MWC.uniformR (0, n - 1) gen
-        if k `elem` avoid then pickOne avoid else return k
-  a <- pickOne [i]
-  b <- pickOne [i, a]
-  c <- pickOne [i, a, b]
-  return [a, b, c]
-
--- | (`sampleUniform` and `clipBound` are now provided by `Hanalyze.Optim.Common`
---    as `sampleUniformIn` / `clipToBounds`.)
diff --git a/src/Hanalyze/Optim/GradAscent.hs b/src/Hanalyze/Optim/GradAscent.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/GradAscent.hs
+++ /dev/null
@@ -1,68 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.GradAscent
--- Description : 素朴な勾配上昇 / 下降法
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Vanilla gradient ascent / descent.
---
--- The numeric-gradient implementation that used to live in
--- @Hanalyze.Model.GP.optimizeGP@, extracted as a shared foundation. The learning
--- rate is shrunk by 0.5 % per iteration; iteration stops early when the
--- gradient norm drops below the configured tolerance.
---
--- When to use which:
---
---   * 'Hanalyze.Optim.Adam.runAdam' — momentum-based, robust, recommended default.
--- - 'Hanalyze.Optim.GradAscent.gradientAscent' — シンプル、軽量、デバッグ容易
--- - 'Hanalyze.Optim.GradAscent.gradientDescent' — 上の符号反転版
-{-# LANGUAGE OverloadedStrings #-}
-module Hanalyze.Optim.GradAscent
-  ( GradConfig (..)
-  , defaultGradConfig
-  , gradientAscent
-  , gradientDescent
-  ) where
-
--- | Configuration for gradient ascent / descent.
-data GradConfig = GradConfig
-  { gradIterations   :: Int     -- ^ Maximum number of iterations.
-  , gradLearningRate :: Double  -- ^ Initial learning rate.
-  , gradDecay        :: Double  -- ^ Per-iteration learning-rate decay (e.g. 0.995).
-  , gradTolerance    :: Double  -- ^ Early-stop threshold on gradient norm.
-  } deriving (Show)
-
--- | Default configuration: 400 iterations, lr 0.1, decay 0.995, tol 1e-8.
-defaultGradConfig :: GradConfig
-defaultGradConfig = GradConfig
-  { gradIterations  = 400
-  , gradLearningRate = 0.1
-  , gradDecay       = 0.995
-  , gradTolerance   = 1e-8
-  }
-
--- | Gradient ascent. Pass the gradient of the objective to maximize it.
---
--- @gradFn x@ returns the gradient at the current point. Each iteration:
---
---   1. Compute the gradient @g@.
---   2. Stop when @|g| < tol@.
---   3. @x ← x + lr × g/|g|@ (normalized for stability).
---   4. @lr ← lr × decay@.
-gradientAscent :: GradConfig -> ([Double] -> [Double]) -> [Double] -> [Double]
-gradientAscent cfg gradFn = go (gradIterations cfg) (gradLearningRate cfg)
-  where
-    go 0   _  x = x
-    go itr lr x =
-      let g     = gradFn x
-          gnorm = sqrt (sum (map (\v -> v * v) g))
-      in if gnorm < gradTolerance cfg
-           then x
-           else
-             let x' = zipWith (\xi gi -> xi + lr * gi / gnorm) x g
-             in go (itr - 1) (lr * gradDecay cfg) x'
-
--- | Gradient descent. Negates the gradient and delegates to
--- 'gradientAscent'.
-gradientDescent :: GradConfig -> ([Double] -> [Double]) -> [Double] -> [Double]
-gradientDescent cfg gradFn = gradientAscent cfg (map negate . gradFn)
diff --git a/src/Hanalyze/Optim/LBFGS.hs b/src/Hanalyze/Optim/LBFGS.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/LBFGS.hs
+++ /dev/null
@@ -1,297 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.LBFGS
--- Description : L-BFGS (限定記憶 BFGS) 準ニュートン法
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- L-BFGS (Limited-memory BFGS) quasi-Newton method.
---
--- Liu & Nocedal (1989). The standard for local optimization of large,
--- smooth objectives — practical at hundreds to tens of thousands of
--- dimensions (memory @O(mn)@ versus BFGS's @O(n²)@; @m = 10@ is typical).
---
--- Features:
---
---   * Two-loop recursion for inverse-Hessian × gradient (history size @m@).
---   * Line search: backtracking + Armijo condition (simple; not full Wolfe).
---   * Numeric-gradient variant ('runLBFGSNumeric').
---
--- Implementation note (L1, the no-list rule): the public API still
--- exchanges @[Double]@ at the boundaries (zero-cost adapter), but every
--- inner-loop arithmetic operation runs on @LA.Vector Double@ via BLAS.
--- This eliminates the per-step Haskell list overhead that previously
--- dominated the runtime (verified on the GLM bench in G2).
-{-# LANGUAGE StrictData #-}
-
-module Hanalyze.Optim.LBFGS
-  ( LBFGSConfig (..)
-  , defaultLBFGSConfig
-  , runLBFGS
-  , runLBFGSWith
-  , runLBFGSWithPure
-  , runLBFGSNumeric
-    -- * Vector-native variants (avoid list↔Vector conversion on every step)
-  , runLBFGSWithV
-  , runLBFGSWithVResult
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import           Hanalyze.Optim.Common
-import qualified Hanalyze.Optim.Numeric as ON
-
--- | L-BFGS 設定。
-data LBFGSConfig = LBFGSConfig
-  { lbStop    :: !StopCriteria
-  , lbMemory   :: !Int        -- ^ History size @m@ (5–20 typical).
-  , lbLSMax    :: !Int        -- ^ Maximum line-search iterations.
-  , lbLSC1     :: !Double     -- ^ Armijo constant @c₁@ (1e-4 typical).
-  , lbLSShrink :: !Double     -- ^ Backtracking shrink rate (0.5 typical).
-  , lbDir      :: !Direction
-  , lbBounds   :: !(Maybe Bounds)  -- ^ Optional box constraints. When set,
-                                   --   adds a quadratic 'boundsPenalty'
-                                   --   (with @k = 10^6@) to both @f@ and
-                                   --   @∇f@ (soft-penalty enforcement).
-  } deriving (Show, Eq)
-
--- | Default L-BFGS configuration: history 10, Armijo c1 1e-4,
--- backtracking shrink 0.5, minimization, no bounds. Stop criteria
--- match scipy's @\"L-BFGS-B\"@ defaults (@maxiter = 1000@,
--- @ftol = 1e-12@) so smooth problems can converge to near-machine
--- precision.
-defaultLBFGSConfig :: LBFGSConfig
-defaultLBFGSConfig = LBFGSConfig
-  { lbStop     = defaultStopCriteria { stMaxIter = 1000
-                                     , stTolFun  = 1e-12
-                                     , stTolX    = 1e-12 }
-  , lbMemory   = 10
-  , lbLSMax    = 25
-  , lbLSC1     = 1e-4
-  , lbLSShrink = 0.5
-  , lbDir      = Minimize
-  , lbBounds   = Nothing
-  }
-
--- | Run L-BFGS with an explicit analytic gradient.
-runLBFGSWith :: LBFGSConfig
-             -> ([Double] -> Double)        -- ^ Objective @f@.
-             -> ([Double] -> [Double])      -- ^ Gradient @∇f@.
-             -> [Double]                    -- ^ Initial point @x₀@.
-             -> IO OptimResult
-runLBFGSWith cfg fUser gUser x0 = pure (runLBFGSWithPure cfg fUser gUser x0)
-
--- | 純粋版 ('runLBFGSWith' は本体が完全に純粋 = @let … in pure result@ ゆえ IO は不要)。
--- 乱数を使わない決定的最適化なので、 純粋に閉じられる ('fitSVMPure' 等が利用)。
-runLBFGSWithPure :: LBFGSConfig
-                 -> ([Double] -> Double)
-                 -> ([Double] -> [Double])
-                 -> [Double]
-                 -> OptimResult
-runLBFGSWithPure cfg fUser gUser x0 =
-  let mbs          = lbBounds cfg
-      sign         = case lbDir cfg of { Minimize -> 1; Maximize -> -1 :: Double }
-      -- The internal objective and gradient operate on LA.Vector Double.
-      -- They wrap the user's [Double] callbacks; the per-call list
-      -- conversion is unavoidable but its cost is dominated by the user
-      -- function itself, not by the optimizer.
-      fV :: LA.Vector Double -> Double
-      fV v = let xs = LA.toList v
-             in sign * (fUser xs + boundsPenalty mbs xs)
-      gV :: LA.Vector Double -> LA.Vector Double
-      gV v =
-        let xs = LA.toList v
-            base = LA.fromList (gUser xs)
-            penalty = case mbs of
-              Nothing -> LA.konst 0 (LA.size v)
-              Just bs ->
-                let k = 1e6 :: Double
-                in LA.fromList
-                     [ if x <  lo then 2*k*(x - lo)
-                       else if x > hi then 2*k*(x - hi)
-                       else 0
-                     | ((lo, hi), x) <- zip bs xs ]
-        in LA.scale sign (base + penalty)
-      x0v   = LA.fromList x0
-      f0    = fV x0v
-      g0    = gV x0v
-      (xEndV, fEnd, hist, iters, conv) =
-        loop cfg fV gV 0 x0v f0 g0 [] [] [f0]
-      vUser = sign * fEnd     -- == fEnd for Minimize, -fEnd for Maximize
-      histUser = case lbDir cfg of
-                   Minimize -> reverse hist
-                   Maximize -> map negate (reverse hist)
-  in OptimResult
-       { orBest      = LA.toList xEndV
-       , orValue     = vUser
-       , orHistory   = histUser
-       , orIters     = iters
-       , orConverged = conv
-       }
-
--- | Run L-BFGS with the default configuration and an analytic gradient.
-runLBFGS :: ([Double] -> Double)
-         -> ([Double] -> [Double])
-         -> [Double]
-         -> IO OptimResult
-runLBFGS = runLBFGSWith defaultLBFGSConfig
-
--- | Numeric-gradient variant: gradients are computed by central
--- differences (@h = 1e-5@).
-runLBFGSNumeric :: LBFGSConfig
-                -> ([Double] -> Double)
-                -> [Double]
-                -> IO OptimResult
-runLBFGSNumeric cfg f x0 =
-  runLBFGSWith cfg f (ON.numGradCentral 1e-5 f) x0
-
--- | Vector-native variant: avoids the @[Double] ↔ Vector Double@
--- conversion that 'runLBFGSWith' incurs on every objective and
--- gradient call. Use this when the caller already has hmatrix
--- vectors / matrices on hand (e.g. GLM, GP).
-runLBFGSWithV
-  :: LBFGSConfig
-  -> (LA.Vector Double -> Double)
-  -> (LA.Vector Double -> LA.Vector Double)
-  -> LA.Vector Double
-  -> IO OptimResult
-runLBFGSWithV cfg fUser gUser x0v = do
-  res <- runLBFGSWithVResult cfg fUser gUser x0v
-  pure res
-
--- | Like 'runLBFGSWithV'. Provided as a longer-named alias so the
--- export list is unambiguous when both list- and Vector-native APIs
--- need to be referenced from a single import.
-runLBFGSWithVResult
-  :: LBFGSConfig
-  -> (LA.Vector Double -> Double)
-  -> (LA.Vector Double -> LA.Vector Double)
-  -> LA.Vector Double
-  -> IO OptimResult
-runLBFGSWithVResult cfg fUser gUser x0v =
-  let mbs   = lbBounds cfg
-      sign  = case lbDir cfg of { Minimize -> 1; Maximize -> -1 :: Double }
-      fV v = let pen = case mbs of
-                   Nothing -> 0
-                   Just bs -> boundsPenalty (Just bs) (LA.toList v)
-             in sign * (fUser v + pen)
-      gV v = case mbs of
-        Nothing -> LA.scale sign (gUser v)
-        Just bs ->
-          let xs    = LA.toList v
-              k     = 1e6 :: Double
-              penG  = LA.fromList
-                [ if x <  lo then 2*k*(x - lo)
-                  else if x > hi then 2*k*(x - hi)
-                  else 0
-                | ((lo, hi), x) <- zip bs xs ]
-          in LA.scale sign (gUser v + penG)
-      f0       = fV x0v
-      g0       = gV x0v
-      (xEndV, fEnd, hist, iters, conv) =
-        loop cfg fV gV 0 x0v f0 g0 [] [] [f0]
-      vUser    = sign * fEnd
-      histUser = case lbDir cfg of
-                   Minimize -> reverse hist
-                   Maximize -> map negate (reverse hist)
-  in pure $ OptimResult
-       { orBest      = LA.toList xEndV
-       , orValue     = vUser
-       , orHistory   = histUser
-       , orIters     = iters
-       , orConverged = conv
-       }
-
--- ---------------------------------------------------------------------------
--- Inner loop, all Vector
--- ---------------------------------------------------------------------------
-
--- | Iteration body. @s_k = x_{k+1} - x_k@, @y_k = g_{k+1} - g_k@; the
--- last @m@ are kept (newest at the head).
-loop :: LBFGSConfig
-     -> (LA.Vector Double -> Double)
-     -> (LA.Vector Double -> LA.Vector Double)
-     -> Int                                       -- 反復カウンタ
-     -> LA.Vector Double                          -- 現在 x
-     -> Double                                    -- f(x)
-     -> LA.Vector Double                          -- ∇f(x)
-     -> [LA.Vector Double]                        -- s 履歴 (新しい先頭)
-     -> [LA.Vector Double]                        -- y 履歴 (新しい先頭)
-     -> [Double]                                  -- best 値履歴 (逆順)
-     -> (LA.Vector Double, Double, [Double], Int, Bool)
-loop cfg f g iter x fx gx ss ys hist
-  | iter >= stMaxIter (lbStop cfg) = (x, fx, hist, iter, False)
-  | gnorm < stTolFun (lbStop cfg)  = (x, fx, hist, iter, True)
-  | otherwise =
-      let d = twoLoop ss ys gx
-          -- 初回反復 (曲率履歴なし) は方向が未スケールの最急降下 (‖d‖=‖g‖)。
-          -- 勾配が大きい問題で α=1 の第1歩を打つと巨大にオーバーシュートし、
-          -- 平坦な退化解に嵌って勾配消失で誤収束する (GP 周辺尤度で実測:
-          -- ℓ が真の峰 105 を越えて 1e12 に飛ぶ)。Nocedal & Wright §3.5 に従い
-          -- 初回のみ α₀ = min(1, 1/‖g‖₁) に抑える (2 回目以降は quasi-Newton
-          -- 方向が自己スケールするので α=1 が適切)。
-          alpha0 | null ss   = min 1 (1 / max 1e-16 (LA.norm_1 gx))
-                 | otherwise = 1
-          (xN, fN, alpha) = lineSearch cfg f x fx gx d alpha0
-      in if alpha < 1e-16
-           then (x, fx, hist, iter, True)
-           else
-             let gN  = g xN
-                 sN  = xN - x
-                 yN  = gN - gx
-                 ssN = take (lbMemory cfg) (sN : ss)
-                 ysN = take (lbMemory cfg) (yN : ys)
-                 dx  = LA.norm_Inf sN
-             in if dx < stTolX (lbStop cfg)
-                   && abs (fx - fN) < stTolFun (lbStop cfg)
-                  then (xN, fN, fN : hist, iter + 1, True)
-                  else loop cfg f g (iter + 1) xN fN gN ssN ysN (fN : hist)
-  where
-    gnorm = LA.norm_2 gx
-
--- | Two-loop recursion: @r = H_k · q@, computed scale-free.
--- @ss@ / @ys@ are aligned with the newest at the head
--- (@s_{k-1}, s_{k-2}, ..., s_{k-m}@).
-twoLoop :: [LA.Vector Double] -> [LA.Vector Double]
-        -> LA.Vector Double -> LA.Vector Double
-twoLoop [] _ q = LA.scale (-1) q                 -- 履歴なし: 単純な負勾配
-twoLoop ss ys q =
-  let pairs   = zip ss ys                          -- 新しい順
-      rhos    = [ 1 / LA.dot y s | (s, y) <- pairs ]
-      triples = zip3 ss ys rhos
-      -- 第 1 ループ
-      step1 (qCur, accAlphas) (s, y, rho) =
-        let a  = rho * LA.dot s qCur
-            qN = qCur - LA.scale a y
-        in (qN, a : accAlphas)
-      (qFinal, alphasNew) = foldl step1 (q, []) triples
-      -- スケーリング: H_0 = γ I, γ = (s_0^T y_0) / (y_0^T y_0)
-      (s0, y0) = (head ss, head ys)
-      gamma    = LA.dot s0 y0 / max 1e-16 (LA.dot y0 y0)
-      r0       = LA.scale gamma qFinal
-      -- 第 2 ループ
-      triplesAlphas = reverse (zip triples (reverse alphasNew))
-      step2 rCur ((s, y, rho), alpha) =
-        let beta = rho * LA.dot y rCur
-            scal = alpha - beta
-        in rCur + LA.scale scal s
-      r        = foldl step2 r0 triplesAlphas
-  in LA.scale (-1) r
-
--- | backtracking + Armijo 条件 @f(x + αd) ≤ f(x) + c1 α gᵀd@。
--- @alpha0@ = 初期ステップ幅 (通常 1.0、初回最急降下では 1/‖g‖₁ 等で抑える)。
-lineSearch :: LBFGSConfig
-           -> (LA.Vector Double -> Double)
-           -> LA.Vector Double -> Double
-           -> LA.Vector Double -> LA.Vector Double
-           -> Double                                  -- ^ 初期ステップ幅 α₀
-           -> (LA.Vector Double, Double, Double)
-lineSearch cfg f x fx g d alpha0 =
-  let gtd = LA.dot g d
-      go alpha k
-        | k >= lbLSMax cfg = (xCand, f xCand, alpha)
-        | armijo           = (xCand, fxCand, alpha)
-        | otherwise        = go (alpha * lbLSShrink cfg) (k + 1)
-        where
-          xCand  = x + LA.scale alpha d
-          fxCand = f xCand
-          armijo = fxCand <= fx + lbLSC1 cfg * alpha * gtd
-  in go alpha0 0
diff --git a/src/Hanalyze/Optim/LineSearch.hs b/src/Hanalyze/Optim/LineSearch.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/LineSearch.hs
+++ /dev/null
@@ -1,201 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.LineSearch
--- Description : 1 次元最適化 (Brent 法・黄金分割探索)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- One-dimensional optimization: Brent's method + golden-section search.
---
--- Both find a local minimum on a unimodal interval @[a, b]@ to high
--- precision.
---
---   * 'goldenSection' — simple and robust; linear convergence on unimodal
---     functions.
---   * 'brent' — Brent (1973): a hybrid of parabolic interpolation and
---     golden section. Superlinear convergence, robust to outliers; matches
---     @scipy.optimize.brent@ and R's @optimize@.
---
--- Both are gradient-free. They need an initial bracket
--- @a < x < b@ with @f(x) < f(a), f(b)@; use 'bracketMinimum' to find one
--- automatically.
-{-# LANGUAGE StrictData #-}
-module Hanalyze.Optim.LineSearch
-  ( BrentConfig (..)
-  , defaultBrentConfig
-  , brent
-  , goldenSection
-  , bracketMinimum
-  ) where
-
-import Hanalyze.Optim.Common
-
--- | The golden ratio @φ@.
-phi :: Double
-phi = (1 + sqrt 5) / 2
-
--- | @1 − 1/φ ≈ 0.382@ — the golden-section shrink ratio.
-gold :: Double
-gold = (3 - sqrt 5) / 2
-
--- | Brent configuration.
-data BrentConfig = BrentConfig
-  { bcMaxIter :: !Int        -- ^ Maximum iterations.
-  , bcTol     :: !Double     -- ^ Relative tolerance (target final bracket width).
-  , bcDir     :: !Direction  -- ^ Optimization direction.
-  } deriving (Show, Eq)
-
--- | Default Brent configuration: 200 iterations, tolerance 1e-8, minimization.
-defaultBrentConfig :: BrentConfig
-defaultBrentConfig = BrentConfig
-  { bcMaxIter = 200
-  , bcTol     = 1e-8
-  , bcDir     = Minimize
-  }
-
--- | Golden-section search.
---
--- Assumes @[a, b]@ is unimodal (a single interior minimum). Maintains four
--- points @a < c < d < b@ with @c = a + gold·(b-a)@, @d = b - gold·(b-a)@
--- (@gold ≈ 0.382@). Each iteration shrinks the interval by @1/φ ≈ 0.618@
--- with one new function evaluation.
-goldenSection :: Direction
-              -> ([Double] -> Double)    -- ^ Objective; @1D@ wrapped in a one-element list.
-              -> Double                  -- ^ Bracket left @a@.
-              -> Double                  -- ^ Bracket right @b@.
-              -> Double                  -- ^ Tolerance.
-              -> Int                     -- ^ Maximum iterations.
-              -> OptimResult
-goldenSection dir fUser a0 b0 tol maxIter =
-  let f x = flipFor dir fUser [x]
-      -- a < c < d < b を維持 (gold ≈ 0.382)
-      go iter a b c d fc fd hist
-        | iter >= maxIter || abs (b - a) < tol =
-            let xm = if fc < fd then c else d
-                fm = min fc fd
-            in (xm, fm, fm : hist, iter, abs (b - a) < tol)
-        | fc < fd =
-            -- 最小は [a, d] にある: 区間を [a, d] に縮め、old c が new d になる
-            let bN  = d
-                dN  = c
-                fdN = fc
-                cN  = a + gold * (bN - a)
-                fcN = f cN
-            in go (iter + 1) a bN cN dN fcN fdN (min fcN fdN : hist)
-        | otherwise =
-            -- 最小は [c, b] にある: 区間を [c, b] に縮め、old d が new c になる
-            let aN  = c
-                cN  = d
-                fcN = fd
-                dN  = b - gold * (b - aN)
-                fdN = f dN
-            in go (iter + 1) aN b cN dN fcN fdN (min fcN fdN : hist)
-      a = min a0 b0
-      b = max a0 b0
-      c = a + gold * (b - a)         -- 左の内点 (約 0.382 of (b-a) from a)
-      d = b - gold * (b - a)         -- 右の内点 (約 0.618 of (b-a) from a)
-      fc = f c
-      fd = f d
-      (xb, vb, hist, iters, conv) = go 0 a b c d fc fd [min fc fd]
-      vUser = case dir of { Minimize -> vb; Maximize -> negate vb }
-      histU = case dir of { Minimize -> reverse hist; Maximize -> map negate (reverse hist) }
-  in OptimResult [xb] vUser histU iters conv
-
--- | Brent's method: a hybrid of parabolic interpolation and
--- golden-section search.
---
--- Compatible with the simple form found in Numerical Recipes and
--- @scipy.optimize.brent@.
-brent :: BrentConfig
-      -> ([Double] -> Double)
-      -> Double                 -- ^ Bracket left @a@.
-      -> Double                 -- ^ Bracket right @b@.
-      -> OptimResult
-brent cfg fUser ax bx =
-  let f x = flipFor (bcDir cfg) fUser [x]
-      a0 = min ax bx
-      b0 = max ax bx
-      x0 = a0 + gold * (b0 - a0)
-      fx0 = f x0
-      (xBest, vBest, hist, iters, conv) =
-        loopBrent cfg f a0 b0 x0 x0 x0 fx0 fx0 fx0 0 0 [fx0]
-      vUser = case bcDir cfg of { Minimize -> vBest; Maximize -> negate vBest }
-      histU = case bcDir cfg of { Minimize -> reverse hist; Maximize -> map negate (reverse hist) }
-  in OptimResult [xBest] vUser histU iters conv
-
--- | Brent 反復。Numerical Recipes "brent" の素直な移植 (簡略版)。
--- 状態: a, b (区間), x (現在最良), w (2 番目), v (3 番目), 対応する f 値。
--- e: 一つ前の @d@ (放物線補間ステップの記憶)、@d@: 現ステップ幅。
-loopBrent :: BrentConfig
-          -> (Double -> Double)
-          -> Double -> Double                 -- a, b
-          -> Double -> Double -> Double       -- x, w, v
-          -> Double -> Double -> Double       -- fx, fw, fv
-          -> Int -> Double                    -- iter, e
-          -> [Double]                         -- hist
-          -> (Double, Double, [Double], Int, Bool)
-loopBrent cfg f a b x w v fx fw fv iter e hist
-  | iter >= bcMaxIter cfg = (x, fx, hist, iter, False)
-  | abs (x - xm) <= tol2 - 0.5 * (b - a) = (x, fx, hist, iter, True)
-  | otherwise =
-      let -- 放物線補間を試み、失敗時は黄金分割
-          (d, eN) = parabolicOrGolden
-          u  = if abs d >= tol1 then x + d else x + signum d * tol1
-          fu = f u
-      in if fu <= fx
-           then
-             let (aN, bN) = if u >= x then (x, b) else (a, x)
-                 (xN, wN, vN, fxN, fwN, fvN) = (u, x, w, fu, fx, fw)
-             in loopBrent cfg f aN bN xN wN vN fxN fwN fvN (iter + 1) eN (fxN : hist)
-           else
-             let (aN, bN) = if u < x then (u, b) else (a, u)
-                 (xN, wN, vN, fxN, fwN, fvN) =
-                   if fu <= fw || w == x
-                     then (x, u, w, fx, fu, fw)
-                     else if fu <= fv || v == x || v == w
-                            then (x, w, u, fx, fw, fu)
-                            else (x, w, v, fx, fw, fv)
-             in loopBrent cfg f aN bN xN wN vN fxN fwN fvN (iter + 1) eN (fxN : hist)
-  where
-    xm   = 0.5 * (a + b)
-    tol1 = bcTol cfg * abs x + 1e-10
-    tol2 = 2 * tol1
-    parabolicOrGolden =
-      if abs e > tol1
-        then
-          let r0 = (x - w) * (fx - fv)
-              q0 = (x - v) * (fx - fw)
-              p0 = (x - v) * q0 - (x - w) * r0
-              q1 = 2 * (q0 - r0)
-              p  = if q1 > 0 then -p0 else p0
-              q  = abs q1
-              eOld = e
-              dCand = p / q
-              ok = abs p < abs (0.5 * q * eOld)
-                   && p > q * (a - x) && p < q * (b - x)
-          in if ok then (dCand, dCand) else goldenStep
-        else goldenStep
-    goldenStep =
-      let eG = if x >= xm then a - x else b - x
-          dG = gold * eG
-      in (dG, eG)
-
--- | Bracket search: find @(a, c, b)@ such that @f(c) < f(a)@ and
--- @f(c) < f(b)@.
---
--- A simple expanding scan (a slimmed-down @mnbrak@ from Numerical
--- Recipes). Returns 'Nothing' if no bracket is found.
-bracketMinimum :: ([Double] -> Double)
-               -> Double               -- ^ Initial @a@.
-               -> Double               -- ^ Initial @b@.
-               -> Maybe (Double, Double, Double)
-                                       -- ^ @(a, c, b)@ with @f(c) < f(a), f(b)@.
-bracketMinimum fUser a0 b0 =
-  let f x = fUser [x]
-      step = (b0 - a0) * 0.5
-      go a b k
-        | k > 100   = Nothing
-        | f c < f a && f c < f b = Just (a, c, b)
-        | otherwise = go (a - step) (b + step) (k + 1)
-        where
-          c = 0.5 * (a + b)
-  in go a0 b0 0
diff --git a/src/Hanalyze/Optim/NSGA.hs b/src/Hanalyze/Optim/NSGA.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/NSGA.hs
+++ /dev/null
@@ -1,1345 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.NSGA
--- Description : NSGA-II (非優越ソート多目的遺伝的アルゴリズム) — Deb et al. 2002
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- NSGA-II (Non-dominated Sorting Genetic Algorithm II) — Deb et al. 2002.
---
--- A widely-used multi-objective evolutionary algorithm based on fast
--- non-dominated sorting + crowding-distance comparison.
---
--- Algorithm:
---
--- @
--- 1. Generate the initial population P_0 (LHS or random).
--- 2. For t = 0..T:
---    a) Generate offspring Q_t (selection + SBX crossover + polynomial mutation).
---    b) R_t = P_t ∪ Q_t.
---    c) Fast non-dominated sort partitions R_t into fronts F_1, F_2, ...
---    d) Sort each front by crowding distance.
---    e) Take the top N to form P_{t+1}.
--- 3. Return the final front as a Pareto approximation.
--- @
-{-# LANGUAGE StrictData #-}
-{-# LANGUAGE OverloadedStrings #-}
-module Hanalyze.Optim.NSGA
-  ( -- * 型
-    Bounds
-  , Solution (..)
-  , NSGAConfig (..)
-  , defaultNSGAConfig
-    -- * High-level API
-  , nsga2
-  , nsga2WithConstraints
-  , nsga2AllFronts
-  , nsga2AllFrontsWithConstraints
-  , nsga2WithProgress
-  , nsga2WithProgressAndConstraints
-  , NSGAProgress (..)
-  , evaluateSolution
-    -- * Building blocks
-  , dominates
-  , paretoDominates
-  , nonDominatedSort
-  , crowdingDistance
-    -- * Matrix-based internal API (N3)
-  , PopMatrix (..)
-  , fromSolutions
-  , toSolutions
-  , dominationMatrix
-    -- * Genetic operators
-  , sbxCrossover
-  , polynomialMutation
-  , randomInBounds
-  , binaryTournament
-  , crowdedCompare
-  ) where
-
-import Control.Monad (forM_, zipWithM)
-import Data.List (sortBy)
-import Data.Ord  (comparing)
-import qualified Data.IntSet as IS
-import qualified Data.Vector as V
-import qualified Data.Vector.Mutable as VM
-import qualified Data.Vector.Storable as VS
-import qualified Data.Vector.Unboxed as VU
-import qualified Data.Vector.Algorithms.Intro as VAI
-import System.Random.MWC (GenIO, uniform, uniformR)
-import qualified Numeric.LinearAlgebra as LA
-import qualified Hanalyze.Optim.Common    as OC
-import qualified Hanalyze.Stat.QuasiRandom as QR
-import Control.DeepSeq (NFData)
-import GHC.Generics (Generic)
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
--- | Per-dimension @(lo, hi)@ bounds. Re-exported from 'Hanalyze.Optim.Common.Bounds'.
-type Bounds = OC.Bounds
-
--- | An individual: decision variables, objective-value vector, and
--- constraint violation.
-data Solution = Solution
-  { solDecision   :: [Double]   -- ^ Decision vector (length @d@).
-  , solObjectives :: [Double]   -- ^ Objective values (length @m@); all
-                                --   objectives are treated as minimized.
-  , solViolation  :: Double     -- ^ Constraint violation (0 = feasible,
-                                --   @> 0@ = violated).
-  } deriving (Show, Eq, Generic)
-
-instance NFData Solution
-
--- ---------------------------------------------------------------------------
--- PopMatrix — Matrix-based internal population representation
--- ---------------------------------------------------------------------------
-
--- | Internal population representation backed by hmatrix matrices.
---
--- The user-facing 'Solution' type stores per-individual lists, which
--- forces the inner non-dominated sort and crowding-distance loops to
--- pay @O(MN)@ list traversals on every pair compare. 'PopMatrix' keeps
--- the same data laid out as one dense matrix per attribute, so that
--- the same loops become a small number of @O(N²)@ BLAS / 'LA.cmap'
--- calls — the same vectorisation that lets pymoo do a generation in
--- ~5 ms on numpy.
---
--- /Layout/:
---
---   * @pmX@ — decision matrix of shape @n × d@ (one row per individual)
---   * @pmF@ — objective matrix of shape @n × m@ (minimisation; smaller
---     is better)
---   * @pmCV@ — constraint-violation vector of length @n@ (zero =
---     feasible, positive = violated)
---
--- The 'Solution' API is preserved as a boundary representation; we
--- convert via 'fromSolutions' / 'toSolutions' once per generation.
-data PopMatrix = PopMatrix
-  { pmX  :: !(LA.Matrix Double)  -- ^ Decision matrix (@n × d@).
-  , pmF  :: !(LA.Matrix Double)  -- ^ Objective matrix (@n × m@).
-  , pmCV :: !(LA.Vector Double)  -- ^ Constraint violations (length @n@).
-  } deriving (Show)
-
--- | Number of individuals in a 'PopMatrix'.
-pmSize :: PopMatrix -> Int
-pmSize = LA.rows . pmF
-
--- | Number of objectives in a 'PopMatrix'.
-pmObjs :: PopMatrix -> Int
-pmObjs = LA.cols . pmF
-
--- | Convert a list of 'Solution' to a 'PopMatrix'. All solutions must
--- share the same dimensions; the empty list yields an empty matrix.
-fromSolutions :: [Solution] -> PopMatrix
-fromSolutions []   = PopMatrix
-  { pmX  = (0 LA.>< 0) []
-  , pmF  = (0 LA.>< 0) []
-  , pmCV = LA.fromList []
-  }
-fromSolutions sols = PopMatrix
-  { pmX  = LA.fromLists (map solDecision   sols)
-  , pmF  = LA.fromLists (map solObjectives sols)
-  , pmCV = LA.fromList  (map solViolation  sols)
-  }
-
--- | Inverse of 'fromSolutions'.
-toSolutions :: PopMatrix -> [Solution]
-toSolutions pm =
-  let xs  = LA.toLists (pmX  pm)
-      fs  = LA.toLists (pmF  pm)
-      cvs = LA.toList  (pmCV pm)
-  in zipWith3 (\d o v -> Solution d o v) xs fs cvs
-
--- | Pairwise constrained-Pareto domination matrix.
---
--- Returns an @n × n@ matrix @M@ in which:
---
---   * @M[i, j] = +1@ iff individual @i@ dominates @j@
---   * @M[i, j] = -1@ iff individual @j@ dominates @i@
---   * @M[i, j] =  0@ otherwise (mutually non-dominated, identical, or
---     diagonal entries)
---
--- Equivalent to calling 'dominates' on every pair, but evaluated as a
--- handful of @n × n@ array operations:
---
---   1. For each objective @k@, build the @n × n@ pairwise-difference
---      matrix @D_k[i, j] = F[i, k] - F[j, k]@ via two outer products.
---   2. @smallerK[i, j] = (D_k[i, j] < 0)@; @largerK[i, j] = (D_k[i, j] > 0)@.
---   3. Aggregate over @k@: @anySm = OR_k smallerK@, @anyLg = OR_k largerK@.
---   4. @iDomJ = anySm AND NOT anyLg@; @jDomI = anyLg AND NOT anySm@.
---   5. Constraint layer: a feasible individual dominates an infeasible
---      one; among two infeasible ones the smaller violation wins.
-dominationMatrix :: PopMatrix -> LA.Matrix Double
-dominationMatrix pm =
-  let f      = pmF pm
-      cv     = pmCV pm
-      n      = LA.rows f
-      m      = LA.cols f
-      ones   = LA.konst 1 n :: LA.Vector Double
-      onesNN = LA.konst 1 (n, n) :: LA.Matrix Double
-      indicator x | x > 0     = 1
-                  | otherwise = 0
-
-      -- Per-objective contributions to "any smaller" and "any larger".
-      -- We accumulate by addition, then collapse with @indicator@; this
-      -- avoids constructing a 3-D tensor.
-      perObj k =
-        let fk = LA.flatten (f LA.¿ [k])
-            d  = LA.outer fk ones - LA.outer ones fk     -- D_k[i,j] = f_k[i] - f_k[j]
-            sm = LA.cmap (\v -> if v < 0 then 1 else 0) d
-            lg = LA.cmap (\v -> if v > 0 then 1 else 0) d
-        in (sm, lg)
-
-      zeroNN = LA.konst 0 (n, n) :: LA.Matrix Double
-      objContribs :: [(LA.Matrix Double, LA.Matrix Double)]
-      objContribs =
-        if m == 0
-          then [(zeroNN, zeroNN)]
-          else map perObj [0 .. m - 1]
-      anySm = LA.cmap indicator (sum (map fst objContribs))
-      anyLg = LA.cmap indicator (sum (map snd objContribs))
-
-      -- Pareto-only domination ignoring constraints.
-      iDomJpar = LA.cmap indicator (anySm * (onesNN - anyLg))
-      jDomIpar = LA.cmap indicator (anyLg * (onesNN - anySm))
-      paretoM  = iDomJpar - jDomIpar
-
-      -- Constraint layer.
-      cvFeas   = LA.cmap (\v -> if v == 0 then 1 else 0) cv
-      cvInfes  = LA.cmap (\v -> if v >  0 then 1 else 0) cv
-      -- a_feas[i,j] = 1 iff i feasible
-      aFeas    = LA.outer cvFeas ones
-      aInfes   = LA.outer cvInfes ones
-      bFeas    = LA.outer ones cvFeas
-      bInfes   = LA.outer ones cvInfes
-      -- Both feasible: keep paretoM
-      bothFeas = aFeas * bFeas
-      -- a feasible, b infeasible: a dominates → +1
-      aBeatsB  = aFeas * bInfes
-      -- a infeasible, b feasible: b dominates → -1
-      bBeatsA  = aInfes * bFeas
-      -- Both infeasible: smaller cv wins
-      cvDiff   = LA.outer cv ones - LA.outer ones cv
-      aSmCV    = LA.cmap (\v -> if v < 0 then 1 else 0) cvDiff
-      bSmCV    = LA.cmap (\v -> if v > 0 then 1 else 0) cvDiff
-      bothInf  = aInfes * bInfes
-      cvLayer  = bothInf * (aSmCV - bSmCV)
-
-      m0 = bothFeas * paretoM + aBeatsB - bBeatsA + cvLayer
-      -- Zero-out diagonal (i == j has no domination).
-      identityMask = onesNN - LA.diag (LA.konst 1 n)
-  in m0 * identityMask
-
--- | NSGA-II configuration.
-data NSGAConfig = NSGAConfig
-  { nsgaPopSize     :: Int            -- ^ Population size @N@ (prefer even).
-  , nsgaGenerations :: Int            -- ^ Number of generations @T@.
-  , nsgaCrossoverP  :: Double         -- ^ Crossover probability @p_c@ (default 0.9).
-  , nsgaMutationP   :: Maybe Double   -- ^ Mutation probability ('Nothing' uses @1/d@).
-  , nsgaEtaCross    :: Double         -- ^ SBX distribution index @η_c@ (default 15).
-  , nsgaEtaMut      :: Double         -- ^ Polynomial-mutation @η_m@ (default 20).
-  } deriving (Show)
-
--- | Default configuration: population 100, 200 generations, @p_c = 0.9@,
--- mutation @1/d@, @η_c = 15@, @η_m = 20@.
-defaultNSGAConfig :: NSGAConfig
-defaultNSGAConfig = NSGAConfig
-  { nsgaPopSize     = 100
-  , nsgaGenerations = 200
-  , nsgaCrossoverP  = 0.9
-  , nsgaMutationP   = Nothing
-  , nsgaEtaCross    = 15.0
-  , nsgaEtaMut      = 20.0
-  }
-
--- ---------------------------------------------------------------------------
--- API (実装は Phase S で行う)
--- ---------------------------------------------------------------------------
-
--- | NSGA-II main entry point. The user-supplied function maps a decision
--- vector to an objective vector. Returns the final generation's Pareto
--- approximation (= rank-0 individuals).
---
--- This is the unconstrained variant; for constraints use
--- 'nsga2WithConstraints'.
-nsga2 :: NSGAConfig
-      -> ([Double] -> [Double])  -- ^ Objective function (@m@-dimensional output).
-      -> Bounds                  -- ^ Search bounds (@d@ dimensions).
-      -> GenIO
-      -> IO [Solution]
-nsga2 cfg f bounds gen =
-  nsga2WithConstraints cfg f (const 0) bounds gen
-
--- | Constrained NSGA-II. The constraint function maps a decision vector
--- to a /violation amount/ (@0@ = feasible, @> 0@ = violated). When there
--- are multiple constraints @g_i(x) ≤ 0@, aggregate them via e.g.
--- @sum [max 0 (g_i x)]@.
-nsga2WithConstraints
-  :: NSGAConfig
-  -> ([Double] -> [Double])    -- ^ Objective function (@m@ dimensions).
-  -> ([Double] -> Double)      -- ^ Constraint violation (@≥ 0@; @0@ = feasible).
-  -> Bounds                    -- ^ Search bounds (@d@ dimensions).
-  -> GenIO
-  -> IO [Solution]
-nsga2WithConstraints cfg f cFn bounds gen = do
-  finalPop <- runNSGAFinalPopulation cfg f cFn bounds gen
-  -- 最終世代の最初の front (Pareto 近似) を返す
-  case nonDominatedSort finalPop of
-    (front : _) -> return front
-    []          -> return []
-
--- | NSGA-II all-fronts variant: 最終世代の population を非優越ソートして
--- **全 front を rank 別に**返す。 @front i@ が @rank i@ (0-origin) に対応:
--- rank 0 = Pareto 近似、 rank 1 = それに dominate される第 2 集団、 …
---
--- フロントエンド app frontend で「最適解 (rank 0) の周辺の代替案 (rank 1, 2)」 を
--- 一覧する UI を実装するために用意。
---
--- 既存 'nsga2' との関係: @nsga2 ≈ head <$> nsga2AllFronts@ (空 population なら
--- empty list)。 内部 helper 'runNSGAFinalPopulation' を共有しているため、
--- 既存 API の挙動は不変。
-nsga2AllFronts
-  :: NSGAConfig
-  -> ([Double] -> [Double])
-  -> Bounds
-  -> GenIO
-  -> IO [[Solution]]
-nsga2AllFronts cfg f bounds gen =
-  nsga2AllFrontsWithConstraints cfg f (const 0) bounds gen
-
--- | Constrained 版 'nsga2AllFronts'。
-nsga2AllFrontsWithConstraints
-  :: NSGAConfig
-  -> ([Double] -> [Double])
-  -> ([Double] -> Double)
-  -> Bounds
-  -> GenIO
-  -> IO [[Solution]]
-nsga2AllFrontsWithConstraints cfg f cFn bounds gen = do
-  finalPop <- runNSGAFinalPopulation cfg f cFn bounds gen
-  return (nonDominatedSort finalPop)
-
--- | 内部 helper: 最終世代の population (未ソート) を返す。 'nsga2WithConstraints'
--- と 'nsga2AllFrontsWithConstraints' で共有する。 callback 無し版。
-runNSGAFinalPopulation
-  :: NSGAConfig
-  -> ([Double] -> [Double])
-  -> ([Double] -> Double)
-  -> Bounds
-  -> GenIO
-  -> IO [Solution]
-runNSGAFinalPopulation cfg f cFn bounds gen =
-  runNSGAFinalPopulationCb cfg f cFn bounds (\_ -> pure ()) gen
-
--- | 内部 helper: 'runNSGAFinalPopulation' の callback 付き版。
-runNSGAFinalPopulationCb
-  :: NSGAConfig
-  -> ([Double] -> [Double])
-  -> ([Double] -> Double)
-  -> Bounds
-  -> (NSGAProgress -> IO ())  -- 各世代終端で呼ぶ progress callback
-  -> GenIO
-  -> IO [Solution]
-runNSGAFinalPopulationCb cfg f cFn bounds onProg gen = do
-  let n  = nsgaPopSize cfg
-      d  = length bounds
-      pM = case nsgaMutationP cfg of
-             Just p  -> p
-             Nothing -> 1.0 / fromIntegral d
-      etaC = nsgaEtaCross cfg
-      etaM = nsgaEtaMut cfg
-      pC   = nsgaCrossoverP cfg
-      tot  = nsgaGenerations cfg
-
-  -- 初期母集団: Latin-Hypercube Sampling で各次元のセルを 1 度ずつ
-  -- 埋める (iid uniform より初期世代の被覆良 → 第 1 世代で既に
-  -- 全域の情報が手に入るため、世代あたりの収束が上がる)。
-  initXs <- QR.lhsSamplesIn n bounds gen
-  let initPop = [ evaluateSolution f cFn x | x <- initXs ]
-  -- 世代ループ (callback 付き)
-  generationLoopCb tot tot initPop pC etaC etaM pM bounds f cFn onProg gen
-
--- | NSGA-II 1 世代ステップの進捗。 'nsga2WithProgress' / 'nsga2WithProgressAndConstraints'
--- の callback 引数で渡される。
-data NSGAProgress = NSGAProgress
-  { ngpGeneration :: !Int       -- ^ 0-origin の現世代番号 (@[0 .. ngpTotal - 1]@ の範囲)
-  , ngpTotal      :: !Int       -- ^ 総世代数 ('NSGAConfig.nsgaGenerations')
-  , ngpParetoSize :: !Int       -- ^ 現 rank-0 (Pareto 近似) のサイズ
-  , ngpBestObjs   :: ![Double]  -- ^ 現 rank-0 中で各目的の最小値
-  } deriving (Show, Eq)
-
--- | 'generationLoop' の callback 付き版。
---   各世代の **終端** で 'NSGAProgress' を構築して @onProg@ を呼ぶ。
-generationLoopCb
-  :: Int                              -- ^ 残り iteration t (countdown)
-  -> Int                              -- ^ 総 iteration T (callback の ngpTotal 用)
-  -> [Solution]
-  -> Double -> Double -> Double -> Double
-  -> Bounds
-  -> ([Double] -> [Double])
-  -> ([Double] -> Double)
-  -> (NSGAProgress -> IO ())
-  -> GenIO
-  -> IO [Solution]
-generationLoopCb 0 _ pop _ _ _ _ _ _ _ _ _ = return pop
-generationLoopCb t tot pop pC etaC etaM pM bounds f cFn onProg gen = do
-  let n = length pop
-      fronts = nonDominatedSort pop
-      sortedFronts = map crowdingDistance fronts
-      ranked = concat
-        [ zip3 (repeat r) (frontDistances fr) fr
-        | (r, fr) <- zip [0 :: Int ..] sortedFronts ]
-  children <- fillOffspring n pop pC etaC etaM pM bounds f cFn ranked gen
-  let combined = pop ++ children
-      combinedFronts = nonDominatedSort combined
-      newPop = selectTopN n combinedFronts
-      -- progress 構築: 次世代 newPop の rank-0 で報告
-      newFronts = nonDominatedSort newPop
-      pareto0   = case newFronts of { (fr:_) -> fr; [] -> [] }
-      paretoSize = length pareto0
-      bestObjs  =
-        case pareto0 of
-          [] -> []
-          _  ->
-            let m = length (solObjectives (head pareto0))
-            in [ minimum [ solObjectives s !! j | s <- pareto0 ]
-               | j <- [0 .. m - 1] ]
-      curGen = tot - t              -- 0-origin
-      progress = NSGAProgress
-        { ngpGeneration = curGen
-        , ngpTotal      = tot
-        , ngpParetoSize = paretoSize
-        , ngpBestObjs   = bestObjs
-        }
-  onProg progress
-  generationLoopCb (t - 1) tot newPop pC etaC etaM pM bounds f cFn onProg gen
-
--- | NSGA-II with per-generation progress callback (unconstrained)。
--- 各世代の終端で 'NSGAProgress' が @onProg@ に渡される。
--- 戻り値は 'nsga2' と同じく rank-0 (Pareto 近似) のみ。
--- 全 rank が欲しい場合は 'nsga2AllFronts' を別途呼ぶ。
---
--- 想定用途: フロントエンド app backend が WebSocket / SSE で生存中世代の
--- progress を frontend に流す。
-nsga2WithProgress
-  :: NSGAConfig
-  -> ([Double] -> [Double])
-  -> Bounds
-  -> (NSGAProgress -> IO ())
-  -> GenIO
-  -> IO [Solution]
-nsga2WithProgress cfg f bounds onProg gen =
-  nsga2WithProgressAndConstraints cfg f (const 0) bounds onProg gen
-
--- | NSGA-II with per-generation progress callback (constrained)。
-nsga2WithProgressAndConstraints
-  :: NSGAConfig
-  -> ([Double] -> [Double])
-  -> ([Double] -> Double)
-  -> Bounds
-  -> (NSGAProgress -> IO ())
-  -> GenIO
-  -> IO [Solution]
-nsga2WithProgressAndConstraints cfg f cFn bounds onProg gen = do
-  finalPop <- runNSGAFinalPopulationCb cfg f cFn bounds onProg gen
-  case nonDominatedSort finalPop of
-    (front : _) -> return front
-    []          -> return []
-
--- | Build a 'Solution' from a decision vector by evaluating both the
--- objective and the constraint function.
-evaluateSolution :: ([Double] -> [Double])
-                 -> ([Double] -> Double)
-                 -> [Double]
-                 -> Solution
-evaluateSolution f cFn x =
-  Solution { solDecision   = x
-           , solObjectives = f x
-           , solViolation  = cFn x
-           }
-
--- | Duplicate-detection threshold (L∞).
-dupEpsilon :: Double
-dupEpsilon = 1e-12
-
--- | Maximum mating retries before giving up.
-dupMaxRetries :: Int
-dupMaxRetries = 10
-
--- | @pop@ との重複を除去しつつ @needed@ 個の child を集めるまで SBX
--- ペア生成を繰り返す。pymoo の InfillCriterion.do と同等の役割。
---
--- 親選びは **random-permutation tournament** (NF3): 各反復で 2 回の
--- pop 順列を取り、各個体が tournament に正確に 2 回出るようにペアを
--- 組む。これで selection pressure の variance が下がり、ZDT のような
--- iid-uniform tournament で convergence がブレる問題を抑える。
-fillOffspring
-  :: Int                         -- ^ 必要な child 数 @n@
-  -> [Solution]                  -- ^ 現世代 pop (重複比較用)
-  -> Double -> Double -> Double -> Double  -- ^ pC, etaC, etaM, pM
-  -> Bounds
-  -> ([Double] -> [Double])
-  -> ([Double] -> Double)
-  -> [(Int, Double, Solution)]
-  -> GenIO
-  -> IO [Solution]
-fillOffspring needed pop pC etaC etaM pM bounds f cFn ranked gen =
-  -- N4c: per-pair Haskell ループを廃止し、1 batch で nPairs ペアの親を
-  -- pickParentsByPermutation で揃え → 親行列 P1, P2 (k×d) を sbxCrossoverMV
-  -- で SBX (matrix) → polynomialMutationMV で PM (matrix) → user objective
-  -- を per-row 適用 → matrix L∞ pairwise distance で dedup。
-  let d  = length bounds
-      go acc retries
-        | length acc >= needed = return (take needed (reverse acc))
-        | retries <= 0         = return (take needed (reverse acc))
-        | otherwise = do
-            let want   = needed - length acc
-                nPairs = max 1 ((want + 1) `div` 2)
-                nPar   = 2 * nPairs                 -- 1 pair = 2 親
-            parentsW <- pickParentsByPermutation nPar ranked gen
-            -- parentsW = [w_0, w_1, w_2, w_3, ...]
-            -- 親行列 P1, P2 を作る (奇数なら最後を捨てる)
-            let pPairs = chunkPairs parentsW
-                k      = length pPairs
-                p1Mat  = LA.fromLists [solDecision a | (a, _) <- pPairs]
-                p2Mat  = LA.fromLists [solDecision b | (_, b) <- pPairs]
-
-            -- crossover gating: 親レベル pC で SBX, それ以外は親そのまま
-            uCross <- VS.replicateM k (uniformR (0, 1) gen :: IO Double)
-            (c1Mat0, c2Mat0) <- sbxCrossoverMV etaC bounds p1Mat p2Mat gen
-            let crossMaskRow = LA.fromList
-                    [ if v < pC then 1 else 0
-                    | v <- VS.toList uCross ]
-                  :: LA.Vector Double
-                onesD = LA.konst 1 d :: LA.Vector Double
-                cMask = LA.outer crossMaskRow onesD     -- k × d
-                ncMask = LA.cmap (\v -> 1 - v) cMask
-                c1raw = cMask * c1Mat0 + ncMask * p1Mat
-                c2raw = cMask * c2Mat0 + ncMask * p2Mat
-
-            -- Polynomial mutation (matrix, all 2k children at once)
-            cAll <- polynomialMutationMV etaM pM bounds
-                       (cAll0 c1raw c2raw) gen
-
-            -- ユーザ評価
-            -- Phase C 試行: parMap rdeepseq で並列化 → ZDT bench で逆
-            -- 効果 (cheap objective ~1µs / spark overhead 数 µs)。
-            -- 高コスト objective (engineering simulation 等) で
-            -- ユーザが明示的に並列化したい場合は Control.Parallel.Strategies
-            -- を直接呼び出す or 別の Async 経路を提供すべき。
-            -- bench-mo (cheap f) では sequential が最適。
-            let xss     = LA.toLists cAll
-                rawSols = [ Solution { solDecision   = xs
-                                     , solObjectives = f xs
-                                     , solViolation  = cFn xs }
-                          | xs <- xss ]
-
-                -- Matrix-based dedup with early-exit.
-                --
-                -- Each candidate row needs to be checked for duplication
-                -- against every reference row in @pop ++ acc@. The
-                -- previous form (@any (\r -> linfDist x r < ε) refs@)
-                -- iterated two @[Double]@ lists in 'linfDist', costing
-                -- ~k × nRefs × d list-zipWith ops per retry. Here we:
-                --
-                --  1. flatten @pop ++ acc@'s decision rows into a single
-                --     Storable Vector @refsFlat@ (@nRefs × d@, row-major)
-                --  2. flatten the candidate matrix similarly
-                --  3. for each candidate row, walk @refsFlat@ row by row
-                --     comparing dimensions in a tight inner loop. The
-                --     check short-circuits as soon as any dim shows
-                --     @|diff| ≥ ε@ — i.e. the row is /not/ a duplicate.
-                --     For random points and the typical @ε = 1e-12@
-                --     threshold, the first dim almost always rejects,
-                --     so the inner loop runs O(1) per ref on average.
-                d_      = length bounds
-                refsFlat
-                  | null pop && null acc = VS.empty
-                  | otherwise            = VS.fromList
-                      (concat [solDecision s | s <- pop ++ acc])
-                nRefs   = VS.length refsFlat `div` max 1 d_
-                kept    = [ s
-                          | s <- rawSols
-                          , not (isDupVS refsFlat nRefs d_
-                                         (VS.fromList (solDecision s)))
-                          ]
-                deduped = dedupBy
-                            (\sa sb ->
-                               linfDist (solDecision sa) (solDecision sb)
-                                 < dupEpsilon)
-                            kept
-                acc'    = foldr (:) acc deduped
-            go acc' (retries - 1)
-  in go [] dupMaxRetries
-  where
-    chunkPairs (a : b : rest) = (a, b) : chunkPairs rest
-    chunkPairs _              = []
-    -- c1raw, c2raw を縦に積んで 2k × d の行列に
-    cAll0 c1raw c2raw =
-      LA.fromBlocks [ [ c1raw ], [ c2raw ] ]
-
--- | Random-permutation tournament: pop 全体の順列を 2 回作って先頭から
--- ペア取り、binaryTournament で勝者を出す。各個体が正確に 2 回出走。
-pickParentsByPermutation
-  :: Int                          -- ^ 必要な親の数 (≤ 2 × pop size、
-                                  --   超える場合は permutation を repeat)
-  -> [(Int, Double, Solution)]    -- ^ ranked pop
-  -> GenIO
-  -> IO [Solution]
-pickParentsByPermutation nNeeded ranked gen = do
-  let popSize = length ranked
-      cmp (r1, d1, _) (r2, d2, _) = crowdedCompare (r1, d1) (r2, d2)
-      -- 1 完全周 (= 2 順列でペア) からは popSize 親が取れる。
-      nRounds = (nNeeded + popSize - 1) `div` popSize
-  rounds <- mapM (\_ -> do
-                    p1 <- shuffle ranked gen
-                    p2 <- shuffle ranked gen
-                    -- 1 round = popSize 親 (各 pair で 1 勝者)
-                    let pairs = zip p1 p2
-                    mapM (\(a, b) -> case cmp a b of
-                            LT -> return (third a)
-                            GT -> return (third b)
-                            EQ -> do
-                              r <- uniform gen :: IO Double
-                              return (third (if r < 0.5 then a else b)))
-                         pairs
-                  ) [1 .. nRounds]
-  return (take nNeeded (concat rounds))
-  where
-    third (_, _, s) = s
-
--- | True Fisher-Yates shuffle on a 'Data.Vector.Vector' boxed buffer.
---
--- The previous version paired each element with a random key and sorted
--- the @[(Double, a)]@ list by key — @O(n log n)@ with list-allocation
--- overhead per call. Tournament selection calls 'shuffle' twice per
--- generation × 200 generations × 4 ZDT/DTLZ benchmarks, so the sort
--- overhead actually showed up. The in-place Fisher-Yates path is
--- @O(n)@ with one random call per element.
-shuffle :: [a] -> GenIO -> IO [a]
-shuffle xs gen = do
-  let n = length xs
-  -- Generate a random key for each element, then sort by key.
-  keys <- mapM (\_ -> uniform gen :: IO Double) [1 .. n]
-  let pairs = zip keys xs
-  return (map snd (sortBy (comparing fst) pairs))
-
--- | 1 ペアの子 (c1, c2) を、すでに選ばれた 2 親から作る。
--- 'makeChildPair' (random-tournament 内蔵版) との重複コードを避ける
--- ため SBX/mutation の本体だけ抽出。
-makeChildPairFromParents
-  :: Double -> Double -> Double -> Double
-  -> Bounds
-  -> ([Double] -> [Double])
-  -> ([Double] -> Double)
-  -> Solution -> Solution
-  -> GenIO
-  -> IO (Solution, Solution)
-makeChildPairFromParents pC etaC etaM pM bounds f cFn parent1 parent2 gen = do
-  u <- uniform gen :: IO Double
-  (c1Vec, c2Vec) <-
-    if u < pC
-      then sbxCrossover etaC bounds (solDecision parent1) (solDecision parent2) gen
-      else return (solDecision parent1, solDecision parent2)
-  c1Mut <- polynomialMutation etaM pM bounds c1Vec gen
-  c2Mut <- polynomialMutation etaM pM bounds c2Vec gen
-  return ( evaluateSolution f cFn c1Mut
-         , evaluateSolution f cFn c2Mut )
-
-linfDist :: [Double] -> [Double] -> Double
-linfDist xs ys = maximum (0 : zipWith (\a b -> abs (a - b)) xs ys)
-
--- | Storable, early-exiting L∞-duplicate check against a packed reference
--- buffer.
---
--- Returns 'True' iff some reference row is within 'dupEpsilon' (L∞) of
--- the candidate. The inner loop short-circuits on the first dimension
--- whose absolute difference reaches @ε@, since /any/ such dimension
--- rules out the row as a duplicate. For random search vectors this
--- typically rejects after one or two dimensions, making the whole
--- check essentially O(nRefs).
-isDupVS
-  :: VS.Vector Double   -- ^ Reference rows packed row-major (@nRefs × d@).
-  -> Int                -- ^ Number of reference rows @nRefs@.
-  -> Int                -- ^ Decision dimension @d@.
-  -> VS.Vector Double   -- ^ Candidate row (length @d@).
-  -> Bool
-isDupVS refsFlat nRefs d cand =
-  let goRow !j
-        | j >= nRefs = False
-        | otherwise  =
-            let !rowOff = j * d
-                isClose !c
-                  | c >= d    = True
-                  | otherwise =
-                      let !ad = abs ((refsFlat `VS.unsafeIndex` (rowOff + c))
-                                   - (cand     `VS.unsafeIndex` c))
-                      in if ad >= dupEpsilon
-                           then False    -- this dim disqualifies the row
-                           else isClose (c + 1)
-            in if isClose 0 then True else goRow (j + 1)
-  in goRow 0
-
-dedupBy :: (a -> a -> Bool) -> [a] -> [a]
-dedupBy _   []     = []
-dedupBy eq (x:xs)  = x : dedupBy eq (filter (not . eq x) xs)
-
--- | 1 ペアの子 (c1, c2) を生成。tournament 選択 → SBX → mutation。
-makeChildPair
-  :: Double -> Double -> Double -> Double  -- pC, etaC, etaM, pM
-  -> Bounds
-  -> ([Double] -> [Double])
-  -> ([Double] -> Double)
-  -> [(Int, Double, Solution)]   -- ranked pop
-  -> GenIO
-  -> IO (Solution, Solution)
-makeChildPair pC etaC etaM pM bounds f cFn ranked gen = do
-  -- 親選び (tournament)
-  let cmp (r1, d1, _) (r2, d2, _) = crowdedCompare (r1, d1) (r2, d2)
-  (_, _, parent1) <- binaryTournament ranked cmp gen
-  (_, _, parent2) <- binaryTournament ranked cmp gen
-
-  -- SBX (確率 pC) または親をそのまま
-  u <- uniform gen :: IO Double
-  (c1Vec, c2Vec) <-
-    if u < pC
-      then sbxCrossover etaC bounds (solDecision parent1) (solDecision parent2) gen
-      else return (solDecision parent1, solDecision parent2)
-
-  -- Polynomial mutation
-  c1Mut <- polynomialMutation etaM pM bounds c1Vec gen
-  c2Mut <- polynomialMutation etaM pM bounds c2Vec gen
-
-  return ( evaluateSolution f cFn c1Mut
-         , evaluateSolution f cFn c2Mut )
-
--- | front の各個体の crowding distance (元の順序で) を返す。
---
--- N3d 改修: 旧版は (1) per-objective sort 後の vals/sorted を !! で
--- index して @O(l)@ ずつ拾う、 (2) totalDist で contrib リストを線形
--- 検索していたため全体 @O(m·l²)@ 以上。新版は
---
---   * 全 front 個体の objective を 'V.Vector' に置く (V.! は @O(1)@)
---   * 各 obj について index 列を sortBy で 1 度だけソート
---   * 隣接 diff を 1 pass で計算、対応 index に直接書き戻す
---     (累積は @LA.accum@ で fused)
---
--- 全体 @O(m·l·log l)@ + @O(m·l)@、ほぼ pymoo (numpy ソート + diff +
--- fancy-indexing) と同 order に。
-frontDistances :: [Solution] -> [Double]
-frontDistances front
-  | l <= 2    = replicate l inf
-  | otherwise =
-      let -- Each individual contributes a per-objective spacing term.
-          -- We sum them all into a single length-l Vector via 'LA.accum'.
-          totals = foldl addObjective zeros [0 .. m - 1]
-      in LA.toList totals
-  where
-    l    = length front
-    m    = if l == 0 then 0 else length (solObjectives (head front))
-    inf  = 1 / 0
-    zeros = LA.konst 0 l :: LA.Vector Double
-
-    -- Per-objective values, indexed by the original front position.
-    objVecs :: V.Vector (LA.Vector Double)
-    objVecs =
-      let mat = LA.fromLists [ solObjectives s | s <- front ]
-                 :: LA.Matrix Double
-      in V.generate m (\k -> LA.flatten (mat LA.¿ [k]))
-
-    addObjective :: LA.Vector Double -> Int -> LA.Vector Double
-    addObjective acc k =
-      let vec    = objVecs V.! k
-          -- Sort indices by objective value (ascending) using
-          -- 'Data.Vector.Algorithms.Intro' on a Storable-Unboxed buffer
-          -- of @Int@. The previous form was @sortBy (comparing
-          -- (\i -> LA.atIndex vec i)) [0..l-1]@, which is a list-based
-          -- mergesort with per-comparison key recomputation. Intro sort
-          -- on an unboxed @Int@ vector with a precomputed key lookup
-          -- is roughly 2-3× faster on 'l = 100' fronts.
-          sortedU = VU.modify (VAI.sortBy (\i j ->
-                                  compare (LA.atIndex vec i)
-                                          (LA.atIndex vec j)))
-                              (VU.generate l id)
-          atSorted i = sortedU VU.! i
-          fMin   = LA.atIndex vec (atSorted 0)
-          fMax   = LA.atIndex vec (atSorted (l - 1))
-          rng    = fMax - fMin
-      in if rng == 0
-           then acc
-           else
-             let endpts = [ (atSorted 0,      inf)
-                          , (atSorted (l-1), inf) ]
-                 mids =
-                   [ (atSorted k', dDist)
-                   | k' <- [1 .. l - 2]
-                   , let prev  = LA.atIndex vec (atSorted (k' - 1))
-                         next  = LA.atIndex vec (atSorted (k' + 1))
-                         dDist = (next - prev) / rng
-                   ]
-             in LA.accum acc (+) (endpts ++ mids)
-
--- | ソート済 fronts (上から良い順) から n 個を選別。
--- - 入る front は丸ごと採用
--- - 最後の front は crowding distance 順で半分採用
-selectTopN :: Int -> [[Solution]] -> [Solution]
-selectTopN _ [] = []
-selectTopN n (fr : rest)
-  | length fr >= n = take n (crowdingDistance fr)
-  | otherwise =
-      let fr' = fr  -- 全採用
-          remaining = n - length fr
-      in fr' ++ selectTopN remaining rest
-
--- | Does individual @a@ /dominate/ @b@ under constrained Pareto
--- dominance?
---
--- 制約 (Deb 2000 "constrained-domination"):
---   1. a が実行可能 (violation = 0) かつ b が不実行可能 → a が支配
---   2. 両方不実行可能 → violation の小さい方が支配
---   3. 両方実行可能 → 通常の Pareto dominance
---      (∀ i: a_i ≤ b_i) かつ (∃ j: a_j < b_j)
-dominates :: Solution -> Solution -> Bool
-dominates a b
-  | va == 0 && vb >  0 = True
-  | va >  0 && vb == 0 = False
-  | va >  0 && vb >  0 = va < vb
-  | otherwise          = paretoDominates (solObjectives a) (solObjectives b)
-  where
-    va = solViolation a
-    vb = solViolation b
-
--- | Standard (constraint-free) Pareto dominance: @a@ dominates @b@ iff
--- @∀ i: aᵢ ≤ bᵢ@ and @∃ j: aⱼ < bⱼ@.
---
--- The implementation walks the two objective lists in a single pass.
--- The previous form built two separate @zip + all + any@ traversals
--- through @[(Double, Double)]@ tuples, doubling the list traversals
--- and forcing pair allocations. The single-pass loop short-circuits
--- the moment we see @aᵢ > bᵢ@ (cannot dominate) and reuses the
--- already-known @∃ j: aⱼ < bⱼ@ flag.
-paretoDominates :: [Double] -> [Double] -> Bool
-paretoDominates = go False
-  where
-    go !sawStrict (x : xs) (y : ys)
-      | x >  y    = False                    -- @a@ violates @∀ i: aᵢ ≤ bᵢ@
-      | x <  y    = go True  xs ys
-      | otherwise = go sawStrict xs ys
-    go sawStrict [] []  = sawStrict
-    go _         _  _   = False              -- length mismatch ⇒ not dominate
-
--- | Fast non-dominated sort (Deb 2002): partitions the population into
--- ranked Pareto fronts.
--- 母集団を Pareto front に分割: F_1 (最も非優越), F_2, ...
---
--- アルゴリズム (O(MN²)):
---
---   for each p in P:
---     n_p = |{q : q dominates p}|        -- p を支配する数
---     S_p = {q : p dominates q}          -- p が支配する集合
---     if n_p = 0: p ∈ F_1
---   for i = 1, 2, ...:
---     for each p in F_i, each q in S_p:
---       n_q -= 1
---       if n_q = 0: q ∈ F_{i+1}
-nonDominatedSort :: [Solution] -> [[Solution]]
-nonDominatedSort [] = []
-nonDominatedSort pop =
-  -- Pop is moved into a 'Data.Vector' so per-individual access is O(1)
-  -- (the original list-based @ps !! j@ was @O(j)@ which made the whole
-  -- sort @O(n³)@ rather than @O(n²m)@). Front/dominance bookkeeping
-  -- still uses BLAS Vector for fused @LA.accum@ updates and an IntSet
-  -- to track placed individuals across iterations.
-  --
-  -- We tried routing through 'nonDominatedSortIdx' (BLAS
-  -- 'dominationMatrix' once + BFS) but for the typical NSGA pop size
-  -- @n = 100@ + 2 objectives, the BLAS dispatch overhead per @n × n@
-  -- broadcast exceeds the gain over per-pair list dominance — measured
-  -- 2.5× regression on ZDT/DTLZ. The list-based pair check wins below
-  -- @n ≈ 500@ with @m = 2..3@; matrix path is reserved for future
-  -- larger-pop / many-objective cases.
-  let n      = length pop
-      ps     = V.fromList pop
-      idxs   = [0 .. n - 1]
-      domInfo i =
-        let pi = ps V.! i
-            (sp, np) = foldr step ([], 0 :: Int) idxs
-            step j (s, c)
-              | i == j                   = (s, c)
-              | dominates pi (ps V.! j)  = (j : s, c)
-              | dominates (ps V.! j) pi  = (s, c + 1)
-              | otherwise                = (s, c)
-        in (sp, np)
-      info   = V.fromList [domInfo i | i <- idxs]
-      sList  = V.map fst info
-      front0 = [ i | (i, (_, c)) <- zip idxs (V.toList info), c == 0 ]
-      nVec0  = LA.fromList (map (fromIntegral . snd) (V.toList info))
-                 :: LA.Vector Double
-      go counts current placedSet acc
-        | null current = reverse acc
-        | otherwise =
-            let decrements = [ (j, -1)
-                             | i <- current
-                             , j <- sList V.! i ]
-                counts'    = LA.accum counts (+) decrements
-                placedSet' = foldr IS.insert placedSet current
-                nextF =
-                  [ j
-                  | j <- [0 .. n - 1]
-                  , not (IS.member j placedSet')
-                  , let v = LA.atIndex counts' j
-                  , v <= 0.5 && v > -0.5
-                  ]
-            in go counts' nextF placedSet' (current : acc)
-      idxFronts = go nVec0 front0 IS.empty []
-  in map (map (ps V.!)) idxFronts
-
--- | Matrix-driven non-dominated sort. Given a 'PopMatrix', returns a
--- list of fronts as @[[Int]]@ index lists.
---
--- Implementation: build the @n × n@ 'dominationMatrix' once; from it
--- derive @S_p@ (set of individuals dominated by @p@) and @n_p@ (count
--- of individuals dominating @p@) by row sums on the @+1@ / @-1@
--- patterns. The remainder is the standard Deb 2002 BFS-style level
--- assignment, but on integer arrays rather than per-element list
--- traversals.
-nonDominatedSortIdx :: PopMatrix -> [[Int]]
-nonDominatedSortIdx pm
-  | pmSize pm == 0 = []
-  | otherwise      =
-      let n     = pmSize pm
-          mDom  = dominationMatrix pm
-          rows  = LA.toRows mDom
-          -- Single pass per row: extract S_i (j with +1) and count
-          -- dominators (entries with -1).
-          dInfo = [ rowToSN (LA.toList r) | r <- rows ]
-          sList = map fst dInfo
-          nVec0 = LA.fromList (map (fromIntegral . snd) dInfo)
-                    :: LA.Vector Double
-          front0 = [ i | (i, (_, c)) <- zip [0 ..] dInfo, c == 0 ]
-          go counts current placedSet acc
-            | null current = reverse acc
-            | otherwise =
-                let decrements = [ (j, -1)
-                                 | i <- current
-                                 , j <- sList !! i ]
-                    counts'    = LA.accum counts (+) decrements
-                    placedSet' = foldr IS.insert placedSet current
-                    nextF =
-                      [ j
-                      | j <- [0 .. n - 1]
-                      , not (IS.member j placedSet')
-                      , let v = LA.atIndex counts' j
-                      , v <= 0.5 && v > -0.5
-                      ]
-                in go counts' nextF placedSet' (current : acc)
-      in go nVec0 front0 IS.empty []
-  where
-    -- Walk one row, producing (S_i, n_i) in a single pass.
-    rowToSN :: [Double] -> ([Int], Int)
-    rowToSN vs = go' 0 [] 0 vs
-      where
-        go' _ s c []     = (reverse s, c)
-        go' j s c (x:xs)
-          | x >  0.5 = go' (j + 1) (j : s) c xs
-          | x < -0.5 = go' (j + 1) s       (c + 1) xs
-          | otherwise = go' (j + 1) s       c       xs
-
--- | Compute the crowding distance (Deb 2002) inside a front and sort it
--- by descending distance.
---
--- アルゴリズム (O(MN log N)):
---
---   for each m in objectives:
---     sort I by f_m
---     I[0].dist = I[l-1].dist = ∞
---     for i = 1..l-2:
---       I[i].dist += (f_m(i+1) - f_m(i-1)) / (f_max_m - f_min_m)
---
--- 戻り値: 距離の降順 (= 多様性が高い個体が先頭)。NSGA-II の選別で使う。
-crowdingDistance :: [Solution] -> [Solution]
-crowdingDistance front
-  | length front <= 2 = front
-  | otherwise =
-      -- N3d: reuse 'frontDistances' (vectorized) instead of recomputing
-      -- everything per individual.
-      let dists = frontDistances front
-          fV    = V.fromList front
-          tagged = zip dists [0 .. length front - 1]
-          sortedDesc = sortBy (\(d1, _) (d2, _) -> compare d2 d1) tagged
-      in [ fV V.! i | (_, i) <- sortedDesc ]
-
--- ---------------------------------------------------------------------------
--- 遺伝的演算子 (Phase S3)
--- ---------------------------------------------------------------------------
-
--- | Simulated Binary Crossover (SBX, Deb 1995). A real-coded analogue of
--- single-point crossover for binary GAs.
---
--- 2 親 (p1, p2) から 2 子 (c1, c2) を生成。各次元独立に:
---
---   1. 確率 0.5 で交叉実施 (それ以外は親をそのままコピー)
---   2. \|p1 - p2\| < eps なら交叉せず親を返す (退化対策)
---   3. β ~ SBX 分布 (η_c で形状制御):
---        u ∈ [0, 0.5)  →  β = (2u)^(1/(η+1))
---        u ∈ [0.5, 1)  →  β = (1/(2(1-u)))^(1/(η+1))
---   4. c1 = 0.5 * ((1+β) p1 + (1-β) p2)
---      c2 = 0.5 * ((1-β) p1 + (1+β) p2)
---   5. 範囲外なら境界に clip
---
--- 大きい η_c は親付近に集中、小さい η_c はより広く探索。
-sbxCrossover :: Double      -- η_c (分布指数、典型 15-20)
-             -> Bounds      -- 各次元の範囲
-             -> [Double]    -- 親 1
-             -> [Double]    -- 親 2
-             -> GenIO
-             -> IO ([Double], [Double])
-sbxCrossover etaC bounds p1 p2 gen = do
-  pairs <- zipWithM (sbxOneVar etaC gen) bounds (zip p1 p2)
-  let (c1, c2) = unzip pairs
-  return (c1, c2)
-  -- 注: pymoo は prob_bin による per-dim c1↔c2 swap を持つが、ZDT2 の
-  -- 凹 Pareto front では親由来 lineage の保持が convergence に重要で
-  -- swap が逆効果になることが計測で確認できたため採用しない (NF5 試行
-  -- → revert)。
-
--- | One-dimensional SBX update — **boundary-aware** form (Deb 1995
--- Algorithm 1, matching pymoo / DEAP / jMetal).
---
--- The key difference vs the simplified variant we used previously is
--- that the spread parameter @β@ depends on **how close the parent is
--- to its bound**: a parent right at the lower bound @xl@ is paired with
--- @β ≈ 1@ (= no spread), so the produced child stays near @xl@. The
--- old @β = (2u)^{1/(η+1)}@ was completely bound-agnostic, which means
--- a parent at @x = 0@ paired with one at @x = 0.5@ would produce a
--- child near @0.25@ — the optimum-tracking behaviour ZDT problems
--- demand was lost.
---
--- Algorithm:
---
--- @
--- y1 = min(a, b);  y2 = max(a, b);  Δ = y2 - y1
---
--- For child c1 (anchored to the lower side):
---   β   = 1 + 2(y1 - xl) / Δ
---   α   = 2 - β^{-(η+1)}
---   β_q = (u·α)^{1/(η+1)}                    if u ≤ 1/α
---       = (1 / (2 - u·α))^{1/(η+1)}          otherwise
---   c1  = 0.5 [(y1 + y2) - β_q · Δ]
---
--- For child c2 (anchored to the upper side):
---   β   = 1 + 2(xu - y2) / Δ
---   α, β_q as above
---   c2  = 0.5 [(y1 + y2) + β_q · Δ]
--- @
-sbxOneVar :: Double -> GenIO -> (Double, Double) -> (Double, Double)
-          -> IO (Double, Double)
-sbxOneVar etaC gen (lo, hi) (a, b) = do
-  flip_ <- uniform gen :: IO Double          -- per-dim 50% gating
-  if flip_ >= 0.5 || abs (a - b) < 1e-14 || hi <= lo
-    then return (a, b)
-    else do
-      u <- uniform gen :: IO Double
-      let (y1, y2) = if a < b then (a, b) else (b, a)
-          delta   = y2 - y1
-          mPow    = 1 / (etaC + 1)
-
-          -- Boundary-aware β_q for one side. 'beta' is the
-          -- distance-to-bound term; 'alpha = 2 - β^{-(η+1)}' is the
-          -- adapted threshold that pymoo's @calc_betaq@ uses.
-          calcBetaQ beta =
-            let alpha = 2 - beta ** (- (etaC + 1))
-                inv   = 1 / alpha
-            in if u <= inv
-                 then (u * alpha) ** mPow
-                 else (1 / (2 - u * alpha)) ** mPow
-
-          beta1 = 1 + 2 * (y1 - lo) / delta
-          beta2 = 1 + 2 * (hi - y2) / delta
-          bq1   = calcBetaQ beta1
-          bq2   = calcBetaQ beta2
-          c1    = 0.5 * ((y1 + y2) - bq1 * delta)
-          c2    = 0.5 * ((y1 + y2) + bq2 * delta)
-          clip x = min hi (max lo x)
-      return (clip c1, clip c2)
-
--- | Polynomial mutation (Deb & Goyal 1996).
---
--- 各次元独立に確率 @pMut@ で:
---
---   δq = (2u)^(1/(η+1)) − 1               (u < 0.5)
---      = 1 − (2(1-u))^(1/(η+1))           (u ≥ 0.5)
---   y' = y + δq * (yU − yL)
---
--- 大きい η_m は元値付近、小さい η_m は大きい変異。
-polynomialMutation :: Double    -- η_m (分布指数、典型 20)
-                   -> Double    -- 突然変異確率 (典型 1/d)
-                   -> Bounds
-                   -> [Double]
-                   -> GenIO
-                   -> IO [Double]
-polynomialMutation etaM pMut bounds xs gen =
-  zipWithM (mutateOneVar etaM pMut gen) bounds xs
-
-mutateOneVar :: Double -> Double -> GenIO -> (Double, Double) -> Double
-             -> IO Double
-mutateOneVar etaM pMut gen (lo, hi) x = do
-  r <- uniform gen :: IO Double
-  if r >= pMut || hi <= lo
-    then return x
-    else do
-      u <- uniform gen :: IO Double
-      -- Deb & Goyal 1996 polynomial mutation with **boundary correction**.
-      -- The simplified variant @(2u)^(1/(η+1)) - 1@ ignores the distance
-      -- to the bounds and produces over-aggressive jumps when @u@ is
-      -- near 0 or 1 (= effectively snaps to the boundary). The corrected
-      -- form below scales the perturbation by how close @x@ already is
-      -- to each bound, which is what pymoo / DEAP / jMetal use.
-      let delta1 = (x - lo) / (hi - lo)        -- normalized distance to lo
-          delta2 = (hi - x) / (hi - lo)        -- normalized distance to hi
-          mp     = 1 / (etaM + 1)
-          dq
-            | u <= 0.5  =
-                let val = 2 * u + (1 - 2 * u) * (1 - delta1) ** (etaM + 1)
-                in val ** mp - 1
-            | otherwise =
-                let val = 2 * (1 - u) + (2 * u - 1) * (1 - delta2) ** (etaM + 1)
-                in 1 - val ** mp
-          y = x + dq * (hi - lo)
-      return (min hi (max lo y))
-
--- | Sample one decision vector uniformly from the bounds (used for the
--- initial population). Thin wrapper around 'Hanalyze.Optim.Common.sampleUniformIn',
--- kept for backwards compatibility.
-randomInBounds :: Bounds -> GenIO -> IO [Double]
-randomInBounds = OC.sampleUniformIn
-
--- ---------------------------------------------------------------------------
--- N4: Matrix-vectorised SBX / PolynomialMutation
---
--- The legacy per-pair / per-individual / per-dimension paths above
--- spend most of NSGA-II's time in Haskell function-call overhead. The
--- helpers below compute the entire mating step as a handful of
--- @LA.Matrix Double@ arithmetic operations — all per-cell work
--- collapses into element-wise @cmap@ + @+ - * /@, which is what
--- pymoo's @cross_sbx@ / @mut_pm@ do via numpy.
---
--- Mutable Vector は使わず、'Data.Vector.Storable.replicateM' で
--- batch RNG → 'LA.reshape' で Matrix 化する (immutable で完結)。
--- ---------------------------------------------------------------------------
-
--- | Batch-generate an @n × d@ matrix of i.i.d. @U[0, 1)@ entries via
--- 'mwc-random'. Cheaper than @replicateM (n*d) (uniform g)@ because the
--- intermediate Storable Vector skips boxing.
-randomMatrixU :: GenIO -> Int -> Int -> IO (LA.Matrix Double)
-randomMatrixU gen n d = do
-  v <- VS.replicateM (n * d) (uniformR (0, 1) gen :: IO Double)
-  return (LA.reshape d v)
-
--- | SBX matrix-version. Performs Deb 1995 boundary-aware SBX on every
--- @(pair, dim)@ cell of two parent matrices simultaneously.
---
--- Inputs:
---
---   * @p1@, @p2@ — parent matrices of shape @k × d@.
---   * @bounds@   — list of @d@ @(xl, xu)@ tuples.
---
--- Output: pair of child matrices of shape @k × d@.
-sbxCrossoverMV
-  :: Double                 -- ^ η_c
-  -> Bounds                 -- ^ length d
-  -> LA.Matrix Double       -- ^ parent matrix P1 (k × d)
-  -> LA.Matrix Double       -- ^ parent matrix P2 (k × d)
-  -> GenIO
-  -> IO (LA.Matrix Double, LA.Matrix Double)
-sbxCrossoverMV etaC bounds p1 p2 gen = do
-  let k     = LA.rows p1
-      d     = LA.cols p1
-      mPow  = 1 / (etaC + 1)
-      mNeg  = - (etaC + 1)
-
-      xl    = LA.fromList (map fst bounds) :: LA.Vector Double
-      xu    = LA.fromList (map snd bounds) :: LA.Vector Double
-      onesK = LA.konst 1 k :: LA.Vector Double
-      xlMat = LA.outer onesK xl                 -- k × d, row-broadcast xl
-      xuMat = LA.outer onesK xu
-
-  -- Per-cell random matrices.
-  flipM <- randomMatrixU gen k d                -- per-dim 50% gating
-  uM    <- randomMatrixU gen k d                -- u for β_q
-
-  let -- y1 = min(p1, p2), y2 = max(p1, p2)
-      y1     = LA.cmap id p1
-      y2     = LA.cmap id p2
-      sm     = LA.cmap (\_ -> 1 :: Double) p1   -- placeholder; will use cell-wise compare below
-      _      = (y1, y2, sm)
-
-      -- We need cell-wise min/max. hmatrix doesn't expose elementwise
-      -- min/max on Matrices directly, so flatten and use Vector ops.
-      p1f    = LA.flatten p1
-      p2f    = LA.flatten p2
-      y1f    = LA.fromList (zipWith min (LA.toList p1f) (LA.toList p2f))
-      y2f    = LA.fromList (zipWith max (LA.toList p1f) (LA.toList p2f))
-      y1m    = LA.reshape d y1f                 -- k × d
-      y2m    = LA.reshape d y2f
-      delta  = y2m - y1m
-
-      -- Crossover mask M[i,j] = 1 iff (flip < 0.5) AND (|p1-p2| > eps)
-      -- AND (xu > xl).
-      epsCross   = 1e-14 :: Double
-      diffM      = LA.cmap abs (p1 - p2)
-      maskFlip   = LA.cmap (\v -> if v < 0.5 then 1 else 0) flipM
-      maskDiff   = LA.cmap (\v -> if v > epsCross then 1 else 0) diffM
-      maskBoundV = LA.fromList
-                     [ if hi > lo then 1 else 0 | (lo, hi) <- bounds ]
-                     :: LA.Vector Double
-      maskBound  = LA.outer onesK maskBoundV
-      mask       = maskFlip * maskDiff * maskBound
-
-      -- Boundary-aware β. To avoid divide-by-zero on cells where
-      -- delta = 0 (mask = 0), bump delta with eps before dividing; the
-      -- mask zeroes out the contribution anyway.
-      deltaSafe = LA.cmap (\v -> if v == 0 then 1 else v) delta
-      beta1     = 1 + LA.scale 2 (y1m - xlMat) / deltaSafe
-      beta2     = 1 + LA.scale 2 (xuMat - y2m) / deltaSafe
-
-      alpha1    = LA.cmap (\b -> 2 - b ** mNeg) beta1
-      alpha2    = LA.cmap (\b -> 2 - b ** mNeg) beta2
-
-      -- Per-cell β_q (= condition u <= 1/α).
-      betaQ alpha =
-        let alphaF = LA.flatten alpha
-            uF     = LA.flatten uM
-            bqF    = LA.fromList
-                       [ if uVal <= 1 / aVal
-                           then (uVal * aVal) ** mPow
-                           else (1 / (2 - uVal * aVal)) ** mPow
-                       | (uVal, aVal) <- zip (LA.toList uF) (LA.toList alphaF) ]
-        in LA.reshape d bqF
-
-      bq1 = betaQ alpha1
-      bq2 = betaQ alpha2
-      avg = LA.scale 0.5 (y1m + y2m)
-      c1' = avg - LA.scale 0.5 (bq1 * delta)
-      c2' = avg + LA.scale 0.5 (bq2 * delta)
-
-      -- mask-blend: cell where mask=0 keeps parent value.
-      one_minus_mask = LA.cmap (\v -> 1 - v) mask
-      c1raw = mask * c1' + one_minus_mask * p1
-      c2raw = mask * c2' + one_minus_mask * p2
-
-      -- Clip to bounds.
-      c1 = clipMatToBounds bounds c1raw
-      c2 = clipMatToBounds bounds c2raw
-
-  return (c1, c2)
-
--- | Polynomial mutation, matrix version. Mutates every cell of @x@
--- with per-dimension probability @pMut@. Bounds-aware (Deb-Goyal 1996).
-polynomialMutationMV
-  :: Double                 -- ^ η_m
-  -> Double                 -- ^ per-dim mutation probability
-  -> Bounds                 -- ^ length d
-  -> LA.Matrix Double       -- ^ X (n × d)
-  -> GenIO
-  -> IO (LA.Matrix Double)
-polynomialMutationMV etaM pMut bounds x gen = do
-  let n     = LA.rows x
-      d     = LA.cols x
-      mPow  = 1 / (etaM + 1)
-      mPow1 = etaM + 1
-
-      xl    = LA.fromList (map fst bounds) :: LA.Vector Double
-      xu    = LA.fromList (map snd bounds) :: LA.Vector Double
-      onesN = LA.konst 1 n :: LA.Vector Double
-      xlMat = LA.outer onesN xl
-      xuMat = LA.outer onesN xu
-      rng   = xuMat - xlMat
-      rngSafe = LA.cmap (\v -> if v == 0 then 1 else v) rng
-
-      maskBoundV = LA.fromList
-                     [ if hi > lo then 1 else 0 | (lo, hi) <- bounds ]
-                     :: LA.Vector Double
-      maskBound  = LA.outer onesN maskBoundV
-
-  rM <- randomMatrixU gen n d                 -- per-cell mutation gate
-  uM <- randomMatrixU gen n d                 -- per-cell u for δ_q
-
-  let maskMut = LA.cmap (\v -> if v < pMut then 1 else 0) rM
-      mask    = maskMut * maskBound
-
-      delta1 = (x - xlMat) / rngSafe
-      delta2 = (xuMat - x) / rngSafe
-
-      -- Per-cell δ_q via flatten / zip / reshape.
-      uF      = LA.flatten uM
-      d1F     = LA.flatten delta1
-      d2F     = LA.flatten delta2
-      deltaQF = LA.fromList
-        [ if uVal <= 0.5
-            then
-              let xy  = 1 - d1
-                  val = 2 * uVal + (1 - 2 * uVal) * xy ** mPow1
-              in val ** mPow - 1
-            else
-              let xy  = 1 - d2
-                  val = 2 * (1 - uVal) + (2 * uVal - 1) * xy ** mPow1
-              in 1 - val ** mPow
-        | (uVal, d1, d2) <- zip3 (LA.toList uF) (LA.toList d1F) (LA.toList d2F) ]
-      deltaQ  = LA.reshape d deltaQF
-
-      yRaw    = x + mask * (deltaQ * rng)
-      y       = clipMatToBounds bounds yRaw
-  return y
-
--- | Clip every cell of a matrix to the per-column @(lo, hi)@ bounds.
-clipMatToBounds :: Bounds -> LA.Matrix Double -> LA.Matrix Double
-clipMatToBounds bounds m =
-  let n     = LA.rows m
-      onesN = LA.konst 1 n :: LA.Vector Double
-      xl    = LA.fromList (map fst bounds) :: LA.Vector Double
-      xu    = LA.fromList (map snd bounds) :: LA.Vector Double
-      xlMat = LA.outer onesN xl
-      xuMat = LA.outer onesN xu
-      mFlat = LA.flatten m
-      lFlat = LA.flatten xlMat
-      uFlat = LA.flatten xuMat
-      cFlat = LA.fromList
-        [ max lo (min hi v)
-        | (v, lo, hi) <- zip3 (LA.toList mFlat) (LA.toList lFlat) (LA.toList uFlat)
-        ]
-  in LA.reshape (LA.cols m) cFlat
-
--- | NSGA-II's crowded-comparison operator:
---   1. rank が低い (front 番号小) 方が良い
---   2. rank 同じなら crowding distance 大が良い
---
--- LT = 第 1 引数が良い、GT = 第 2 引数が良い、EQ = 同等。
-crowdedCompare :: (Int, Double) -> (Int, Double) -> Ordering
-crowdedCompare (r1, d1) (r2, d2)
-  | r1 < r2          = LT
-  | r1 > r2          = GT
-  | d1 > d2          = LT   -- 距離大が良い
-  | d1 < d2          = GT
-  | otherwise        = EQ
-
--- | 二項トーナメント選択。
--- pop からランダムに 2 個体取り、cmp に従って勝者を返す。
--- cmp x y == LT のとき x が勝者。
--- EQ (両者同等) の場合は **ランダムに勝敗を決める** (pymoo / DEAP と同方式)。
--- 以前は常に xi を返していたため early-population indices が選択圧で
--- 有利になり ZDT 系で per-generation 収束が遅れていた。
-binaryTournament :: [a] -> (a -> a -> Ordering) -> GenIO -> IO a
-binaryTournament pop cmp gen = do
-  let n = length pop
-  i <- uniformR (0, n - 1) gen
-  j <- uniformR (0, n - 1) gen
-  let xi = pop !! i
-      xj = pop !! j
-  case cmp xi xj of
-    LT -> return xi
-    GT -> return xj
-    EQ -> do
-      r <- uniform gen :: IO Double
-      return (if r < 0.5 then xi else xj)
diff --git a/src/Hanalyze/Optim/NelderMead.hs b/src/Hanalyze/Optim/NelderMead.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/NelderMead.hs
+++ /dev/null
@@ -1,196 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.NelderMead
--- Description : Nelder-Mead シンプレックス法
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Nelder-Mead simplex method (downhill simplex).
---
--- Nelder & Mead (1965). Gradient-free, easy to implement at low dimension
--- (1-30), and stable for local optimization. The default behind R's
--- @optim(method="Nelder-Mead")@.
---
--- Algorithm: maintain an @n+1@-vertex simplex; each iteration replaces the
--- worst vertex via reflect / expand / contract / shrink. Standard Wright
--- (1996) parameters @ρ = 1, χ = 2, γ = 1/2, σ = 1/2@. This implementation
--- follows the canonical form of Lagarias et al. (1998).
---
--- Cost: 1-2 function evaluations per iteration (@n@ on shrink). Convergence
--- becomes slow for larger @n@ — practical up to @n ≤ 10@.
-{-# LANGUAGE StrictData #-}
-module Hanalyze.Optim.NelderMead
-  ( NMConfig (..)
-  , defaultNMConfig
-  , runNelderMead
-  , runNelderMeadWith
-  ) where
-
-import Data.List (sortBy)
-import Data.Ord (comparing)
-import Hanalyze.Optim.Common
-
--- | Nelder-Mead configuration.
---
--- Standard parameters:
---
---   * Reflection      @ρ = 1.0@
---   * Expansion       @χ = 2.0@
---   * Contraction     @γ = 0.5@
---   * Shrink          @σ = 0.5@
-data NMConfig = NMConfig
-  { nmStop     :: !StopCriteria
-  , nmInitStep :: !Double      -- ^ Initial simplex step (per axis).
-  , nmRho      :: !Double      -- ^ Reflection coefficient @ρ@.
-  , nmChi      :: !Double      -- ^ Expansion coefficient @χ@.
-  , nmGamma    :: !Double      -- ^ Contraction coefficient @γ@.
-  , nmSigma    :: !Double      -- ^ Shrink coefficient @σ@.
-  , nmDir      :: !Direction
-  , nmBounds   :: !(Maybe Bounds)  -- ^ Optional box constraints; when set,
-                                   --   adds 'boundsPenalty' to the objective
-                                   --   (soft-penalty enforcement).
-  } deriving (Show, Eq)
-
--- | Default configuration: standard parameters, minimization, no bounds,
--- step 0.5. The stop criteria are tightened beyond
--- 'defaultStopCriteria' so the simplex can settle to near-machine
--- precision on smooth unimodal problems (matches the @scipy.optimize@
--- @\"Nelder-Mead\"@ defaults: @xatol = fatol = 1e-10@, @maxiter = 10000@).
-defaultNMConfig :: NMConfig
-defaultNMConfig = NMConfig
-  { nmStop     = defaultStopCriteria { stMaxIter = 10000
-                                     , stTolFun  = 1e-12
-                                     , stTolX    = 1e-12 }
-  , nmInitStep = 0.5
-  , nmRho      = 1.0
-  , nmChi      = 2.0
-  , nmGamma    = 0.5
-  , nmSigma    = 0.5
-  , nmDir      = Minimize
-  , nmBounds   = Nothing
-  }
-
--- | Run Nelder-Mead with the default configuration.
-runNelderMead :: ([Double] -> Double)   -- ^ Objective function.
-              -> [Double]                -- ^ Initial point @x₀@.
-              -> IO OptimResult
-runNelderMead = runNelderMeadWith defaultNMConfig
-
--- | Run Nelder-Mead with a user-specified configuration.
-runNelderMeadWith :: NMConfig
-                  -> ([Double] -> Double)
-                  -> [Double]
-                  -> IO OptimResult
-runNelderMeadWith cfg fUser x0 =
-  let n         = length x0
-      fPenal xs = fUser xs + boundsPenalty (nmBounds cfg) xs
-      f         = flipFor (nmDir cfg) fPenal   -- 内部は常に最小化
-      step      = nmInitStep cfg
-      -- 初期単体: x0 + step*e_i
-      vertices0 = (x0, f x0) : [ (x, f x) | i <- [0 .. n - 1]
-                                          , let x = perturb x0 i step ]
-      sortedV   = sortBy (comparing snd) vertices0
-      stop      = nmStop cfg
-      hist0     = [ snd (head sortedV) ]
-      (vEnd, hEnd, iters, conv) = loop cfg stop f 0 sortedV hist0
-      (xb, vb) = head vEnd
-      vbUser   = case nmDir cfg of
-                   Minimize -> vb
-                   Maximize -> negate vb
-      histUser = case nmDir cfg of
-                   Minimize -> reverse hEnd
-                   Maximize -> map negate (reverse hEnd)
-  in pure $ OptimResult
-       { orBest      = xb
-       , orValue     = vbUser
-       , orHistory   = histUser
-       , orIters     = iters
-       , orConverged = conv
-       }
-
--- | 軸 i 方向に step だけ動かす。
-perturb :: [Double] -> Int -> Double -> [Double]
-perturb xs i step =
-  [ if k == i then v + (if v == 0 then step else step * (1 + abs v))
-              else v
-  | (k, v) <- zip [0 ..] xs ]
-
--- | 反復本体。引数 vertices は f 値で昇順ソート済を維持する。
-loop :: NMConfig -> StopCriteria
-     -> ([Double] -> Double)
-     -> Int                      -- 反復カウンタ
-     -> [([Double], Double)]      -- 単体頂点 ([(x, f x)] sorted ascending)
-     -> [Double]                  -- best 値履歴 (逆順、新しい先頭)
-     -> ([([Double], Double)], [Double], Int, Bool)
-loop cfg stop f iter vertices hist
-  | iter >= stMaxIter stop  = (vertices, hist, iter, False)
-  | converged                = (vertices, hist, iter, True)
-  | otherwise                = loop cfg stop f (iter + 1) newV newH
-  where
-    n        = length vertices - 1
-    fBest    = snd (head vertices)
-    fWorst   = snd (last vertices)
-    fSecond  = snd (vertices !! (n - 1))     -- 2 番目に悪い
-    -- 収束判定: f 値の幅 < tolFun または (将来) 単体の x 幅 < tolX
-    converged = abs (fWorst - fBest) < stTolFun stop
-                || simplexSpread vertices < stTolX stop
-    -- 重心 (worst を除外して平均)
-    centroid = avgVecs (map fst (init vertices))
-    xWorst   = fst (last vertices)
-    -- 反射点
-    xR  = combine (1 + nmRho cfg) centroid (nmRho cfg) xWorst
-    fR  = f xR
-    (newV, newH) =
-      if fR < fBest
-        then -- 拡張
-          let xE = combine (1 + nmRho cfg * nmChi cfg) centroid
-                           (nmRho cfg * nmChi cfg) xWorst
-              fE = f xE
-              chosen = if fE < fR then (xE, fE) else (xR, fR)
-          in update chosen vertices
-      else if fR < fSecond
-        then update (xR, fR) vertices
-      else
-        let -- 縮小
-            (xC, fC) =
-              if fR < fWorst
-                then -- 外縮小
-                  let xOC = combine (1 + nmRho cfg * nmGamma cfg) centroid
-                                    (nmRho cfg * nmGamma cfg) xWorst
-                  in (xOC, f xOC)
-                else -- 内縮小
-                  let xIC = combine (1 - nmGamma cfg) centroid
-                                    (- nmGamma cfg) xWorst
-                  in (xIC, f xIC)
-        in if fC < fWorst
-             then update (xC, fC) vertices
-             else
-               -- 全縮小: best を中心に他全頂点を σ 倍に縮める
-               let xb = fst (head vertices)
-                   shrunk = head vertices :
-                            [ let xk = zipWith (\b v -> b + nmSigma cfg * (v - b)) xb x
-                              in (xk, f xk)
-                            | (x, _) <- tail vertices ]
-                   sortedS = sortBy (comparing snd) shrunk
-               in (sortedS, snd (head sortedS) : hist)
-    update (xN, fN) vs =
-      let replaced = init vs ++ [(xN, fN)]
-          sortedR  = sortBy (comparing snd) replaced
-      in (sortedR, snd (head sortedR) : hist)
-
--- | 単体の最大辺長 (∞-norm)。tolX 判定用。
-simplexSpread :: [([Double], Double)] -> Double
-simplexSpread vs =
-  let xs = map fst vs
-      x0 = head xs
-  in maximum [ maximum (zipWith (\a b -> abs (a - b)) x0 x) | x <- tail xs ]
-
--- | s1 * a - s2 * b の線形結合 (純粋にベクトル演算ユーティリティ)。
-combine :: Double -> [Double] -> Double -> [Double] -> [Double]
-combine s1 a s2 b = zipWith (\ai bi -> s1 * ai - s2 * bi) a b
-
--- | 同じ長さの複数ベクトルの平均。
-avgVecs :: [[Double]] -> [Double]
-avgVecs xs =
-  let n = fromIntegral (length xs) :: Double
-  in foldr1 (zipWith (+)) (map (map (/ n)) xs)
-    -- 等価: map (/n) (foldr1 (zipWith (+)) xs)、こちらの方が overflow 緩和的
diff --git a/src/Hanalyze/Optim/Numeric.hs b/src/Hanalyze/Optim/Numeric.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/Numeric.hs
+++ /dev/null
@@ -1,68 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.Numeric
--- Description : 数値勾配 (有限差分法)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Numeric gradients (finite differences).
---
--- For situations where automatic differentiation is impractical (e.g. GP
--- log-marginal likelihood whose @det@ is computed inside hmatrix and would
--- be cumbersome to AD-ify).
---
---   * 'numGradCentral' — central differences (error @O(h²)@; recommended).
---   * 'numGradForward' — forward differences (error @O(h)@; half the cost).
---   * 'numHessianCentral' — Hessian approximation via central differences.
-{-# LANGUAGE OverloadedStrings #-}
-module Hanalyze.Optim.Numeric
-  ( numGradCentral
-  , numGradForward
-  , numHessianCentral
-  ) where
-
--- | Central-difference gradient.
---
--- @∂f/∂x_i ≈ (f(x + h e_i) − f(x − h e_i)) / (2h)@.
-numGradCentral :: Double                       -- ^ Step size @h@.
-               -> ([Double] -> Double)         -- ^ Objective @f@.
-               -> [Double] -> [Double]
-numGradCentral h f x =
-  [ (f (set i (x !! i + h)) - f (set i (x !! i - h))) / (2 * h)
-  | i <- [0 .. length x - 1] ]
-  where
-    set i v = take i x ++ [v] ++ drop (i + 1) x
-
--- | One-sided forward-difference gradient (half the cost of
--- 'numGradCentral'):
---
--- @∂f/∂x_i ≈ (f(x + h e_i) − f(x)) / h@.
-numGradForward :: Double -> ([Double] -> Double) -> [Double] -> [Double]
-numGradForward h f x =
-  let fx = f x
-  in [ (f (set i (x !! i + h)) - fx) / h
-     | i <- [0 .. length x - 1] ]
-  where
-    set i v = take i x ++ [v] ++ drop (i + 1) x
-
--- | Hessian approximation by mixed forward differences.
---
--- @∂²f/∂x_i∂x_j ≈ [f(x+h eᵢ+h eⱼ) − f(x+h eᵢ) − f(x+h eⱼ) + f(x)] / h²@.
---
--- Forward-only, so accuracy is @O(h)@. The fully central variant would
--- be more accurate at four times the cost.
-numHessianCentral :: Double -> ([Double] -> Double) -> [Double] -> [[Double]]
-numHessianCentral h f x =
-  [ [ second i j | j <- [0 .. n - 1] ]
-  | i <- [0 .. n - 1] ]
-  where
-    n = length x
-    set k v = take k x ++ [v] ++ drop (k + 1) x
-    setBoth i j vi vj =
-      let x1 = set i vi
-      in take j x1 ++ [vj] ++ drop (j + 1) x1
-    fx = f x
-    second i j =
-      let f_ij = f (setBoth i j (x !! i + h) (x !! j + h))
-          f_i  = f (set i (x !! i + h))
-          f_j  = f (set j (x !! j + h))
-      in (f_ij - f_i - f_j + fx) / (h * h)
diff --git a/src/Hanalyze/Optim/Pareto.hs b/src/Hanalyze/Optim/Pareto.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/Pareto.hs
+++ /dev/null
@@ -1,140 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.Pareto
--- Description : 多目的最適化結果評価のための Pareto フロント関連ユーティリティ
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Pareto-front utilities for evaluating multi-objective results.
---
---   * 'isNonDominated' — is a given point non-dominated within the front?
---   * 'paretoFront'    — extract just the non-dominated points from a set.
---   * 'hypervolume'    — front volume indicator (larger is better).
---   * 'igd'            — Inverted Generational Distance (distance from the
---     true front to the approximation).
---   * 'gd'             — Generational Distance (distance from the
---     approximation to the true front).
---
--- All objectives are treated as **minimized**, matching the NSGA-II
--- convention.
-{-# LANGUAGE StrictData #-}
-{-# LANGUAGE OverloadedStrings #-}
-module Hanalyze.Optim.Pareto
-  ( isNonDominated
-  , paretoFront
-  , hypervolume
-  , igd
-  , gd
-  ) where
-
-import Data.List (sortBy, sortOn)
-
--- | True iff @p@ is non-dominated within the set @ps@ (no element of @ps@
--- dominates it).
-isNonDominated :: [Double] -> [[Double]] -> Bool
-isNonDominated p ps = not (any (`dominates'` p) ps)
-
--- | Plain Pareto dominance (internal helper; same definition as
--- 'Hanalyze.Optim.NSGA.paretoDominates').
-dominates' :: [Double] -> [Double] -> Bool
-dominates' a b =
-  all (uncurry (<=)) zipped && any (uncurry (<)) zipped
-  where zipped = zip a b
-
--- | Extract just the non-dominated points from a set. When points repeat,
--- only the first occurrence is kept.
-paretoFront :: [[Double]] -> [[Double]]
-paretoFront pts =
-  [p | (i, p) <- indexed,
-       not (any (\(j, q) -> j /= i && dominates' q p) indexed) ]
-  where
-    indexed = zip [0 :: Int ..] pts
-
--- | Hypervolume (HV) indicator: the volume dominated by the Pareto
--- front, measured from a reference point @r@. Larger is better
--- (captures both convergence and diversity).
---
--- 2D uses the exact area formula; higher dimensions use HSO
--- (Hypervolume by Slicing Objectives) recursively.
---
--- All objectives are assumed to be minimized (NSGA-II convention).
-hypervolume :: [Double] -> [[Double]] -> Double
-hypervolume ref front
-  | null front = 0
-  | any (\p -> length p /= dim) front = error "hypervolume: 次元不一致"
-  | dim == 2 = hv2D ref front
-  | otherwise = hvND ref front
-  where
-    dim = length ref
-
--- 2D: y 降順にソート → x 増加順に階段状の面積を積む
-hv2D :: [Double] -> [[Double]] -> Double
-hv2D [rx, ry] front =
-  let valid    = [p | p <- front, head p < rx, p !! 1 < ry]
-      sorted   = sortOn head valid    -- x 昇順
-      go _    [] acc          = acc
-      go yPrev (p:ps) acc =
-        let xCur = head p
-            yCur = p !! 1
-        in if yCur >= yPrev   -- 支配されてる (= 重複点) → 寄与なし
-             then go yPrev ps acc
-             else go yCur ps (acc + (rx - xCur) * (yPrev - yCur))
-  in go ry sorted 0
-hv2D _ _ = 0
-
--- 一般 N 次元: 第 1 軸 (x_1) で降順にスライスして再帰。
---
--- HSO (Hypervolume by Slicing Objectives) アルゴリズム:
---   x_1 で降順にソートし、各点 p で:
---     width = (前のスライス境界) - p[0]
---     slice = HV(p から見える残り次元の front, 残り参照点)
---     vol += width × slice
---   前のスライス境界は ref[0] から始まり、各 p で更新。
-hvND :: [Double] -> [[Double]] -> Double
-hvND ref front =
-  let front'   = paretoFront [p | p <- front
-                                , and (zipWith (<) p ref) ]  -- ref 内のみ
-      sortedDesc = sortBy (\a b -> compare (head b) (head a)) front'
-                   -- x_1 降順
-      r1       = head ref
-      restRef  = tail ref
-      go _    []     acc = acc
-      go xPrev (p:ps) acc =
-        let xCur  = head p
-            width = xPrev - xCur
-            -- 残り次元への射影: 現在の p より x_1 が小さい点 (= まだ処理してない)
-            -- + p 自身
-            activeRest = (tail p) :
-                         [ tail q | q <- ps ]
-            slice = hypervolume restRef activeRest
-        in if width <= 0
-             then go xPrev ps acc
-             else go xCur ps (acc + width * slice)
-  in go r1 sortedDesc 0
-
--- | Inverted Generational Distance: the average of, for each point in
--- the /true/ front, the minimum distance to the /estimated/ front.
--- Smaller is better; rewards diversity as well as convergence.
---
--- @IGD = (1/|R|) Σ_{r ∈ R} min_{e ∈ E} dist(r, e)@.
-igd :: [[Double]] -> [[Double]] -> Double
-igd trueF estF
-  | null trueF || null estF = 1 / 0
-  | otherwise =
-      let n = length trueF
-          minDistTo r = minimum [euclid r e | e <- estF]
-      in sum (map minDistTo trueF) / fromIntegral n
-
--- | Generational Distance: the average minimum distance from each point
--- of the /estimated/ front to the /true/ front. Smaller is better, but
--- this does not penalize a lack of diversity.
-gd :: [[Double]] -> [[Double]] -> Double
-gd trueF estF
-  | null trueF || null estF = 1 / 0
-  | otherwise =
-      let n = length estF
-          minDistTo e = minimum [euclid e t | t <- trueF]
-      in sum (map minDistTo estF) / fromIntegral n
-
--- | Euclidean distance.
-euclid :: [Double] -> [Double] -> Double
-euclid a b = sqrt (sum [(x - y) ^ (2 :: Int) | (x, y) <- zip a b])
diff --git a/src/Hanalyze/Optim/ParticleSwarm.hs b/src/Hanalyze/Optim/ParticleSwarm.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/ParticleSwarm.hs
+++ /dev/null
@@ -1,147 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.ParticleSwarm
--- Description : Particle Swarm Optimization (PSO) — Kennedy & Eberhart 1995
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Particle Swarm Optimization (PSO).
---
--- Kennedy & Eberhart (1995). A metaheuristic in which a swarm of particles
--- updates velocity by being attracted to its personal best (pbest) and the
--- global best (gbest).
---
--- Velocity / position update:
---
--- @
--- v_{t+1} = w · v_t + c_1 · r_1 · (pbest - x) + c_2 · r_2 · (gbest - x)
--- x_{t+1} = x_t + v_{t+1}
--- @
---
--- Here @w@ is inertia, @c_1@ the cognitive coefficient, @c_2@ the social
--- coefficient, and @r_1, r_2 ~ U(0, 1)@.
-{-# LANGUAGE StrictData #-}
-module Hanalyze.Optim.ParticleSwarm
-  ( PSOConfig (..)
-  , defaultPSOConfig
-  , runPSO
-  , runPSOWith
-  ) where
-
-import Control.Monad (forM, replicateM)
-import Data.List (minimumBy)
-import Data.Ord (comparing)
-import Data.IORef
-import qualified System.Random.MWC as MWC
-import Hanalyze.Optim.Common
-
--- | PSO configuration.
-data PSOConfig = PSOConfig
-  { psoStop     :: !StopCriteria
-  , psoNum      :: !Int        -- ^ Number of particles (20–50 typical).
-  , psoInertia  :: !Double     -- ^ Inertia @w@ (0.4–0.9 typical).
-  , psoCog      :: !Double     -- ^ Cognitive coefficient @c₁@ (1.5–2.0 typical).
-  , psoSoc      :: !Double     -- ^ Social coefficient @c₂@ (1.5–2.0 typical).
-  , psoBounds   :: !Bounds     -- ^ Per-dimension bounds.
-  , psoVMax     :: !Double     -- ^ Velocity cap as a fraction of the
-                               --   range per dimension (e.g. 0.5).
-  , psoDir      :: !Direction
-  } deriving (Show, Eq)
-
--- | Default configuration: 200 iterations, swarm size @max(20, 5×D)@,
--- @w = 0.7@, @c₁ = c₂ = 1.5@, @vMax = 0.5@.
-defaultPSOConfig :: [(Double, Double)] -> PSOConfig
-defaultPSOConfig bs = PSOConfig
-  { psoStop    = defaultStopCriteria { stMaxIter = 200 }
-  , psoNum     = max 20 (5 * length bs)
-  , psoInertia = 0.7
-  , psoCog     = 1.5
-  , psoSoc     = 1.5
-  , psoBounds  = bs
-  , psoVMax    = 0.5
-  , psoDir     = Minimize
-  }
-
--- | Run PSO with the default configuration built from @bounds@.
-runPSO :: [(Double, Double)]
-       -> ([Double] -> Double)
-       -> MWC.GenIO
-       -> IO OptimResult
-runPSO bs f gen = runPSOWith (defaultPSOConfig bs) f gen
-
--- | Run PSO with a user-specified configuration.
-runPSOWith :: PSOConfig
-           -> ([Double] -> Double)
-           -> MWC.GenIO
-           -> IO OptimResult
-runPSOWith cfg fUser gen = do
-  let f      = flipFor (psoDir cfg) fUser
-      bs     = psoBounds cfg
-      n      = length bs
-      np     = psoNum cfg
-      vMaxes = [ psoVMax cfg * (hi - lo) | (lo, hi) <- bs ]
-
-  -- 初期化
-  xs0 <- replicateM np (sampleUniformIn bs gen)
-  vs0 <- replicateM np $ forM (zip bs vMaxes) $ \((lo, hi), vM) -> do
-           u <- MWC.uniformR (-1, 1) gen
-           return ((u :: Double) * vM * 0.1)
-  let fs0 = map f xs0
-
-  posRef     <- newIORef xs0
-  velRef     <- newIORef vs0
-  pbestRef   <- newIORef (zip xs0 fs0)
-  gbestRef   <- newIORef (minimumBy (comparing snd) (zip xs0 fs0))
-  histRef    <- newIORef [snd (minimumBy (comparing snd) (zip xs0 fs0))]
-  iterRef    <- newIORef 0
-
-  let stop = psoStop cfg
-      maxI = stMaxIter stop
-
-  let loop = do
-        i <- readIORef iterRef
-        if i >= maxI then return ()
-          else do
-            xs <- readIORef posRef
-            vs <- readIORef velRef
-            pb <- readIORef pbestRef
-            (gbX, gbF) <- readIORef gbestRef
-            -- 更新
-            updated <- forM (zip3 xs vs pb) $ \(x, v, (px, pf)) -> do
-              vNew <- forM (zip4 x v px gbX) $ \(xi, vi, pxi, gxi) -> do
-                r1 <- MWC.uniformR (0, 1) gen :: IO Double
-                r2 <- MWC.uniformR (0, 1) gen :: IO Double
-                pure $ psoInertia cfg * vi
-                       + psoCog cfg * r1 * (pxi - xi)
-                       + psoSoc cfg * r2 * (gxi - xi)
-              -- vMax クリップ
-              let vClipped = zipWith (\vi vM -> max (-vM) (min vM vi)) vNew vMaxes
-              -- 位置更新 + bounds 反射
-              let xNew = clipToBounds bs (zipWith (+) x vClipped)
-              let fNew = f xNew
-              -- pbest 更新
-              let (pxN, pfN) = if fNew < pf then (xNew, fNew) else (px, pf)
-              return (xNew, vClipped, (pxN, pfN), fNew)
-            let xsN = [a | (a, _, _, _) <- updated]
-                vsN = [b | (_, b, _, _) <- updated]
-                pbN = [c | (_, _, c, _) <- updated]
-                bestC = minimumBy (comparing snd) [(a, d) | (a, _, _, d) <- updated]
-                (gbXN, gbFN) = if snd bestC < gbF then bestC else (gbX, gbF)
-            writeIORef posRef xsN
-            writeIORef velRef vsN
-            writeIORef pbestRef pbN
-            writeIORef gbestRef (gbXN, gbFN)
-            modifyIORef histRef (gbFN :)
-            writeIORef iterRef (i + 1)
-            loop
-  loop
-  (gbX, gbF) <- readIORef gbestRef
-  iters      <- readIORef iterRef
-  histR      <- readIORef histRef
-  let vUser = case psoDir cfg of { Minimize -> gbF; Maximize -> negate gbF }
-      hU    = case psoDir cfg of
-                Minimize -> reverse histR
-                Maximize -> map negate (reverse histR)
-  return $ OptimResult gbX vUser hU iters False
-  where
-    zip4 (a:as) (b:bs) (c:cs) (d:ds) = (a, b, c, d) : zip4 as bs cs ds
-    zip4 _ _ _ _ = []
diff --git a/src/Hanalyze/Optim/SimulatedAnnealing.hs b/src/Hanalyze/Optim/SimulatedAnnealing.hs
deleted file mode 100644
--- a/src/Hanalyze/Optim/SimulatedAnnealing.hs
+++ /dev/null
@@ -1,406 +0,0 @@
--- |
--- Module      : Hanalyze.Optim.SimulatedAnnealing
--- Description : Simulated Annealing (焼きなまし法) — Kirkpatrick, Gelatt, Vecchi 1983
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Simulated Annealing.
---
--- Kirkpatrick, Gelatt, Vecchi (1983). A physical analogy (cooling solids):
--- a random walk with probabilistic acceptance approaches a global
--- optimum.
---
--- Acceptance probability (Metropolis criterion):
---
---   * Improvement (@Δf < 0@): always accept.
---   * Deterioration (@Δf ≥ 0@): accept with probability @exp(-Δf / T)@.
---
--- Temperature schedule: @T_k = T_0 · α^k@ (geometric cooling, with
--- @α ∈ [0.85, 0.99]@).
---
--- Proposal: add @Normal(0, sigma)@ independently per dimension and reflect
--- against the bounds.
-{-# LANGUAGE StrictData #-}
-module Hanalyze.Optim.SimulatedAnnealing
-  ( SAConfig (..)
-  , SACoolingSchedule (..)
-  , SAProposal (..)
-  , SALocalMethod (..)
-  , SAAccept (..)
-  , defaultSAConfig
-  , runSA
-  , runSAWith
-  ) where
-
-import Control.Monad (forM)
-import qualified System.Random.MWC as MWC
-import qualified System.Random.MWC.Distributions as MWCD
-import Hanalyze.Optim.Common
-import qualified Hanalyze.Optim.NelderMead as NM
-import qualified Hanalyze.Optim.LBFGS as LB
-import Control.Exception (SomeException, try, evaluate)
-import           System.IO.Unsafe (unsafePerformIO)
-
--- | Cooling schedule for the SA temperature.
---
---   * 'Geometric' α — @T_{k+1} = α · T_k@ (the original Kirkpatrick form).
---   * 'Linear'    a — @T_{k+1} = T_k − a@ (rarely useful in practice).
---   * 'LundyMees' β — @T_{k+1} = T_k / (1 + β · T_k)@ (Lundy & Mees 1986;
---     spends more time at low temperatures, robust default).
---   * 'Cauchy'    — @T_k = T_0 / (1 + k)@ ("fast SA"; matches the
---     Cauchy-distributed proposal in classical analyses).
-data SACoolingSchedule
-  = Geometric !Double
-  | Linear    !Double
-  | LundyMees !Double
-  | Cauchy
-  | TsallisCool !Double
-    -- ^ Generalised SA cooling (Xiang-Gong-Liu-Yan 1997, scipy
-    --   dual_annealing). With parameter @q_v@:
-    --   @T(t) = T_0 · (2^(q_v−1) − 1) / ((t+2)^(q_v−1) − 1)@.
-    --   Drops fast initially then asymptotically slow; pairs naturally
-    --   with the 'Tsallis' visiting distribution.
-  deriving (Show, Eq)
-
--- | Proposal (visiting) distribution for the next-x candidate.
---
---   * @Gaussian@: classical Kirkpatrick — @x' = x + N(0, σ)@ per dim.
---   * @Cauchy@: Szu-Hartley "Fast SA" (1987) — @x' = x + Cauchy(0, σ)@.
---     Heavy-tailed → occasional big jumps escape local minima.
---   * @Tsallis q_v@: Generalized SA visiting distribution
---     (Xiang-Gong-Liu-Yan 1997, Tsallis-Stariolo 1996), the engine
---     behind scipy's @dual_annealing@. For @q_v = 2.62@ (scipy default)
---     the jump distribution interpolates between Cauchy (@q_v = 2@)
---     and even fatter tails, while a temperature-dependent scale
---     contracts the typical jump as the system cools. The strongest
---     option for highly multi-modal landscapes (Rastrigin, Schwefel
---     etc.) at modest budgets.
-data SAProposal
-  = Gaussian
-  | Cauchy_
-  | Tsallis !Double
-  deriving (Show, Eq)
-
--- | Local refinement method used by 'saLocalEvery' and the final
--- polish.
---
---   * @LocalNelderMead@: derivative-free, robust on noisy/discontinuous
---     objectives. Default.
---   * @LocalLBFGS@: numeric-gradient L-BFGS-B with @stMaxIter = 100@.
---     Significantly more efficient on smooth landscapes per call;
---     mirrors scipy @dual_annealing@'s every-iteration L-BFGS-B
---     refinement and is what closes the Rastrigin gap to machine
---     precision.
-data SALocalMethod
-  = LocalNelderMead
-  | LocalLBFGS
-  deriving (Show, Eq)
-
--- | Acceptance criterion for worsening proposals.
---
---   * @Boltzmann@: classical Metropolis — @P_acc = exp(-ΔF / T)@.
---   * @TsallisAccept q_a@: generalised acceptance
---     @P_acc = max(0, 1 - (1 - q_a) ΔF / T)^(1/(1-q_a))@.
---     For @q_a = -5@ (scipy dual_annealing default) the worsening tail
---     is heavier than Boltzmann at high T, encouraging escape from
---     local minima. As @q_a → 1@ this reduces to Boltzmann.
-data SAAccept
-  = Boltzmann
-  | TsallisAccept !Double
-  | GreedyAccept
-    -- ^ Accept only improvements. The exploration role is delegated
-    --   entirely to the proposal distribution (set 'saProposal' to
-    --   'Tsallis q_v' for heavy-tailed jumps). This matches scipy's
-    --   @dual_annealing@ effective behaviour (its Tsallis acceptance
-    --   with @q_a = -5@ essentially rejects all worsenings).
-  deriving (Show, Eq)
-
--- | SA configuration.
-data SAConfig = SAConfig
-  { saStop       :: !StopCriteria
-  , saInitTemp   :: !Double            -- ^ Initial temperature @T₀@.
-  , saSchedule   :: !SACoolingSchedule -- ^ Cooling schedule.
-  , saStepSigma  :: !Double            -- ^ Proposal SD.
-  , saStepDecay  :: !Double            -- ^ Per-iteration shrink for the SD
-                                       --   (1.0 leaves the SD constant).
-  , saBounds     :: !Bounds            -- ^ Per-dimension bounds for reflection.
-  , saDir        :: !Direction
-  , saLocalEvery :: !(Maybe Int)
-    -- ^ When @Just k@, run a local 'Hanalyze.Optim.NelderMead' refinement on
-    --   @x_best@ every @k@ iterations and replace @(x_best, f_best)@
-    --   if the refinement improves it. This turns vanilla SA into a
-    --   hybrid (analogous to scipy's @dual_annealing@), which is the
-    --   only way to reach machine-precision-level minima on
-    --   multi-modal problems with the modest 5000-iteration budget.
-  , saPolish     :: !Bool
-    -- ^ When 'True', run a high-precision Nelder-Mead refinement on
-    --   @x_best@ once at SA termination (separate from
-    --   'saLocalEvery'). Uses a small-simplex starting step
-    --   (@0.001 × bound width@) to polish the result to near-machine
-    --   precision on smooth landscapes.
-  , saRestartIfStuck :: !(Maybe Int)
-    -- ^ When @Just k@, perturb @x@ to a fresh random point in
-    --   'saBounds' if @x_best@ has not improved in @k@ iterations.
-    --   Helps SA escape pathological multi-modal landscapes
-    --   (Rastrigin etc.) where vanilla SA — even with periodic NM
-    --   refinement — gets trapped in a single basin.
-  , saProposal       :: !SAProposal
-    -- ^ Proposal (visiting) distribution. Default 'Gaussian' for
-    --   back-compat. Set 'Tsallis 2.62' for scipy-style dual_annealing
-    --   behaviour on multi-modal problems.
-  , saLocalMethod    :: !SALocalMethod
-    -- ^ Local refinement method (see 'saLocalEvery' and the final
-    --   polish). Default 'LocalNelderMead'.
-  , saAccept         :: !SAAccept
-    -- ^ Acceptance criterion for worsening proposals. Default
-    --   'Boltzmann'. 'TsallisAccept (-5)' = scipy dual_annealing
-    --   default.
-  } deriving (Show, Eq)
-
--- | Default configuration: 5000 iterations, @T₀ = 1.0@, geometric
--- cooling with @α = 0.995@, proposal SD 0.5 with decay 0.999.
---
--- Geometric is empirically the best general default; switch to
--- @LundyMees 0.2@ (slower asymptotic decay, retains exploration)
--- for very multi-modal problems with large budgets, or 'Cauchy' for
--- short-budget runs (rapid cool-down).
-defaultSAConfig :: [(Double, Double)] -> SAConfig
-defaultSAConfig bs = SAConfig
-  { saStop           = defaultStopCriteria { stMaxIter = 5000 }
-  , saInitTemp       = 1.0
-  , saSchedule       = Geometric 0.995
-  , saStepSigma      = 0.5
-  , saStepDecay      = 0.999
-  , saBounds         = bs
-  , saDir            = Minimize
-  , saLocalEvery     = Just 200            -- 5000 / 200 = 25 NM refines
-  , saPolish         = True                -- final high-precision NM
-  , saRestartIfStuck = Nothing             -- off by default; useful for
-                                           -- pathological multi-modal
-                                           -- (Rastrigin etc.) but hurts
-                                           -- problems whose basin needs
-                                           -- continuous refinement
-                                           -- (Levy regressed by 12 orders
-                                           --  of magnitude with restart on)
-  , saProposal       = Gaussian            -- back-compat default; switch to
-                                           -- 'Tsallis 2.62' for Rastrigin-
-                                           -- like multi-modal problems.
-  , saLocalMethod    = LocalNelderMead     -- back-compat default; switch to
-                                           -- 'LocalLBFGS' for smooth
-                                           -- objectives where every-iter
-                                           -- gradient refinement helps
-                                           -- (Rastrigin etc.).
-  , saAccept         = Boltzmann           -- back-compat default; switch to
-                                           -- 'TsallisAccept (-5)' for
-                                           -- scipy-style dual_annealing
-                                           -- (heavier acceptance tail at
-                                           -- high T → escapes basins).
-  }
-
--- | Draw a single per-dimension proposal increment for the current
--- 'SAProposal' and (sigma, T) state.
---
--- For Tsallis q_v: sample @ξ / |η|^((q_v-1)/(3-q_v))@ where
--- @ξ ~ N(0, T^(1/(q_v-1)))@ and @η ~ N(0, 1)@. This is the
--- Xiang-Gong-Liu-Yan 1997 visiting distribution; the typical jump
--- shrinks as T cools but the heavy tails (~ |η|^-α) keep occasional
--- large jumps possible. q_v = 2 reduces to Cauchy(0, T); q_v → 1
--- approaches Gaussian.
-sampleProposal :: SAProposal -> Double -> Double -> MWC.GenIO -> IO Double
-sampleProposal Gaussian       sigma _ gen = MWCD.normal 0 sigma gen
-sampleProposal Cauchy_        sigma _ gen = do
-  u <- MWC.uniformR (1e-12, 1 - 1e-12 :: Double) gen
-  pure (sigma * tan (pi * (u - 0.5)))
-sampleProposal (Tsallis q) _ temp gen = do
-  let qm   = q - 1
-      qmp  = 3 - q
-      -- T-dependent scale: σ_T = T^(1/(q-1))
-      sigT = max 1e-30 temp ** (1 / qm)
-      -- exponent on |η|
-      expo = qm / qmp
-  xi  <- MWCD.normal 0 sigT gen
-  eta <- MWCD.normal 0 1   gen
-  let etaA = max 1e-300 (abs eta)
-  pure (xi / (etaA ** expo))
-nextTemp :: SACoolingSchedule -> Double -> Int -> Double -> Double
-nextTemp sched t0 iter t = case sched of
-  Geometric alpha -> t * alpha
-  Linear    a     -> max 1e-12 (t - a)
-  LundyMees beta  -> t / (1 + beta * t)
-  Cauchy          -> t0 / (1 + fromIntegral (iter + 1))
-  TsallisCool qv  ->
-    let s = fromIntegral (iter + 2) :: Double
-        e = qv - 1
-    in t0 * (2 ** e - 1) / (s ** e - 1)
-
--- | Run SA with the default configuration built from @bounds@.
-runSA :: [(Double, Double)]
-      -> ([Double] -> Double)
-      -> [Double]                  -- ^ Initial point.
-      -> MWC.GenIO
-      -> IO OptimResult
-runSA bs f x0 gen = runSAWith (defaultSAConfig bs) f x0 gen
-
--- | Run SA with a user-specified configuration.
-runSAWith :: SAConfig
-          -> ([Double] -> Double)
-          -> [Double]
-          -> MWC.GenIO
-          -> IO OptimResult
-runSAWith cfg fUser x0 gen = do
-  let f    = flipFor (saDir cfg) fUser
-      f0   = f x0
-  finalRes <- go 0 0 x0 f0 x0 f0 (saInitTemp cfg) (saStepSigma cfg) [f0]
-  -- Optional final high-precision polish on x_best.
-  if saPolish cfg
-    then do
-      let (xb, fb) = polishNM cfg f (orBest finalRes)
-                       (case saDir cfg of
-                          Minimize -> orValue finalRes
-                          Maximize -> negate (orValue finalRes))
-          vUser = case saDir cfg of
-                    Minimize -> fb
-                    Maximize -> negate fb
-      pure finalRes
-        { orBest  = xb
-        , orValue = vUser
-        }
-    else pure finalRes
-  where
-    f = flipFor (saDir cfg) fUser
-
-    -- Loop carries (iter, sinceImprove). 'sinceImprove' is the number
-    -- of iterations since 'fBest' last decreased, used by the
-    -- 'saRestartIfStuck' option.
-    go iter sinceImprove x fx xBest fBest temp sigma hist
-      | iter >= stMaxIter (saStop cfg) =
-          mkRes (saDir cfg) xBest fBest hist iter False
-      | temp < 1e-12 =
-          mkRes (saDir cfg) xBest fBest hist iter True
-      | otherwise = do
-          -- Random-restart trigger.
-          let stuck = case saRestartIfStuck cfg of
-                Just k | k > 0 && sinceImprove >= k -> True
-                _                                    -> False
-          (xR, fxR, sinceR, sigmaR) <-
-            if stuck
-              then do
-                xNew <- mapM (\(lo, hi) -> MWC.uniformR (lo, hi) gen)
-                             (saBounds cfg)
-                pure (xNew, f xNew, 0, saStepSigma cfg)
-              else pure (x, fx, sinceImprove, sigma)
-
-          xRaw <- forM xR $ \xi -> do
-                    eps <- sampleProposal (saProposal cfg) sigmaR temp gen
-                    pure (xi + eps)
-          let xCand = clipToBounds (saBounds cfg) xRaw
-          let fNew = f xCand
-          u <- MWC.uniformR (0, 1 :: Double) gen
-          let dF = fNew - fxR
-              -- Tsallis acceptance: P_acc = max(0, 1 - (1-q_a)·dF/T)^(1/(1-q_a))
-              -- For q_a → 1, reduces to Boltzmann exp(-dF/T).
-              -- For q_a < 1 (e.g. -5), heavier tail at high T.
-              accept =
-                dF < 0 ||
-                  case saAccept cfg of
-                    Boltzmann ->
-                      u < exp (- dF / temp)
-                    TsallisAccept qa ->
-                      let qm    = 1 - qa
-                          base' = 1 - qm * dF / temp
-                          pAcc
-                            | base' <= 0 = 0
-                            | otherwise  = base' ** (1 / qm)
-                      in u < pAcc
-                    GreedyAccept -> False
-              (xN, fxN)  = if accept then (xCand, fNew) else (xR, fxR)
-              (xBN0, fBN0) = if fxN < fBest then (xN, fxN) else (xBest, fBest)
-              improved   = fBN0 < fBest
-              sinceN     = if improved then 0 else sinceR + 1
-              -- Local refinement on x_best every k iterations (hybrid SA).
-              shouldRefine = case saLocalEvery cfg of
-                Just k | k > 0 && (iter + 1) `mod` k == 0
-                       , iter > 0 -> True
-                _                  -> False
-              (xBN, fBN) =
-                if shouldRefine
-                  then case saLocalMethod cfg of
-                         LocalNelderMead -> refineNM    cfg f xBN0 fBN0
-                         LocalLBFGS      -> refineLBFGS cfg f xBN0 fBN0
-                  else (xBN0, fBN0)
-              tempN  = nextTemp (saSchedule cfg) (saInitTemp cfg) iter temp
-              sigmaN = sigmaR * saStepDecay cfg
-              histN  = fBN : hist
-          go (iter + 1) sinceN xN fxN xBN fBN tempN sigmaN histN
-
--- | Apply a Nelder-Mead refinement at the current best point. Returns
--- the refined @(x, f)@ if it improves on the input, otherwise the
--- input unchanged. Bounded by the SA box (any out-of-range coordinate
--- after refinement is clipped before re-evaluation).
-refineNM :: SAConfig -> ([Double] -> Double) -> [Double] -> Double
-         -> ([Double], Double)
-refineNM cfg f x fx =
-  let r     = unsafePerformIO (NM.runNelderMeadWith
-                (NM.defaultNMConfig
-                   { NM.nmStop = defaultStopCriteria
-                                   { stMaxIter = 200
-                                   , stTolFun  = 1e-10
-                                   , stTolX    = 1e-10 }
-                   , NM.nmInitStep = 0.01
-                   }) f x)
-      xRef  = clipToBounds (saBounds cfg) (orBest r)
-      fRef  = f xRef
-  in if fRef < fx then (xRef, fRef) else (x, fx)
-
--- | L-BFGS-B (numeric gradient) refinement at the current best point.
--- Used when 'saLocalMethod = LocalLBFGS'. Catches numeric exceptions
--- (singular Hessian / Cholesky failures inside f) and falls back to
--- the input unchanged.
-refineLBFGS :: SAConfig -> ([Double] -> Double) -> [Double] -> Double
-            -> ([Double], Double)
-refineLBFGS cfg f x fx = unsafePerformIO $ do
-  let polCfg = LB.defaultLBFGSConfig
-                 { LB.lbStop   = defaultStopCriteria
-                                   { stMaxIter = 50
-                                   , stTolFun  = 1e-12
-                                   , stTolX    = 1e-12 }
-                 , LB.lbBounds = Just (saBounds cfg)
-                 }
-  eR <- try (LB.runLBFGSNumeric polCfg f x) :: IO (Either SomeException OptimResult)
-  case eR of
-    Left _  -> pure (x, fx)
-    Right r ->
-      let xRef = clipToBounds (saBounds cfg) (orBest r)
-      in do
-        evF <- try (evaluate (f xRef)) :: IO (Either SomeException Double)
-        case evF of
-          Right fRef | fRef < fx -> pure (xRef, fRef)
-          _                       -> pure (x, fx)
-
--- | High-precision polish on @x_best@ at SA termination. Uses a much
--- smaller initial simplex and tighter tolerances so that smooth
--- landscapes (Sphere, Levy etc.) reach near-machine precision after
--- the SA + periodic-NM walk has localised the basin.
-polishNM :: SAConfig -> ([Double] -> Double) -> [Double] -> Double
-         -> ([Double], Double)
-polishNM cfg f x fx =
-  let r    = unsafePerformIO (NM.runNelderMeadWith
-               (NM.defaultNMConfig
-                  { NM.nmStop = defaultStopCriteria
-                                  { stMaxIter = 2000
-                                  , stTolFun  = 1e-15
-                                  , stTolX    = 1e-15 }
-                  , NM.nmInitStep = 0.001
-                  }) f x)
-      xRef = clipToBounds (saBounds cfg) (orBest r)
-      fRef = f xRef
-  in if fRef < fx then (xRef, fRef) else (x, fx)
-
-mkRes :: Direction -> [Double] -> Double -> [Double]
-      -> Int -> Bool -> IO OptimResult
-mkRes dir xb fb hist iter conv =
-  let vUser = case dir of { Minimize -> fb; Maximize -> negate fb }
-      hU    = case dir of
-                Minimize -> reverse hist
-                Maximize -> map negate (reverse hist)
-  in pure $ OptimResult xb vUser hU iter conv
diff --git a/src/Hanalyze/Plot.hs b/src/Hanalyze/Plot.hs
deleted file mode 100644
--- a/src/Hanalyze/Plot.hs
+++ /dev/null
@@ -1,1050 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-} 
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE ImpredicativeTypes #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE UndecidableInstances #-}
--- |
--- Module      : Hanalyze.Plot
--- Description : 解析モデルを hgg の VisualSpec へ変換する連携層 (flag plot-integration)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- hgg 連携層 (= 解析モデル → 図 'VisualSpec')。
---
--- ⚠ 本モジュールは cabal flag @plot-integration@ (既定 off) を on にしたときのみ
--- build される。 @hgg-core@ に依存するため **upstream hanalyze には
--- cherry-pick しない** (= 依存方向 analyze→plot-core を flag で隔離。 plot Phase 15
--- / analyze Phase 46 の設計)。 中立 protocol ('Hanalyze.Model.Core' の
--- 'ResidualModel' / 'PredictiveModel') は portable、 こちらは非 portable。
---
--- 系統 A (モデル・アウト型): フィット済みモデルを 'toPlot' で 'VisualSpec' 化し、
--- hgg の layer 文法に @df |>> (layer scatter <> toPlot fit)@ で重畳する
--- ('VisualSpec' は Monoid なので新コンビネータ不要)。
-module Hanalyze.Plot
-  ( Plottable (..)
-    -- * ルート1 grid 評価 (滑らかな回帰曲線・CI 帯) — Phase 16 §3 C1
-  , ModelSpec
-  , SingleVarModel (..)
-  , GridOpts (..)
-  , statModel
-  , grid
-  , gridRange
-  , BandMode (..)
-  , bandMode
-    -- 予測区間の算出法セレクタ (closed-form / bootstrap — Phase 70.H)
-  , PIMethod (..)
-  , piMethod
-  , statColor
-  , statFill
-  , statLinetype
-  , LineType (..)
-  , statLinewidth
-  , statAlpha
-  , statLabel
-  , statEquation
-  , statR2
-  , statLevel
-  , predAt
-    -- * 多変量 effect plot — Phase 16 §3 C3
-  , MultiVarModel (..)
-  , AlongSpec
-  , along
-  , statModelMulti
-  , HoldAgg (..)
-  , holdAt
-  , byVar
-  , MultiLMModel (..)
-  , multiLMModel
-  , multiLMModelF
-  , MultiGLMModel (..)
-  , multiGLMModel
-  , multiGLMModelF
-    -- 多変量ロバスト回帰 (formula 不要・列名リスト — Phase 70.D)
-  , MultiRobustModel (..)
-  , multiRobustModelF
-  , additiveFormula
-    -- PLS effect plot (frame 保持ラッパ + 出力セレクタ — Phase 70.B2/B3)
-  , PLSModel (..)
-  , plsModel
-  , selectOutput
-    -- * 応答曲面 3D 直結 — plot Phase 24 A3
-  , SurfaceOpts (..)
-  , defaultSurfaceOpts
-  , surfaceGrid
-  , surfaceOf
-  , surfaceOfWith
-  , dataScatter3DOf
-  , epredSurfaceOf
-  , epredSurfaceOfWith
-    -- * モデル API 層 (描画と独立: predict / describe / coefficients)
-  , ModelAPI (..)
-  , Coef (..)
-    -- * 統一係数サマリ (t/z・p 値・95% CI — Phase 70.D)
-  , CoefRow (..)
-  , HasCoefSummary (..)
-  , HasCoefBoot (..)
-  , coefSummaryBoot
-    -- * 平滑項単位の近似有意性 (mgcv 流 edf + 近似 F — Phase 72.2)
-  , TermRow (..)
-  , HasTermSummary (..)
-  , termSummary
-    -- * 統一玄関 (.summary() 風 — Phase 72.3)
-  , ModelReport (..)
-  , HasReport (..)
-  , modelReport
-  , showReport
-    -- * 回帰診断の可視化 (係数 forest / 実測vs予測 — Phase 72.4/72.5)
-  , HasObsPred (..)
-  , obsVsPred
-  , obsPredSpec
-  , coefForest
-    -- * 線形モデル (描画可能 = X 同梱)
-  , LMModel (..)
-  , lmModel
-    -- * 一般化線形モデル (描画可能 = X + family/link 同梱)
-  , GLMModel (..)
-  , glmModel
-    -- * ガウス過程 (描画可能 = 予測 grid 同梱の 'GPResult' をそのまま)
-  , GPResult (..)
-    -- * カーネル法ファミリ統合 (GP / KRR / RFF・df |-> gp) — Phase 70.5 項目 E
-  , Kernel (..)
-  , GPParams (..)
-  , defaultGPParams
-  , GPMethod (..)
-  , HyperStrategy (..)
-  , GPConfig (..)
-  , defaultGP
-  , GPSpec
-  , gp
-  , GPRegModel (..)
-  , GPMultiSpec
-  , gpMulti
-  , GPRegModelN (..)
-    -- * 罰則付き回帰 統合 (Ridge/Lasso/EN/MCP/SCAD/Adaptive/Group・df |-> regularized) — Phase 70.7 項目 G
-  , RegMethod (..)
-  , LambdaStrat (..)
-  , RegConfig (..)
-  , defaultRidge
-  , defaultLasso
-  , RegSpec
-  , regularized
-  , regularizedMulti
-  , ridge
-  , ridgeMulti
-  , lasso
-  , lassoMulti
-  , elasticNet
-  , elasticNetMulti
-  , RegModel (..)
-  , regPredict
-    -- * スプライン回帰 (描画可能 = X 同梱、 平滑曲線 + CI band)
-  , SplineModel (..)
-  , splineModel
-    -- * 一般化加法モデル (描画可能 = X 同梱、 平滑曲線のみ・band 非提供)
-  , GAMModel (..)
-  , gamModel
-    -- ** GAM 基底一般化 + GCV (Phase 70.6 F3・df|-> 高レベル)
-  , GAMBasis (..)
-  , GAMLambda (..)
-  , GAMConfig (..)
-  , defaultGAMConfig
-  , GAMSpec (..)
-  , gam
-  , gamMulti
-  , GAMModelN (..)
-  , fitGAMWith
-    -- * ロバスト回帰 (描画可能 = X 同梱、 ロバスト直線・重み diagnostic)
-  , RobustModel (..)
-  , robustModel
-    -- * 多出力線形回帰 (描画可能 = 自己完結の 'MultiFit'、 残差相関 heatmap)
-  , MultiFit (..)
-    -- * 分位点回帰 (描画可能 = X 同梱、 複数分位線を色分け重畳)
-  , QuantileModel (..)
-  , quantileModel
-    -- * MCMC チェーン (描画可能 = trace + 周辺事後密度、 ベイズ出入口)
-  , ChainModel (..)
-  , chainModel
-    -- * 生存解析 (描画可能 = 自己完結、 KM 生存曲線 / 競合リスク CIF)
-  , KMResult (..)
-  , CRFit (..)
-    -- * 時系列予測 (描画可能 = 履歴 + AR 予測 + 予測区間 band)
-  , ForecastModel (..)
-  , forecastModel
-    -- * 多変量・木 (描画可能 = 自己完結、 PCA scree / RF 重要度)
-  , PCAResult (..)
-  , RandomForest (..)
-    -- * 木/アンサンブル — Phase 68 A2 (重要度 bar / 決定木 樹形図)
-    --   GradientBoosting / RandomForestClassifier = 特徴重要度 bar、
-    --   DecisionTree = MDAG 再利用の樹形図 (新規 mark 不要)
-  , GBRegressor (..)
-  , GBClassifier (..)
-  , RFClassifierFit (..)
-  , DTree (..)
-  , DTFit (..)
-  , treeImportances
-  , treePlot
-  , treePlotRaw
-    -- * 分類 — Phase 68 A3 (決定境界 + confusion + 代表散布)
-    --   Discriminant / NaiveBayes / KNN。 決定境界・confusion はヘルパ (要範囲/データ)、
-    --   toPlot は KNN=訓練点散布 / Discriminant・NB=クラス平均散布
-  , ClassPredict (..)
-  , decisionBoundaryOf
-  , confusionOf
-  , MDSView
-  , mdsView
-  , mdsGroupBy
-  , nnLossOf
-  , ResidualMode (..)
-  , ProfilerSpec (..)
-  , profiler
-  , profilerResidual
-  , contourOf
-    -- DOE ワークフロー (Phase 78・Hanalyze.Fit 由来)
-  , Design (..)
-  , DesignFactor (..)
-  , FactorKind (..)
-  , FactorScale (..)
-  , DesignKind (..)
-  , contFactor
-  , contFactorLog
-  , numFactor
-  , catFactor
-  , CustomSpec (..)
-  , customSpec
-  , customDesign
-  , Structure (..)
-  , splitPlot
-  , stripPlot
-  , blocked
-  , Constraint (..)
-  , ConstraintRel (..)
-  , ConstraintGuard (..)
-  , FactorValue (..)
-  , NatConstraint (..)
-  , natLeq
-  , natGeq
-  , natEq
-  , natForbid
-  , formulaToCustomModel
-  , factorialDesign
-  , centralCompositeDesign
-  , boxBehnkenDesign
-  , Resolution (..)
-  , resNum
-  , fractionalDesign
-  , fractionalDesignGen
-  , fractionalDesignInter
-  , fractionalDesignGenInter
-  , fractionalCatalog
-  , fracResolution
-  , aliasStructure
-  , OATable (..)
-  , taguchiDesign
-  , taguchiDesignOA
-  , OptCriterion (..)
-  , optimalDesign
-  , optimalDesignWith
-  , optimalDesignLevels
-  , mainEffects
-  , twoWay
-  , quadratic
-  , designTable
-  , designFrame
-  , designFrameRound
-  , designFactorNames
-  , designFormula
-  , RSMNature (..)
-  , RSMReport (..)
-  , rsmAnalysis
-  , steepestAscentNatural
-  , saveDesign
-  , planFromFrame
-  , DesignModelSpec (..)
-  , designModel
-  , DesignModelGPSpec (..)
-  , designModelGP
-  , ranIntercept
-  , ranSlope
-  , DesignHBMFit (..)
-  , designModelHBM
-  , MultiOutputSpec (..)
-  , multiOutput
-  , modelFor
-  , svmSupportVectorsOf
-  , ScorePredict (..)
-  , decisionLineOf
-    -- 部分従属図 (PDP / ICE) — Phase 75.27
-  , RegPredict (..)
-  , PDPView
-  , pdp
-  , pdpIce
-  , pdpOf
-  , pdpIceOf
-  , pdpPlot
-  , pdpIcePlot
-  , partialDependencePlot
-  , partialDependenceIcePlot
-  , DiscriminantFit (..)
-  , NBModel (..)
-  , GaussianNB (..)
-  , KNNClassifier (..)
-    -- * 次元圧縮 — Phase 68 A4 (PLS score/loading/VIP, MultiGP 多出力 curve)
-  , PLSFit (..)
-    -- ** PLS 診断ビュー (中間 Plottable Spec・HBM 式統一 — Phase 70.B)
-  , PLSView (..)
-  , PLSViewKind (..)
-  , scoreView
-  , loadingView
-  , vipView
-  , MultiGPResult (..)
-  , multiGpCurves
-    -- * 時系列・生存・FDA — Phase 68 A5
-    --   GARCH=volatility 帯付き線 / AFT=生存曲線 / FDA=平均+固有関数 / β(t)
-  , GARCHFit (..)
-  , garchVolatility
-  , AFTFit (..)
-  , aftSurvivalAt
-  , FunctionalPCA (..)
-  , FLMResult (..)
-    -- * 罰則回帰・因果探索 — Phase 68 A6
-    --   Regularized=係数 bar/係数パス / LiNGAM=因果 DAG (MDAG 再利用)
-  , RegFit (..)
-  , regPathPlot
-  , DirectLiNGAMFit (..)
-  , lingamDag
-    -- * 記述統計・検定 — Phase 68 A7 (describe 分布図 / 検定 effect-CI forest)
-  , TestResult (..)
-  , testForest
-  , testForestLabeled
-  , describeBox
-    -- * クラスタリング (Phase 68 A1) — KMeans の図
-    --   'Plottable' 'KMeansResult' (toPlot = centroid 散布) + データ点ヘルパ
-  , clusterScatterOf
-  , centroidsOf
-  , clusterHullOf
-  , clusterEllipseOf
-  , DendroOpts (..)
-  , defaultDendroOpts
-  , dendrogramOf
-  , dendrogramOf'
-    -- * HBM (ベイズ確率プログラム) の学習 — Phase 49 A1
-  , HBMConfig (..)
-  , defaultHBM
-  , HBMModel (..)
-  , hbmModel
-  , hbmModelPure
-  , hbmModelIO
-    -- * HBM の出力抽出子 — Phase 49 A2 / Phase 74 (trace / forest)
-  , hbmParamNames
-  , TraceOpts (..)
-  , defaultTraceOpts
-  , tracesOf
-  , tracesOfWith
-  , marginalsOf
-  , marginalsByChainOf
-    -- * HBM のサンプリング診断 — Phase 59 (divergence 可視化)
-  , divergencesOf
-  , pairOf
-  , energyOf
-  , autocorrOf
-  , autocorrOfLag
-  , defaultAutocorrMaxLag
-  , rankOf
-  , rankOfBins
-  , defaultRankBins
-  , ForestSpec (..)
-  , forestOf
-  , forestOfLevel
-    -- * HBM の出力抽出子 — Phase 49 A3 (epred = 事後予測平均 + HDI band)
-  , epred
-  , epredAt
-    -- * HBM の出力抽出子 — Phase 49 A4 (ppc = 事後予測チェック)
-  , PPCConfig (..)
-  , defaultPPC
-  , PPCSpec (..)
-  , ppcOf
-  , ppcOfWith
-  , ppcOfIO
-  , ppcOfWithIO
-    -- * HBM の出力抽出子 — Phase 49 A5 (dag = モデル構造の DAG)
-  , DagSpec (..)
-  , dagOf
-  , dagOfRaw
-  , dagOfModel
-  , dagOfModelWith
-    -- * HBM 診断ダッシュボード — Phase 74.8 (抽出子束ね)
-  , dashboardOf
-  , dashboardFullOf
-  , traceDensityOf
-    -- * df |-> spec 統一 fit API — Phase 51 (ColumnSource から学習)
-  , Fit (..)
-  , (|->)
-  , (|->!)
-    -- ** 二変量近道 spec (列名2つ) — Phase 51.2
-  , LMSpec (..)
-  , lm
-  , GLMSpec (..)
-  , glm
-  , SplineSpec (..)
-  , spline
-  , RobustSpec (..)
-  , rlm
-  , QuantileSpec (..)
-  , rq
-    -- ** 行列入力モデルの高レベル spec (列名リスト) — Phase 70.A
-  , PCASpec (..)
-  , pca
-    -- MDS (Phase 75.21)
-  , MDSSpec (..)
-  , mds
-  , MDSConfig (..)
-  , MDSMethod (..)
-  , defaultMDS
-  , MDSResult (..)
-  , PCAStandardize (..)
-  , PLSSpec (..)
-  , pls
-  , PLSConfig (..)
-  , defaultPLS
-  , LDASpec (..)
-  , lda
-  , CCASpec (..)
-  , ccaOf
-  , CCAFit (..)
-    -- ** 教師あり ML 分類器/回帰器 spec (特徴列 + ラベル列) — Phase 70.A
-  , GBRSpec (..)
-  , gbmReg
-  , GBCSpec (..)
-  , gbmCls
-  , GBConfig (..)
-  , defaultGBM
-  , DTSpec (..)
-  , decisionTree
-  , DTConfig (..)
-  , defaultDecisionTree
-  , KNNCSpec (..)
-  , knnCls
-  , KNNRSpec (..)
-  , knnReg
-  , NBSpec (..)
-  , naiveBayes
-    -- ** seed 純粋化した RNG モデル spec (KMeans / RandomForest) — Phase 70.A
-  , KMeansSpec (..)
-  , kmeans
-  , KMeansConfig (..)
-  , defaultKMeans
-  , RFSpec (..)
-  , randomForestReg
-    -- 因果探索 LiNGAM (高レベル df|-> ・Phase 77)
-  , DirectLiNGAMSpec (..)
-  , directLingam
-  , ParceLiNGAMSpec (..)
-  , parceLingam
-  , MultiGroupLiNGAMSpec (..)
-  , multiGroupLingam
-  , VARLiNGAMSpec (..)
-  , varLingam
-  , PairwiseLiNGAMSpec (..)
-  , pairwiseLingam
-  , BootstrapLiNGAMSpec (..)
-  , bootstrapLingam
-  , ICALiNGAMSpec (..)
-  , icaLingam
-  , CorrelationSpec (..)
-  , correlationOf
-  , CorrelationGraph (..)
-  , LiNGAMFitted (..)
-  , lingamDagNamed
-  , varLagDagNamed
-  , bootstrapEdgeProbOf
-  , RFCSpec (..)
-  , randomForestCls
-  , RFCConfig (..)
-  , defaultRFCConfig
-  , RFConfig (..)
-  , defaultRandomForest
-    -- ** SVM / 古典 MLP 高レベル spec (純粋・df |->) — Phase 75.9
-  , MLPClsSpec (..)
-  , mlpCls
-  , MLPRegSpec (..)
-  , mlpReg
-  , SVMSpec (..)
-  , svmCls
-  , SVMHyper (..)
-  , SVMTuneGrid (..)
-  , defaultSVMTuneGrid
-  , SVMConfig (..)
-  , defaultSVM
-  , SVM (..)
-  , SVMMulti (..)
-  , numSupportVectors
-    -- ** 重み付き最小二乗 (WLS) spec — Phase 52.A6
-  , WeightedLMSpec (..)
-  , weighted
-  , WeightedLMModel (..)
-    -- ** 透過標準化ラッパ (自動逆変換) — Phase 70.3 項目 C
-  , StandardizedSpec (..)
-  , standardized
-  , standardizedY
-  , StandardizedModel (..)
-    -- ** 群別フィット spec — Phase 52.A4
-  , GroupedSpec (..)
-  , grouped
-  , GroupedFit (..)
-  , groupModels
-  , groupLabels
-  , groupedFullrange
-    -- ** 係数診断の薄アクセサ — Phase 52.A9
-  , CoefStats (..)
-  , lmDiag
-  , groupedLmDiag
-    -- ** formula 多変量 spec (R 流) — Phase 51.3
-  , LMFormulaSpec (..)
-  , lmF
-  , GLMFormulaSpec (..)
-  , glmF
-  , GLMMFormulaSpec (..)
-  , glmmF
-    -- ** 重回帰 spec (列名リスト・formula 不要) — Phase 70.D
-  , LMMultiSpec (..)
-  , lmMulti
-  , GLMMultiSpec (..)
-  , glmMulti
-  , RobustMultiSpec (..)
-  , rlmMulti
-  , QuantileMultiSpec (..)
-  , rqMulti
-  , MultiQuantileModel (..)
-    -- ** HBM spec + データ散布図 — Phase 51.4
-  , HBMSpec
-  , hbm
-  , dataScatterOf
-  ) where
-
-import qualified Data.Map.Strict       as Map
-import           Data.Maybe            (fromMaybe)
-import qualified Data.Vector           as V
-import qualified Data.Vector.Unboxed    as VU
-import qualified Numeric.LinearAlgebra as LA
-
-import           Data.Text             (Text)
-import qualified Data.Text             as T
--- (DataFrame の直接 import は未使用のため削除 = upstream decomp PR#2 移植の副産物調査で判明)
-
-import           Hanalyze.Data.ColumnSource     (ColumnSource (..))
-
-import           Hgg.Plot.Spec     ( VisualSpec, layer, inline, inlineCat
-                                       , ColData (..)
-                                       , scatter, line
-                                       , heatmap, colorBy
-                                       , scaleColorManual, legend
-                                       , bar, title
-                                       , LineType (..) )
-import qualified Hgg.Plot.ThreeD.Spec  as P3
-
-import           Hanalyze.Model.Wrappers
-import           Hanalyze.Plot.Core
--- 族別 instance module (Phase 71.5)。 orphan instance を scope に取り込み、
--- 移した族固有 helper (multiGpCurves) を re-export する。
-import           Hanalyze.Plot.Linear ()
-import           Hanalyze.Plot.Smooth (multiGpCurves)
-import           Hanalyze.Plot.Robust ()
--- ベイズ / HBM 連携族 (Phase 71.6)。 orphan instance を scope に取り込み (())、
--- 移した抽出子・型を re-export する。 epredPredRange は本 module の
--- epredSurfaceOfWith でも使うため明示 import する。
-import           Hanalyze.Plot.Bayes ()
-import           Hanalyze.Plot.Bayes
-                   ( hbmParamNames, TraceOpts (..), defaultTraceOpts
-                   , tracesOf, tracesOfWith, marginalsOf
-                   , marginalsByChainOf, divergencesOf
-                   , pairOf, energyOf, autocorrOf, autocorrOfLag, defaultAutocorrMaxLag
-                   , rankOf, rankOfBins, defaultRankBins
-                   , ForestSpec (..), forestOf, forestOfLevel
-                   , epred, epredAt, epredPredRange
-                   , PPCConfig (..), defaultPPC, PPCSpec (..)
-                   , ppcOf, ppcOfWith, ppcOfIO, ppcOfWithIO
-                   , DagSpec (..), dagOf, dagOfRaw, dagOfModel, dagOfModelWith
-                   , dashboardOf, dashboardFullOf, traceDensityOf
-                   , epredSurfaceOf, epredSurfaceOfWith, dataScatterOf )
--- 汎用ラッパ族 (Phase 71.7)。 orphan instance を scope に取り込み (())、
--- 移したヘルパ (lmDiag / groupedLmDiag / groupedFullrange) を re-export する。
-import           Hanalyze.Plot.Wrappers ()
-import           Hanalyze.Plot.Wrappers
-                   ( lmDiag, groupedLmDiag, groupedFullrange )
--- ML / 統計モデル連携族 (Phase 71.6)。 orphan instance を scope に取り込み (())、
--- 移した抽出子・ヘルパ・型を re-export する。
-import           Hanalyze.Plot.ML ()
-import           Hanalyze.Plot.ML
-                   ( clusterScatterOf, centroidsOf, clusterHullOf, clusterEllipseOf
-                   , DendroOpts (..), defaultDendroOpts, dendrogramOf, dendrogramOf'
-                   , treeImportances, treePlot, treePlotRaw
-                   , decisionBoundaryOf, confusionOf, MDSView, mdsView, mdsGroupBy, nnLossOf, svmSupportVectorsOf, ScorePredict (..), decisionLineOf
-                   , RegPredict (..), PDPView, pdp, pdpIce
-                   , pdpOf, pdpIceOf, pdpPlot, pdpIcePlot, partialDependencePlot, partialDependenceIcePlot
-                   , PLSView (..), PLSViewKind (..), scoreView, loadingView, vipView
-                   , garchVolatility, aftSurvivalAt
-                   , regPathPlot, lingamDag, lingamDagNamed, varLagDagNamed, bootstrapEdgeProbOf
-                   , ResidualMode (..), ProfilerSpec (..), profiler, profilerResidual, contourOf
-                   , testForest, testForestLabeled, describeBox )
-import           Hanalyze.Diagnostics
-import           Hanalyze.Fit
-import           Hanalyze.Model.SVM (SVMConfig (..)
-                                       , defaultSVM, SVM (..)
-                                       , SVMMulti (..), numSupportVectors
-                                       , SVMHyper (..)
-                                       , SVMTuneGrid (..), defaultSVMTuneGrid)
-import           Hanalyze.Model.MDS (MDSResult (..))
-import           Hanalyze.Model.LM.Diagnostics (CoefStats (..), lmCoefStats)
-import           Hanalyze.Model.GP     (GPResult (..), Kernel (..), GPParams (..), defaultGPParams)
-import           Hanalyze.Model.LM     (linspace)
-import           Hanalyze.Model.GAM    (GAMBasis (..), GAMLambda (..)
-                                              , fitGAMWith)
-import           Hanalyze.Model.MultiLM (MultiFit (..))
-import           Hanalyze.Model.Cluster (KMeansConfig (..), defaultKMeans)
-import           Hanalyze.MCMC.Core     (Chain (..))
-import           Hanalyze.Model.HBM     (ModelP, withData
-                                       , runDeterministics)
-import           Hanalyze.Model.Survival (KMResult (..))
-import           Hanalyze.Model.CompetingRisks (CRFit (..))
-import           Hanalyze.Model.PCA     (PCAResult (..), PCAStandardize (..))
-import           Hanalyze.Stat.Standardize
-                   ( Standardizer (..)
-                   , applyStandardizerCol )
-import           Hanalyze.Model.RandomForest (RandomForest (..)
-                                       , RFConfig (..), defaultRandomForest)
-import           Hanalyze.Model.GradientBoosting (GBRegressor (..), GBClassifier (..)
-                                       , GBConfig (..), defaultGBM)
-import           Hanalyze.Model.RandomForestClassifier (RFClassifierFit (..)
-                                       , RFCConfig (..), defaultRFCConfig)
-import           Hanalyze.Model.DecisionTree (DTree (..), DTFit (..), DTConfig (..), defaultDecisionTree)
-import           Hanalyze.Model.Discriminant (DiscriminantFit (..))
-import           Hanalyze.Model.Multivariate (CCAFit (..))
-import           Hanalyze.Model.NaiveBayes (NBModel (..), GaussianNB (..))
-import           Hanalyze.Model.KNN (KNNClassifier (..)
-                                       , KNNRegressor (..), predictKNNR)
-import           Hanalyze.Model.PLS (PLSFit (..), PLSConfig (..), defaultPLS)
-import           Hanalyze.Model.MultiGP (MultiGPResult (..))
-import           Hanalyze.Model.GARCH (GARCHFit (..))
-import           Hanalyze.Model.AFT (AFTFit (..))
-import           Hanalyze.Model.FDA (FunctionalPCA (..), FLMResult (..))
-import           Hanalyze.Model.Regularized (RegFit (..))
-import           Hanalyze.Model.LiNGAM.Direct (DirectLiNGAMFit (..))
-import           Hanalyze.Stat.Test (TestResult (..))
-
--- ===========================================================================
--- 共通基盤 (class / ModelSpec / grid 評価核) は 'Hanalyze.Plot.Core' へ
--- 切り出した (Phase 71.4)。 本モジュールは Core を import して従来 export を
--- re-export しつつ、 各モデル族固有の instance を残置する。
--- ===========================================================================
-
--- ===========================================================================
--- ルート1 grid 評価 (ModelSpec) — Phase 16 §3 C1 [→ Plot.Core へ移動]
---
--- fit 済モデルの回帰曲線・CI 帯を **訓練点ではなく等間隔 grid** で評価して描く。
--- 疎・不均一データで曲線がガタつくのを解消する (散布図の点は従来通り訓練データ)。
--- 'statModel' で 'ModelSpec' を作り、 @<>@ でオプションを足す:
---
--- > df |>> (layer (scatter "x" "y") <> toPlot (statModel m <> grid 200))
---
--- 'ModelSpec' は Monoid。 学習済モデル @m@ はクロージャに閉じ込め、 予測は
--- 'toPlot' (描画時) に grid 評価する (ユーザ直感「m は学習・layer で予測」)。
--- ===========================================================================
-
-
--- ===========================================================================
--- 多変量 effect plot (Phase 16 §3 C3)
---
--- 単変数 grid 評価 (C1) を多変量モデルへ一般化する。 along 変数を grid で動かし、
--- 他の説明変数を 'HoldAgg' で固定した「評価点 ModelFrame」 を合成して、 訓練 formula の
--- 'designMatrixF' で評価点設計行列を組み CI を評価する。
---
--- ★評価点 ModelFrame の合成は **DataFrame を経由せず VarRole を直接差し替える**
--- ('designMatrixF' は 'mfRoles' のみ参照し応答列は使わない = Design.hs:331)。 列構造・
--- 順序が訓練と完全一致するので 'confidenceBandAt' / 'predictGlmMuWithCI' がそのまま使える。
--- 型で単/多変量を分離し ('SingleVarModel' / 'MultiVarModel')、 along 忘れをコンパイル時に弾く。
--- ===========================================================================
-
-
-
--- ===========================================================================
--- 多変量モデル型 (effect plot 用、 新規 fit)
---
--- 既存の単変数 'LMModel' / 'GLMModel' (設計行列が @[1, x]@ 固定) とは別型。
--- formula 文字列 + 'DataFrame' で多変量 fit し、 formula を保持して評価点設計行列を
--- 組む (HoldAgg 固定 + along grid)。 ★GLM は formula 経路が未整備なので
--- 'designMatrixF' で設計行列を作り 'fitGLMFull' を直接呼ぶ。
--- ===========================================================================
-
--- (instance MultiVarModel MultiLMModel は Hanalyze.Plot.Linear へ移動 — Phase 71.5)
-
--- ===========================================================================
--- 列名リスト → 加法線形 Formula AST (パース無し直接合成) — Phase 70.D
---
--- 重回帰 (multiple regression) は formula DSL とは別概念: 説明変数の列名リストから
--- 設計行列 @[1, x1, …, xp]@ を作るだけ。 これを文字列を介さず 'Formula' AST に直接
--- 組み立て、 既存の 'multiLMModelF' / 'designMatrixF' / effect plot 機構をそのまま使う
--- (= @parseModel "y ~ x1 + … + xp"@ と同一 AST。 パラメータ名 @_p0.._pp@ も同じ規約)。
--- ===========================================================================
-
--- ===========================================================================
--- 多変量ロバスト回帰 (effect plot + 係数サマリ) — Phase 70.D
---
--- ロバスト回帰は formula 経路を持たない (単回帰 'RobustModel' のみだった) ので、
--- 'MultiLMModel' と同型の frame-carrying ラッパを新設する。 設計行列は
--- 'additiveFormula' 由来 ('designMatrixF' で @[1, x1,…,xp]@)、 fit は 'fitRobustLM'、
--- CI 帯は M 推定量サンドイッチ共分散 ('robustCovBeta'・statsmodels RLM 一致)。
--- ===========================================================================
-
--- (instance MultiVarModel MultiRobustModel は Hanalyze.Plot.Robust へ移動 — Phase 71.5)
-
--- (instance MultiVarModel MultiGLMModel は Hanalyze.Plot.Linear へ移動 — Phase 71.5)
-
--- (instance MultiVarModel PLSModel は Hanalyze.Plot.ML へ移動 — Phase 71.6)
-
--- ===========================================================================
--- 線形モデル (描画可能)
---
--- 'FitResult' (数値核) は設計行列 X を保持しないが、 回帰線・CI band を描くには
--- X が要る ('confidenceBand' は X 引数)。 そこで X と生 predictor を束ねた
--- 「描画可能なモデル」 を別型にする (= plot Phase 15 §2.1 の開放論点を (i) で確定)。
--- ===========================================================================
-
-
--- (instance Plottable LMModel / SingleVarModel LMModel は
---  Hanalyze.Plot.Linear へ移動 — Phase 71.5)
-
--- ===========================================================================
--- 一般化線形モデル (描画可能)
---
--- GLM の不確実性帯は **μ (応答) スケールで非対称** (線形予測子 η の対称 Wald CI を
--- 逆リンク gInv で μ に写すため、 Logit/Log 等では下側・上側の半幅が異なる)。 ゆえに
--- LMModel/GPResult の対称 band (ŷ±se) では忠実に描けない。 そこで
--- 下境界 lo / 上境界 hi を別々に持てる 'band' layer (= MBand area fill) を使い、 μ 曲線は
--- 'line' で重ねる。 帯は **訓練点での Wald CI** を 'predictGlmMuWithCI' で評価する
--- (= grid 補間でなく fit と整合)。 'fitGLMFull' が返す逆 Fisher 情報 Σ=(XᵀWX)⁻¹ が要る。
--- ===========================================================================
-
--- (instance Plottable GLMModel / SingleVarModel GLMModel は
---  Hanalyze.Plot.Linear へ移動 — Phase 71.5)
-
--- ===========================================================================
--- ガウス過程 (描画可能)
---
--- 'GPResult' (Hanalyze.Model.GP) は予測 grid (gpTestX) + 事後平均 (gpMean) +
--- credible band (gpLower/gpUpper) を **自己完結** で保持する。 ゆえに LMModel の
--- ように X を別途束ねる必要がなく、 結果型をそのまま 'Plottable' にできる
--- (= 'FitResult' 系と異なる形でも protocol が成り立つことの実証 = plot Phase 15
--- / analyze Phase 46 A6)。
--- ===========================================================================
-
--- (instance Plottable GPResult は Hanalyze.Plot.Smooth へ移動 — Phase 71.5)
-
--- ===========================================================================
--- スプライン回帰 (描画可能)
---
--- 'SplineFit' (Hanalyze.Model.Spline) は基底係数 'sfBeta' と、 基底行列で fit した
--- 線形モデル核 'sfResult' (= 'FitResult') を保持する。 ゆえに **基底行列を設計行列と
--- みなせば** LMModel と同じ 'confidenceBand' (= X (XᵀX)⁻¹ Xᵀ の対角) がそのまま使える。
--- 違いは「曲線」 である点だけ: 単回帰の直線でなく、 訓練点を x 昇順に結ぶと基底展開に
--- よる平滑曲線になる ('renderRegression' は encX/encY を線形再フィットせず折れ線で
--- 結ぶため、 ソート済みの点列を渡せば曲線がそのまま描ける = GP と同じ性質)。 帯は
--- LM と同じ **線形モデルの対称 Wald CI** (基底空間での予測分散) なので意味付けも明快。
--- ===========================================================================
-
--- (splineBasisAt / instance Plottable SplineModel / SingleVarModel SplineModel /
---  Plottable GAMModel / gamGridCI / SingleVarModel GAMModel / SingleVarModel GAMModelN /
---  Plottable GAMModelN は Hanalyze.Plot.Smooth へ移動 — Phase 71.5)
-
--- ===========================================================================
--- ロバスト回帰 (描画可能)
---
--- 'RobustFit' (Hanalyze.Model.Robust) は M-estimator IRLS の係数 'rfCoef' / fitted
--- 'rfFitted' / 最終重み 'rfWeights' (≤ 1、 外れ値ほど小) を持つが、 **CI / 予測帯を
--- 返す helper を持たない** (sandwich 分散等を別途計算すれば帯は出せるが本 Phase 対象外)。
--- ゆえに代表図 ('toPlot') は **ロバスト直線のみ** (band 無し)。 ロバスト回帰の価値=
--- 「どの点がダウンウェイトされたか」 は 'diagnosticPlots' 側で **点サイズ = IRLS 重み**
--- の散布図に encode して見せる (主図に点を描くと合成 @df |>> layer scatter <> toPlot@
--- で点が二重になるため、 主図は直線だけにして重み表示は診断束へ回す = user 決定 2026-06-04)。
--- ===========================================================================
-
--- (instance Plottable RobustModel / robustBand / SingleVarModel RobustModel は
---  Hanalyze.Plot.Robust へ移動 — Phase 71.5)
-
--- ===========================================================================
--- 多出力線形回帰 (描画可能)
---
--- 'MultiFit' (Hanalyze.Model.MultiLM) は q 個の応答を共通の予測子で同時回帰し、
--- 固有の成果物として **出力間の残差相関 'mfResidCor' (q×q)** を保持する。 q 本の回帰
--- 関係を単一図に素直に載せる方法は一意でない (出力ごとスケールが異なり得る) ため、
--- 代表図 ('toPlot') は **残差相関 heatmap** とする (= 多出力回帰固有の図。 user 決定
--- 2026-06-04)。 'MultiFit' は heatmap に必要な相関行列を自己完結で持つので、 'GPResult'
--- 同様 X を別途束ねず結果型をそのまま 'Plottable' にできる。 個別の出力 j の回帰線は
--- 'predictMultiLM' で別途描ける (本 instance の対象外)。
---
--- ⚠ 'heatmap' (geom_tile) は **categorical 軸専用** (renderHeatmap が x/y をラベルとして
--- カテゴリ軸の index に引く。 実測: Render/Statistical.hs)。 ゆえに格子座標は数値でなく
--- **出力名ラベル** ("y1", "y2", …) を 'inlineCat' で渡す (数値だとカテゴリ軸が立たず
--- 全セルが drop されてタイルが描かれない = 計測で確認)。
--- ===========================================================================
-
--- (instance Plottable MultiFit は Hanalyze.Plot.Wrappers へ移動 — Phase 71.7)
-
--- ===========================================================================
--- 分位点回帰 (描画可能)
---
--- 'QRFit' (Hanalyze.Model.Quantile) は 1 つの分位 τ に対する係数 + fitted 'qfYHat' を
--- 持つ。 OLS が条件付き平均を引くのに対し分位回帰は条件付き τ-分位を引くので、 複数の
--- τ (例 0.1/0.5/0.9) の fit を重ねると **予測区間そのものを線群で** 表現できる
--- (= heteroscedastic データで帯より直接的)。 ゆえに 'QuantileModel' は複数の τ-fit を
--- 束ね、 'toPlot' で **分位ごとに 1 本の line layer を色分けして重畳** する (band は使わ
--- ない。 分位線自体が区間の縁を成すため)。 各線は 'color' ('fromHex') で固定色を割り当てる。
--- ===========================================================================
-
--- (instance Plottable QuantileModel / Plottable MultiQuantileModel は
---  Hanalyze.Plot.Robust へ移動 — Phase 71.5)
-
-
--- ===========================================================================
--- クラスタリング (KMeans) の図 — Phase 68 A1
---
--- KMeans の分野定番の図は「クラスタ別散布 (色=ラベル)」。 ただし
--- 'KMeansResult' は centroids + labels + inertia のみ保持し **生データ座標を
--- 持たない**。 そこで 'surfaceOf' <> 'dataScatter3DOf' と同じ **model 層 / data
--- 層の二層イディオム**に分ける:
---
---   * 'Plottable' 'KMeansResult' の 'toPlot' = centroid 散布のみ (データ不要・
---     クラス契約 @m -> VisualSpec@ を満たす)。 既定は centroid 行列の第 0/1 次元。
---   * 'clusterScatterOf' = データ点をラベル色で散布 (要データ源・列名指定)。
---   * 'centroidsOf' = centroid を任意 2 次元で重畳 (✚ マーカー・次元 index 明示)。
---
--- 定番図 = @df |>> (clusterScatterOf df res \"x\" \"y\" <> centroidsOf res 0 1)@。
--- ⚠ centroid 行列は **学習時の特徴量列順**のみで列名を持たない。 重畳時は
--- データ列 (@xn@, @yn@) と centroid 次元 (@i@, @j@) の対応をユーザが揃える。
--- ===========================================================================
-
--- (instance Plottable KMeansResult / clusterScatterOf / centroidsOf は
---  Hanalyze.Plot.ML へ移動 — Phase 71.6)
-
--- ===========================================================================
--- HBM (ベイズ確率プログラム) の学習 — Phase 49 A1
---
--- 'Hanalyze.Model.HBM' の free-monad DSL で書いた確率プログラム ('ModelP') を
--- NUTS で学習し、 「学習済 HBM モデル」 ('HBMModel') という第一級の値にする。
--- 命名は頻度論側の @lmModel → LMModel@ / @glmModel → GLMModel@ と対称的
--- ('hbmModel → HBMModel')。 違いは学習が MCMC ゆえ IO・重い・async 並列
--- (既存 'nutsChains' が 'mapConcurrently' で multi-chain 並列) という点のみ。
---
--- データは df 由来の列 (名前付き) を 'withData' でモデル中の placeholder
--- ('dataNamed' / observe の参照名) に **自動 bind** する。 これは PyMC の
--- @pm.Data@ + @set_data@ と同型 (= 同じモデルを別データで再評価できる設計)。
---
--- ★ 'HBMModel' は **直接 'Plottable' にしない** (確率プログラムは「単一の図」 に
--- 一意に落ちない)。 描画は抽出子 ('epred' / 'tracesOf' / 'ppcOf' / 'forestOf' /
--- 'dagOf'、 後続 sub で追加) を明示する設計 (Phase 49 計画 Q1)。
--- ===========================================================================
-
-
--- ===========================================================================
--- 生存解析 (描画可能)
---
--- 生存関数 Ŝ(t) (Kaplan-Meier) と累積発生関数 CIF (競合リスク) はいずれも **階段関数**
--- (イベント時刻で不連続にジャンプ、 その間は平坦)。 折れ線 ('line') は点間を線形に結ぶので、
--- そのまま渡すとジャンプが斜めになる。 ゆえに **階段頂点を明示展開** する helper
--- 'stepVerts' で (0, s0) から各イベント時刻の「水平→垂直」 2 頂点を作り、 line で結ぶ
--- (= 正しい階段形)。 KM は s0=1 で下降、 CIF は s0=0 で上昇。 KMResult / CRFit は時刻と
--- 値を自己完結で持つので 'GPResult' 同様そのまま 'Plottable' にできる。
--- ===========================================================================
-
-
--- (instance Plottable KMResult / Plottable CRFit は
---  Hanalyze.Plot.ML へ移動 — Phase 71.6)
-
--- ===========================================================================
--- 時系列予測 (描画可能)
---
--- AR(p) の点予測 'forecastAR' は将来値の中心のみを返す。 予測の不確実性帯は **h-step
--- 予測分散** から得る: AR の MA(∞) 表現の ψ-weights (ψ₀=1, ψⱼ=Σφᵢψⱼ₋ᵢ) を用いて
--- @Var(ŷ_{n+k}) = σ² Σ_{j=0}^{k-1} ψⱼ²@ (σ² = 革新分散 'arResidVar')。 これは Gaussian
--- 革新の下での正統な予測区間 (地平 k とともに単調に広がる)。 対称ゆえ band は
--- @中心 ± z·se@。 'toPlot' は履歴折れ線 + 予測折れ線 + 予測区間 band を 1 枚に重ねる。
--- ===========================================================================
-
--- (arPsiWeights / arForecastSE / instance Plottable ForecastModel は
---  Hanalyze.Plot.ML へ移動 — Phase 71.6)
-
--- ===========================================================================
--- 多変量・木 (描画可能)
---
--- PCA の代表図は **scree plot** (各主成分の寄与率 'pcaExplainedRatio' を棒で)、 木 (RF) の
--- 代表図は **特徴重要度バー** ('featureImportance')。 いずれも自己完結ゆえそのまま
--- 'Plottable'。 棒の x 軸はラベル ("PC1".. / "f1"..) なので 'inlineCat' (categorical) で渡す
--- (heatmap A9 と同じく 'bar' も categorical 軸が必要)。 優先低 (§3.5 A14) ゆえ scree/重要度
--- の 1 枚ずつに絞る (biplot や木構造図は将来拡張)。
--- ===========================================================================
-
--- (instance Plottable PCAResult / Plottable RandomForest は
---  Hanalyze.Plot.ML へ移動 — Phase 71.6)
-
--- ===========================================================================
--- 木/アンサンブル — Phase 68 A2
---
--- 各モデルの分野定番図を **既存 mark のみ**で描く (新規 plot mark 不要):
---
---   * GradientBoosting (回帰/分類)・RandomForestClassifier = **特徴重要度 bar**。
---     GBM は重要度フィールドを持たないので弱学習器 ('Tree') の split 使用回数から
---     純粋計算する ('treeImportances'・RF.'featureImportance' と同方式・正規化)。
---   * DecisionTree = **樹形図**。 決定木は DAG の特殊形 (二分木) ゆえ、 HBM の
---     ModelGraph と同じ MDAG (Sugiyama 階層 layout) を **再利用**して node-link で描く
---     (split ノード = "f{j} ≤ {thr}"、 葉 = "y={class}")。
---
--- ⚠ DecisionTree の edge True/False ラベル・gini・サンプル数表示 (sklearn plot_tree
--- 相当) は DAGNode/DAGEdge が持たないため v1 では描かない。 必要なら専用 mark を
--- plot 側 Phase として起こす (= dendrogram Phase 48 と同型の判断)。
--- ===========================================================================
-
--- (treeImportances / instance Plottable GBRegressor / GBClassifier /
---  RFClassifierFit / DTree / dtreeToDag は Hanalyze.Plot.ML へ移動 — Phase 71.6)
-
--- ===========================================================================
--- 分類 (Discriminant / NaiveBayes / KNN) — Phase 68 A3
---
--- 代表図は **決定境界** と **confusion 行列**。 いずれも「学習済モデルを評価点で
--- 走らせる」 図ゆえ、 KMeans (A1) と同じく **データ/範囲を取るヘルパ**で提供する
--- (新規 plot mark 不要):
---
---   * 'decisionBoundaryOf' = 2D grid を予測しクラス色で塗る (= 連続軸の散布を
---     四角マーカー・低 alpha で「領域」表現。 ★renderHeatmap はカテゴリ軸なので
---     連続 grid には不適 → 'MScatter' + 'colorBy' (離散色) を採用)。 2 特徴前提。
---   * 'confusionOf' = テストデータの真値×予測の件数を 'MHeatmap' で (カテゴリ軸が適合)。
---
--- 'Plottable' の 'toPlot' (データ非保持で描ける代表 1 枚):
---   * KNN は訓練データ ('knnCX'/'knnCY') を保持 → **ラベル色の訓練点散布**。
---   * Discriminant / NaiveBayes(Gaussian) は **クラス平均散布** (✚)、
---     NaiveBayes(Multinomial) は **クラス事前確率 bar**。
--- ===========================================================================
-
-
--- (instance ClassPredict DiscriminantFit / NBModel / KNNClassifier /
---  decisionBoundaryOf / confusionOf / instance Plottable KNNClassifier /
---  DiscriminantFit / NBModel は Hanalyze.Plot.ML へ移動 — Phase 71.6)
-
--- ===========================================================================
--- 次元圧縮 (PLS / MultiGP) — Phase 68 A4
---
--- どちらも結果が自己完結 ('PCAResult' 同様) なので外部データ不要で 'Plottable':
---
---   * 'PLSFit' = 潜在空間の **score plot** (標本 T) を代表図に、 'loading plot' (変数 P)
---     と **VIP bar** を診断図束に。 いずれも既存 'MScatter'/'bar'。
---   * 'MultiGPResult' = **多出力の予測曲線 + 95% band** (出力ごとに色分け・x=index)。
---     'MLine' + 'MBand' を出力数ぶん重畳。
---
--- ※ 'Hanalyze.Model.MultiOutput' は変換+メトリクスの **ユーティリティ**で
--- fit 結果型を持たないため 'Plottable' 対象外 (多出力の「相関」図は既存
--- 'MultiFit' = 残差相関 heatmap が担当)。 新規 plot mark は不要。
--- ===========================================================================
-
-
--- (PLSViewKind / PLSView / scoreView / loadingView / vipView /
---  instance Plottable PLSView / PLSFit は Hanalyze.Plot.ML へ移動 — Phase 71.6)
-
--- (multiGpCurves / instance Plottable MultiGPResult は
---  Hanalyze.Plot.Smooth へ移動 — Phase 71.5)
-
--- ===========================================================================
--- 時系列・生存・FDA (GARCH / AFT / FDA) — Phase 68 A5
---
--- 新規 plot mark は不要 (既存 line/band の重畳):
---
---   * 'GARCHFit'      = 系列 (μ + ε_t) + 条件付き volatility 帯 (μ ± 2σ_t) の帯付き線。
---   * 'AFTFit'        = パラメトリック生存曲線 S(t|x)。 fit は観測時刻を持たないので
---                       代表図 ('toPlot') は **基準共変量** (intercept のみ) の曲線、
---                       任意共変量は 'aftSurvivalAt' ヘルパ。 t 範囲は予測平均寿命から導出。
---   * 'FunctionalPCA' = 平均関数 + 上位固有関数を grid 上に重畳 (x = grid index)。
---   * 'FLMResult'     = 関数回帰係数 β(t) の曲線。
--- ===========================================================================
-
--- (garchVolatility / instance Plottable GARCHFit / aftSurvivalAt /
---  instance Plottable AFTFit / FunctionalPCA / FLMResult は
---  Hanalyze.Plot.ML へ移動 — Phase 71.6)
-
--- ===========================================================================
--- 罰則回帰・因果探索 (Regularized / LiNGAM) — Phase 68 A6
---
--- 新規 plot mark は不要:
---
---   * 'RegFit'          = 単一 λ の係数 ('rfBeta') を bar (代表図)。
---   * 'regPathPlot'     = 正則化パス @[(λ, [β_j])]@ ('regularizationPath' 出力) を、
---                         係数ごとに 1 本の line で λ-横軸に重畳 (= LASSO 係数パス図)。
---   * 'DirectLiNGAMFit' = 推定した因果構造を **MDAG** で描く (B 行列 → node/edge、
---                         決定木と同じ MDAG 再利用)。 edge j→i は @|adjacency[i,j]|>0@。
--- ===========================================================================
-
--- (instance Plottable RegFit / regPathPlot / lingamDag /
---  instance Plottable DirectLiNGAMFit は Hanalyze.Plot.ML へ移動 — Phase 71.6)
-
--- ===========================================================================
--- 記述統計・検定 (Stat.*) — Phase 68 A7
---
--- 新規 plot mark は不要:
---
---   * 'TestResult'  = 効果量 + 95% CI の **forest** (検定パラメータの区間 + 0 基準線)。
---                     代表図 ('toPlot') は 1 行 forest、 複数検定は 'testForest'。
---   * 'describeBox' = 生データ列の **box plot** (= describe の分布図・5 数要約を可視化)。
--- ===========================================================================
-
--- (testForest / testForestLabeled / instance Plottable TestResult /
---  describeBox は Hanalyze.Plot.ML へ移動 — Phase 71.6)
-
-
--- (instance SingleVarModel WeightedLMModel / Plottable WeightedLMModel は
---  Hanalyze.Plot.Linear へ移動 — Phase 71.5)
-
-
-
--- C2: 元スケール逆変換 instance (Phase 70.3 項目 C) -------------------------
---
--- 内側モデルは標準化空間で学習されている。 ここで予測子 x を入力時に標準化し、
--- ('standardizedY' なら) 応答 y を出力時に逆変換することで、 図・予測を**元スケール**で
--- 返す。 単変量 (1 特徴) 描画が対象 (smXStd の 0 次元を使う)。
-
--- (instance SingleVarModel KNNRegressor / stMu1 / stSd1 / unstdY /
---  SingleVarModel (StandardizedModel m) / Plottable (StandardizedModel m) は
---  Hanalyze.Plot.Wrappers へ移動 — Phase 71.7)
-
--- ===========================================================================
--- 混合効果モデル (random effects) — Phase 52 D3
---
--- 'GLMMResultRE' (Phase 48 の vector random effects: random intercept + slope)
--- を caterpillar plot で描く。 各 group の BLUP @b̂_j@ を **値で昇順ソート**し、
--- forest mark (水平棒) で並べる。 0 (= 固定効果からの偏差ゼロ) に参照線を引く。
--- group 間の random effect のばらつき・外れ群を一目で読めるのが GLMM 固有の定番図。
---
--- ★ CI 帯は現状なし (点のみ): 'GLMMResultRE' は per-group の conditional variance
--- も観測数 @n_j@ も格納しておらず (scalar 専用の 'glmmBLUPSE' は 'GLMMResult' 用で
--- 流用不可)、 BLUP の標準誤差を単体から計算できない。 将来 conditional variance を
--- 持たせれば forest の誤差半幅を埋めて帯化できる (forest mark は対称 CI 対応済)。
---
--- 'toPlot'          = random-effect 第 1 列 (通常 intercept) の caterpillar 1 枚。
--- 'diagnosticPlots' = 全 r 列 (intercept + 各 slope) の caterpillar list。
--- ===========================================================================
-
-
-
--- (instance SingleVarModel GPRegModel / Plottable GPRegModel /
---  SingleVarModel GPRegModelN / Plottable GPRegModelN は
---  Hanalyze.Plot.Smooth へ移動 — Phase 71.5)
-
-
--- (instance Plottable RegModel / regMethodName / roundTo は
---  Hanalyze.Plot.Wrappers へ移動 — Phase 71.7)
-
--- (familyObsDist は Hanalyze.Plot.Linear へ移動 — Phase 71.5)
-
--- (lmDiag / groupedLmDiag / instance Plottable (GroupedFit spec) /
---  renderGrouped / groupedFullrange / renderGroupedWith /
---  instance ColumnSource [(Text, ColData)] は
---  Hanalyze.Plot.Wrappers へ移動 — Phase 71.7)
-
--- (dataScatterOf は Hanalyze.Plot.Bayes へ移動 — Phase 71.7)
-
diff --git a/src/Hanalyze/Plot/Bayes.hs b/src/Hanalyze/Plot/Bayes.hs
deleted file mode 100644
--- a/src/Hanalyze/Plot/Bayes.hs
+++ /dev/null
@@ -1,1011 +0,0 @@
--- |
--- Module      : Hanalyze.Plot.Bayes
--- Description : hgg 連携層 — ベイズ / HBM 連携族の図化 instance + 抽出子
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- hgg 連携層 — **ベイズ / HBM 連携族** の図化 instance + 抽出子 (Phase 71.6)。
---
--- ⚠ 親 'Hanalyze.Plot' と同じ cabal flag @plot-integration@ (既定 off) を
--- on にしたときのみ build される。 共通基盤 (class / ModelSpec / grid 評価核) は
--- 'Hanalyze.Plot.Core' を import して取り込む (orphan instance を許容)。
---
--- 担当する型・抽出子 (= MCMC chain / HBM 出力):
---   ChainModel の trace / 周辺事後密度・HBM の trace/forest/epred/ppc/dag 抽出子 (Phase 74 統一)・
---   GLMMResultRE の caterpillar plot。 HBM の *学習* (hbmModel 等) は
---   'Hanalyze.Fit' / 'Hanalyze.Model.Wrappers' 側 (こちらは描画連携のみ)。
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE ImpredicativeTypes #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE FlexibleContexts #-}
-module Hanalyze.Plot.Bayes
-  ( -- * HBM の出力抽出子 — Phase 49 A2 / Phase 74 (trace / forest)
-    hbmParamNames
-  , TraceOpts (..)
-  , defaultTraceOpts
-  , tracesOf
-  , tracesOfWith
-  , marginalsOf
-  , marginalsByChainOf
-    -- * HBM のサンプリング診断 — Phase 59 (divergence 可視化)
-  , divergencesOf
-  , pairOf
-  , energyOf
-  , autocorrOf
-  , autocorrOfLag
-  , defaultAutocorrMaxLag
-  , rankOf
-  , rankOfBins
-  , defaultRankBins
-  , ForestSpec (..)
-  , forestOf
-  , forestOfLevel
-    -- * HBM の出力抽出子 — Phase 49 A3 (epred = 事後予測平均 + HDI band)
-  , epred
-  , epredAt
-  , epredPredRange
-    -- * 応答曲面 3D / 散布 (HBM 固有) — Phase 71.7
-  , epredSurfaceOf
-  , epredSurfaceOfWith
-  , dataScatterOf
-    -- * HBM の出力抽出子 — Phase 49 A4 (ppc = 事後予測チェック)
-  , PPCConfig (..)
-  , defaultPPC
-  , PPCSpec (..)
-  , ppcOf
-  , ppcOfWith
-  , ppcOfIO
-  , ppcOfWithIO
-    -- * HBM の出力抽出子 — Phase 49 A5 (dag = モデル構造の DAG)
-  , DagSpec (..)
-  , dagOf
-  , dagOfRaw
-  , dagOfModel
-  , dagOfModelWith
-    -- * HBM 診断ダッシュボード — Phase 74.8 (抽出子束ね)
-  , dashboardOf
-  , dashboardFullOf
-  , traceDensityOf
-  ) where
-
-import           Data.List             (sortBy, transpose)
-import qualified Data.Map.Strict       as Map
-import           Data.Maybe            (fromMaybe)
-import           Data.Ord              (comparing)
-import           Data.Word             (Word32)
-import qualified Data.Vector           as V
-import           System.Random.MWC     (createSystemRandom, initialize, Gen)
-import           Control.Monad.Primitive (PrimMonad, PrimState)
-import           Control.Monad.ST      (runST)
-import qualified Numeric.LinearAlgebra as LA
-
-import           Data.Text             (Text)
-import qualified Data.Text             as T
-
-import           Hgg.Plot.Spec     ( VisualSpec, layer, inline, inlineCat
-                                       , Color (..), fromHex
-                                       , scatter, line, band, bar
-                                       , position, Position (..)
-                                       , color, colorBy, lineRange
-                                       , scaleColorManual, legendOff
-                                       , legendPos, LegendPosition (..)
-                                       , trace, density, forest, forestNull
-                                       , subplots, subplotCols, width, height
-                                       , xLabel, yLabel, title
-                                       , ecdf, alpha
-                                       , dagFromListsWithPlates
-                                       , DAGNode (..), DAGEdge (..), DAGPlate (..)
-                                       , DAGNodeKind (..), DAGLayoutAlgorithm (..) )
-import           Hgg.Plot.DAG      (layoutHierarchicalFullWithPlates)
-import           Hgg.Plot.Render.Special (bakeDAGRoutesInSpec)
-import qualified Hgg.Plot.ThreeD.Spec  as P3
-
-import           Hanalyze.Model.Wrappers
-import           Hanalyze.Plot.Core
-import           Hanalyze.MCMC.Core     (Chain (..), chainVals)
-import           Hanalyze.MCMC.BayesianTest (highestDensityInterval)
-import           Hanalyze.Stat.MCMC    (kde, autocorr, rankHist)
-import           Hanalyze.Model.HBM.Sampling (sampleObsRep)
-import           Hanalyze.Model.HBM     (ModelP, withData
-                                       , runDeterministics, runObserveDists
-                                       , buildModelGraph, ModelGraph (..)
-                                       , collapseIndexedPlateNodes
-                                       , sampleNames
-                                       , Node (..), NodeKind (..))
-import           Hanalyze.Model.LM     (linspace)
-import           Hanalyze.Model.GLMM   (GLMMResultRE (..))
-
--- ===========================================================================
--- MCMC チェーン (描画可能)
---
--- 'Chain' (Hanalyze.MCMC.Core) は post-burn-in の draw 列 'chainSamples' を保持する
--- (各 draw は Map パラメータ名→値)。 ベイズの「出入口」 = サンプラの収束診断と周辺事後の
--- 可視化。 1 つのパラメータを選び、 代表図 ('toPlot') は **trace plot** (draw index 対値、
--- = 混合・定常性の目視)、 診断束 ('diagnosticPlots') に **周辺事後密度** (MDensity) を加える。
--- trace と density は座標系が異なる (index-値 vs 値-密度) ため 1 枚に混ぜず別図にする。
--- ===========================================================================
-
-instance Plottable ChainModel where
-  -- trace plot: draw index 対 パラメータ値 (折れ線 = MTrace)。
-  toPlot m =
-    let vals  = chainVals (cmParam m) (cmChain m)
-        iters = [ fromIntegral i | i <- [1 .. length vals] ] :: [Double]
-    in layer (trace (inline iters) (inline vals))
-
-  -- 診断束: trace + 周辺事後密度 (MDensity)。
-  diagnosticPlots m =
-    let vals  = chainVals (cmParam m) (cmChain m)
-        iters = [ fromIntegral i | i <- [1 .. length vals] ] :: [Double]
-    in [ layer (trace (inline iters) (inline vals))
-       , layer (density (inline vals))
-       ]
-
--- ===========================================================================
--- HBM の出力抽出子 — Phase 49 A2 / Phase 74 (trace / forest)
---
--- 'HBMModel' は直接 'Plottable' にしない (確率プログラムは単一の図に一意に落ちない)。
--- 代わりに抽出子を明示する。 trace は 'tracesOf' / 'tracesOfWith' に統一:
---   * 'tracesOf'     = 各 latent パラメータの trace plot を **param ごと独立パネル**
---                      ('[VisualSpec]') で返す。 divergence rug は既定 ON (ArviZ 流)。
---   * 'tracesOfWith' = 'TraceOpts' で divergence on/off と chain 別重畳を切り替える。
---   * 'forestOf'     = 各 latent の事後区間 (事後平均 + 94% HDI) を 'MForest' mark で。
---
--- ★ Phase 74 で旧 'traceOf' ([ChainModel]) / 'tracesByChainOf' /
--- 'tracesWithDivergencesOf' の 3 本を統合した。 戻り型を兄弟抽出子 (marginalsOf 等)
--- と同じ '[VisualSpec]' に揃え、 @vconcat (tracesOf m)@ で param ごと縦並びに描ける
--- (旧 docs の @foldMap toPlot (traceOf m)@ = 全 param を 1 軸に重畳する誤りを排除)。
--- ===========================================================================
-
--- | 学習済モデルの latent パラメータ名 (= 事後を持つ未知数の一覧)。
-hbmParamNames :: HBMModel -> [Text]
-hbmParamNames = sampleNames . hbmModelSpec
-
--- | 1 パラメータの post-burn-in draw を全 chain 連結で取り出す。
-hbmDraws :: Text -> HBMModel -> [Double]
-hbmDraws name = concatMap (chainVals name) . hbmChainsR
-
--- | 全 chain の draw を 1 本に連結した 'Chain' (trace 表示用)。 index は
--- chain を端から端へ並べた通し番号になる (A2 の trace は混合の目視が目的)。
--- Phase 59.4: divergence index も同じ連結順の通し番号に変換する
--- ('pooledDivergences' が正本。 chain 内 index のまま連結すると merged frame で
--- 別の draw を指してしまう)。
-mergeChains :: [Chain] -> Chain
-mergeChains []  = Chain [] 0 0 [] [] []
-mergeChains chs = Chain
-  { chainSamples     = concatMap chainSamples chs
-  , chainAccepted    = sum (map chainAccepted chs)
-  , chainTotal       = sum (map chainTotal chs)
-  , chainEnergy      = concatMap chainEnergy chs
-  , chainDivergences = pooledDivergences chs
-  , chainTreeDepths  = concatMap chainTreeDepths chs
-  }
-
--- | trace 診断の設定 ('ppcOf' / 'PPCConfig' と同じ「関数 + config」 慣用)。
-data TraceOpts = TraceOpts
-  { toShowDivergences :: !Bool  -- ^ 発散 draw の rug を重ねる (既定 True・ArviZ 流)。
-  , toByChain         :: !Bool  -- ^ True で chain 別重畳、 False で全 chain merged (既定)。
-  } deriving (Show, Eq)
-
--- | 既定の trace 設定 = divergence rug ON・全 chain merged。
-defaultTraceOpts :: TraceOpts
-defaultTraceOpts = TraceOpts { toShowDivergences = True, toByChain = False }
-
--- | 各 latent パラメータの trace plot を **param ごと独立パネル** ('[VisualSpec]')
--- で返す (divergence rug 既定 ON)。 @noDf |>> vconcat (tracesOf m)@ で param ごとに
--- 縦並びの trace になる (= ArviZ @plot_trace@ 右列)。 設定は 'tracesOfWith'。
-tracesOf :: HBMModel -> [VisualSpec]
-tracesOf = tracesOfWith defaultTraceOpts
-
--- | 'TraceOpts' を明示する 'tracesOf'。 旧 'traceOf' (merged 単線) /
--- 'tracesByChainOf' (chain 別重畳) / 'tracesWithDivergencesOf' (chain 別 + rug) の
--- 3 本を 1 つに統合したもの:
---
---   * @tracesOfWith (TraceOpts False False)@ = 旧 'traceOf' 相当 (merged 単線・rug 無し)
---   * @tracesOfWith (TraceOpts False True )@ = 旧 'tracesByChainOf' (chain 別重畳・rug 無し)
---   * @tracesOfWith (TraceOpts True  True )@ = 旧 'tracesWithDivergencesOf' (chain 別 + rug)
---   * 既定 @tracesOf@ = @TraceOpts True False@ (merged + rug)
---
--- divergence rug は各図下端 (y = 当該 param の全 chain 最小値) に発散 draw の x 位置を
--- 縦棒 ('lineRange') で打つ。 merged では通し index ('divergencesOf')、 chain 別では
--- chain 内 1-based iteration を x にする (それぞれの trace の x 軸と整合)。
--- divergence が無ければ rug レイヤは付かない。
-tracesOfWith :: TraceOpts -> HBMModel -> [VisualSpec]
-tracesOfWith opts hbm =
-  [ traceLayers nm <> rugLayer nm <> title nm | nm <- hbmParamNames hbm ]
-  where
-    chs = hbmChainsR hbm
-    traceLayers nm
-      | toByChain opts =
-          foldMap (\(k, ch) ->
-              let vals  = chainVals nm ch
-                  iters = [ fromIntegral i | i <- [1 .. length vals] ] :: [Double]
-              in layer (trace (inline iters) (inline vals) <> color (fromHex (chainColor k))))
-            (zip [0 ..] chs)
-      | otherwise =
-          let vals  = chainVals nm (mergeChains chs)
-              iters = [ fromIntegral i | i <- [1 .. length vals] ] :: [Double]
-          in layer (trace (inline iters) (inline vals))
-    rugLayer nm
-      | not (toShowDivergences opts) = mempty
-      | otherwise =
-          let allVals = concatMap (chainVals nm) chs
-              -- merged: 連結通し index (divergencesOf)。chain 別: chain 内 1-based iteration。
-              xs | toByChain opts = [ fromIntegral (i + 1) | ch <- chs, i <- chainDivergences ch ] :: [Double]
-                 | otherwise      = [ fromIntegral (i + 1) | i <- divergencesOf hbm ] :: [Double]
-          in if null xs || null allVals
-               then mempty
-               -- ArviZ tick 同型 = 下端から値域 2% の短い縦棒。 定数 trace では 1e-9 最小高。
-               -- ★lineRange の意味論は (x, 中心 y, ±err) = 下端 yMin〜yMin+tick の棒。
-               else let yMin = minimum allVals
-                        yMax = maximum allVals
-                        tick = max ((yMax - yMin) * 0.02) 1e-9
-                        nDiv = length xs
-                    in layer (lineRange (inline xs)
-                                        (inline (replicate nDiv (yMin + tick / 2)))
-                                        (inline (replicate nDiv (tick / 2)))
-                              <> color (fromHex divergenceColor))
-
--- | 各 latent パラメータの **周辺事後密度** を per-param で list 返しする。
--- 'tracesOf' (per-param trace) の密度版で、 'ChainModel' の @diagnosticPlots@ が出す
--- 周辺事後密度 (@density@、 root: 'diagnosticPlots' ChainModel 経路) を 1 パラメータ
--- 1 図に切り出したもの。 全 chain の post-burn-in draw をプール ('hbmDraws') した
--- 周辺分布を描き、 図タイトルにパラメータ名を付す。
---
--- @subplots (map toPlot (marginalsOf fit)) <> subplotCols 1@ で周辺事後の grid を組め、
--- B1 の入れ子 subplots と合わせて HBM ダッシュボードの 1 列になる。
-marginalsOf :: HBMModel -> [VisualSpec]
-marginalsOf hbm =
-  [ layer (density (inline (hbmDraws nm hbm))) <> title nm
-  | nm <- hbmParamNames hbm ]
-
--- ===========================================================================
--- HBM のサンプリング診断 — Phase 59.4 / 74 (divergence の通し index + pair/energy)
---
--- 'Chain' は NUTS の発散 draw index ('chainDivergences' = chain 内 0-based・
--- post-burn-in、 root: request/255 §4) と Hamiltonian energy を記録済み。 ここでは
--- それを plot-core の語彙 (scatter + color) で図示する。 rug 用の新 MarkKind は
--- 追加しない (計画 md の設計判断: 既存 mark の組合せで足りることを確認してから諮る)。
--- trace の divergence rug 自体は 'tracesOfWith' (Phase 74 統合) に移譲した。
--- ===========================================================================
-
--- | [Chain] の発散 draw を連結順の通し index に変換する内部正本
--- (chain c の offset = それ以前の chain の draw 数合計)。 'mergeChains' /
--- 'divergencesOf' の双方がこれを使う (重複実装しない)。
-pooledDivergences :: [Chain] -> [Int]
-pooledDivergences chs =
-  concat [ map (+ off) (chainDivergences ch)
-         | (off, ch) <- zip offsets chs ]
-  where offsets = scanl (+) 0 (map (length . chainSamples) chs)
-
--- | 全 chain を pool した発散 draw の通し index ('mergeChains' の連結順と整合)。
--- 'tracesOf' (merged trace) の rug 位置や、 発散 draw の抽出
--- (@map (chainSamples merged !!) (divergencesOf fit)@) に使う。
-divergencesOf :: HBMModel -> [Int]
-divergencesOf = pooledDivergences . hbmChainsR
-
--- | divergence rug / 強調点の色 ('Hanalyze.Viz.MCMC' の pairScatterDiv と同じ赤)。
-divergenceColor :: Text
-divergenceColor = "#dd2222"   -- 小文字 (toCss 出力と byte 一致・視覚は #DD2222 と同一)
-
--- | ArviZ @plot_pair(divergences=True)@ 流: 指定パラメータ対の joint 散布
--- (全 chain pool・薄表示) + 発散 draw を強調色で重畳。 funnel 診断の本命
--- (例: @pairOf fit [("tau_b1", "b1_2")]@ で漏斗の首に発散が集中するのが見える)。
--- 発散 draw の抽出は 'divergencesOf' の通し index を pool 後の draw 列に引く
--- (chain 連結順は 'hbmDraws' = 'mergeChains' と同一)。
--- divergence が無ければ強調レイヤは付かない。
-pairOf :: HBMModel -> [(Text, Text)] -> [VisualSpec]
-pairOf hbm prs =
-  [ let xs   = hbmDraws xn hbm
-        ys   = hbmDraws yn hbm
-        n    = min (length xs) (length ys)
-        dIdx = [ i | i <- divergencesOf hbm, i < n ]
-        dxs  = map (xs !!) dIdx
-        dys  = map (ys !!) dIdx
-    in layer (scatter (inline xs) (inline ys) <> alpha 0.25)
-       <> (if null dIdx
-             then mempty
-             else layer (scatter (inline dxs) (inline dys)
-                         <> color (fromHex divergenceColor)))
-       <> xLabel xn <> yLabel yn <> title (xn <> " × " <> yn)
-  | (xn, yn) <- prs ]
-
--- | ArviZ @plot_energy@ 流: marginal energy (E − Ē、 chain 別中心化) と
--- transition energy (ΔE = E_{i+1} − E_i、 chain 内差分・境界を跨がない) の密度重畳。
--- ΔE 分布が marginal より極端に狭ければ、 サンプラが posterior の energy 分布を
--- 探索しきれていないサイン (低 BFMI 相当。 数値は 'Hanalyze.Viz.MCMC' の bfmi)。
--- energy ('chainEnergy' = draw ごとの Hamiltonian) は HMC / NUTS のみ記録される
--- ため、 MH / Gibbs 等の fit では空図になる。 系列名は Viz 側 energyPlot と同一。
---
--- ★mark は 'density' でなく KDE ('Hanalyze.Stat.MCMC' の kde 200 = Viz energyPlot と
--- 同一) + 'line'。 理由: 固定色 'color' と categorical 'colorBy' は同一 field (lyColor) の
--- Last で相互排他、 かつ renderDensity は categorical 色を見ない (staticColorOr のみ) ため、
--- density mark では「2 色の曲線 + 凡例」 が両立できない。 line は群色対応済なので
--- 多モデル重畳 (line + color inlineCat + scaleColorManual + legend) の確立パターンに
--- 乗せる。
-energyOf :: HBMModel -> VisualSpec
-energyOf hbm =
-  curve lblMar eMar <> curve lblTr eTrans <> legendSpec
-    <> xLabel "Energy" <> yLabel "Density" <> title "energy"
-  where
-    lblMar = "marginal E (centered)"
-    lblTr  = "transition ΔE"
-    ess    = filter (not . null) (map chainEnergy (hbmChainsR hbm))
-    center es = let mu = sum es / fromIntegral (length es)
-                in map (subtract mu) es
-    eMar   = concatMap center ess
-    eTrans = concatMap (\es -> zipWith (-) (drop 1 es) es) ess
-    curve lbl vals
-      | length vals < 2 = mempty
-      | otherwise =
-          let (gx, gy) = unzip (kde 200 vals)
-          in layer (line (inline gx) (inline gy)
-                    <> colorBy (inlineCat (replicate (length gx) lbl)))
-    legendSpec
-      | null eMar = mempty
-      | otherwise = scaleColorManual [ (lblMar, "#4C72B0"), (lblTr, "#DD8452") ]
-                      -- 凡例は図内 (右上)。 密度は中央が高く右裾は 0 ゆえ右上が空く。
-                      -- 外・右だと右に余白が出て subplot/dashboard が不格好になる。
-                      <> legendPos LegendInsideTopRight
-
--- | chain 別の **周辺事後密度** を 1 図に重畳した per-param list (= ArviZ @plot_trace@ 左側 /
--- @plot_posterior@ の chain 重ね)。 'marginalsOf' が全 chain プールの 1 本を描くのに対し、
--- こちらは chain ごとに別レイヤを 'color' ('fromHex') で重ねる。
-marginalsByChainOf :: HBMModel -> [VisualSpec]
-marginalsByChainOf hbm =
-  [ foldMap (\(k, ch) -> layer (density (inline (chainVals nm ch)) <> color (fromHex (chainColor k))))
-            (zip [0 ..] (hbmChainsR hbm))
-    <> title nm
-  | nm <- hbmParamNames hbm ]
-
--- | 自己相関 plot の既定最大ラグ (= ArviZ @plot_autocorr@ の見やすさに合わせた 30。
--- ArviZ 既定の 100 は SVG では横に潰れるので短めにする)。
-defaultAutocorrMaxLag :: Int
-defaultAutocorrMaxLag = 30
-
--- | 各 latent パラメータの **自己相関** を per-param list で返す (= ArviZ @plot_autocorr@)。
--- lag 0..'defaultAutocorrMaxLag' の ACF を縦棒 ('bar') で描く。 chain 連結の境界アーティ
--- ファクトを避けるため **chain ごとに 'autocorr' を計算し lag ごとに平均**する
--- ('energyOf' が chain 別に算出して連結するのと同方針)。 ACF が速く 0 に減衰するほど
--- mixing が良い (高い自己相関 = ESS 低下のサイン)。
-autocorrOf :: HBMModel -> [VisualSpec]
-autocorrOf = autocorrOfLag defaultAutocorrMaxLag
-
--- | 最大ラグを明示する 'autocorrOf'。
-autocorrOfLag :: Int -> HBMModel -> [VisualSpec]
-autocorrOfLag maxLag hbm =
-  [ acSpec nm | nm <- hbmParamNames hbm ]
-  where
-    chains = hbmChainsR hbm
-    acSpec nm =
-      let perChain = [ autocorr maxLag vs
-                     | c <- chains, let vs = chainVals nm c, not (null vs) ]
-      in case perChain of
-           []      -> mempty
-           (ac0:_) ->
-             let lags    = map (fromIntegral . fst) ac0 :: [Double]
-                 acfByCh = map (map snd) perChain                  -- [chain][lag]
-                 meanACF = map (\col -> sum col / fromIntegral (length col))
-                               (transpose acfByCh)                 -- lag ごとの chain 平均
-             -- y 軸ラベルは省く (図が潰れるため。 title でパラメータ名は分かる)。
-             in layer (bar (inline lags) (inline meanACF))
-                  <> title nm <> xLabel "lag"
-
--- | rank plot の既定ビン数 (= PyMC @plot_rank@ 既定 20)。
-defaultRankBins :: Int
-defaultRankBins = 20
-
--- | 各 latent パラメータの **rank plot** を per-param list で返す (= ArviZ @plot_rank@・
--- Vehtari et al. 2021)。 全 chain をプールした値の rank を chain ごとにヒストグラム化し、
--- chain 別の棒を色分けして重畳する。 **収束時は各 chain がほぼ一様** (= どのビンも同程度)。
--- chain が偏る (= 山ができる) と R̂ 悪化のサイン。 rank 計算は 'rankHist' (Stat.MCMC) に
--- 一元化し Viz 経路と共有する。 **要 chain ≥ 2** (1 本だと rank が自明に一様ゆえ空図)。
-rankOf :: HBMModel -> [VisualSpec]
-rankOf = rankOfBins defaultRankBins
-
--- | ビン数を明示する 'rankOf'。
-rankOfBins :: Int -> HBMModel -> [VisualSpec]
-rankOfBins nBins hbm =
-  [ rankSpec nm | nm <- hbmParamNames hbm ]
-  where
-    chains = hbmChainsR hbm
-    nCh    = length chains
-    rankSpec nm =
-      let perChain = map (chainVals nm) chains
-      in if nCh < 2 || all null perChain
-           then mempty
-           else
-             -- chain を横並び (dodge) にした 1 層の bar (= ArviZ plot_rank の単一パネル版)。
-             -- long-form: (bin, count, chain) を chain×bin 行で展開し colorBy + PosDodge。
-             let hists    = rankHist nBins perChain                 -- [chain][bin]
-                 -- ビンは categorical だが軸はアルファベット順ゆえ、 数値順を保つよう
-                 -- 0 埋めラベル ("00".."19") にする (= 文字列ソート = 数値順)。
-                 w        = length (show (nBins - 1))
-                 pad i    = let s = show (i :: Int)
-                            in T.pack (replicate (w - length s) '0' ++ s)
-                 binCat   = concat [ [ pad b | b <- [0 .. nBins - 1] ] | _ <- [1 .. nCh] ]
-                 cntCol   = concatMap (map fromIntegral) hists :: [Double]
-                 chainCat = concat [ replicate nBins (T.pack ("chain " <> show k))
-                                   | k <- [0 .. nCh - 1] ]
-             -- y 軸ラベル・凡例は省く (図が潰れるため。 chain は色 dodge で判別可)。
-             -- colorBy は既定で凡例を出すので legendOff で明示的に抑制する。
-             in layer ( bar (inlineCat binCat) (inline cntCol)
-                        <> colorBy (inlineCat chainCat)
-                        <> position PosDodge )
-                  <> title nm <> xLabel "rank bin" <> legendOff
-
-
--- | 係数 forest plot の描画仕様。 'HBMModel' を直接 'Plottable' にしないため、
--- 抽出後の図を包む薄い newtype (後続 sub の ppc/epred/dag も同型に揃える)。
-newtype ForestSpec = ForestSpec { unForestSpec :: VisualSpec }
-
-instance Plottable ForestSpec where
-  toPlot = unForestSpec
-
--- | 各 latent パラメータの事後区間を 1 枚の forest plot にする (94% HDI 既定)。
-forestOf :: HBMModel -> ForestSpec
-forestOf = forestOfLevel 0.94
-
--- | 信頼水準を明示する 'forestOf'。 point = 事後平均、 bar 半幅 = HDI 半幅。
---
--- ★ 'forest' mark は対称 CI (± 半幅) のみ対応するため、 非対称な HDI は
--- 「事後平均 ± (hi−lo)/2」 の対称バーで近似表示する (mark 側の TODO = 非対称 forest)。
-forestOfLevel :: Double -> HBMModel -> ForestSpec
-forestOfLevel level hbm = ForestSpec $
-  layer (forest (inlineCat names) (inline ests) (inline errs) <> forestNull 0)
-  where
-    names = hbmParamNames hbm
-    rows  = [ (mean, (hi - lo) / 2)
-            | nm <- names
-            , let d        = hbmDraws nm hbm
-                  mean     = if null d then 0 else sum d / fromIntegral (length d)
-                  (lo, hi) = highestDensityInterval level d
-            ]
-    ests = map fst rows
-    errs = map snd rows
-
--- ===========================================================================
--- HBM の事後予測平均 — Phase 49 A3 (epred = E[y|x] の grid 評価 + HDI band)
---
--- ベイズ回帰の代表図。 予測子 (@predName@、 学習時 'dataNamed' の参照名) を grid 上で
--- 1 点ずつ動かし、 各 posterior draw でモデル中の deterministic ノード (@muName@、
--- 通常は線形予測子の平均 μ) を 'runDeterministics' で評価する。 これで grid 点ごとに
--- N draws 分の μ サンプルが得られ、 その **事後平均** (線) と **94% HDI** (帯、 ArviZ 既定)
--- を描く。 これは PyMC の @pm.sample_posterior_predictive@ で得る epred (expected value of
--- the posterior predictive) に相当する (観測ノイズを含まない平均の不確実性)。
---
--- ★ O1 規約: epred 用モデルは予測子を @dataNamed predName@ で受け、 その平均を
--- @deterministic muName@ で 1 点スカラとして公開する (学習 likelihood とは併存)。
--- grid 評価では @withData predName [xi]@ で 1 点に差し替えるため、 deterministic 内で
--- @head x@ を取れば @xi@ が読める。
---
--- ★ Phase 74: 多予測子の hold。 非軸の予測子 slot は、 既定では 'HoldAgg' に従って
--- bind データの集約値 ('Mean' 既定) で固定する (旧実装は bind データ先頭値 @head@ に
--- 固定で選択不能だった)。 頻度論 effect plot ('statModelMulti') と **同じ語彙**を共有:
---   * @epred fit "x1" "mu" \<\> holdAt Median@         … 非軸を中央値で固定
---   * @epred fit "x1" "mu" \<\> holdAt (Fixed [("x2", 5)])@  … x2 のみ 5・他は Mean
---   * @epred fit "x1" "mu" \<\> byVar "x2" [0, 1]@      … x2 の水準別に曲線色分け重畳
--- 'holdAt' / 'byVar' は 'Hanalyze.Plot.Core' の既存コンビネータ (= ModelSpec の
--- @msHoldAt@ / @msByVar@ を設定) をそのまま使う (epred 専用版は作らない)。
---
--- ★ 設計: 専用 newtype を作らず **'ModelSpec' を再利用**する (record は描画クロージャの
--- 容れ物で 'SingleVarModel' 束縛ではない)。 これにより @epred hbm "x" "mu" \<\> grid 200
--- \<\> statLevel 0.9@ が Phase 16 C1 のコンビネータと同綴りで合成できる (既定 level 0.94 と
--- 帯 ON = ArviZ 流の HDI 帯を焼き込む。 epred の帯はオプトアウト不可)。
--- ===========================================================================
-
--- | 1 つの予測子値 @x@ における事後予測平均と HDI (非軸予測子は bind データのまま)。
--- @predName@ を @[x]@ に差し替え、 全 chain の各 draw で deterministic @muName@ を
--- 評価し、 (事後平均, (lo, hi)) を返す。 非軸予測子を固定する版は 'epredAtHeld'。
-epredAt
-  :: HBMModel
-  -> Text     -- ^ 予測子の data 参照名 (@dataNamed@ / @withData@ の名前)。
-  -> Text     -- ^ 平均の deterministic ノード名。
-  -> Double   -- ^ HDI 水準 (例 0.94)。
-  -> Double   -- ^ 予測子値 x。
-  -> (Double, (Double, Double))
-epredAt hbm = epredAtHeld hbm []
-
--- | (slot 名, 固定値) のリストを @withData@ で 1 点ずつ bind してネストする。
--- 'ModelP' は impredicative (@forall a. Model a r@) ゆえ foldr では多相が逃げる。
--- トップレベル再帰なら各 'withData' が @ModelP r -> ModelP r@ を保つので通る。
-bindHolds :: [(Text, Double)] -> ModelP r -> ModelP r
-bindHolds []              m = m
-bindHolds ((nm, v) : rest) m = withData nm [v] (bindHolds rest m)
-
--- | 'epredAt' の多予測子版。 @holds@ = 非軸予測子の (slot 名, 固定値) を 1 点ずつ
--- @withData@ で bind し ('head' でその値が読める)、 軸 @predName@ を @[gx]@ に差し替える。
-epredAtHeld
-  :: HBMModel
-  -> [(Text, Double)]   -- ^ 非軸予測子の固定 (slot 名, 値)。
-  -> Text -> Text -> Double -> Double
-  -> (Double, (Double, Double))
-epredAtHeld hbm holds predName muName level gx =
-  let bound :: ModelP ()
-      bound = withData predName [gx] (bindHolds holds (hbmModelSpec hbm))
-      draws = concatMap chainSamples (hbmChainsR hbm)
-      mus   = [ v | ps <- draws
-                  , Just v <- [Map.lookup muName (runDeterministics bound ps)] ]
-      mean  = if null mus then 0 else sum mus / fromIntegral (length mus)
-  in (mean, highestDensityInterval level mus)
-
--- | grid 点 @gx@ における事後予測区間 (PI = 観測ノイズ込みの新規 1 点の HDI)。
--- 'epredAtHeld' が deterministic μ の HDI (= CI 相当) を返すのに対し、 こちらは
--- **観測ノードの予測分布から y をサンプルしてプール**し HDI を取る。 観測ノード名は
--- 引数に取らず 'runObserveDists' でモデルから自動検出する (頻度論 'svGridPI' が obs 名を
--- 要らないのと対称)。 単一 likelihood の通常ケースが対象で、 observe が複数なら全プール。
--- 任意の観測分布 (Normal/Poisson/NegBinom…) に効く ('ppc' の 'sampleDist' を再利用)。
--- @runST@ + 固定 seed (既定 'epredPISeed' = 42・'ppcOfWith' と同方式) で純粋・決定的。
-epredPIAtHeld
-  :: HBMModel
-  -> [(Text, Double)]   -- ^ 非軸予測子の固定 (slot 名, 値)。
-  -> Text               -- ^ 軸予測子の data 参照名。
-  -> Word32             -- ^ サンプリング seed。
-  -> Double             -- ^ HDI 水準。
-  -> Double             -- ^ 予測子値 x。
-  -> (Double, Double)
-epredPIAtHeld hbm holds predName seed level gx =
-  let bound :: ModelP ()
-      bound = withData predName [gx] (bindHolds holds (hbmModelSpec hbm))
-      draws = concatMap chainSamples (hbmChainsR hbm)
-      samples = runST $ do
-        gen <- initialize (V.singleton seed)
-        concat <$> mapM
-          (\ps ->
-             let nodes = [ (d, ys) | (_, d, ys) <- runObserveDists bound ps ]
-             in concat <$> mapM (\(d, ys) -> sampleObsRep gen d ys) nodes)
-          draws
-  in if null samples then (0, 0) else highestDensityInterval level samples
-
--- | 'epredPIAtHeld' の既定サンプリング seed (純粋・決定的に閉じる。 'ppcOfWith' と同値)。
-epredPISeed :: Word32
-epredPISeed = 42
-
--- | 非軸予測子 1 slot の固定値を 'HoldAgg' (+ byVar override) から決める。
--- @override@ (byVar の明示固定) が 'HoldAgg' より優先。 HBM データは数値列ゆえ
--- factor / Reference は無く、 Reference\/Marginalize は安全側に Mean とする
--- (Marginalize の真の周辺化は epred では未対応)。
-epredHoldValue :: HoldAgg -> Text -> [Double] -> [(Text, Double)] -> Double
-epredHoldValue hold nm vs override =
-  case lookup nm override of
-    Just v  -> v
-    Nothing -> case hold of
-      Mean        -> meanL vs
-      Median      -> medianL vs
-      Mode        -> medianL vs                       -- 数値連続に最頻は無意味 → 中央値で代替
-      Reference   -> meanL vs
-      Marginalize -> meanL vs
-      Fixed fm    -> fromMaybe (meanL vs) (lookup nm fm)
-  where
-    meanL xs   = if null xs then 0 else sum xs / fromIntegral (length xs)
-    medianL xs = case xs of
-      [] -> 0
-      _  -> let s = sortBy compare xs
-                k = length s
-            in if even k
-                 then (s !! (k `div` 2 - 1) + s !! (k `div` 2)) / 2
-                 else s !! (k `div` 2)
-
--- | grid 上の事後予測平均線 + HDI 帯を組む 'GridOpts' クロージャ ('epred' が設定)。
--- 'renderGridMulti' (頻度論 effect plot) と同型: 非軸予測子を 'goHoldAt' で固定し、
--- 'goByVar' があれば第2予測子の水準ごとに曲線を色分け重畳する。 各曲線は帯 (先) +
--- 線 (後)。 'goPredAt' 指定点は lineRange (区間) + scatter (事後平均) で重畳する。
--- 帯は非対称な HDI を lo/hi で忠実に描く。
-renderEpred :: HBMModel -> Text -> Text -> GridOpts -> VisualSpec
-renderEpred hbm predName muName opts =
-  let (lo0, hi0) = epredPredRange hbm predName
-      (lo, hi)   = fromMaybe (lo0, hi0) (goRange opts)
-      n          = max 2 (goN opts)
-      gxs        = linspace lo hi n
-      level      = goLevel opts
-      hold       = goHoldAt opts
-      -- 非軸予測子 (= hbmData の predName 以外の slot) を HoldAgg + byVar override で固定。
-      -- 応答列も含むが deterministic μ は応答に依存しないため無害。
-      holdBinds override =
-        [ (nm, epredHoldValue hold nm vs override)
-        | (nm, vs) <- hbmData hbm, nm /= predName ]
-      -- 1 曲線分 (override = byVar 固定の (名,値)、 mCol = 線/帯色)。
-      -- BandMode で CI (μ HDI) / PI (観測ノイズ込み) / CIPI (入れ子) / なし を切替
-      -- (頻度論 'renderGrid' と同型)。 PI 系は遅延ゆえ CI/Off では評価されない。
-      oneCurve override mCol =
-        let holds = holdBinds override
-            rows  = map (epredAtHeld hbm holds predName muName level) gxs
-            mu    = map fst rows
-            ciLos = map (fst . snd) rows
-            ciHis = map (snd . snd) rows
-            piPairs = map (epredPIAtHeld hbm holds predName epredPISeed level) gxs
-            piLos = map fst piPairs
-            piHis = map snd piPairs
-            lineL = layer (goLineDeco opts mCol n (line (inline gxs) (inline mu)))
-            ciDeco = goBandDeco opts mCol
-            -- 入れ子時の PI 帯は薄め (CI が内側で見えるように・頻度論と同じ既定)。
-            piA    = maybe 0.10 (* 0.5) (goAlpha opts)
-            piDeco = goBandDeco (opts { goAlpha = Just piA }) mCol
-            mkBand deco los his = layer (deco (band (inline gxs) (inline los) (inline his)))
-        in case goBandMode opts of
-             BandOff  -> lineL
-             BandCI   -> mkBand ciDeco ciLos ciHis <> lineL
-             BandPI   -> mkBand ciDeco piLos piHis <> lineL
-             BandCIPI -> mkBand piDeco piLos piHis        -- 外: PI 薄 (下)
-                      <> mkBand ciDeco ciLos ciHis        -- 内: CI 濃 (上)
-                      <> lineL
-      curves = case goByVar opts of
-        Nothing         -> oneCurve [] Nothing
-        Just (v2, vals) ->
-          foldMap
-            (\(i, val) ->
-               let col = fromHex (effectPalette !! (i `mod` length effectPalette))
-               in oneCurve [(v2, val)] (Just col))
-            (zip [0 :: Int ..] vals)
-      pts = goPredAt opts
-      predLayers
-        | null pts  = mempty
-        | otherwise =
-            let prows = map (epredAtHeld hbm (holdBinds []) predName muName level) pts
-                pmu   = map fst prows
-                mids  = map (\(_, (l, h)) -> (l + h) / 2) prows
-                halfs = map (\(_, (l, h)) -> (h - l) / 2) prows
-            in layer (lineRange (inline pts) (inline mids) (inline halfs))
-                 <> layer (scatter (inline pts) (inline pmu))
-  in curves <> predLayers <> labelLegend opts
-
--- | 予測子列の観測範囲 (grid 既定範囲)。 bind 済みデータ ('hbmData') から引く。
-epredPredRange :: HBMModel -> Text -> (Double, Double)
-epredPredRange hbm predName =
-  case lookup predName (hbmData hbm) of
-    Just vs | not (null vs) -> (minimum vs, maximum vs)
-    _                       -> (0, 1)
-
--- | HBM の事後予測平均 (E[y|x]) を grid 評価する 'ModelSpec' を作る。 既定は 94% HDI 帯
--- (ArviZ 流・帯 ON 焼き込み)、 grid 100 点、 範囲 = 予測子の観測 min/max。 @\<\>@ で
--- 'grid' / 'gridRange' / 'statLevel' / 'predAt' を合成できる (Phase 16 C1 と同綴り)。
---
--- @
--- noDf |>> toPlot (epred fit \"x\" \"mu\" \<\> grid 200 \<\> statLevel 0.9)
--- @
-epred
-  :: HBMModel
-  -> Text   -- ^ 予測子の data 参照名。
-  -> Text   -- ^ 平均の deterministic ノード名。
-  -> ModelSpec
-epred hbm predName muName = mempty
-  { msRender = Just (renderEpred hbm predName muName)
-  , msLevel  = Just 0.94          -- ArviZ 既定の 94% HDI (statLevel で上書き可)
-  , msBandMode = Just BandCI      -- HDI 帯が epred の本体ゆえ既定で出す
-  }
-
--- ===========================================================================
--- HBM の事後予測チェック — Phase 49 A4 (ppc = posterior predictive check)
---
--- 観測 y の分布に対して、 学習済モデルが再現する複製データ y_rep の分布を重ねる
--- (ArviZ @az.plot_ppc@ 相当)。 各 posterior draw について 'runObserveDists' で
--- observe ノードの分布 (= 観測ノイズ込みの予測分布) を取り出し、 'sampleDist' で
--- 1 セット y_rep をサンプリングする。 これを N draw 分重ねると「観測がモデルの予測
--- 分布の典型から外れていないか」 を目視できる。
---
--- 描画 (ArviZ 流):
---   * 観測 density (濃色・実線)            … 実データ。
---   * y_rep density を N 本 (薄色・低 alpha) … 各 draw の複製データ。
---   * プール y_rep density (破線)          … 事後予測分布全体 (= ppc の中心)。
---
--- ★ サンプリングに RNG が要るため 'IO' (頻度論 toPlot や epred/forest と違い純粋に
--- できない)。 'hbmModel' 自体 IO なので非対称ではない。 cumulative 版は density を
--- 'ecdf' に差し替える ('ppcCumulative')。
--- ===========================================================================
-
--- | ppc の設定: 重ねる複製データ本数 ('ppcReps')、 乱数シード、 累積版 (ecdf) 切替。
-data PPCConfig = PPCConfig
-  { ppcReps       :: !Int            -- ^ 重ねる y_rep 本数 (既定 40・draw から等間隔抽出)。
-  , ppcSeed       :: !(Maybe Word32) -- ^ サンプリングのシード (Nothing = system)。
-  , ppcCumulative :: !Bool           -- ^ True で density を ecdf (累積分布) に差し替える。
-  } deriving (Show, Eq)
-
--- | 既定 ppc 設定: y_rep 40 本・system 乱数・density 表示。
-defaultPPC :: PPCConfig
-defaultPPC = PPCConfig { ppcReps = 40, ppcSeed = Nothing, ppcCumulative = False }
-
--- | 事後予測チェック plot の描画仕様 ('forestOf' 等と同型の薄い newtype)。
-newtype PPCSpec = PPCSpec { unPPCSpec :: VisualSpec }
-
-instance Plottable PPCSpec where
-  toPlot = unPPCSpec
-
--- | observe ノード名が prefix に一致するか。 単一 @observe \"obs\"@ (n == prefix) と
--- 'observeColumns' 由来の @\"obs_0\"@.. (prefix <> \"_\" が接頭辞) の両方を拾う。
-ppcMatches :: Text -> Text -> Bool
-ppcMatches prefix n = n == prefix || (prefix <> "_") `T.isPrefixOf` n
-
--- | 1 draw 分の複製データ y_rep をサンプリングする。 prefix 一致の各 observe ノードの
--- 分布から、 観測値と同数だけ引いてプールする。 Phase 50: 'PrimMonad' に一般化
--- (IO でも ST でも引ける → 純粋な 'ppcOf' が runST で決定的にサンプリングできる)。
-sampleYRep :: PrimMonad m
-           => Gen (PrimState m) -> ModelP () -> Text -> Map.Map Text Double -> m [Double]
-sampleYRep gen spec prefix ps =
-  let nodes = [ (d, ys) | (n, d, ys) <- runObserveDists spec ps, ppcMatches prefix n ]
-  in concat <$> mapM (\(d, ys) -> sampleObsRep gen d ys) nodes
-
--- | 観測値 (prefix 一致 observe ノードの ys をプール)。 params に依らないので任意 draw から。
-ppcObserved :: HBMModel -> Text -> [Double]
-ppcObserved hbm prefix =
-  case concatMap chainSamples (hbmChainsR hbm) of
-    (p0:_) -> concat [ ys | (n, _, ys) <- runObserveDists (hbmModelSpec hbm) p0
-                          , ppcMatches prefix n ]
-    []     -> []
-
--- | ppc の対象 draw 群 ('ppcReps' 本に間引き)。
-ppcDrawsFor :: PPCConfig -> HBMModel -> [Map.Map Text Double]
-ppcDrawsFor cfg hbm = selectEvenly (ppcReps cfg) (concatMap chainSamples (hbmChainsR hbm))
-
--- | 観測値・y_rep 群から ppc plot を組む (純粋)。 薄い y_rep 群 (背景・各 draw) を先に、
--- 観測 (濃) を上に重ねる。 純粋 'ppcOfWith' と IO 'ppcOfWithIO' で共有。
---
--- ★ 旧実装はプール y_rep (全 draw 連結) の密度を赤破線で重ねていたが、 KDE の Silverman
--- バンド幅が **n 依存** (@h ∝ n^(-0.2)@) ゆえ、 n=Σ(draw×n_obs) のプールは観測 (n=n_obs) より
--- バンド幅が小さく過小平滑になり、 観測と異なる形 (外側へ膨らむ) に見えて誤解を招いた。
--- 比較は観測 (黒) vs 各 draw の y_rep (青・同じ n) で行うべきなので、 プール線は削除した
--- (ArviZ @plot_ppc@ もプール KDE は描かない)。
-buildPPCSpec :: PPCConfig -> [Double] -> [[Double]] -> PPCSpec
-buildPPCSpec cfg observed yreps =
-  let densLayer = if ppcCumulative cfg then ecdf else density
-      repLayers = foldMap
-        (\yr -> layer (densLayer (inline yr) <> color (fromHex "#1f77b4") <> alpha 0.15))
-        yreps
-      obsLayer    = layer (densLayer (inline observed) <> color (fromHex "#000000"))
-  in PPCSpec (repLayers <> obsLayer)
-
--- | draw 列から 'ppcReps' 本を等間隔で抽出する (本数以下ならそのまま)。
-selectEvenly :: Int -> [a] -> [a]
-selectEvenly k xs
-  | k <= 0 || n <= k = xs
-  | otherwise        = [ xs !! (i * n `div` k) | i <- [0 .. k - 1] ]
-  where n = length xs
-
--- | 既定設定の事後予測チェック (純粋・決定的が**正本**。 'ppcOfWith' 'defaultPPC')。
--- y_rep サンプリングを @runST@ で閉じ、 @ppcSeed@ 既定 (42) で常に再現可能。 IO 版は 'ppcOfIO'。
-ppcOf :: HBMModel -> Text -> PPCSpec
-ppcOf = ppcOfWith defaultPPC
-
--- | 事後予測チェックを組む (純粋・正本)。 @prefix@ は observe ノード名 (@observeColumns@ なら接頭辞)。
--- y_rep サンプリングを @runST@ で閉じる。 @ppcSeed@ が 'Nothing' のときは固定既定 seed (42) で再現可能。
-ppcOfWith :: PPCConfig -> HBMModel -> Text -> PPCSpec
-ppcOfWith cfg hbm prefix =
-  let spec :: ModelP ()
-      spec  = hbmModelSpec hbm
-      draws = ppcDrawsFor cfg hbm
-      seed  = fromMaybe 42 (ppcSeed cfg)
-      yreps = runST $ do
-        gen <- initialize (V.singleton seed)
-        mapM (sampleYRep gen spec prefix) draws
-  in buildPPCSpec cfg (ppcObserved hbm prefix) yreps
-
--- | 既定設定の事後予測チェック (IO 版・'ppcOfWithIO' 'defaultPPC')。 通常は純粋な 'ppcOf' を使う
--- (将来 deprecate 予定)。 @ppcSeed@ 'Nothing' でシステム乱数を引きたいときだけ IO 版が要る。
-ppcOfIO :: HBMModel -> Text -> IO PPCSpec
-ppcOfIO = ppcOfWithIO defaultPPC
-
--- | 事後予測チェックを組む (IO 版)。 @ppcSeed@ 'Nothing' で 'createSystemRandom' を引く。
-ppcOfWithIO :: PPCConfig -> HBMModel -> Text -> IO PPCSpec
-ppcOfWithIO cfg hbm prefix = do
-  let spec :: ModelP ()
-      spec  = hbmModelSpec hbm
-      draws = ppcDrawsFor cfg hbm
-  gen <- case ppcSeed cfg of
-           Nothing -> createSystemRandom
-           Just w  -> initialize (V.singleton w)
-  yreps <- mapM (sampleYRep gen spec prefix) draws
-  pure $ buildPPCSpec cfg (ppcObserved hbm prefix) yreps
-
--- ===========================================================================
--- HBM 診断ダッシュボード — 複数の抽出子を 1 枚に束ねる便宜関数 (Phase 74.8)
---
--- 個別の抽出子 (dagOf / forestOf / ppcOf / energyOf / tracesOf / marginalsOf) を
--- 'subplots' で並べ「構造・推定・当てはまり・収束を一目で点検する」 パネル束にする。 2 種:
---   * 'dashboardOf'     … コンパクト 2×2 (構造 / 推定値 / 当てはまり / サンプラ健全性)。
---                          各 1 パネルゆえ param 数に依らず一定で見やすい。
---   * 'dashboardFullOf' … 上段に同じ 2×2、 その下に param ごと [事後分布 | trace] を 2 列で
---                          連結 (ArviZ @plot_trace@ 流)。 係数が増えると下へ行が増えるだけ。
--- どちらも observe ノード名を引数に取る (ppc 用)。 @noDf |>> dashboardOf m "obs"@。
--- autocorr/rank はダッシュボードに入れない (mixing は trace・BFMI は energy で見えるため。
--- ESS 定量は個別 'autocorrOf'、 chain 一様性は 'rankOf' で見る)。
--- ===========================================================================
-
--- | コンパクト健全性 2×2 のパネル群 (左上から 構造 / 推定値 / 当てはまり / サンプラ健全性)。
--- 'dashboardOf' (単体) と 'dashboardFullOf' (上段) で共有する内部ヘルパ。
-dashboardHealthPanels :: HBMModel -> Text -> [VisualSpec]
-dashboardHealthPanels hbm obsName =
-  [ toPlot (dagOf hbm)         <> title "構造 (DAG)"
-  , toPlot (forestOf hbm)      <> title "推定値 (forest 94% HDI)"
-  , toPlot (ppcOf hbm obsName) <> title "当てはまり (PPC: 観測 vs 事後予測)"
-  , energyOf hbm               <> title "サンプラ健全性 (energy / BFMI)" ]
-
--- | コンパクトな HBM 診断ダッシュボード (2×2)。 **構造** ('dagOf'・左上)・**推定値**
--- ('forestOf'・94% HDI)・**当てはまり** ('ppcOf'・観測 vs 事後予測の密度重ね)・**サンプラ
--- 健全性** ('energyOf'・BFMI) を 1 パネルずつ。 各 1 パネルゆえ param 数に依らず見やすい
--- (係数が増えても forest が縦に密になるだけ。 収束 R̂/trace は 'dashboardFullOf' で見る)。
-dashboardOf :: HBMModel -> Text -> VisualSpec
-dashboardOf hbm obsName =
-  subplots (dashboardHealthPanels hbm obsName)
-    <> subplotCols 2 <> width 1100 <> height 760
-
--- | param ごと **[事後分布 (左) | trace (右)]** のパネル群 (ArviZ @plot_trace@ の中身)。
--- 'traceDensityOf' (単体) と 'dashboardFullOf' (下段) で共有する内部ヘルパ。 事後分布・
--- trace とも chain 別を色違いで重畳する ('marginalsByChainOf' / 'tracesOfWith' byChain)。
-tracePostPanels :: HBMModel -> [VisualSpec]
-tracePostPanels hbm =
-  concat (zipWith (\p t -> [p, t])
-            (marginalsByChainOf hbm)
-            (tracesOfWith defaultTraceOpts { toByChain = True } hbm))
-
--- | trace と事後分布だけのダッシュボード (= ArviZ @plot_trace@ 相当)。 param ごとに
--- **[事後分布 (左) | trace (右)]** を 2 列で並べる (chain は色違いで重畳)。 収束 (定常・
--- chain 一致) と事後の形を同時に確認する定番。 係数が増えると下に行が増える。
-traceDensityOf :: HBMModel -> VisualSpec
-traceDensityOf hbm =
-  let np = max 1 (length (hbmParamNames hbm))
-  in subplots (tracePostPanels hbm)
-       <> subplotCols 2 <> width 900 <> height (180 * fromIntegral np)
-
--- | フルの HBM 診断ダッシュボード。 上段に 'dashboardOf' と同じ健全性 2×2、 その下に
--- param ごと **[事後分布 (左) | trace (右)]** を 2 列で連結する (ArviZ @plot_trace@ 流・
--- chain は色違いで重畳)。 全体が 1 つの 2 列グリッドなので、 **係数が増えると下に行が
--- 増えるだけ** (高さを行数 = 2 + param 数 に比例させ各パネルを潰さない)。 epred (予測曲線)
--- はモデル固有の予測子/平均ノード名と df が要るためここには含めない (個別に描く)。
-dashboardFullOf :: HBMModel -> Text -> VisualSpec
-dashboardFullOf hbm obsName =
-  let np   = max 1 (length (hbmParamNames hbm))
-      rows = 2 + np                                  -- 健全性 2 行 + param 行
-  in subplots (dashboardHealthPanels hbm obsName ++ tracePostPanels hbm)
-       <> subplotCols 2 <> width 1100 <> height (220 * fromIntegral rows)
-
--- ===========================================================================
--- HBM のモデル構造 DAG — Phase 49 A5 (dag = 確率プログラムの依存グラフ)
---
--- 確率プログラム ('ModelP') の依存構造を 'buildModelGraph' (= 'extractDeps' +
--- 同名ノード統合) で 'ModelGraph' (nodes / edges / plates) にし、 plot-core の
--- DAG 描画 ('dagFromListsWithPlates'、 Sugiyama 階層 layout) に橋渡しする。 PyMC の
--- @pm.model_to_graphviz@ に相当する「モデルの絵」。
---
--- ノード種 (latent / observed) と分布名は 'Node' のメタデータをそのまま 'DAGNode' に
--- 写す。 plate ('plate' で囲んだ繰り返し) は 'mgPlates' を 'DAGPlate' に変換する
--- (plate メンバは 'nodePlates' から逆引き)。 plate を使わないモデルでは
--- 'observeColumns' 由来の @obs_0..@ が個別ノードとして出る (collapse したい場合は
--- モデル側を 'plate' で囲む)。
--- ===========================================================================
-
--- | モデル構造 DAG の描画仕様 ('forestOf' 等と同型の薄い newtype)。
-newtype DagSpec = DagSpec { unDagSpec :: VisualSpec }
-
-instance Plottable DagSpec where
-  toPlot = unDagSpec
-
--- | 学習済モデルの構造を DAG にする ('buildModelGraph' → plate-collapse →
--- plot-core DAG)。 layout は階層 ('LayoutHierarchical')。 学習結果には依存しない
--- (構造のみ)。 Phase 59.3: plate 内の indexed RV (@b0_0..b0_2@ 等) を
--- 'collapseIndexedPlateNodes' で 1 ノードに畳むのが既定 (PyMC
--- @model_to_graphviz@ と同じ見た目)。 indexed 個別ノードのまま見たい場合は
--- 'dagOfRaw'。
-dagOf :: HBMModel -> DagSpec
-dagOf = dagFromModelGraph . collapseIndexedPlateNodes . buildModelGraph . hbmModelSpec
-
--- | 'dagOf' の plate-collapse 無し版 (Phase 49-59.2 の旧既定。 plate 内 indexed RV を
--- 個別ノードで列挙する。 展開後の全ノード/エッジを確認するデバッグ用)。
-dagOfRaw :: HBMModel -> DagSpec
-dagOfRaw = dagFromModelGraph . buildModelGraph . hbmModelSpec
-
--- | **学習前**にモデル構造だけを DAG にする (PyMC @pm.model_to_graphviz@ 相当。 Phase 74.9)。
--- 'dagOf' が学習済 'HBMModel' を取るのに対し、 こちらは生の 'ModelP' を直接取り
--- **サンプリングを一切しない** (構造は事後に依らないため)。 @noDf |>> toPlot (dagOfModel m)@。
---
--- ★ 注意: データ駆動 plate (@plateForM_@ / @observeColumns@ で plate サイズを **データ長**から
--- 決めるモデル) は、 データ未束縛 (slot が @[]@) だとループ本体が回らず plate 内ノード
--- (mu / obs 等) が出ない。 その場合は 'dagOfModelWith' でダミーでないデータを束ねてから描く
--- (サンプリングは走らない)。 明示 plate (@plate name N@ / @plateI@ で N を直書き) のモデルは
--- データ無しでも構造が完全に出る。
-dagOfModel :: ModelP () -> DagSpec
-dagOfModel = dagFromModelGraph . collapseIndexedPlateNodes . buildModelGraph
-
--- | 'dagOfModel' のデータ束ね版 (PyMC で観測を渡してから @model_to_graphviz@ する形)。
--- @dat@ を 'bindCols' でモデルへ束ねてから DAG を組む = **データ駆動 plate のサイズが
--- 正しく出る**。 'hbmModel' と同じ束ね方だが **NUTS は走らない** (学習前のプレビュー)。
--- @noDf |>> toPlot (dagOfModelWith [("x", xs), ("y", ys)] m)@。
-dagOfModelWith :: [(Text, [Double])] -> ModelP () -> DagSpec
-dagOfModelWith dat = dagOfModel . bindCols dat
-
--- | 'ModelGraph' → plot-core DAG 描画仕様 ('dagOf' / 'dagOfRaw' の共通部)。
-dagFromModelGraph :: ModelGraph -> DagSpec
-dagFromModelGraph mg =
-  -- ★ renderDAG は dnX/dnY をそのまま使い layout を実行しない。 ゆえに描画前に
-  -- Sugiyama 階層 layout ('layoutHierarchicalFullWithPlates') で座標を確定させる
-  -- (これを省くと全ノードが原点 (0,0) に重なる)。
-  let (positioned, routed) = layoutHierarchicalFullWithPlates dnodes dedges dplates
-  -- ★ HS=PS parity: routing を spec へ焼き込む (= 'deRoute' 充填)。 これが無いと PS canvas
-  --   は 'deRoute = Nothing' で直線フォールバックになり、 HS の live routing (曲線) と乖離する。
-  --   baking は area 非依存 (dagToScreen が 0..1 domain を正規化 pt 空間へ map・描画時に
-  --   fitPrimsToArea で affine fit) なので layout 直後のここで焼ける。
-  in DagSpec $ bakeDAGRoutesInSpec $
-       layer (dagFromListsWithPlates positioned routed LayoutHierarchical dplates)
-  where
-    ns = mgNodes mg
-    dnodes = map toDNode ns
-    dedges = [ DAGEdge { deFrom = p, deTo = c, dePath = Nothing, deRoute = Nothing }
-             | (p, c) <- mgEdges mg ]
-    dplates = [ DAGPlate
-                  { dpLabel   = nm <> " (" <> T.pack (show sz) <> ")"
-                  , dpNodeIds = [ nodeName n | n <- ns, nm `elem` nodePlates n ] }
-              | (nm, sz) <- Map.toList (mgPlates mg) ]
-    toDNode n = DAGNode
-      { dnId    = nodeName n
-      , dnLabel = nodeName n
-      , dnKind  = case nodeKind n of
-                    LatentN        -> NodeLatent
-                    ObservedN _    -> NodeObserved
-                    DeterministicN -> NodeDeterministic
-                    -- Phase 60.4: NodeData は plot-core に既実装 (Phase 26 §E-6)
-                    DataN _        -> NodeData
-      , dnDist  = Just (nodeDist n)
-      , dnX     = 0
-      , dnY     = 0
-      }
-
--- | random-effect 第 @k@ 列の caterpillar plot。 BLUP を group ごとに取り、
--- **値で昇順ソート**して forest mark (errs=0 の点) で並べ、 0 に 'forestNull' 参照線。
-caterpillarColumn :: GLMMResultRE -> Int -> VisualSpec
-caterpillarColumn res k =
-  let cols   = LA.toColumns (reBLUPs res)
-      blups  = if k >= 0 && k < length cols then LA.toList (cols !! k) else []
-      groups = V.toList (reGroups res)
-      sorted = sortBy (comparing snd) (zip groups blups)
-      gs     = map fst sorted
-      es     = map snd sorted
-      zeros  = map (const (0 :: Double)) es
-  in layer (forest (inlineCat gs) (inline es) (inline zeros) <> forestNull 0)
-       <> title ("Random effects (col " <> T.pack (show k) <> ")")
-
-instance Plottable GLMMResultRE where
-  -- 代表 1 枚 = 第 1 列 (通常 random intercept) の caterpillar。
-  toPlot res = caterpillarColumn res 0
-  -- 診断束 = 全 r 列 (intercept + slope) の caterpillar。
-  diagnosticPlots res =
-    [ caterpillarColumn res k | k <- [0 .. LA.cols (reBLUPs res) - 1] ]
-
--- | HBM の事後予測平均 (epred) 応答曲面。 2 つの予測子 slot (@p1@, @p2@) を
---   grid で動かし、 各点で deterministic @muName@ の事後平均を取る
---   ('epredAt' の 2 変数版・O1 規約は 'renderEpred' の節を参照)。
---   ★コスト = grid 点数² × 全 draw のモデル評価。 既定 n=30 (900 点)。
-epredSurfaceOf :: HBMModel -> Text -> Text -> Text -> P3.VisualSpec3D
-epredSurfaceOf hbm p1 p2 muName =
-  epredSurfaceOfWith hbm p1 p2 muName defaultSurfaceOpts { soN = 30 }
-
-epredSurfaceOfWith :: HBMModel -> Text -> Text -> Text -> SurfaceOpts -> P3.VisualSpec3D
-epredSurfaceOfWith hbm p1 p2 muName opts =
-  let (xlo, xhi) = fromMaybe (epredPredRange hbm p1) (soXRange opts)
-      (ylo, yhi) = fromMaybe (epredPredRange hbm p2) (soYRange opts)
-      n     = max 2 (soN opts)
-      gxs   = linspace xlo xhi n
-      gys   = linspace ylo yhi n
-      draws = concatMap chainSamples (hbmChainsR hbm)
-      muAt gx gy =
-        let bound :: ModelP ()
-            bound = withData p1 [gx] (withData p2 [gy] (hbmModelSpec hbm))
-            mus   = [ v | ps <- draws
-                        , Just v <- [Map.lookup muName (runDeterministics bound ps)] ]
-        in if null mus then 0 else sum mus / fromIntegral (length mus)
-      grid = [ [ muAt gx gy | gx <- gxs ] | gy <- gys ]
-  in P3.layer3D ( P3.surface3DGrid grid
-               <> P3.xRange3D (xlo, xhi)
-               <> P3.yRange3D (ylo, yhi)
-               <> P3.colormap3D )
-
--- | 学習済 HBM が保持するデータ列 ('hbmData') から散布図層を作る (B10)。
---
--- @df |-> hbm cfg model@ で学習した後、 @dataScatterOf m \"x\" \"y\"@ で
--- 観測散布図を出せるので、 epred\/forest 等の抽出子と重畳するとき
--- **df を学習時 1 回だけ**書けばよい:
---
--- > let m = df |-> hbm defaultHBM model
--- > noDf |>> (dataScatterOf m "x" "y" <> toPlot (epred m "x" "mu"))
-dataScatterOf :: HBMModel -> Text -> Text -> VisualSpec
-dataScatterOf m xn yn =
-  case (lookup xn (hbmData m), lookup yn (hbmData m)) of
-    (Just xs, Just ys) -> layer (scatter (inline xs) (inline ys))
-    _                  -> mempty
diff --git a/src/Hanalyze/Plot/Core.hs b/src/Hanalyze/Plot/Core.hs
deleted file mode 100644
--- a/src/Hanalyze/Plot/Core.hs
+++ /dev/null
@@ -1,918 +0,0 @@
--- |
--- Module      : Hanalyze.Plot.Core
--- Description : hgg 連携層の共通基盤 (モデル族非依存のクラス・型・評価核)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- hgg 連携層の **共通基盤** (= モデル族非依存のクラス・型・評価核)。
---
--- ⚠ 本モジュールは親 'Hanalyze.Plot' と同じ cabal flag @plot-integration@
--- (既定 off) を on にしたときのみ build される。 @hgg-core@ に依存するため
--- **upstream hanalyze には cherry-pick しない**。
---
--- ここに集約するもの (Phase 71.4 確定):
---
---   * 図化能力の最終クラス 'Plottable'、 grid 評価クラス 'SingleVarModel' /
---     'MultiVarModel'、 分類器抽象 'ClassPredict'。
---   * grid 評価の仕様 'ModelSpec' (Semigroup\/Monoid)・確定オプション 'GridOpts'・
---     ブートストラップ素材 'BootKit'、 および @statModel@\/@grid@\/@bandMode@ 等の
---     合成子 (smart ctor)。
---   * grid 評価核 ('renderGrid' \/ 'renderGridMulti' \/ 'bootstrapBands' \/ 'evalFrame'
---     系)・応答曲面核 ('surfaceGrid' \/ 'surfaceOf' 系) と、 複数のモデル族が共有する
---     描画 helper。
---
--- 各モデル族固有の @instance Plottable XxxModel@ 等は親 'Hanalyze.Plot' 側に
--- 残置する (orphan instance を許容: クラスは Core・instance は Plot・型は Wrappers)。
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE FlexibleContexts #-}
-module Hanalyze.Plot.Core
-  ( -- * Plottable protocol
-    Plottable (..)
-    -- * ルート1 grid 評価 (ModelSpec)
-  , ModelSpec (..)
-  , GridOpts (..)
-  , BootKit (..)
-  , SingleVarModel (..)
-  , MultiVarModel (..)
-    -- ** 合成子 (smart ctor)
-  , statModel
-  , grid
-  , gridRange
-  , bandMode
-  , piMethod
-  , statColor
-  , statFill
-  , statLinetype
-  , statLinewidth
-  , statAlpha
-  , statLabel
-  , statEquation
-  , statR2
-  , statLevel
-  , holdAt
-  , byVar
-  , predAt
-  , statModelMulti
-    -- ** 描画 deco / 凡例 helper
-  , goLineDeco
-  , goBandDeco
-  , labelLegend
-  , fitLabelText
-    -- ** grid 評価核
-  , bootstrapBands
-  , renderGrid
-  , renderGridMulti
-  , marginalizeCurve
-  , alongRange
-  , evalFrame
-  , setAlong
-  , isResponseRole
-  , holdRole
-  , fixedRole
-  , clampIdx
-  , effectPalette
-    -- * 応答曲面 3D 核
-  , evalFrame2
-  , surfaceGrid
-  , chunkRows
-  , surfaceOf
-  , surfaceOfWith
-  , dataScatter3DOf
-    -- * 集約 helper (連続列の代表値)
-  , meanV
-  , medianV
-  , modeV
-  , modeIdx
-  , mostCommon
-    -- * 共有描画 helper (複数族が利用)
-  , defaultCILevel
-  , quantilePalette
-  , stepVerts
-  , gridCurves
-  , importanceBar
-  , matCols2
-  , classMeansScatter
-  , classMeansScatterNamed
-  , chainColor
-    -- * 分類器抽象
-  , ClassPredict (..)
-    -- * 回帰診断の可視化 (係数 forest / 実測vs予測) — Phase 72.4/72.5
-  , HasObsPred (..)
-  , obsVsPred
-  , obsPredSpec
-  , coefForest
-  ) where
-
-import           Control.Applicative   ((<|>))
-import           Data.List             (group, maximumBy, sort, transpose)
-import           Data.Maybe            (fromMaybe)
-import           Data.Ord              (comparing)
-import           Data.Word             (Word32)
-import qualified Data.Vector           as V
-import           System.Random.MWC     (initialize, uniformR)
-import           Control.Monad.ST      (runST)
-import           Control.Monad         (replicateM)
-import           Hanalyze.Model.HBM.Interp (percentileOf)
-import           Hanalyze.Model.HBM.Sampling (sampleDist)
-import qualified Hanalyze.Model.HBM.Distribution as BD
-import qualified Numeric.LinearAlgebra as LA
-
-import           Data.Text             (Text)
-import qualified Data.Text             as T
-
-import           Hgg.Plot.Spec     ( VisualSpec, Layer, layer, inline, inlineCat
-                                       , Color (..), fromHex
-                                       , scatter, line, band
-                                       , shape, MarkShape (..)
-                                       , color, colorBy, lineRange, bar
-                                       , scaleColorManual, legend
-                                       , forest, forestNull
-                                       , xLabel, yLabel
-                                       , LineType (..)
-                                       , linetype, alpha, stroke )
-import           Hanalyze.Diagnostics ( CoefRow (..), HasCoefSummary (..) )
-import           Hgg.Plot.Unit     (pt', (*~))
-import qualified Hgg.Plot.ThreeD.Spec  as P3
-import           Hgg.Plot.ThreeD.Types (Point3 (..))
-import           Hgg.Plot.Color        (toCss)
-
-import           Hanalyze.Model.Wrappers
-import           Hanalyze.Model.Formula.Frame   (ModelFrame (..), VarRole (..))
-import           Hanalyze.Model.LM     (linspace)
-import           Numeric               (showFFloat)
-
--- ===========================================================================
--- Plottable protocol
--- ===========================================================================
-
--- | 解析オブジェクトを図 ('VisualSpec') に変換できる能力。
---
--- 能力差は中立 protocol ('Hanalyze.Model.Core' の 'ResidualModel' /
--- 'PredictiveModel') 側に持たせ、 ここは「図にできる」 という最終能力のみを表す。
-class Plottable m where
-  -- | 代表 1 枚の図 (= layer 重畳の主役、 @<>@ で他 layer と合成可)。
-  toPlot          :: m -> VisualSpec
-
-  -- | 診断図の束 (= レポート用)。 既定は代表 1 枚のみ。
-  diagnosticPlots :: m -> [VisualSpec]
-  diagnosticPlots m = [toPlot m]
-
--- ===========================================================================
--- ルート1 grid 評価 (ModelSpec) — Phase 16 §3 C1
---
--- fit 済モデルの回帰曲線・CI 帯を **訓練点ではなく等間隔 grid** で評価して描く。
--- 疎・不均一データで曲線がガタつくのを解消する (散布図の点は従来通り訓練データ)。
--- 'statModel' で 'ModelSpec' を作り、 @<>@ でオプションを足す:
---
--- > df |>> (layer (scatter "x" "y") <> toPlot (statModel m <> grid 200))
---
--- 'ModelSpec' は Monoid。 学習済モデル @m@ はクロージャに閉じ込め、 予測は
--- 'toPlot' (描画時) に grid 評価する (ユーザ直感「m は学習・layer で予測」)。
--- ===========================================================================
-
--- | grid 評価の確定オプション ('ModelSpec' の Maybe field を既定で埋めたもの)。
-data GridOpts = GridOpts
-  { goN       :: Int                    -- ^ 評価点数 (既定 100)。
-  , goRange   :: Maybe (Double, Double) -- ^ 評価範囲 (既定 = 説明変数 min/max)。
-  , goLevel   :: Double                 -- ^ CI 水準 (既定 0.95)。
-  , goBandMode :: BandMode              -- ^ 帯モード (既定 'BandCI'。 Phase 70.F)。
-  , goPIMethod :: PIMethod              -- ^ 帯の算出法 (既定 'PIClosedForm'。 Phase 70.H)。
-  , goPredAt  :: [Double]               -- ^ 予測点 x のリスト (C2)。
-  , goHoldAt  :: HoldAgg                 -- ^ 多変量 effect の他変数固定方式 (既定 Mean, C3)。
-  , goByVar   :: Maybe (Text, [Double])  -- ^ 層別 = 第2変数を複数値で固定 (C3)。
-  , goColor   :: Maybe Color             -- ^ 線の固定色 (statColor, A2)。
-  , goFill    :: Maybe Color             -- ^ 帯の塗り色 (statFill, A2)。
-  , goLinetype  :: Maybe LineType        -- ^ 線種 (statLinetype, A2)。
-  , goLinewidth :: Maybe Double          -- ^ 線幅 = stroke (statLinewidth, A2)。
-  , goAlpha   :: Maybe Double            -- ^ 帯/線の透明度 (statAlpha, A2)。
-  , goLabel   :: Maybe Text              -- ^ 単線の凡例ラベル (statLabel, A3)。
-  , goShowEq  :: Bool                    -- ^ 回帰式を凡例ラベルに出す (statEquation, A8)。
-  , goShowR2  :: Bool                    -- ^ R² を凡例ラベルに出す (statR2, A8)。
-  }
-
--- | grid 上で曲線評価できる単変数モデル。 'statModel' が要求する能力。
-class SingleVarModel m where
-  -- | 生 predictor の範囲 (grid 既定範囲の算出元)。
-  svRange :: m -> (Double, Double)
-  -- | 信頼水準と grid x 列から (中心 μ̂, 帯 @(lo, hi)@) を評価する。
-  -- band を持たないモデル (GAM/Robust) は 'Nothing'。
-  svGrid  :: m -> Double -> [Double] -> ([Double], Maybe ([Double], [Double]))
-  -- | **予測区間** (PI) 帯 @(lo, hi)@ を評価する (A5)。 観測分散 σ̂² を持つモデル
-  -- (LM・Gaussian/Identity GLM) のみ実装し、 それ以外は既定の 'Nothing' (= PI 非提供)。
-  -- 中心 μ̂ は 'svGrid' と共通ゆえここでは帯のみ返す。
-  svGridPI :: m -> Double -> [Double] -> Maybe ([Double], [Double])
-  svGridPI _ _ _ = Nothing
-  -- | 当てはめ係数 @[β₀, β₁]@ と R² (A8 の式/R² 凡例注釈用)。 線形で「式」 の意味が
-  -- 明快なモデル (LM) のみ実装し、 それ以外は既定の 'Nothing' (= 式注釈を出さない)。
-  -- GLM は係数が η (リンク) スケールゆえ @y = β₀ + β₁x@ の素朴な式が成り立たず Nothing。
-  svCoefR2 :: m -> Maybe ([Double], Double)
-  svCoefR2 _ = Nothing
-  -- | ブートストラップ ('piMethod (PIBootstrap …)') 用の素材。 訓練 (x, y)・再標本化データで
-  -- refit する関数・新規観測の分布 (GLM family。 'Nothing' = 加法残差) を束ねて返す。 既定
-  -- 'Nothing' (= ブートストラップ非対応 → 閉形式へフォールバック)。 closed-form を持たない
-  -- モデル (非 Gaussian GLM / ロバスト) でも、 これを実装すれば PI を出せる。
-  svBootKit :: m -> Maybe (BootKit m)
-  svBootKit _ = Nothing
-
--- | ブートストラップに必要な素材 ('svBootKit' が返す。 内部利用)。
-data BootKit m = BootKit
-  { bkX       :: [Double]                              -- ^ 訓練 x。
-  , bkY       :: [Double]                              -- ^ 訓練 y。
-  , bkRefit   :: [Double] -> [Double] -> m             -- ^ 再標本化 (x, y) で refit。
-  , bkObsDist :: Maybe (Double -> BD.Distribution Double) -- ^ 新規観測の分布 (GLM)。 Nothing=加法残差。
-  }
-
--- | grid 評価の仕様。 'statModel' で生成し @<>@ でオプション合成 (Monoid)。
--- @msRender@ にモデルの grid 評価関数をクロージャで保持する (= 案A)。
-data ModelSpec = ModelSpec
-  { msRender  :: Maybe (GridOpts -> VisualSpec)  -- ^ statModel が設定 (先勝ち)。
-  , msN       :: Maybe Int                       -- ^ grid 点数 (後勝ち)。
-  , msRange   :: Maybe (Double, Double)          -- ^ grid 範囲 (後勝ち)。
-  , msLevel   :: Maybe Double                    -- ^ CI 水準 (後勝ち)。
-  , msBandMode :: Maybe BandMode                 -- ^ 帯モード (後勝ち、 既定 'BandCI'。 Phase 70.F
-                                                 --   で帯 ON/OFF と CI/PI を 'bandMode' 1 本に統合)。
-  , msPIMethod :: Maybe PIMethod                 -- ^ 帯の算出法 (後勝ち、 既定 'PIClosedForm'。 Phase 70.H)。
-  , msPredAt  :: [Double]                        -- ^ 予測点 x (リスト累積 ++)。
-  , msHoldAt  :: Maybe HoldAgg                   -- ^ 多変量 effect の固定方式 (後勝ち、 既定 Mean)。
-  , msByVar   :: Maybe (Text, [Double])          -- ^ 層別変数 (後勝ち)。
-  , msColor   :: Maybe Color                      -- ^ 線の固定色 (後勝ち, A2)。
-  , msFill    :: Maybe Color                      -- ^ 帯の塗り色 (後勝ち, A2)。
-  , msLinetype  :: Maybe LineType                 -- ^ 線種 (後勝ち, A2)。
-  , msLinewidth :: Maybe Double                   -- ^ 線幅 = stroke (後勝ち, A2)。
-  , msAlpha   :: Maybe Double                      -- ^ 帯/線の透明度 (後勝ち, A2)。
-  , msLabel   :: Maybe Text                         -- ^ 単線の凡例ラベル (後勝ち, A3)。
-  , msShowEq  :: Bool                                 -- ^ 回帰式を凡例に出す (Any, A8)。
-  , msShowR2  :: Bool                                 -- ^ R² を凡例に出す (Any, A8)。
-  }
-
-instance Semigroup ModelSpec where
-  a <> b = ModelSpec
-    { msRender  = msRender a <|> msRender b      -- モデルは先勝ち (通常 1 個)
-    , msN       = msN b      <|> msN a           -- オプションは後勝ち
-    , msRange   = msRange b  <|> msRange a
-    , msLevel   = msLevel b  <|> msLevel a
-    , msBandMode = msBandMode b <|> msBandMode a  -- 帯モードは後勝ち
-    , msPIMethod = msPIMethod b <|> msPIMethod a  -- 算出法も後勝ち
-    , msPredAt  = msPredAt a ++ msPredAt b       -- 予測点はリスト累積
-    , msHoldAt  = msHoldAt b  <|> msHoldAt a
-    , msByVar   = msByVar b   <|> msByVar a
-    , msColor   = msColor b     <|> msColor a     -- aes は後勝ち
-    , msFill    = msFill b      <|> msFill a
-    , msLinetype  = msLinetype b  <|> msLinetype a
-    , msLinewidth = msLinewidth b <|> msLinewidth a
-    , msAlpha   = msAlpha b     <|> msAlpha a
-    , msLabel   = msLabel b     <|> msLabel a
-    , msShowEq   = msShowEq a || msShowEq b           -- 注釈はオプトイン (Any)
-    , msShowR2   = msShowR2 a || msShowR2 b
-    }
-
-instance Monoid ModelSpec where
-  mempty = ModelSpec
-    { msRender = Nothing, msN = Nothing, msRange = Nothing, msLevel = Nothing
-    , msBandMode = Nothing, msPIMethod = Nothing, msPredAt = [], msHoldAt = Nothing, msByVar = Nothing
-    , msColor = Nothing, msFill = Nothing, msLinetype = Nothing
-    , msLinewidth = Nothing, msAlpha = Nothing, msLabel = Nothing
-    , msShowEq = False, msShowR2 = False }
-
--- | 学習済の単変数モデルから grid 評価 'ModelSpec' を作る (along 不要)。
-statModel :: SingleVarModel m => m -> ModelSpec
-statModel m = mempty { msRender = Just (renderGrid m) }
-
--- | grid 評価点数を指定 (既定 100)。
-grid :: Int -> ModelSpec
-grid n = mempty { msN = Just n }
-
--- | grid 評価範囲を指定 (既定 = 説明変数 min/max)。
-gridRange :: Double -> Double -> ModelSpec
-gridRange lo hi = mempty { msRange = Just (lo, hi) }
-
--- | 出す帯を 1 つの値で選ぶ (Phase 70.F で帯 ON/OFF と CI/PI を統合)。 'BandMode' は
---   @BandOff@ (なし) \/ @BandCI@ (既定・信頼区間) \/ @BandPI@ (予測区間) \/ @BandCIPI@
---   (入れ子)。 既定 (未指定) は @BandCI@。 PI 非提供モデルでは PI 系は CI へフォールバック。
---
---   @statModel m \<\> bandMode BandPI@ \/ @… \<\> bandMode BandCIPI@ \/ @… \<\> bandMode BandOff@。
-bandMode :: BandMode -> ModelSpec
-bandMode m = mempty { msBandMode = Just m }
-
--- | 帯 (CI/PI) の**算出法**を選ぶ (Phase 70.H)。 @bandMode@ が「どの帯を出すか」を選ぶのに対し、
---   @piMethod@ は「どう計算するか」を選ぶ直交軸:
---
---     * @PIClosedForm@   = 閉形式 (Wald / 基底空間 OLS。 **既定**)。
---     * @PIBootstrap seed draws@ = case-resampling ブートストラップ (seed で決定的)。
---       閉形式 CI/PI を持たないモデル (非 Gaussian GLM / ロバスト) でも PI を出せる。
---
---   @statModel m \<\> bandMode BandPI \<\> piMethod (PIBootstrap 42 2000)@。
-piMethod :: PIMethod -> ModelSpec
-piMethod p = mempty { msPIMethod = Just p }
-
--- | 回帰線の固定色 (ggplot @geom_smooth(color=)@, A2)。 凡例は付かない (単線命名は 'statLabel')。
---   型安全な 'Color' を受ける (plot-core の 'color' と同じ方針)。 @statColor (fromHex "#ff0000")@
---   / @statColor N.red@ / @statColor (rgb 255 0 0)@。 Text→Color は 'fromHex' に委ねる。
-statColor :: Color -> ModelSpec
-statColor c = mempty { msColor = Just c }
-
--- | CI 帯の塗り色 (ggplot @geom_smooth(fill=)@, A2)。 型安全な 'Color' を受ける。
-statFill :: Color -> ModelSpec
-statFill c = mempty { msFill = Just c }
-
--- | 回帰線の線種 (ggplot @geom_smooth(linetype=)@, A2)。 'LineType' = 'LtSolid' / 'LtDashed' 等。
-statLinetype :: LineType -> ModelSpec
-statLinetype lt = mempty { msLinetype = Just lt }
-
--- | 回帰線の太さ (= stroke 幅。 ggplot @geom_smooth(linewidth=)@, A2)。
-statLinewidth :: Double -> ModelSpec
-statLinewidth w = mempty { msLinewidth = Just w }
-
--- | 帯/線の透明度 (ggplot @geom_smooth(alpha=)@, A2)。 帯に適用 (薄い塗り潰しの ggplot 流)。
-statAlpha :: Double -> ModelSpec
-statAlpha a = mempty { msAlpha = Just a }
-
--- | 単線に凡例ラベルを付ける (A3)。 1 群カテゴリ ('ColorByCol') + 'scaleColorManual' で
--- 色を固定し凡例エントリを 1 つ出す (固定色 'color' は @hasColorEncoding=False@ で
--- 凡例が出ない罠を回避)。 色は 'statColor' があればそれ、 なければ既定パレット先頭。
--- ★モデル比較で各線に名前を付ける用途 (= 群数 1 の 'byGroup' 特殊形)。
-statLabel :: Text -> ModelSpec
-statLabel lbl = mempty { msLabel = Just lbl }
-
--- | 回帰式を凡例ラベルに出す (A8。 ggplot @ggpubr::stat_regline_equation@ 相当)。
--- 'svCoefR2' を持つモデル (LM) で @y = β₀ + β₁x@ を自動生成し A3 機構 (凡例) に載せる。
--- 明示 'statLabel' があればそちらを優先。 式の出せないモデル (GLM 等) では注釈なし。
--- 'statR2' と併用すると @y = … + …x, R² = …@ のように 1 ラベルに連結する。
-statEquation :: ModelSpec
-statEquation = mempty { msShowEq = True }
-
--- | R² を凡例ラベルに出す (A8。 ggplot @ggpubr::stat_cor(aes(label=..rr.label..))@ 相当)。
--- 'svCoefR2' を持つモデル (LM) の R² を @R² = 0.987@ の形で凡例に載せる。
-statR2 :: ModelSpec
-statR2 = mempty { msShowR2 = True }
-
--- | CI 水準を指定 (既定 0.95)。
-statLevel :: Double -> ModelSpec
-statLevel l = mempty { msLevel = Just l }
-
--- | 多変量 effect で along 以外の説明変数の固定方式を指定 (既定 'Mean', C3)。
-holdAt :: HoldAgg -> ModelSpec
-holdAt h = mempty { msHoldAt = Just h }
-
--- | 層別 = 第2変数 @v@ を複数値 @vals@ で固定し、 値ごとに 1 曲線を色分け重畳する
--- (R @ggpredict@ terms 第2項相当, C3)。 多変量モデル ('statModelMulti') 専用。
-byVar :: Text -> [Double] -> ModelSpec
-byVar v vals = mempty { msByVar = Just (v, vals) }
-
--- | 予測点を 1 つ足す (C2)。 @<>@ でリスト累積 → @… <> predAt 1 <> predAt 3@ で複数点。
--- 各点は μ̂ (scatter) + CI 区間 [lo, hi] (lineRange) で描かれる (band を持たない GAM/
--- Robust は μ̂ 点のみ)。 単変数モデル前提 (多変量 effect は C3 の statModelMulti で対応)。
-predAt :: Double -> ModelSpec
-predAt x = mempty { msPredAt = [x] }
-
--- | A2/A3: 線レイヤへ aes (色・線種・太さ) を適用。 色の決定順は
--- (1) 群色 @mCol@ (byVar) → 'color'、 (2) 'statLabel' (@goLabel@) → 1 群 'colorBy'
--- (凡例を出すため・@n@ 点ぶんのカテゴリ列)、 (3) 'statColor' → 'color'。
--- 線種・太さは色と独立に適用。 @n@ = grid 点数 (label カテゴリ列の長さ)。
-goLineDeco :: GridOpts -> Maybe Color -> Int -> Layer -> Layer
-goLineDeco o mCol n l =
-  let colorL = case (mCol, goLabel o) of
-        (Just c, _)         -> color c                                   -- 群色優先
-        (Nothing, Just lbl) -> colorBy (inlineCat (replicate n lbl))      -- statLabel: ColorByCol で凡例
-        (Nothing, Nothing)  -> maybe mempty color (goColor o)            -- statColor or 無色
-  in l <> colorL
-       <> maybe mempty linetype (goLinetype o)
-       <> maybe mempty (\lw -> stroke (lw *~ pt')) (goLinewidth o)
-
--- | A2: 帯レイヤへ fill 色・透明度を適用。 群色 @mCol@ があれば fill は群色を優先 ('statFill' で上書き不可)。
-goBandDeco :: GridOpts -> Maybe Color -> Layer -> Layer
-goBandDeco o mCol b =
-  b <> maybe mempty color (mCol <|> goFill o)
-    <> maybe mempty alpha       (goAlpha o)
-
--- | A3: 'statLabel' があれば @scaleColorManual@ で色を固定し @legend@ を出す 'VisualSpec'。
--- 色は 'statColor' (@goColor@) 優先・なければ既定パレット先頭。 ラベル無しは空。
-labelLegend :: GridOpts -> VisualSpec
-labelLegend o = case goLabel o of
-  Just lbl -> scaleColorManual [(lbl, maybe (head effectPalette) toCss (goColor o))] <> legend
-  Nothing  -> mempty
-
--- | A8: 式/R² 凡例ラベル文字列を組む。 @showEq@ で @y = β₀ + β₁x@、 @showR2@ で
--- @R² = 0.987@ を入れ、 両方なら @", "@ で連結する。 係数は単回帰 @[β₀, β₁]@ を想定
--- (β₁ の符号で @+@/@-@ を切替)。 どちらの flag も立っていなければ 'Nothing'。
-fitLabelText :: Bool -> Bool -> [Double] -> Double -> Maybe Text
-fitLabelText showEq showR2 coefs r2 =
-  let f3 x = T.pack (showFFloat (Just 3) x "")          -- 小数 3 桁固定
-      eqPart = case coefs of
-        (b0 : b1 : _) ->
-          let sgn = if b1 < 0 then " − " else " + "
-          in "y = " <> f3 b0 <> sgn <> f3 (abs b1) <> "x"
-        [b0]          -> "y = " <> f3 b0
-        _             -> "y = ?"
-      r2Part = "R² = " <> f3 r2
-      parts  = [ eqPart | showEq ] ++ [ r2Part | showR2 ]
-  in if null parts then Nothing else Just (T.intercalate ", " parts)
-
--- | case-resampling ブートストラップで grid 上の CI / PI 帯を計算する (Phase 70.H)。
---   訓練 (x, y) を seed 付きで再標本化 → 'bkRefit' で refit → 'svGrid' で grid μ を予測、
---   を @draws@ 回。 CI = μ_b の分位点 (係数の不確実性)。 PI = 新規観測 y* の分位点
---   (加法残差 'bkObsDist'=Nothing、 または Family(μ) からの parametric ドロー)。 seed 純粋
---   (runST + mwc・同 seed でビット同一)。 戻り = (CI (lo,hi), PI (lo,hi))。
-bootstrapBands :: SingleVarModel m
-               => m -> BootKit m -> Word32 -> Int -> Double -> [Double]
-               -> (([Double], [Double]), ([Double], [Double]))
-bootstrapBands m kit seed draws level gxs =
-  let xs    = V.fromList (bkX kit)
-      ys    = V.fromList (bkY kit)
-      n     = V.length xs
-      ng    = length gxs
-      a2    = (1 - level) / 2
-      resid = V.fromList (zipWith (-) (bkY kit) (fst (svGrid m level (bkX kit))))
-      paths = runST $ do
-        gen <- initialize (V.singleton seed)
-        replicateM draws $ do
-          idx <- replicateM n (uniformR (0, n - 1) gen)
-          let xs' = [ xs V.! i | i <- idx ]
-              ys' = [ ys V.! i | i <- idx ]
-              muB = fst (svGrid (bkRefit kit xs' ys') level gxs)
-          pis <- case bkObsDist kit of
-            Just toDist -> mapM (\mu -> sampleDist (toDist mu) gen) muB
-            Nothing     -> mapM (\mu -> do j <- uniformR (0, n - 1) gen
-                                           pure (mu + resid V.! j)) muB
-          pure (muB, pis)
-      muT = transpose (map fst paths)   -- ng × draws
-      piT = transpose (map snd paths)
-      q lo xss = map (percentileOf lo) xss
-  in if n < 2 || ng == 0
-       then (([], []), ([], []))
-       else ( (q a2 muT, q (1 - a2) muT), (q a2 piT, q (1 - a2) piT) )
-
--- | grid 評価して曲線 (+ 帯) + 予測点の 'VisualSpec' を組む。 'statModel' がクロージャ化。
--- 帯がある場合は @band@ を先に置き @line@ (μ̂ 曲線) を上に重ねる。 予測点 (goPredAt) は
--- CI 区間を @lineRange@ (縦線 [lo,hi]) + μ̂ を @scatter@ で重ね、 μ̂ が区間内のどこにあるか
--- (非対称な GLM 帯でも) 忠実に示す。
-renderGrid :: SingleVarModel m => m -> GridOpts -> VisualSpec
-renderGrid m opts0 =
-  -- A8: statEquation/statR2 が立っていれば svCoefR2 から式/R² 文字列を作り、
-  -- A3 と同じ凡例経路 (goLabel) に流す。 明示 statLabel が優先 (上書きしない)。
-  let autoLabel = case (goShowEq opts0 || goShowR2 opts0, svCoefR2 m) of
-        (True, Just (coefs, r2)) -> fitLabelText (goShowEq opts0) (goShowR2 opts0) coefs r2
-        _                        -> Nothing
-      opts = case goLabel opts0 of
-        Just _  -> opts0                              -- 明示ラベル優先
-        Nothing -> opts0 { goLabel = autoLabel }
-      (lo0, hi0) = svRange m
-      (lo, hi)   = fromMaybe (lo0, hi0) (goRange opts)
-      n          = max 2 (goN opts)
-      gxs        = linspace lo hi n
-      (mu, mbCIcf) = svGrid m (goLevel opts) gxs
-      -- 帯の算出法 (Phase 70.H): 既定 closed-form、 PIBootstrap で case-resampling。
-      -- bootstrap は CI/PI を両方その場で計算 ('svBootKit' を持つモデルのみ。 無ければ
-      -- closed-form へフォールバック)。 中心曲線 mu は元の当てはめのまま。
-      (mbCI, mbPI) = case goPIMethod opts of
-        PIBootstrap seed draws
-          | Just kit <- svBootKit m ->
-              let (ci, pii) = bootstrapBands m kit seed draws (goLevel opts) gxs
-              in (Just ci, Just pii)
-        _ -> (mbCIcf, svGridPI m (goLevel opts) gxs)
-      -- 帯モードで CI/PI/両方/なしを描く (Phase 70.F)。 PI 非提供は CI へフォールバック。
-      lineL    = layer (goLineDeco opts Nothing n (line (inline gxs) (inline mu)))
-      bandL deco mb = case mb of
-        Just (los, his) -> layer (deco (band (inline gxs) (inline los) (inline his)))
-        Nothing         -> mempty
-      ciDeco = goBandDeco opts Nothing
-      -- 入れ子時の PI 帯は薄め (CI が内側で見えるように)。
-      piA    = maybe 0.10 (* 0.5) (goAlpha opts)
-      piDeco = goBandDeco (opts { goAlpha = Just piA }) Nothing
-      curve = case goBandMode opts of
-        BandOff  -> lineL
-        BandCI   -> bandL ciDeco mbCI <> lineL
-        BandPI   -> case mbPI of
-                      Just _  -> bandL ciDeco mbPI <> lineL   -- PI 単独 (通常の濃さ)
-                      Nothing -> bandL ciDeco mbCI <> lineL   -- PI 非提供 → CI
-        BandCIPI -> case mbPI of
-                      Just _  -> bandL piDeco mbPI            -- 外: PI 薄 (下)
-                              <> bandL ciDeco mbCI            -- 内: CI 濃 (上)
-                              <> lineL
-                      Nothing -> bandL ciDeco mbCI <> lineL   -- PI 非提供 → CI のみ
-      pts = goPredAt opts
-      predLayers
-        | null pts  = mempty
-        | otherwise =
-            let (pmu, pmb) = svGrid m (goLevel opts) pts
-            in case pmb of
-                 Just (plos, phis) ->
-                   let mids  = zipWith (\l h -> (l + h) / 2) plos phis
-                       halfs = zipWith (\l h -> (h - l) / 2) plos phis
-                   in layer (lineRange (inline pts) (inline mids) (inline halfs))
-                        <> layer (scatter (inline pts) (inline pmu))
-                 Nothing -> layer (scatter (inline pts) (inline pmu))
-  in curve <> predLayers <> labelLegend opts
-
--- ★案B: 既存 'Plottable' の 'toPlot' を 'ModelSpec' にも overload (同綴り)。
-instance Plottable ModelSpec where
-  toPlot ms = case msRender ms of
-    Nothing -> mempty   -- モデル未設定 (オプションのみ) は空図。
-    Just f  -> f GridOpts
-      { goN       = fromMaybe 100 (msN ms)
-      , goRange   = msRange ms
-      , goLevel   = fromMaybe 0.95 (msLevel ms)
-      , goBandMode = fromMaybe BandCI (msBandMode ms)
-      , goPIMethod = fromMaybe PIClosedForm (msPIMethod ms)
-      , goPredAt  = msPredAt ms
-      , goHoldAt  = fromMaybe Mean (msHoldAt ms)
-      , goByVar   = msByVar ms
-      , goColor     = msColor ms
-      , goFill      = msFill ms
-      , goLinetype  = msLinetype ms
-      , goLinewidth = msLinewidth ms
-      , goAlpha     = msAlpha ms
-      , goLabel     = msLabel ms
-      , goShowEq     = msShowEq ms
-      , goShowR2     = msShowR2 ms
-      }
-
--- ===========================================================================
--- 多変量 effect plot (Phase 16 §3 C3)
---
--- 単変数 grid 評価 (C1) を多変量モデルへ一般化する。 along 変数を grid で動かし、
--- 他の説明変数を 'HoldAgg' で固定した「評価点 ModelFrame」 を合成して、 訓練 formula の
--- 'designMatrixF' で評価点設計行列を組み CI を評価する。
---
--- ★評価点 ModelFrame の合成は **DataFrame を経由せず VarRole を直接差し替える**
--- ('designMatrixF' は 'mfRoles' のみ参照し応答列は使わない = Design.hs:331)。 列構造・
--- 順序が訓練と完全一致するので 'confidenceBandAt' / 'predictGlmMuWithCI' がそのまま使える。
--- 型で単/多変量を分離し ('SingleVarModel' / 'MultiVarModel')、 along 忘れをコンパイル時に弾く。
--- ===========================================================================
-
--- | along を必須引数に持つ多変量モデル。 'statModelMulti' が要求する能力。
-class MultiVarModel m where
-  -- | 訓練 'ModelFrame' (along の range と他変数の集約元)。
-  mvFrame     :: m -> ModelFrame
-  -- | 評価点 'ModelFrame' から (中心 μ̂, CI 帯 @(lo, hi)@) を評価する。
-  --   設計行列が組めない場合は空 + 'Nothing'。
-  mvEvalFrame :: m -> Double -> ModelFrame -> ([Double], Maybe ([Double], [Double]))
-  -- | 評価点での予測区間 (PI)。 既定 'Nothing' (PI 非提供)。 closed-form PI を持つ
-  --   モデル ('MultiLMModel' = 多変量 OLS) のみ override する ('svGridPI' と同じ方針)。
-  mvEvalFramePI :: m -> Double -> ModelFrame -> Maybe ([Double], [Double])
-  mvEvalFramePI _ _ _ = Nothing
-
--- | 学習済の多変量モデルと along 変数から effect plot の 'ModelSpec' を作る。
---   along は **必須引数** (型で単/多変量を分離し誤用を弾く)。
---   @df |>> (layer (scatter \"x1\" \"y\") <> toPlot (statModelMulti m (along \"x1\") <> holdAt Median))@。
-statModelMulti :: MultiVarModel m => m -> AlongSpec -> ModelSpec
-statModelMulti m (AlongSpec v) = mempty { msRender = Just (renderGridMulti m v) }
-
--- | effect plot の 'VisualSpec' を組む。 along を grid で動かし他変数を 'HoldAgg' で固定。
---   byVar があれば第2変数の各値で曲線を色分け重畳する。 'statModelMulti' がクロージャ化。
-renderGridMulti :: MultiVarModel m => m -> Text -> GridOpts -> VisualSpec
-renderGridMulti m alongV opts =
-  let mf         = mvFrame m
-      (lo0, hi0) = alongRange mf alongV
-      (lo, hi)   = fromMaybe (lo0, hi0) (goRange opts)
-      n          = max 2 (goN opts)
-      gxs        = linspace lo hi n
-      level      = goLevel opts
-      hold       = goHoldAt opts
-      -- 1 曲線分 (override = byVar 固定, mCol = 線色)。
-      oneCurve override mCol =
-        case hold of
-          Marginalize -> marginalizeCurve opts m alongV level gxs override mCol
-          _ ->
-            let ef         = evalFrame mf alongV hold override gxs
-                (mu, mbCI) = mvEvalFrame m level ef
-                mbPI       = mvEvalFramePI m level ef
-                lineL      = layer (goLineDeco opts mCol n (line (inline gxs) (inline mu)))
-                bL deco mb = case mb of
-                  Just (los, his) -> layer (deco (band (inline gxs) (inline los) (inline his)))
-                  Nothing         -> mempty
-                ciDeco = goBandDeco opts mCol
-                piA    = maybe 0.10 (* 0.5) (goAlpha opts)
-                piDeco = goBandDeco (opts { goAlpha = Just piA }) mCol
-                bands  = case goBandMode opts of
-                  BandOff  -> mempty
-                  BandCI   -> bL ciDeco mbCI
-                  BandPI   -> case mbPI of
-                                Just _  -> bL ciDeco mbPI
-                                Nothing -> bL ciDeco mbCI       -- PI 非提供 → CI
-                  BandCIPI -> case mbPI of
-                                Just _  -> bL piDeco mbPI <> bL ciDeco mbCI
-                                Nothing -> bL ciDeco mbCI       -- PI 非提供 → CI のみ
-            in bands <> lineL
-  in case goByVar opts of
-       Nothing          -> oneCurve [] Nothing <> labelLegend opts
-       Just (v2, vals)  ->
-         foldMap
-           (\(i, val) ->
-              let col = fromHex (effectPalette !! (i `mod` length effectPalette))
-              in oneCurve [(v2, val)] (Just col))
-           (zip [0 :: Int ..] vals)
-
--- | Marginalize (PDP/AME): 各 grid 点で along=gx に固定し他変数は **観測分布のまま**、
---   μ̂ を全観測行で平均する (band なし・曲線のみ。 全観測行 × grid で重い)。
-marginalizeCurve :: MultiVarModel m
-                 => GridOpts -> m -> Text -> Double -> [Double] -> [(Text, Double)] -> Maybe Color -> VisualSpec
-marginalizeCurve opts m alongV level gxs override mCol =
-  let mf   = mvFrame m
-      nObs = mfNRows mf
-      base = mf { mfRoles = [ (nm, baseRole nm r) | (nm, r) <- mfRoles mf ] }
-      baseRole nm r
-        | isResponseRole r              = RoleResponse (V.replicate nObs 0)
-        | Just fv <- lookup nm override = fixedRole r nObs fv
-        | otherwise                     = r                       -- 観測分布のまま
-      muAt gx =
-        let (mu, _) = mvEvalFrame m level (setAlong base alongV gx)
-        in sum mu / fromIntegral (max 1 (length mu))
-      mus  = map muAt gxs
-  in layer (goLineDeco opts mCol (length gxs) (line (inline gxs) (inline mus)))
-
--- | along 変数の観測範囲 (effect grid の既定範囲)。 along が連続でなければ退避 @(0,1)@。
-alongRange :: ModelFrame -> Text -> (Double, Double)
-alongRange mf v = case lookup v (mfRoles mf) of
-  Just (RoleContinuous xs) | not (V.null xs) -> (V.minimum xs, V.maximum xs)
-  _                                          -> (0, 1)
-
--- | 各説明変数を 'HoldAgg' で固定値の定数列に差し替えた評価点 'ModelFrame' を合成する。
---   along 変数は grid (gxs)、 応答列はダミー ('designMatrixF' は応答を使わない)。
---   override は byVar 等の明示固定で 'HoldAgg' より優先する。
-evalFrame :: ModelFrame -> Text -> HoldAgg -> [(Text, Double)] -> [Double] -> ModelFrame
-evalFrame mf alongV hold override gxs =
-  let n = length gxs
-      adjust (nm, role)
-        | isResponseRole role           = (nm, RoleResponse (V.replicate n 0))
-        | nm == alongV                  = (nm, RoleContinuous (V.fromList gxs))
-        | Just fv <- lookup nm override = (nm, fixedRole role n fv)
-        | otherwise                     = (nm, holdRole hold n nm role)
-  in mf { mfRoles = map adjust (mfRoles mf), mfNRows = n }
-
--- | frame の along 列だけを定数 gx に差し替える (行数据え置き、 Marginalize 用)。
-setAlong :: ModelFrame -> Text -> Double -> ModelFrame
-setAlong mf alongV gx =
-  let n = mfNRows mf
-      adj (nm, role)
-        | nm == alongV = (nm, RoleContinuous (V.replicate n gx))
-        | otherwise    = (nm, role)
-  in mf { mfRoles = map adj (mfRoles mf) }
-
-isResponseRole :: VarRole -> Bool
-isResponseRole (RoleResponse _) = True
-isResponseRole _                = False
-
--- | 1 変数を 'HoldAgg' で固定した定数列にする (連続は集約値、 factor は固定水準 index)。
---   factor は Mean\/Median\/Mode\/Fixed すべて最頻水準に振替 (Reference のみ参照=index 0)。
-holdRole :: HoldAgg -> Int -> Text -> VarRole -> VarRole
-holdRole hold n nm role = case role of
-  RoleContinuous xs ->
-    let v = case hold of
-              Mean        -> meanV xs
-              Median      -> medianV xs
-              Mode        -> modeV xs
-              Reference   -> meanV xs              -- 連続に参照水準は無し → 平均で代替
-              Marginalize -> meanV xs              -- (Marginalize は別経路。 安全側に平均)
-              Fixed fm    -> fromMaybe (meanV xs) (lookup nm fm)
-    in RoleContinuous (V.replicate n v)
-  RoleFactor levels idx ->
-    let fixIdx = case hold of
-                   Reference -> 0
-                   Fixed fm  -> maybe (modeIdx idx) (clampIdx levels . round) (lookup nm fm)
-                   _         -> modeIdx idx
-    in RoleFactor levels (V.replicate n fixIdx)
-  RoleResponse _ -> RoleResponse (V.replicate n 0)
-
--- | 明示値 (byVar / Fixed override) で 1 変数を定数列にする。
-fixedRole :: VarRole -> Int -> Double -> VarRole
-fixedRole role n fv = case role of
-  RoleContinuous _    -> RoleContinuous (V.replicate n fv)
-  RoleFactor levels _ -> RoleFactor levels (V.replicate n (clampIdx levels (round fv)))
-  RoleResponse _      -> RoleResponse (V.replicate n fv)
-
-clampIdx :: [Text] -> Int -> Int
-clampIdx levels i = max 0 (min (length levels - 1) i)
-
--- | byVar 曲線の固定色パレット (層別の値ごとに 1 色)。
-effectPalette :: [Text]
-effectPalette =
-  [ "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2" ]
-
--- ===========================================================================
--- 応答曲面 3D 直結 — plot Phase 24 A3 (fit 済み多変量モデル → surface)
---
--- JMP Surface Profiler 同型: 2 因子 (v1, v2) を grid で動かし他変数を 'HoldAgg'
--- で固定、 μ̂ を 3D surface (z colormap 既定 ON) で描く。 effect plot
--- ('statModelMulti') の 2 因子版で、 評価核は同じ 'mvEvalFrame'。
--- ===========================================================================
-
--- | 2 因子 grid + 'HoldAgg' の評価点 frame ('evalFrame' の 2 変数版)。
---   行 = v2 (外側)、 列 = v1 (内側) — 'P3.surface3D' の grid 規約
---   (row = y 方向) に一致させる。
-evalFrame2 :: ModelFrame -> Text -> Text -> HoldAgg -> [Double] -> [Double] -> ModelFrame
-evalFrame2 mf v1 v2 hold gxs gys =
-  let n   = length gxs * length gys
-      x1s = [ gx | _  <- gys, gx <- gxs ]
-      x2s = [ gy | gy <- gys, _  <- gxs ]
-      adjust (nm, role)
-        | isResponseRole role = (nm, RoleResponse (V.replicate n 0))
-        | nm == v1            = (nm, RoleContinuous (V.fromList x1s))
-        | nm == v2            = (nm, RoleContinuous (V.fromList x2s))
-        | otherwise           = (nm, holdRole hold n nm role)
-  in mf { mfRoles = map adjust (mfRoles mf), mfNRows = n }
-
--- | 応答曲面の数値核: @(gxs, gys, grid)@。 @grid !! j !! i = μ̂(gxs!!i, gys!!j)@。
-surfaceGrid :: MultiVarModel m
-            => m -> Text -> Text -> SurfaceOpts -> ([Double], [Double], [[Double]])
-surfaceGrid m v1 v2 opts =
-  let mf         = mvFrame m
-      (xlo, xhi) = fromMaybe (alongRange mf v1) (soXRange opts)
-      (ylo, yhi) = fromMaybe (alongRange mf v2) (soYRange opts)
-      n          = max 2 (soN opts)
-      gxs        = linspace xlo xhi n
-      gys        = linspace ylo yhi n
-      ef         = evalFrame2 mf v1 v2 (soHoldAt opts) gxs gys
-      (mu, _)    = mvEvalFrame m 0.95 ef
-  in (gxs, gys, chunkRows n mu)
-
-chunkRows :: Int -> [a] -> [[a]]
-chunkRows k = go
-  where go [] = []
-        go xs = let (h, t) = splitAt k xs in h : go t
-
--- | fit 済み多変量モデル → 3D 応答曲面 (z colormap 既定 ON・colorbar 自動)。
---   @saveSVG3D path (surfaceOf m "x1" "x2" <> dataScatter3DOf m "x1" "x2")@。
-surfaceOf :: MultiVarModel m => m -> Text -> Text -> P3.VisualSpec3D
-surfaceOf m v1 v2 = surfaceOfWith m v1 v2 defaultSurfaceOpts
-
--- | オプション付き ('SurfaceOpts': grid 点数・hold・範囲)。
-surfaceOfWith :: MultiVarModel m => m -> Text -> Text -> SurfaceOpts -> P3.VisualSpec3D
-surfaceOfWith m v1 v2 opts =
-  let (gxs, gys, grid') = surfaceGrid m v1 v2 opts
-  in P3.layer3D ( P3.surface3DGrid grid'
-               <> P3.xRange3D (head gxs, last gxs)
-               <> P3.yRange3D (head gys, last gys)
-               <> P3.colormap3D )
-
--- | 実測点の 3D overlay: 訓練データの @(v1, v2, y)@ を scatter3D で重畳。
-dataScatter3DOf :: MultiVarModel m => m -> Text -> Text -> P3.VisualSpec3D
-dataScatter3DOf m v1 v2 =
-  let mf = mvFrame m
-      contOf nm = case lookup nm (mfRoles mf) of
-        Just (RoleContinuous xs) -> V.toList xs
-        _                        -> []
-      ys = case [ v | (_, RoleResponse v) <- mfRoles mf ] of
-        (v : _) -> V.toList v
-        []      -> []
-      pts = zipWith3 Point3 (contOf v1) (contOf v2) ys
-  in P3.layer3D (P3.scatter3DPoints pts <> P3.color3D (fromHex "#d62728") <> P3.size3D 4)
-
-meanV :: V.Vector Double -> Double
-meanV xs | V.null xs = 0
-         | otherwise = V.sum xs / fromIntegral (V.length xs)
-
-medianV :: V.Vector Double -> Double
-medianV xs
-  | null ys   = 0
-  | odd k     = ys !! (k `div` 2)
-  | otherwise = (ys !! (k `div` 2 - 1) + ys !! (k `div` 2)) / 2
-  where ys = sort (V.toList xs)
-        k  = length ys
-
--- | 連続列の最頻 (観測値の完全一致でグループ化。 繰り返しのない真の連続では任意)。
-modeV :: V.Vector Double -> Double
-modeV xs | V.null xs = 0
-         | otherwise = mostCommon (V.toList xs)
-
--- | factor の最頻水準 index。
-modeIdx :: V.Vector Int -> Int
-modeIdx idx | V.null idx = 0
-            | otherwise  = mostCommon (V.toList idx)
-
-mostCommon :: Ord a => [a] -> a
-mostCommon = fst . maximumBy (comparing snd)
-           . map (\g -> (head g, length g)) . group . sort
-
--- ===========================================================================
--- 共有描画 helper (複数のモデル族が利用)
--- ===========================================================================
-
--- | CI band の既定 level (95%)。
-defaultCILevel :: Double
-defaultCILevel = 0.95
-
--- | 分位線の色パレット (τ 昇順に割当て。 必要数を循環)。
-quantilePalette :: [T.Text]
-quantilePalette =
-  [ "#4575b4", "#d73027", "#1a9850", "#984ea3", "#ff7f00", "#377eb8" ]
-
--- | 階段関数の頂点列を作る。 開始値 @s0@ (= t=0 での値) から、 各 @(tᵢ, sᵢ)@ について
--- 直前の高さで @tᵢ@ まで水平に来てから @sᵢ@ に垂直に跳ぶ 2 頂点を出す。
-stepVerts :: Double -> [(Double, Double)] -> [(Double, Double)]
-stepVerts s0 pts = (0, s0) : go s0 pts
-  where
-    go _    []            = []
-    go prev ((t, s) : rest) = (t, prev) : (t, s) : go s rest
-
--- | grid index を x として複数曲線を色分け重畳する内部 helper。
-gridCurves :: [(Text, [Double])] -> VisualSpec
-gridCurves named =
-  let mkLine (lbl, ys) =
-        let xs = [ fromIntegral i | i <- [1 .. length ys] ] :: [Double]
-        in layer ( line (inline xs) (inline ys)
-                 <> colorBy (inlineCat (replicate (length ys) lbl)) )
-  in mconcat (map mkLine named)
-
--- | 特徴重要度 → bar layer ("f1", "f2", … をカテゴリ軸に・値=重要度)。
-importanceBar :: [Double] -> VisualSpec
-importanceBar imps =
-  let labels = [ "f" <> T.pack (show k) | k <- [1 .. length imps] ]
-  in layer (bar (inlineCat labels) (inline imps))
-
--- | 行列の第 @i@/@j@ 列を (xs, ys) として取り出す (列不足は 0 埋め)。
-matCols2 :: LA.Matrix Double -> Int -> Int -> ([Double], [Double])
-matCols2 m i j =
-  let cols = LA.toColumns m
-      colAt k = if k < length cols then LA.toList (cols !! k) else replicate (LA.rows m) 0
-  in (colAt i, colAt j)
-
--- | クラス代表点 (平均) をクラス色 ✚ で散布する (第 0/1 特徴)。 Discriminant /
---   NaiveBayes(Gaussian) の data-free 代表図。
-classMeansScatter :: [[Double]] -> [Int] -> VisualSpec
-classMeansScatter rows cids = classMeansScatterNamed rows cids []
-
--- | 'classMeansScatter' の **クラス名つき**版。 @names@ があれば凡例をクラス名 (levels)
---   に、 無ければ整数へフォールバック (@names !! k@・範囲外は show)。 df|-> 経路が
---   levels を載せた分類モデルの代表図で使う。
-classMeansScatterNamed :: [[Double]] -> [Int] -> [Text] -> VisualSpec
-classMeansScatterNamed rows cids names
-  | null rows = mempty
-  | otherwise =
-      let xs   = [ if not (null r) then head r else 0 | r <- rows ]
-          ys   = [ if length r >= 2 then r !! 1 else 0 | r <- rows ]
-          nameOf k | k >= 0 && k < length names = names !! k
-                   | otherwise                  = T.pack (show k)
-          labs = map nameOf cids
-      in layer ( scatter (inline xs) (inline ys)
-               <> colorBy (inlineCat labs)
-               <> shape MShCross )
-
--- | chain index → 色 (effectPalette を巡回)。
-chainColor :: Int -> Text
-chainColor k = effectPalette !! (k `mod` length effectPalette)
-
--- ===========================================================================
--- 分類器抽象 (Discriminant / NaiveBayes / KNN 共通) — Phase 68 A3
--- ===========================================================================
-
--- | 学習済分類器を評価点行列で走らせ、 各行の予測クラスを返す共通インターフェース。
---   ('decisionBoundaryOf' / 'confusionOf' が分類器種に依らず動くための薄い抽象)。
-class ClassPredict c where
-  predictClasses :: c -> LA.Matrix Double -> [Int]
-  -- | クラス番号 0..K-1 に対応する **クラス名 (levels)**。 高レベル @df |->@ 経路が
-  --   fit 時に載せる (factor 列なら levels 名・数値列なら数値)。 既定は空 = 名前を
-  --   持たないモデル ('confusionOf' 等は空なら整数ラベルにフォールバック)。
-  classNamesOf :: c -> [Text]
-  classNamesOf _ = []
-
--- ===========================================================================
--- 回帰診断の可視化 (係数 forest / 実測vs予測) — Phase 72.4/72.5
---
--- 係数表 ('coefSummary'・'Hanalyze.Diagnostics') と各モデルの実測/予測ペアを
--- 図に落とす薄い玄関。 数値層 (係数統計・予測) は非ゲートの 'Diagnostics' / 各 fit が
--- 持ち、 ここはゲート (plot-integration) 配下で 'VisualSpec' 化だけを担う。
--- ===========================================================================
-
--- | fit 済モデルから (実測値, 予測値) の対を取り出せる能力。 実測値は
---   @fitted + residual@ で復元する (回帰一般で成り立つ)。 instance は各モデル族の
---   'Plottable' と同じ Plot.* 側に置く (orphan・クラス=Core / instance=族 module)。
-class HasObsPred m where
-  -- | @(observed, predicted)@。 長さは観測数 n で一致する。
-  obsPredPairs :: m -> ([Double], [Double])
-
--- | 実測 vs 予測プロット。 x=実測値・y=予測値の散布に @y = x@ の参照線 (灰の破線) を
---   重ねる。 点が参照線に近いほど当てはまりが良い (残差が小さい)。
-obsVsPred :: HasObsPred m => m -> VisualSpec
-obsVsPred m = let (obs, prd) = obsPredPairs m in obsPredSpec obs prd
-
--- | (実測, 予測) のリストから実測 vs 予測 spec を組む。 'obsVsPred' の純データ版
---   (テスト・任意のペアからの作図に再利用)。 空入力は空図。
-obsPredSpec :: [Double] -> [Double] -> VisualSpec
-obsPredSpec obs prd
-  | null obs  = mempty
-  | otherwise =
-      let lo = minimum (obs ++ prd)
-          hi = maximum (obs ++ prd)
-      in  layer ( line (inline [lo, hi]) (inline [lo, hi])
-                <> linetype LtDashed
-                <> color (fromHex "#888888") )
-       <> layer (scatter (inline obs) (inline prd))
-       <> xLabel "observed"
-       <> yLabel "predicted"
-
--- | 係数 forest plot。 各係数の点推定 ('crEstimate') を中心、 95% CI ('crCI95') の
---   半幅を誤差バーとして 1 行ずつ水平に並べ、 0 (= 効果なし) に参照線を引く。 解析
---   Wald CI ('coefSummary') を持つ線形系で使う (CI は左右対称なので半幅で表せる)。
---   bootstrap 由来の非対称 CI を図にしたい場合は 'coefSummaryBoot' の行から個別に組む。
-coefForest :: HasCoefSummary m => m -> VisualSpec
-coefForest m =
-  let rows  = coefSummary m
-      names = [ crTerm r | r <- rows ]
-      ests  = [ crEstimate r | r <- rows ]
-      errs  = [ (hi - lo) / 2 | r <- rows, let (lo, hi) = crCI95 r ]
-  in if null rows
-       then mempty
-       else layer (forest (inlineCat names) (inline ests) (inline errs) <> forestNull 0)
diff --git a/src/Hanalyze/Plot/Linear.hs b/src/Hanalyze/Plot/Linear.hs
deleted file mode 100644
--- a/src/Hanalyze/Plot/Linear.hs
+++ /dev/null
@@ -1,272 +0,0 @@
--- |
--- Module      : Hanalyze.Plot.Linear
--- Description : hgg 連携層 — 線形モデル族の図化 instance
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- hgg 連携層 — **線形モデル族** の図化 instance (Phase 71.5)。
---
--- ⚠ 親 'Hanalyze.Plot' と同じ cabal flag @plot-integration@ (既定 off) を
--- on にしたときのみ build される。 共通基盤 (class / ModelSpec / grid 評価核) は
--- 'Hanalyze.Plot.Core' を import して取り込む (orphan instance を許容:
--- クラス=Core・instance=ここ・型=Wrappers)。
---
--- 担当する型 (= LM 系・GLM 系・WLS):
---   LMModel / MultiLMModel / WeightedLMModel / GLMModel / MultiGLMModel。
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE FlexibleContexts #-}
-module Hanalyze.Plot.Linear
-  ( familyObsDist
-  ) where
-
-import           Data.List             (sortBy, zip4)
-import           Data.Ord              (comparing)
-import qualified Hanalyze.Model.HBM.Distribution as BD
-import qualified Numeric.LinearAlgebra as LA
-
-import           Hgg.Plot.Spec     ( layer, inline
-                                       , scatter, line, band )
-
-import           Hanalyze.Model.Wrappers
-import           Hanalyze.Plot.Core
-import           Hanalyze.Fit            (weightedR2)
-import           Hanalyze.Model.Core     (FitResult, coefficientsV, fittedV, residualsV, rSquared1)
-import           Hanalyze.Model.GLM      ( Family (..), LinkFn (..), GlmPredictCI (..)
-                                                , predictGlmMuWithCI )
-import           Hanalyze.Model.LM       ( CIBand (..), confidenceBand, confidenceBandAt
-                                                , predictionBandAt )
-import           Hanalyze.Model.Formula.Design  (designMatrixF)
-
--- ===========================================================================
--- 多変量モデル型 (effect plot 用、 新規 fit)
---
--- 既存の単変数 'LMModel' / 'GLMModel' (設計行列が @[1, x]@ 固定) とは別型。
--- formula 文字列 + 'DataFrame' で多変量 fit し、 formula を保持して評価点設計行列を
--- 組む (HoldAgg 固定 + along grid)。 ★GLM は formula 経路が未整備なので
--- 'designMatrixF' で設計行列を作り 'fitGLMFull' を直接呼ぶ。
--- ===========================================================================
-
-instance MultiVarModel MultiLMModel where
-  mvFrame = mlmFrame
-  mvEvalFrame m level ef =
-    case designMatrixF (mlmFormula m) ef of
-      Left _        -> ([], Nothing)
-      Right (xe, _) ->
-        let cib = confidenceBandAt (mlmDesign m) (mlmResult m) level xe
-            los = lowerBound cib
-            his = upperBound cib
-            mu  = zipWith (\l h -> (l + h) / 2) los his
-        in (mu, Just (los, his))
-  -- 多変量 OLS の closed-form PI (評価点設計行列 → predictionBandAt)。
-  mvEvalFramePI m level ef =
-    case designMatrixF (mlmFormula m) ef of
-      Left _        -> Nothing
-      Right (xe, _) ->
-        let pib = predictionBandAt (mlmDesign m) (mlmResult m) level xe
-        in Just (lowerBound pib, upperBound pib)
-
-instance MultiVarModel MultiGLMModel where
-  mvFrame = mglmFrame
-  mvEvalFrame m level ef =
-    case designMatrixF (mglmFormula m) ef of
-      Left _        -> ([], Nothing)
-      Right (xe, _) ->
-        let beta = coefficientsV (mglmResult m)
-            cis  = [ predictGlmMuWithCI (mglmLink m) level beta (mglmSigma m) r
-                   | r <- LA.toRows xe ]
-        in (map gpMu cis, Just (map gpLo cis, map gpHi cis))
-
--- ===========================================================================
--- 線形モデル (描画可能)
---
--- 'FitResult' (数値核) は設計行列 X を保持しないが、 回帰線・CI band を描くには
--- X が要る ('confidenceBand' は X 引数)。 そこで X と生 predictor を束ねた
--- 「描画可能なモデル」 を別型にする (= plot Phase 15 §2.1 の開放論点を (i) で確定)。
--- ===========================================================================
-
-
-instance Plottable LMModel where
-  -- 散布図に重ねる回帰線 + CI band。 'confidenceBand' は **訓練点**で評価し
-  -- @yHats ± se@ を返す (= grid を渡すと fitted と不整合)。 ゆえに合成 grid を
-  -- 使わず、 訓練 x を昇順ソートして直線を結ぶ (= 単回帰なら直線で grid と同形、
-  -- かつ 'confidenceBand' を無改修で再利用できる)。 ± 半幅 errorY = se。
-  toPlot m =
-    let res    = lmResult m
-        xs     = LA.toList (lmXraw m)
-        yhat   = LA.toList (fittedV res)
-        cib    = confidenceBand (lmDesign m) res defaultCILevel
-        se     = zipWith (-) (upperBound cib) yhat   -- upper - ŷ = 片側半幅
-        sorted = sortBy (comparing (\(x, _, _) -> x)) (zip3 xs yhat se)
-        xsS    = [ x | (x, _, _) <- sorted ]
-        yhatS  = [ y | (_, y, _) <- sorted ]
-        seS    = [ e | (_, _, e) <- sorted ]
-    in layer (band (inline xsS) (inline (zipWith (-) yhatS seS)) (inline (zipWith (+) yhatS seS)))
-         <> layer (line (inline xsS) (inline yhatS))
-
-  -- 残差診断 (代表回帰線 + 残差 vs fitted)。
-  diagnosticPlots m =
-    let res  = lmResult m
-        yhat = LA.toList (fittedV res)
-        resd = LA.toList (residualsV res)
-    in [ toPlot m
-       , layer (scatter (inline yhat) (inline resd))
-       ]
-
--- | grid 評価 (Phase 16 C1)。 grid x で設計行列 @[1, x]@ を再構築し、
--- 訓練の分散核を流用する 'confidenceBandAt' で滑らかな曲線 + 対称 CI 帯を出す。
-instance SingleVarModel LMModel where
-  svRange m = let xs = LA.toList (lmXraw m) in (minimum xs, maximum xs)
-  svGrid m level gxs =
-    let xEval = LA.fromColumns [ LA.konst 1 (length gxs), LA.fromList gxs ]
-        cib   = confidenceBandAt (lmDesign m) (lmResult m) level xEval
-        los   = lowerBound cib
-        his   = upperBound cib
-        mu    = zipWith (\l h -> (l + h) / 2) los his
-    in (mu, Just (los, his))
-  -- PI = closed form σ̂²(1 + xᵀ(XᵀX)⁻¹x) (statsmodels obs_ci と一致)。
-  svGridPI m level gxs =
-    let xEval = LA.fromColumns [ LA.konst 1 (length gxs), LA.fromList gxs ]
-        pib   = predictionBandAt (lmDesign m) (lmResult m) level xEval
-    in Just (lowerBound pib, upperBound pib)
-  -- A8: 係数 [β₀, β₁] と R² (式/R² 凡例注釈用)。
-  svCoefR2 m = Just (LA.toList (coefficientsV (lmResult m)), rSquared1 (lmResult m))
-  -- ブートストラップ: 加法誤差ゆえ obsDist=Nothing (μ + 再標本化残差)。
-  svBootKit m = Just BootKit
-    { bkX = LA.toList (lmXraw m)
-    , bkY = zipWith (+) (LA.toList (fittedV (lmResult m))) (LA.toList (residualsV (lmResult m)))
-    , bkRefit = \xs ys -> lmModel (LA.fromList xs) (LA.fromList ys)
-    , bkObsDist = Nothing }
-
--- ===========================================================================
--- 一般化線形モデル (描画可能)
---
--- GLM の不確実性帯は **μ (応答) スケールで非対称** (線形予測子 η の対称 Wald CI を
--- 逆リンク gInv で μ に写すため、 Logit/Log 等では下側・上側の半幅が異なる)。 ゆえに
--- LMModel/GPResult の対称 band (ŷ±se) では忠実に描けない。 そこで
--- 下境界 lo / 上境界 hi を別々に持てる 'band' layer (= MBand area fill) を使い、 μ 曲線は
--- 'line' で重ねる。 帯は **訓練点での Wald CI** を 'predictGlmMuWithCI' で評価する
--- (= grid 補間でなく fit と整合)。 'fitGLMFull' が返す逆 Fisher 情報 Σ=(XᵀWX)⁻¹ が要る。
--- ===========================================================================
-
-instance Plottable GLMModel where
-  -- μ 曲線 + 非対称 Wald CI 帯。 各訓練点 (設計行列の行) で 'predictGlmMuWithCI' を
-  -- 評価し、 x 昇順にソートして band (lo→hi の area) と μ 折れ線を重ねる。 帯を先に
-  -- 置いて μ 線を上に描く。
-  toPlot m =
-    let beta  = coefficientsV (glmResult m)
-        rows  = LA.toRows (glmDesign m)
-        cis   = [ predictGlmMuWithCI (glmLink m) defaultCILevel beta (glmSigma m) r
-                | r <- rows ]
-        quads = sortBy (comparing (\(x, _, _, _) -> x))
-                  (zip4 (LA.toList (glmXraw m))
-                        (map gpMu cis) (map gpLo cis) (map gpHi cis))
-        xsS = [ x | (x, _, _, _) <- quads ]
-        muS = [ u | (_, u, _, _) <- quads ]
-        loS = [ l | (_, _, l, _) <- quads ]
-        hiS = [ h | (_, _, _, h) <- quads ]
-    in layer (band (inline xsS) (inline loS) (inline hiS))
-         <> layer (line (inline xsS) (inline muS))
-
-  -- 残差診断 (μ 曲線 + 帯、 残差 vs fitted μ̂)。
-  diagnosticPlots m =
-    let res  = glmResult m
-        yhat = LA.toList (fittedV res)
-        resd = LA.toList (residualsV res)
-    in [ toPlot m
-       , layer (scatter (inline yhat) (inline resd))
-       ]
-
--- | grid 評価 (Phase 16 C1)。 grid x の行 @[1, x]@ を 'predictGlmMuWithCI' に渡し、
--- μ スケールの非対称 Wald CI 帯を滑らかに評価する (band lo/hi は別々に保持)。
-instance SingleVarModel GLMModel where
-  svRange m = let xs = LA.toList (glmXraw m) in (minimum xs, maximum xs)
-  svGrid m level gxs =
-    let beta = coefficientsV (glmResult m)
-        cis  = [ predictGlmMuWithCI (glmLink m) level beta (glmSigma m)
-                   (LA.fromList [1, gx])
-               | gx <- gxs ]
-    in (map gpMu cis, Just (map gpLo cis, map gpHi cis))
-  -- PI は **Gaussian + Identity のみ** = LM の closed form に帰着 (μ̂ = Xβ・W=I)。
-  -- 非 Gaussian (Poisson/Binomial) は予測区間が応答分布の離散/非対称分位を要し
-  -- closed form で出ないため 'Nothing' (over-claim しない・CI 帯と同じ部分集合方針)。
-  svGridPI m level gxs = case (glmFamily m, glmLink m) of
-    (Gaussian, Identity) ->
-      let xEval = LA.fromColumns [ LA.konst 1 (length gxs), LA.fromList gxs ]
-          pib   = predictionBandAt (glmDesign m) (glmResult m) level xEval
-      in Just (lowerBound pib, upperBound pib)
-    _ -> Nothing
-  -- ブートストラップ: 新規観測は Family(μ) から parametric にドロー (Poisson/Bernoulli)。
-  -- これにより closed form PI を持たない非 Gaussian GLM でも PI を出せる。
-  svBootKit m = Just BootKit
-    { bkX = LA.toList (glmXraw m)
-    , bkY = zipWith (+) (LA.toList (fittedV (glmResult m))) (LA.toList (residualsV (glmResult m)))
-    , bkRefit = \xs ys -> glmModel (glmFamily m) (glmLink m) (LA.fromList xs) (LA.fromList ys)
-    , bkObsDist = familyObsDist (glmFamily m) }
-
--- ===========================================================================
--- 重み付き最小二乗 (WLS)
--- ===========================================================================
-
--- | grid 経路に委譲 (内側 LM の svGrid/PI は非スケール xEval × スケール設計で正しい
---   WLS CI を出す)。 'svRange' は元 x ('lmXraw') から。 'svCoefR2' のみ override し、
---   R² は statsmodels WLS と一致する weighted R² を返す (β̂ は内側のスケール OLS が WLS)。
-instance SingleVarModel WeightedLMModel where
-  svRange  (WeightedLMModel m _ _)  = svRange m
-  svGrid   (WeightedLMModel m _ _)  = svGrid m
-  svGridPI (WeightedLMModel m _ _)  = svGridPI m
-  svCoefR2 (WeightedLMModel m ws ys) =
-    let coefs = LA.toList (coefficientsV (lmResult m))
-        yhats = case coefs of                                  -- ŷ = β₀ + β₁x (元スケール)
-          (b0 : b1 : _) -> [ b0 + b1 * x | x <- LA.toList (lmXraw m) ]
-          [b0]          -> [ b0 | _ <- LA.toList (lmXraw m) ]
-          _             -> ys
-    in Just (coefs, weightedR2 ws ys yhats)
-
--- | ★訓練点経路 ('LMModel' の素の 'toPlot') を**使わず** grid 経路 ('statModel') に
---   固定する。 これで WLS 線+CI が元 x スケールで出て、 元データ散布図と整合する。
-instance Plottable WeightedLMModel where
-  toPlot = toPlot . statModel
-
--- ===========================================================================
--- GLM family → 観測分布 (ブートストラップ PI 用)
--- ===========================================================================
-
--- | GLM family → 新規観測の分布関数 (μ ↦ 分布。 ブートストラップ PI の parametric ドロー用)。
---   Gaussian は加法残差で扱うため 'Nothing' (σ̂ を別途要さない)。 'svBootKit' が使う。
-familyObsDist :: Family -> Maybe (Double -> BD.Distribution Double)
-familyObsDist Poisson  = Just (\mu -> BD.Poisson  (max 1e-9 mu))
-familyObsDist Binomial = Just (\mu -> BD.Bernoulli (min (1 - 1e-12) (max 1e-12 mu)))
-familyObsDist Gaussian = Nothing
-
--- ===========================================================================
--- 実測 vs 予測 (HasObsPred) — Phase 72.4
---
--- 実測値 = fitted + residual で復元する (回帰一般)。 WLS は内側 fit が √w スケール
--- なので予測を 1/√w で元スケールへ戻し、 実測は保持した元 y ('wlmY') を使う。
--- ===========================================================================
-
--- | FitResult から (実測, 予測) を復元する共通ヘルパ。
-obsPredFromFit :: FitResult -> ([Double], [Double])
-obsPredFromFit r =
-  let f = LA.toList (fittedV r)
-      e = LA.toList (residualsV r)
-  in (zipWith (+) f e, f)
-
-instance HasObsPred LMModel where
-  obsPredPairs = obsPredFromFit . lmResult
-
-instance HasObsPred MultiLMModel where
-  obsPredPairs = obsPredFromFit . mlmResult
-
-instance HasObsPred GLMModel where
-  obsPredPairs = obsPredFromFit . glmResult
-
-instance HasObsPred MultiGLMModel where
-  obsPredPairs = obsPredFromFit . mglmResult
-
-instance HasObsPred WeightedLMModel where
-  obsPredPairs m =
-    let fScaled = LA.toList (fittedV (lmResult (wlmInner m)))
-        prd     = zipWith (\f w -> if w > 0 then f / sqrt w else f) fScaled (wlmWeights m)
-    in (wlmY m, prd)
diff --git a/src/Hanalyze/Plot/ML.hs b/src/Hanalyze/Plot/ML.hs
deleted file mode 100644
--- a/src/Hanalyze/Plot/ML.hs
+++ /dev/null
@@ -1,1801 +0,0 @@
--- |
--- Module      : Hanalyze.Plot.ML
--- Description : hgg 連携層 — ML / 統計モデル連携族の図化 instance + 抽出子
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- hgg 連携層 — **ML / 統計モデル連携族** の図化 instance + 抽出子 (Phase 71.6)。
---
--- ⚠ 親 'Hanalyze.Plot' と同じ cabal flag @plot-integration@ (既定 off) を
--- on にしたときのみ build される。 共通基盤 (class / ModelSpec / grid 評価核) は
--- 'Hanalyze.Plot.Core' を import して取り込む (orphan instance を許容:
--- クラス=Core・instance=ここ・型=Wrappers/各 Model module)。
---
--- 担当する型・ヘルパ (= Phase 68 A1-A7 群):
---   クラスタリング (KMeans) / 木・アンサンブル (PCA/RF/GB/DT) / 分類
---   (Discriminant/NaiveBayes/KNN) / 次元圧縮 (PLS) / 時系列・生存・FDA
---   (Forecast/GARCH/AFT/FunctionalPCA/FLM) / 罰則回帰・因果探索 (Reg/LiNGAM) /
---   記述統計・検定 (TestResult)。 新規 plot mark は不要 (既存 mark の組合せ)。
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE FlexibleContexts #-}
-module Hanalyze.Plot.ML
-  ( -- * クラスタリング (Phase 68 A1)
-    clusterScatterOf
-  , centroidsOf
-    -- * クラスタを囲む (凸包輪郭 / 95% 共分散楕円) — Phase 76.B
-  , clusterHullOf
-  , clusterEllipseOf
-    -- * DOE prediction profiler — Phase 78.C / 78.D / 78.E / 78.F
-  , ResidualMode (..)
-  , ProfilerSpec (..)
-  , profiler
-  , profilerResidual
-  , contourOf
-    -- * 階層クラスタリング dendrogram — Phase 76.C
-  , DendroOpts (..)
-  , defaultDendroOpts
-  , dendrogramOf
-  , dendrogramOf'
-    -- * 木/アンサンブル — Phase 68 A2
-  , treeImportances
-    -- * 決定木 樹形図 (rpart.plot 流・annotation ベース) — Phase 75.26
-  , treePlot
-  , treePlotRaw
-    -- * 分類 — Phase 68 A3
-  , decisionBoundaryOf
-  , confusionOf
-    -- * MDS 埋め込み (モデル型 + 群色オプション) — Phase 75.21
-  , MDSView
-  , mdsView
-  , mdsGroupBy
-    -- * NN 可視化 — Phase 75.5
-  , nnLossOf
-    -- * カーネル SVM サポートベクタ可視化 — Phase 75.12
-  , svmSupportVectorsOf
-    -- * 決定境界を線で描く (等高線) — Phase 75.13b
-  , ScorePredict (..)
-  , decisionLineOf
-    -- * 部分従属図 (PDP / ICE) — Phase 75.27
-  , RegPredict (..)
-    -- ** Plottable 中間型 (Phase 76.D・HBM 抽出子と同型・toPlot で描画)
-  , PDPView
-  , pdp
-  , pdpIce
-  , pdpOf
-  , pdpIceOf
-  , pdpPlot
-  , pdpIcePlot
-  , partialDependencePlot
-  , partialDependenceIcePlot
-    -- * 次元圧縮 (PLS 診断ビュー) — Phase 68 A4 / 70.B
-  , PLSView (..)
-  , PLSViewKind (..)
-  , scoreView
-  , loadingView
-  , vipView
-    -- * 時系列・生存・FDA — Phase 68 A5
-  , garchVolatility
-  , aftSurvivalAt
-    -- * 罰則回帰・因果探索 — Phase 68 A6
-  , regPathPlot
-  , lingamDag
-  , lingamDagNamed
-  , varLagDagNamed
-  , bootstrapEdgeProbOf
-    -- * 記述統計・検定 — Phase 68 A7
-  , testForest
-  , testForestLabeled
-  , describeBox
-  ) where
-
-import           Control.Applicative   ((<|>))
-import           Data.Maybe            (fromMaybe)
-import           Data.List             (nub, sort, elemIndex, sortBy, foldl')
-import           Data.Ord              (comparing)
-import qualified Data.Map.Strict       as Map
-import qualified Data.Vector           as V
-import qualified Data.Vector.Unboxed    as VU
-import qualified Numeric.LinearAlgebra as LA
-
-import           Data.Text             (Text)
-import qualified Data.Text             as T
-
-import           Hgg.Plot.Spec     ( VisualSpec, layer, inline, inlineCat
-                                       , fromHex
-                                       , scatter, line, band
-                                       , shape, MarkShape (..)
-                                       , heatmap, contour, contourFilled, contourLevels
-                                       , label, color, colorBy, bar, boxplot, forest, forestNull
-                                       , legendOff
-                                       , title, coordFlip, coordCartesian, subplots, subplotCols
-                                       , scaleXDiscreteLimits
-                                       , xLabel, yLabel
-                                       , annotTextP, annotRectP
-                                       , annotate, Annotation (..)
-                                       , theme, ThemeName (..), themeGrid, themeAxisLine, panelBorder
-                                       , tickColor
-                                       , xAxis, yAxis, hideTicks
-                                       , axisBreaksLabeled, axisRotate
-                                       , scaleColorManual
-                                       , themeLegendFont, fontSize
-                                       , alpha
-                                       , dagFromListsWithPlates
-                                       , DAGNode (..), DAGEdge (..)
-                                       , DAGNodeKind (..), DAGLayoutAlgorithm (..) )
-import           Hgg.Plot.Unit     (Pos (..))
-import           Hgg.Plot.Palette  (ggplotHue)
-import           Hgg.Plot.Custom.Dendrogram (DendroSeg (..), DendroPayload (..), dendrogramMark)  -- Phase 48
-import           Hgg.Plot.DAG      (layoutHierarchicalFullWithPlates)
-import           Hgg.Plot.Render.Special (bakeDAGRoutesInSpec)
-
-import           Numeric               (showFFloat)
-
-import           Hanalyze.Data.ColumnSource     (ColumnSource (..))
-import           Hanalyze.Model.Formula.Frame   (ModelFrame (..), VarRole (..))
-import           Hanalyze.Model.Formula.Design  (designMatrixF)
-import           Hanalyze.Fit                   (DesignHBMFit (..))
-import           Hanalyze.Model.Wrappers
-import           Hanalyze.Plot.Core
-import           Hanalyze.Model.LM     (linspace)
-import           Hanalyze.Model.GP     (gpNoiseVar)
-import           Hanalyze.Model.Weibull (quantileNormal)
-import           Hanalyze.Model.PLS    (predictPLS)
-import           Hanalyze.Model.Cluster (KMeansResult (..))
-import           Hanalyze.Model.HierarchicalCluster
-                   (HClusterFit (..), cutTree)
-import           Hanalyze.Model.RandomForest (RandomForest (..), featureImportance, rfPermutationImportance, defaultFeatureNames, Tree)
-import qualified Hanalyze.Model.RandomForest as RF
-import           Hanalyze.Model.GradientBoosting (GBRegressor (..), GBClassifier (..), predictGBR)
-import           Hanalyze.Model.PartialDependence
-                   (PDPResult, partialDependence, pdpGrid, pdpMean)   -- pdpIce 欄は PD. で参照 (関数名と衝突回避)
-import qualified Hanalyze.Model.PartialDependence as PD
-import           Hanalyze.Model.RandomForestClassifier (RFClassifierFit (..))
-import           Hanalyze.Model.DecisionTree (DTree (..), DTFit (..))
-import           Hanalyze.Model.Discriminant (DiscriminantFit (..), predictDiscriminant)
-import           Hanalyze.Model.NaiveBayes (NBModel (..), GaussianNB (..)
-                                       , MultinomialNB (..), predictNB)
-import           Hanalyze.Model.KNN (KNNClassifier (..), predictKNNC)
-import           Hanalyze.Model.NeuralNetwork (MLPFit (..), predictMLPClass)
-import           Hanalyze.Model.SVM (SVM (..), SVMMulti (..)
-                                       , predictSVM, predictSVMMulti, predictSVMScore)
-import           Hanalyze.Model.MDS (MDSResult (..))
-import           Hanalyze.DataIO.Convert (getTextVec, getDoubleVec)
-import qualified DataFrame.Internal.DataFrame  as DXD
-import           Hanalyze.Model.PLS (PLSFit (..))
-import           Hanalyze.Model.GARCH (GARCHFit (..))
-import           Hanalyze.Model.AFT (AFTFit (..), logS, predictAFT)
-import           Hanalyze.Model.FDA (FunctionalPCA (..), FLMResult (..))
-import           Hanalyze.Model.Regularized (RegFit (..))
-import           Hanalyze.Model.LiNGAM.Direct (DirectLiNGAMFit (..))
-import           Hanalyze.Model.LiNGAM.Parce (ParceFit (..))
-import           Hanalyze.Model.LiNGAM.MultiGroup (MultiGroupFit (..))
-import           Hanalyze.Model.LiNGAM.VAR (VARLiNGAMFit (..))
-import           Hanalyze.Model.LiNGAM.Pairwise (PairwiseResult (..), PairwiseDirection (..))
-import           Hanalyze.Model.LiNGAM.Bootstrap (BootstrapResult (..))
-import           Hanalyze.Model.LiNGAM.ICA (ICALiNGAMFit (..))
-import           Hanalyze.Stat.CorrelationNetwork (CorrelationGraph (..))
-import           Hanalyze.Stat.Test (TestResult (..))
-import           Hanalyze.Model.PCA     (PCAResult (..))
-import           Hanalyze.Model.Survival (KMResult (..))
-import           Hanalyze.Model.CompetingRisks (CRFit (..))
-import           Hanalyze.Model.TimeSeries (ARFit (..), forecastAR)
-
--- ===========================================================================
--- クラスタリング (KMeans) の図 — Phase 68 A1
---
--- KMeans の分野定番の図は「クラスタ別散布 (色=ラベル)」。 ただし
--- 'KMeansResult' は centroids + labels + inertia のみ保持し **生データ座標を
--- 持たない**。 そこで 'surfaceOf' <> 'dataScatter3DOf' と同じ **model 層 / data
--- 層の二層イディオム**に分ける:
---
---   * 'Plottable' 'KMeansResult' の 'toPlot' = centroid 散布のみ (データ不要・
---     クラス契約 @m -> VisualSpec@ を満たす)。 既定は centroid 行列の第 0/1 次元。
---   * 'clusterScatterOf' = データ点をラベル色で散布 (要データ源・列名指定)。
---   * 'centroidsOf' = centroid を任意 2 次元で重畳 (✚ マーカー・次元 index 明示)。
---
--- 定番図 = @df |>> (clusterScatterOf df res \"x\" \"y\" <> centroidsOf res 0 1)@。
--- ⚠ centroid 行列は **学習時の特徴量列順**のみで列名を持たない。 重畳時は
--- データ列 (@xn@, @yn@) と centroid 次元 (@i@, @j@) の対応をユーザが揃える。
--- ===========================================================================
-
--- | KMeans クラスタの代表図 = centroid 散布 (第 0/1 次元・クラスタ色・✚ マーカー)。
---   生データ点は 'clusterScatterOf' で別 layer に重ねる
---   (cf. 'surfaceOf' (model) <> 'dataScatter3DOf' (data) の二層イディオム)。
-instance Plottable KMeansResult where
-  toPlot res = centroidsOf res 0 1
-
--- | データ点をラベル色で散布する (= KMeans の定番「クラスタ別散布」)。
---   @d@ は 'ColumnSource' (DataFrame / assoc / Map 等)、 @xn@\/@yn@ は描く列名。
---   色はクラスタラベル ('kmrLabels') の categorical (= 点と同順)。
---   列が無ければ空 ('mempty')。
-clusterScatterOf :: ColumnSource d => d -> KMeansResult -> Text -> Text -> VisualSpec
-clusterScatterOf d res xn yn =
-  case (lookupCol xn d, lookupCol yn d) of
-    (Just xs, Just ys) ->
-      layer ( scatter (inline xs) (inline ys)
-            <> colorBy (inlineCat (map (T.pack . show) (kmrLabels res))) )
-    _ -> mempty
-
--- | centroid を任意 2 次元 (@i@, @j@) で散布 (クラスタ色・✚ マーカーで点と区別)。
---   index が centroid 次元数を超える / 負なら空 ('mempty')。
-centroidsOf :: KMeansResult -> Int -> Int -> VisualSpec
-centroidsOf res i j
-  | i < 0 || j < 0 || i >= d || j >= d = mempty
-  | otherwise =
-      layer ( scatter (inline xs) (inline ys)
-            <> colorBy (inlineCat cids)
-            <> shape MShCross )
-  where
-    cs   = kmrCentroids res
-    d    = LA.cols cs
-    k    = LA.rows cs
-    cols = LA.toColumns cs
-    xs   = LA.toList (cols !! i)
-    ys   = LA.toList (cols !! j)
-    cids = map (T.pack . show) [0 .. k - 1 :: Int]
-
--- | render の categorical 群色 (colorBy → @sort.nub@ 順 → 'ggplotHue') を analyze 側で
---   再現し、 カテゴリ名 → 色(hex) の辞書を返す。 annotation の色は spec 時に確定するため
---   ('clusterScatterOf'/'toPlot' の凡例色と一致させる用)。
-hueColorMap :: [Text] -> Map.Map Text Text
-hueColorMap labels =
-  let cats = sort (nub labels)
-  in Map.fromList (zip cats (ggplotHue (length cats) ++ repeat "#cccccc"))
-
--- | 色・太さ指定の線分注釈 ('annotLineP' は色固定なので 'AnnLine' を直接構築)。
-annotLineC :: Text -> Double -> (Double, Double) -> (Double, Double) -> VisualSpec
-annotLineC col w (x1, y1) (x2, y2) = annotate AnnLine
-  { anX1 = PNative x1, anY1 = PNative y1, anX2 = PNative x2, anY2 = PNative y2
-  , anColor = col, anWidth = w }
-
--- | 頂点列を閉じた折れ線 (最後→最初も結ぶ) として色付き線分で描く。
-closedPolyline :: Text -> Double -> [(Double, Double)] -> VisualSpec
-closedPolyline _   _ []  = mempty
-closedPolyline _   _ [_] = mempty
-closedPolyline col w vs  =
-  mconcat [ annotLineC col w p q | (p, q) <- zip vs (tail vs ++ [head vs]) ]
-
--- | 2D 凸包 (Andrew monotone chain)・反時計回り頂点列。 3 点未満は入力そのまま。
-convexHull :: [(Double, Double)] -> [(Double, Double)]
-convexHull ps0 =
-  let ps = sort (nub ps0)                 -- lexicographic (x, y)
-  in if length ps <= 2 then ps
-     else let lower = half ps
-              upper = half (reverse ps)
-          in init lower ++ init upper      -- 端点重複を除いて連結
-  where
-    -- 単調鎖: 直近 2 点と p が右回り (cross<=0) の間は pop。 stack は head=最新。
-    half = reverse . foldl step []
-    step acc p = p : popRight acc p
-    popRight (b : a : rest) p
-      | cross a b p <= 0 = popRight (a : rest) p
-    popRight acc _ = acc
-    cross (ox, oy) (ax, ay) (bx, by) = (ax - ox) * (by - oy) - (ay - oy) * (bx - ox)
-
--- | クラスタ点群をラベルごとにグルーピング (色は 'clusterScatterOf' と一致)。
---   d が xn/yn 列を持たなければ空。
-clusterGroups
-  :: ColumnSource d => d -> KMeansResult -> Text -> Text
-  -> [(Text, [(Double, Double)])]         -- (群色 hex, 点列)
-clusterGroups d res xn yn =
-  case (lookupCol xn d, lookupCol yn d) of
-    (Just xs, Just ys) ->
-      let labs = kmrLabels res
-          cmap = hueColorMap (map (T.pack . show) labs)
-          gmap = Map.fromListWith (flip (++))
-                   [ (l, [(x, y)]) | (l, x, y) <- zip3 labs xs ys ]
-      in [ (Map.findWithDefault "#cccccc" (T.pack (show l)) cmap, ps)
-         | (l, ps) <- Map.toList gmap ]
-    _ -> []
-
--- | 各クラスタを **凸包の輪郭線**で囲む (ggplot @geom_encircle@ 相当・塗りなし)。
---   群色は 'clusterScatterOf' と一致。 定番 = @cdf |>> (clusterScatterOf … \<\> clusterHullOf …)@。
---   ⚠ annotation は軸平行矩形しか塗れないため**輪郭線のみ** (半透明塗りは将来 'MPolygon' 移譲)。
-clusterHullOf :: ColumnSource d => d -> KMeansResult -> Text -> Text -> VisualSpec
-clusterHullOf d res xn yn =
-  mconcat [ closedPolyline col 1.5 (convexHull ps)
-          | (col, ps) <- clusterGroups d res xn yn ]
-
--- | 各クラスタを **95% 共分散楕円** (χ²(0.95, 2)=5.991) の輪郭で囲む (ggplot @stat_ellipse@
---   相当・正規分布仮定)。 群平均 μ・共分散 Σ を固有分解 ('LA.eigSH') し、 固有軸方向へ
---   半径 √5.991·√λ の楕円点列を折れ線で近似。 群色は 'clusterScatterOf' と一致。
---   点数 3 未満の群は描かない (共分散が定義できないため)。
-clusterEllipseOf :: ColumnSource d => d -> KMeansResult -> Text -> Text -> VisualSpec
-clusterEllipseOf d res xn yn =
-  let ellipses = [ (col, ellipse95 ps) | (col, ps) <- clusterGroups d res xn yn ]
-      outlines = mconcat [ closedPolyline col 1.5 pts | (col, pts) <- ellipses ]
-      allPts   = concatMap snd ellipses
-      -- annotation は軸ドメインを駆動しないため、 95% 楕円 (データ点より外へ広がる) が
-      -- フレームをはみ出す。 楕円点を alpha=0 の不可視散布で載せ軸を広げる (colorBy 無し=
-      -- 凡例に出ない)。 決定境界の coordCartesian と違い、 ここは重畳データ点も含めて
-      -- auto-fit させたいので固定でなく anchor 方式。
-      anchor
-        | null allPts = mempty
-        | otherwise   = layer ( scatter (inline (map fst allPts)) (inline (map snd allPts))
-                                <> alpha 0 )
-  in outlines <> anchor
-  where
-    seg   = 64 :: Int
-    scl   = sqrt 5.991                       -- χ²(0.95, 2)
-    ellipse95 ps
-      | n < 3     = []
-      | otherwise =
-          [ ( mux + a * (v1 !! 0) + b * (v2 !! 0)
-            , muy + a * (v1 !! 1) + b * (v2 !! 1) )
-          | t <- [ 2 * pi * fromIntegral k / fromIntegral seg | k <- [0 .. seg - 1] ]
-          , let a = scl * sqrt (max 0 l1) * cos t
-                b = scl * sqrt (max 0 l2) * sin t ]
-      where
-        n   = length ps
-        xs  = map fst ps; ys = map snd ps
-        mux = sum xs / fromIntegral n
-        muy = sum ys / fromIntegral n
-        sxx = sum [ (x - mux) ^ (2 :: Int) | x <- xs ] / fromIntegral (n - 1)
-        syy = sum [ (y - muy) ^ (2 :: Int) | y <- ys ] / fromIntegral (n - 1)
-        sxy = sum [ (x - mux) * (y - muy) | (x, y) <- ps ] / fromIntegral (n - 1)
-        sigma = LA.fromLists [[sxx, sxy], [sxy, syy]]
-        (vals, vecs) = LA.eigSH (LA.trustSym sigma)   -- λ 降順・列=固有ベクトル
-        l1 = vals `LA.atIndex` 0
-        l2 = vals `LA.atIndex` 1
-        cols = LA.toColumns vecs
-        v1 = LA.toList (cols !! 0)
-        v2 = LA.toList (cols !! 1)
-
--- | dendrogram の描画オプション。
-data DendroOpts = DendroOpts
-  { doLineColor      :: !Text            -- ^ 閾値超 (または閾値未指定) の線色。
-  , doWidth          :: !Double          -- ^ 線幅。
-  , doColorThreshold :: !(Maybe Double)  -- ^ @Just t@ で高さ @t@ 未満のサブツリーをクラスタ色分け
-                                         --   (scipy @color_threshold@ 流)。 @Nothing@ で単色。
-  } deriving (Show)
-
--- | 既定 = 単色 (grey20 相当・閾値なし)。
-defaultDendroOpts :: DendroOpts
-defaultDendroOpts = DendroOpts "#4C4C4C" 1.2 Nothing
-
-instance Plottable HClusterFit where
-  toPlot = dendrogramOf
-
--- | 階層クラスタリング結果を **dendrogram** で描く (scipy @dendrogram@ / ggdendro 流)。
---   マージ列 ('hcMerges') と高さ ('hcHeights') から U 字リンク (縦 2 + 横 1) を 'AnnLine' で
---   描画。 葉は x 軸に等間隔・各マージノードの x = 子の中点・y = マージ高。 リーフに元サンプル
---   ID ラベル。 plot core は触らず annotation で描く (将来 plot 正式 mark 移譲予定)。
-dendrogramOf :: HClusterFit -> VisualSpec
-dendrogramOf = dendrogramOf' defaultDendroOpts
-
--- | 色閾値・線色等を指定できる版。
-dendrogramOf' :: DendroOpts -> HClusterFit -> VisualSpec
-dendrogramOf' opts fit
-  | n <= 1 || null merges = mempty
-  | otherwise =
-      -- R base / scipy 同様 grid・軸線・枠なし (theme_minimal + grid off)。
-      theme ThemeMinimal <> themeGrid False <> themeAxisLine False <> panelBorder False
-        <> tickColor "transparent"      -- 目盛マーク (短線) を消す。 数字ラベルは残る。
-        -- 葉ラベルは x 軸目盛 (slot 位置・縦書き) で。 軸ラベルは margin を予約するので
-        -- リンク根と被らない (annotText と違い R と同挙動)。
-        -- ★ axisRotate は CCW 正 (R/matplotlib/ggplot 準拠・hgg Phase 50 A1)。
-        --   90 = CCW 90 = 下→上読みで R base / scipy dendrogram の既定向きと一致。
-        <> xAxis (axisBreaksLabeled leafTicks <> axisRotate 90)
-        <> layer (dendrogramMark payload)  -- ★ Phase 48: U字リンクを custom mark で描く (焼き込み)。
-                                           --   encX/encY で軸 range を束ねる (旧 anchor 不要)。
-        <> yAxisLine                    -- 軸線は 2 辺一括制御しか無いので y 軸線だけ自前描画。
-        <> yLabel "height"              -- y = マージ高 (結合時の非類似度・Ward 増分)。
-  where
-    n       = hcNumOriginals fit
-    merges  = hcMerges fit
-    heights = hcHeights fit
-    root    = 2 * n - 2                                 -- 最終マージ = 根ノード
-    childrenOf node = merges !! (node - n)
-    leavesOf node
-      | node < n  = [node]
-      | otherwise = let (a, b) = childrenOf node in leavesOf a ++ leavesOf b
-    order   = leavesOf root                             -- 葉 ID を左→右の並びで
-    slotOf  = Map.fromList (zip order [0 :: Int ..])
-    -- ノードの x (子の中点)・高さ・代表葉を fold で確定 (子は id が小さく先に入る)。
-    (nodeX, nodeH, leafRep) = foldl' step (x0, h0, r0) (zip [0 :: Int ..] merges)
-      where
-        x0 = Map.fromList [ (l, fromIntegral (slotOf Map.! l)) | l <- [0 .. n - 1] ]
-        h0 = Map.fromList [ (l, 0 :: Double) | l <- [0 .. n - 1] ]
-        r0 = Map.fromList [ (l, l) | l <- [0 .. n - 1] ]
-        step (mx, mh, mr) (i, (a, b)) =
-          let node = n + i
-          in ( Map.insert node ((mx Map.! a + mx Map.! b) / 2) mx
-             , Map.insert node (heights !! i) mh
-             , Map.insert node (mr Map.! a) mr )
-    maxH    = maximum heights
-    -- 葉ラベル = x 軸目盛 (slot 位置に元サンプル ID)。 縦書きは axisRotate 90。
-    leafTicks = [ (fromIntegral slot, T.pack (show leaf))
-                | (leaf, slot) <- zip order [0 :: Int ..] ]
-    -- 色閾値: t 未満マージ数だけ切って各葉のクラスタ ID を得る (hcMerges は高さ昇順)。
-    thrInf     = maybe (1 / 0) id (doColorThreshold opts)
-    kCut       = n - length (filter (< thrInf) heights)
-    clusterIds = cutTree fit kCut
-    distinctCs = foldr (\c acc -> if c `elem` acc then acc else acc ++ [c])
-                       [] (V.toList clusterIds)         -- 出現順
-    cmap       = Map.fromList (zip distinctCs
-                   (ggplotHue (length distinctCs) ++ repeat "#999999"))
-    linkColor i = case doColorThreshold opts of
-      Just t | heights !! i < t ->
-        Map.findWithDefault (doLineColor opts)
-                            (clusterIds V.! (leafRep Map.! (n + i))) cmap
-      _ -> doLineColor opts
-    -- U 字リンク (子の高さ→マージ高の縦線 2 本 + マージ高の横線 1 本) を焼き込み線分に。
-    -- 座標系は従来の annotLine 版と同一 (x=葉 slot/node 中点、 y=height)。
-    payload = DendroPayload
-      { dpSegments = concat
-          [ [ DendroSeg xa ha  xa hgt col w
-            , DendroSeg xa hgt xb hgt col w
-            , DendroSeg xb hgt xb hb  col w ]
-          | (i, (a, b)) <- zip [0 :: Int ..] merges
-          , let xa  = nodeX Map.! a; xb = nodeX Map.! b
-                ha  = nodeH Map.! a; hb = nodeH Map.! b
-                hgt = heights !! i
-                col = linkColor i
-                w   = doWidth opts ]
-      , dpXRange = (-0.6, fromIntegral n - 0.4)   -- 旧 anchor と同じ range
-      , dpYRange = (0, maxH * 1.05)
-      }
-    -- 左辺 (panel npc x=0) に y 軸線を 1 本 (下辺 x 軸線は出さない = R 流)。
-    yAxisLine = annotate AnnLine
-      { anX1 = PNpc 0, anY1 = PNpc 0, anX2 = PNpc 0, anY2 = PNpc 1
-      , anColor = "#333333", anWidth = 1 }
-
--- ===========================================================================
--- 時系列予測 (描画可能)
---
--- AR(p) の点予測 'forecastAR' は将来値の中心のみを返す。 予測の不確実性帯は **h-step
--- 予測分散** から得る: AR の MA(∞) 表現の ψ-weights (ψ₀=1, ψⱼ=Σφᵢψⱼ₋ᵢ) を用いて
--- @Var(ŷ_{n+k}) = σ² Σ_{j=0}^{k-1} ψⱼ²@ (σ² = 革新分散 'arResidVar')。 これは Gaussian
--- 革新の下での正統な予測区間 (地平 k とともに単調に広がる)。 対称ゆえ band は
--- @中心 ± z·se@。 'toPlot' は履歴折れ線 + 予測折れ線 + 予測区間 band を 1 枚に重ねる。
--- ===========================================================================
-
--- | AR(p) の MA(∞) 表現の ψ-weights ψ₀..ψ_{h-1} (ψ₀=1, ψⱼ=Σ_{i=1}^{min j p} φᵢ ψⱼ₋ᵢ)。
-arPsiWeights :: [Double] -> Int -> [Double]
-arPsiWeights phi h = go [1.0]
-  where
-    p = length phi
-    go ps
-      | length ps >= h = take h ps
-      | otherwise =
-          let j  = length ps
-              pj = sum [ (phi !! (i - 1)) * (ps !! (j - i)) | i <- [1 .. min j p] ]
-          in go (ps ++ [pj])
-
--- | k-step (k=1..h) 予測標準誤差 se_k = sqrt(σ² Σ_{j<k} ψⱼ²)。
-arForecastSE :: ARFit -> Int -> [Double]
-arForecastSE fit h =
-  let phi  = LA.toList (arPhi fit)
-      s2   = arResidVar fit
-      psis = arPsiWeights phi h
-  in [ sqrt (s2 * sum (map (^ (2 :: Int)) (take k psis))) | k <- [1 .. h] ]
-
-instance Plottable ForecastModel where
-  -- 履歴折れ線 + 予測折れ線 + 予測区間 band (中心 ± 1.96·se)。 x = 時刻 index
-  -- (履歴 1..n、 予測 n+1..n+h)。 予測線は履歴末尾点から繋げる。 帯を先・線を後に重ねる。
-  toPlot m =
-    let fit  = fmFit m
-        hist = LA.toList (fmHistory m)
-        n    = length hist
-        h    = fmHorizon m
-        fc   = LA.toList (forecastAR fit (fmHistory m) h)
-        se   = arForecastSE fit h
-        fx   = [ fromIntegral (n + k) | k <- [1 .. h] ] :: [Double]
-        lo   = zipWith (\f s -> f - 1.96 * s) fc se
-        hi   = zipWith (\f s -> f + 1.96 * s) fc se
-        histX = [ fromIntegral i | i <- [1 .. n] ] :: [Double]
-        -- 予測線は履歴末尾 (n, hist[n-1]) から始めて連続させる。
-        lineX = fromIntegral n : fx
-        lineY = last hist : fc
-    in layer (band (inline fx) (inline lo) (inline hi))
-         <> layer (line (inline histX) (inline hist))
-         <> layer (line (inline lineX) (inline lineY))
-
--- ===========================================================================
--- 生存解析 (描画可能)
---
--- KM 生存曲線・CIF (競合リスク) はいずれも階段関数。 'stepVerts' (Core) で階段頂点を
--- 明示展開して line で結ぶ。 KM は s0=1 で下降、 CIF は s0=0 で上昇。
--- ===========================================================================
-
-instance Plottable KMResult where
-  -- KM 生存曲線 (階段、 S=1 から下降)。
-  toPlot km =
-    let pts   = zip (kmrTimes km) (kmrSurvival km)
-        verts = stepVerts 1.0 pts
-    in layer (line (inline (map fst verts)) (inline (map snd verts)))
-
-instance Plottable CRFit where
-  -- 競合リスク CIF (cause ごとに 0 から上昇する階段、 色分け重畳)。
-  toPlot cr =
-    let ts = LA.toList (crfTimes cr)
-        mkCause (i, (_cause, cifV)) =
-          let pts   = zip ts (LA.toList cifV)
-              verts = stepVerts 0.0 pts
-              col   = quantilePalette !! (i `mod` length quantilePalette)
-          in layer (line (inline (map fst verts)) (inline (map snd verts))
-                      <> color (fromHex col))
-    in foldMap mkCause (zip [0 ..] (crfCIF cr))
-
--- ===========================================================================
--- 多変量・木 (描画可能)
---
--- PCA の代表図は **scree plot** (各主成分の寄与率 'pcaExplainedRatio' を棒で)、 木 (RF) の
--- 代表図は **特徴重要度バー** ('featureImportance')。 いずれも自己完結ゆえそのまま
--- 'Plottable'。 棒の x 軸はラベル ("PC1".. / "f1"..) なので 'inlineCat' (categorical) で渡す
--- (heatmap A9 と同じく 'bar' も categorical 軸が必要)。 優先低 (§3.5 A14) ゆえ scree/重要度
--- の 1 枚ずつに絞る (biplot や木構造図は将来拡張)。
--- ===========================================================================
-
-instance Plottable PCAResult where
-  -- scree plot: 各主成分 (PC1, PC2, …) の寄与率を棒で。
-  toPlot res =
-    let ratios = LA.toList (pcaExplainedRatio res)
-        labels = [ "PC" <> T.pack (show k) | k <- [1 .. length ratios] ]
-    in layer (bar (inlineCat labels) (inline ratios))
-
-instance Plottable RandomForest where
-  -- R 'varImpPlot' 流の 2 パネル: 左 = impurity (IncNodePurity)、 右 = permutation
-  -- (%IncMSE)。 各パネルは降順ソート + 実列名 + 横棒 ('coordFlip')。
-  toPlot rf =
-    let n     = V.length (featureImportance rf)
-        names = case rfFeatureNames rf of
-                  [] -> defaultFeatureNames n
-                  ns -> ns
-        imp   = V.toList (featureImportance rf)
-        perm  = V.toList (rfPermutationImportance rf)
-    in subplots
-         [ importanceBarNamed "IncNodePurity (impurity)" names imp
-         , importanceBarNamed "%IncMSE (permutation)"    names perm ]
-       <> subplotCols 2
-
--- | 名前つき importance を横棒 ('coordFlip') で描く (R 'varImpPlot' 流)。 重要度で
---   ソートするため 'scaleXDiscreteLimits' でカテゴリ順を明示する (bar 軸は既定
---   アルファベット順ゆえデータ並びでは効かない)。 coordFlip 後は limits 順が下→上
---   なので、 昇順 limits を渡して最重要を上端に置く。 タイトル付き。
-importanceBarNamed :: T.Text -> [T.Text] -> [Double] -> VisualSpec
-importanceBarNamed ttl names vals =
-  let ascByVal = map fst (sortBy (comparing snd) (zip names vals))  -- 昇順 → 最大が末尾 = 上端
-  in layer (bar (inlineCat names) (inline vals))
-       <> scaleXDiscreteLimits ascByVal
-       <> coordFlip <> title ttl
-
--- ===========================================================================
--- 木/アンサンブル — Phase 68 A2
---
--- 各モデルの分野定番図を **既存 mark のみ**で描く (新規 plot mark 不要):
---
---   * GradientBoosting (回帰/分類)・RandomForestClassifier = **特徴重要度 bar**。
---     GBM は重要度フィールドを持たないので弱学習器 ('Tree') の split 使用回数から
---     純粋計算する ('treeImportances'・RF.'featureImportance' と同方式・正規化)。
---   * DecisionTree = **樹形図**。 決定木は DAG の特殊形 (二分木) ゆえ、 HBM の
---     ModelGraph と同じ MDAG (Sugiyama 階層 layout) を **再利用**して node-link で描く
---     (split ノード = "f{j} ≤ {thr}"、 葉 = "y={class}")。
---
--- ⚠ DecisionTree の edge True/False ラベル・gini・サンプル数表示 (sklearn plot_tree
--- 相当) は DAGNode/DAGEdge が持たないため v1 では描かない。 必要なら専用 mark を
--- plot 側 Phase として起こす (= dendrogram Phase 48 と同型の判断)。
--- ===========================================================================
-
--- | 弱学習器 ('Tree') 列の split 使用回数による特徴重要度 (RF と同方式・合計 1 に正規化)。
---   特徴数は出現した最大 index + 1 (= 木で一度も使われない末尾特徴は現れない)。
-treeImportances :: [Tree] -> [Double]
-treeImportances trees =
-  let counts = foldr walk Map.empty trees
-      walk (RF.Leaf _)       m = m
-      walk (RF.Node j _ l r) m = walk l (walk r (Map.insertWith (+) j (1 :: Double) m))
-      d   = if Map.null counts then 0 else maximum (Map.keys counts) + 1
-      raw = [ Map.findWithDefault 0 j counts | j <- [0 .. d - 1] ]
-      tot = sum raw
-  in if tot <= 0 then raw else map (/ tot) raw
-
-instance Plottable GBRegressor where
-  -- 弱学習器の split 使用回数による特徴重要度 bar。
-  toPlot gb = importanceBar (treeImportances (gbrTrees gb))
-
-instance Plottable GBClassifier where
-  toPlot gb = importanceBar (treeImportances (gbcTrees gb))
-
-instance Plottable RFClassifierFit where
-  -- R 'varImpPlot' 流の 2 パネル: 左 = permutation (MeanDecreaseAccuracy)、
-  -- 右 = gini 減少 (MeanDecreaseGini・MDI)。 各パネル降順・実列名・横棒。
-  toPlot fit =
-    let perm  = LA.toList (rfcImportance fit)
-        gini  = LA.toList (rfcGiniImportance fit)
-        names = case rfcFeatureNames fit of
-                  [] -> defaultFeatureNames (length perm)
-                  ns -> ns
-    in subplots
-         [ importanceBarNamed "MeanDecreaseAccuracy" names perm
-         , importanceBarNamed "MeanDecreaseGini"     names gini ]
-       <> subplotCols 2
-
-instance Plottable DTree where
-  -- 決定木 → node-link 樹形図 (MDAG 再利用・Sugiyama 階層 layout)。
-  toPlot t =
-    let (dnodes, dedges)     = dtreeToDag t
-        (positioned, routed) = layoutHierarchicalFullWithPlates dnodes dedges []
-    in bakeDAGRoutesInSpec $
-         layer (dagFromListsWithPlates positioned routed LayoutHierarchical [])
-
--- | 学習済み 'DTFit' → **rpart.plot 流**の樹形図 ('treePlot' と同じ)。 @df |-> decisionTree@
---   の返り値をそのまま @toPlot@ に渡せる。 素の node-link 図は 'DTree' の 'Plottable'。
-instance Plottable DTFit where
-  toPlot = treePlot
-
--- | 'DTree' を MDAG の node/edge 列へ変換する。 ノード id は根から L/R を辿る経路
---   ("n" / "nL" / "nLR" …) で一意。 split ノードは @NodeOther@、 葉は @NodeObserved@
---   (色で区別)。 左 child = 条件成立 (≤)・右 = 不成立 (>) の慣例で並べる。
-dtreeToDag :: DTree -> ([DAGNode], [DAGEdge])
-dtreeToDag = go "n"
-  where
-    mkNode nid lbl kind = DAGNode
-      { dnId = nid, dnLabel = lbl, dnKind = kind, dnDist = Nothing, dnX = 0, dnY = 0 }
-    go nid DLeaf{dlMajority = maj} =
-      ( [ mkNode nid ("y=" <> T.pack (show maj)) NodeObserved ], [] )
-    go nid DNode{dnFeature = f, dnThr = thr, dnLeft = l, dnRight = r} =
-      let self     = mkNode nid ("f" <> T.pack (show f) <> " ≤ " <> fmt2 thr) NodeOther
-          lid      = nid <> "L"
-          rid      = nid <> "R"
-          (ln, le) = go lid l
-          (rn, re) = go rid r
-          edges    = [ DAGEdge nid lid Nothing Nothing
-                     , DAGEdge nid rid Nothing Nothing ]
-      in (self : ln ++ rn, edges ++ le ++ re)
-    fmt2 x = T.pack (showFFloat (Just 2) x "")
-
--- ---------------------------------------------------------------------------
--- Phase 75.26: 決定木 樹形図 (rpart.plot 流・annotation ベース)
--- ---------------------------------------------------------------------------
-
--- | 位置付け済みの決定木ノード (annotation 描画用の中間表現)。 @tpU@ は葉単位の
---   水平座標 (葉 = 0,1,2,…・内部 = 子の中点)、 @tpDepth@ は根からの深さ。
-data TPNode = TPNode
-  { tpU     :: !Double                -- ^ 葉単位の水平座標。
-  , tpDepth :: !Int                   -- ^ 根からの深さ (根 = 0)。
-  , tpMaj   :: !Int                   -- ^ 多数決 (予測) クラス。
-  , tpN     :: !Int                   -- ^ ノードのサンプル数。
-  , tpProbs :: !(Map.Map Int Double)  -- ^ クラス割合。
-  , tpSplit :: !(Maybe (Int, Double)) -- ^ 分岐なら (特徴 index, 閾値)。 葉は Nothing。
-  , tpKids  :: [TPNode]               -- ^ [] = 葉、 [左, 右] = 分岐。
-  }
-
--- | Phase 75.26: 決定木を **rpart.plot 流**の樹形図で描く (analyze 側 annotation ベース)。
---
--- 各ノードを矩形で表し、 内部に **予測クラス / 全クラス確率 / サンプル割合** を 3 行で
--- 書く (rpart.plot @type=2@ 既定に相当)。 配線は R と同じく **親→バスの縦線を引かず**、
--- 分割条件 @feat < thr@ を親の少し下の水平バス上に置き、 枝はその両端から出て子の真上で
--- 折れる。 条件の両脇 (**根の分岐のみ**) に枠付き白箱で @yes@ (左=成立)・@no@ (右) を添える。
---
--- 塗り色は rpart.plot @box.palette="auto"@ 準拠で、 クラスごとに ColorBrewer 連番
--- パレット (Reds/Greys/Greens/…) を割当て、 **濃淡で予測クラスの確率 (確信度)** を表す
--- (淡=低・濃=高)。 暗い塗りには白文字を自動選択。 右上にクラス色の凡例を出す。
---
--- 第 1 = 特徴量名、 第 2 = クラス名 ('printRpart' と同型・長さ不足は @f{i}@/整数へ
--- フォールバック)。 木レイアウトは葉を左→右へ等間隔・深さ→縦位置で配置し、 座標は
--- panel 正規化 (PNpc) で算術する。 plot core の型は触らず annotation だけで描く
--- (図が固まれば plot 正式 mark へ移譲予定・PS parity は移譲時に対応)。
---
--- ⚠ 文字幅は annotation では実測できないため npc で概算する ('wpc')。 既定は図幅
--- 〜680px 前提に調律してあり、 極端なサイズでは箱幅/マスク幅が僅かにズレる。
---
--- 高レベル 'treePlot' は 'DTFit' 一つを取り (@df |-> decisionTree@ の返り値をそのまま
--- 渡せる)、 内部に載った特徴量名・クラス名を使う。 名前を手渡ししたい行列 fit 用は
--- 'treePlotRaw'。 'DTFit' は 'Plottable' なので @toPlot@ でも同じ図が出る。
-treePlot :: DTFit -> VisualSpec
-treePlot (DTFit tree feats classes) = treePlotRaw feats classes tree
-
--- | 行列 fit 用の低レベル版 — 特徴量名・クラス名を明示的に渡す (名無しは @f{i}@/整数へ
---   フォールバック)。
-treePlotRaw :: [Text] -> [Text] -> DTree -> VisualSpec
-treePlotRaw featNames classNames tree =
-  theme ThemeVoid
-    <> xAxis hideTicks <> yAxis hideTicks       -- 目盛線・目盛ラベルを消す (樹形図は座標軸不要)。
-    <> legendLayer                              -- クラス色の凡例 (標準機構・他マークと同じ)。
-    <> themeLegendFont (fontSize 11)            -- 凡例文字をノード (class 11pt) に揃える。
-    <> mconcat (concatMap edgesOf allNodes)     -- 枝を先に (ノード矩形の下敷き)。
-    <> mconcat (concatMap nodeAnns allNodes)
-  where
-    (nLeaves, root) = assign 0 0 tree
-    allNodes        = flatten root
-    total           = tpN root
-    maxD            = maximum (map tpDepth allNodes)
-    classes         = Map.keys (Map.fromList
-                        [ (c, ()) | t <- allNodes
-                        , c <- tpMaj t : Map.keys (tpProbs t) ])
-    nClasses        = length classes
-    colorIx         = Map.fromList (zip classes [0 :: Int ..])
-
-    -- ---- 配色: rpart.plot box.palette="auto" 準拠 --------------------------
-    --   クラスごとに ColorBrewer 連番パレット (Reds/Greys/Greens/…) を割当て、
-    --   塗りの **濃淡で予測クラスの確率 (確信度)** を表す。 R iris 実測と一致:
-    --   setosa=Reds・versicolor=Greys・virginica=Greens、 淡=低確率・濃=高確率。
-    nodeFill t =
-      let pi_  = maybe 0 id (Map.lookup (tpMaj t) colorIx)
-          pal9 = ix greysP brewerPals (pi_ `mod` length brewerPals)
-          p    = Map.findWithDefault 0 (tpMaj t) (tpProbs t)
-      in ix "#cccccc" pal9 (shadeIx p)
-    -- 予測確率 p∈[1/K,1] を 9 段 palette の index (概ね 1..5) へ (R 実測に fit)。
-    shadeIx p =
-      let k = fromIntegral (max 2 nClasses) :: Double
-      in max 0 (min 8 (round (1 + (p - 1 / k) / (1 - 1 / k) * 4) :: Int))
-    -- 塗りが暗いときは白文字 (簡易輝度判定)。
-    textColorFor hex = if luminance hex < 0.5 then "#ffffff" else "#111111"
-
-    -- ---- npc 座標変換 -----------------------------------------------------
-    leftM = 0.04; rightM = 0.04; topM = 0.85; botM = 0.16
-    spanX = 1 - leftM - rightM
-    xNpc u = leftM + (u + 0.5) / fromIntegral nLeaves * spanX
-    yNpc d | maxD <= 0 = topM
-           | otherwise = topM - fromIntegral d / fromIntegral maxD * (topM - botM)
-    colW = spanX / fromIntegral nLeaves
-    -- 箱は中身 (最長のクラス名 / 確率行) に合わせて締める (スカスカ回避)。 フォントは
-    -- **凡例 (themeLegendFont 11pt) と揃える** (class 11 / 数値 10)。 font を膨らませず
-    -- 箱側を締めて詰めて見せる (凡例とノードのサイズを統一)。
-    contentW = maximum (0.06 : [ wpc 10 (plineOf t) | t <- allNodes ]
-                            ++ [ wpc 11 (classLabel (tpMaj t)) | t <- allNodes ])
-    hw   = min (colW * 0.47) (contentW / 2 + 0.016)  -- 矩形半幅。
-    hh   = 0.054                      -- 矩形半高。
-    dy   = 0.030                      -- 3 行ラベルの行間 (npc)。
-    bc   = -0.011                     -- ベースライン補正 (npc・下げて上下中央に見せる)。
-    plineOf t = T.intercalate "  "
-                  [ fmtP (Map.findWithDefault 0 c (tpProbs t)) | c <- classes ]
-
-    -- ---- ノード矩形 + 3 行ラベル (rpart.plot type=2 相当・上下中央) ---------
-    --   1 行目 = 予測クラス、 2 行目 = 全クラス確率 (.34 .30 .35 形式)、
-    --   3 行目 = 全体に占めるサンプル割合 (%)。
-    nodeAnns t =
-      let x    = xNpc (tpU t); y = yNpc (tpDepth t)
-          fill = nodeFill t
-          tc   = textColorFor fill
-          pct  = 100 * fromIntegral (tpN t) / fromIntegral total :: Double
-          box  = rectA fill "#404040" 0.7 (x - hw) (y - hh) (x + hw) (y + hh)
-          l1   = textC tc x (y + dy + bc) 11 (classLabel (tpMaj t))
-          l2   = textC tc x (y      + bc) 10 (plineOf t)
-          l3   = textC tc x (y - dy + bc) 10 (fmt0 pct <> "%")
-      in [box, l1, l2, l3]
-
-    -- ---- 凡例 (標準機構) --------------------------------------------------
-    --   手描き annotation は中央アンカーで文字が揃わないため、 **他マークと同じ
-    --   凡例機構**に載せる: 不可視 (alpha 0) の colorBy 散布レイヤを 1 枚足し、
-    --   'scaleColorManual' で各クラス名→代表色 (ColorBrewer index 4) を固定する。
-    --   凡例スウォッチは layer alpha 非適用ゆえ満色で出る (グリフだけ不可視)。
-    reprColor i = ix "#888888" (ix greysP brewerPals (i `mod` length brewerPals)) 4
-    legendLayer =
-      let cats = [ classLabel c | c <- classes ] :: [Text]
-          xs   = [ fromIntegral i | i <- [0 .. nClasses - 1] ] :: [Double]
-          dict = [ (classLabel c, reprColor i) | (i, c) <- zip [0 :: Int ..] classes ]
-      in layer (scatter (inline xs) (inline xs) <> colorBy (inlineCat cats) <> alpha 0)
-           <> scaleColorManual dict
-
-    -- ---- 枝 = rpart.plot type=2 の配線 -----------------------------------
-    --   ★親→バスの縦線は引かない (R 準拠)。 分割ラベルを親の少し下に置き、 枝は
-    --   ラベル両端から水平に出て子の真上で下へ折れる。 中央 (ラベル/yes-no) 部分は
-    --   線を描かないことで枝線をマスクする。 yes/no は **根の分岐のみ**・枠付き白箱。
-    edgesOf t = case (tpKids t, tpSplit t) of
-      ([l, r], Just (f, thr)) ->
-        let px   = xNpc (tpU t); pBot = yNpc (tpDepth t) - hh
-            lx   = xNpc (tpU l); rx   = xNpc (tpU r)
-            cTop = yNpc (tpDepth l) + hh          -- 子上端 (左右子は同じ深さ)。
-            busY = pBot - 0.03                    -- バスは親の少し下 (縦線なし)。
-            condTxt = featName f <> " < " <> fmt2 thr
-            lw    = wpc 11 condTxt
-            isRoot = tpDepth t == 0
-            -- 中央の非描画幅 (ラベル + 根なら yes/no 箱ぶん)。
-            clr   = lw / 2 + (if isRoot then 0.075 else 0.008)
-            branch = [ lineA lx busY (px - clr) busY  -- 左枝 (水平)。
-                     , lineA (px + clr) busY rx busY  -- 右枝 (水平)。
-                     , lineA lx busY lx cTop          -- 左子へ縦。
-                     , lineA rx busY rx cTop ]        -- 右子へ縦。
-            cond = textA px (busY - 0.004) 11 condTxt
-            yn   = if isRoot
-                     then labelBox (px - lw / 2 - 0.03) busY "yes"
-                       ++ labelBox (px + lw / 2 + 0.026) busY "no"
-                     else []
-        in branch ++ cond : yn
-      _ -> []
-
-    -- yes/no の枠付き白箱 (中央にテキスト)。
-    labelBox cx cy txt =
-      let w = wpc 10 txt + 0.014; h = 0.03
-      in [ rectA "#ffffff" "#555555" 0.7 (cx - w / 2) (cy - h / 2) (cx + w / 2) (cy + h / 2)
-         , textA cx (cy - 0.004) 10 txt ]
-
-    -- ---- annotation プリミティブ (PNpc 固定) ----------------------------
-    rectA fill stroke sw x1 y1 x2 y2 = annotate $
-      AnnRect (PNpc x1) (PNpc y1) (PNpc x2) (PNpc y2) fill stroke sw 1.0
-    textA = textC "#111111"
-    textC col x y sz t = annotate $
-      AnnText (PNpc x) (PNpc y) t col sz
-    lineA x1 y1 x2 y2 = annotate $
-      AnnLine (PNpc x1) (PNpc y1) (PNpc x2) (PNpc y2) "#606060" 0.8
-
-    -- 文字列の描画幅を npc で概算 (font px と文字数から線形近似・図幅 ~680px 前提)。
-    -- annotation は実測不可ゆえの heuristic。 doc/demo は size を指定して調律に合わせる。
-    wpc fs t = 0.00095 * fs * fromIntegral (T.length t)
-
-    -- ---- 名前解決 ('printRpart' と同じ規則) ------------------------------
-    featName i   = pick i featNames  ("f" <> tShowI i)
-    classLabel i = pick i classNames (tShowI i)
-    pick i xs d  = case drop i xs of
-      (nm : _) | not (T.null nm) -> nm
-      _                          -> d
-
-    tShowI = T.pack . show :: Int -> Text
-    fmt2 x = T.pack (showFFloat (Just 2) x "")
-    fmt0 x = T.pack (showFFloat (Just 0) x "")
-    -- rpart.plot 流の確率表記 (先頭 0 を落として ".34"、 1.00 は据置き)。
-    fmtP x = let s = T.pack (showFFloat (Just 2) x "")
-             in maybe s id (T.stripPrefix "0" s)
-    ix d xs i = if i >= 0 && i < length xs then xs !! i else d
-
--- | 'DTree' を葉単位で位置付けした 'TPNode' へ変換する。 葉に左→右で連番 (slot) を
---   振り、 内部ノードは左右子の中点を水平座標にする。 戻りは (葉総数, 根ノード)。
-assign :: Int -> Int -> DTree -> (Int, TPNode)
-assign depth k node = case node of
-  DLeaf p m n _ ->
-    (k + 1, TPNode (fromIntegral k) depth m n p Nothing [])
-  DNode f thr l r n _ p m ->
-    let (k1, lp) = assign (depth + 1) k  l
-        (k2, rp) = assign (depth + 1) k1 r
-        u        = (tpU lp + tpU rp) / 2
-    in (k2, TPNode u depth m n p (Just (f, thr)) [lp, rp])
-
--- | 'TPNode' 木を前順で平坦化する。
-flatten :: TPNode -> [TPNode]
-flatten t = t : concatMap flatten (tpKids t)
-
--- | ColorBrewer 9 段連番パレット (rpart.plot box.palette="auto" の per-class 割当)。
---   クラス index 0,1,2,… に Reds, Greys, Greens, Blues, Purples, Oranges を循環割当。
-brewerPals :: [[Text]]
-brewerPals = [redsP, greysP, greensP, bluesP, purplesP, orangesP]
-
-redsP, greysP, greensP, bluesP, purplesP, orangesP :: [Text]
-redsP    = ["#fff5f0","#fee0d2","#fcbba1","#fc9272","#fb6a4a","#ef3b2c","#cb181d","#a50f15","#67000d"]
-greysP   = ["#ffffff","#f0f0f0","#d9d9d9","#bdbdbd","#969696","#737373","#525252","#252525","#000000"]
-greensP  = ["#f7fcf5","#e5f5e0","#c7e9c0","#a1d99b","#74c476","#41ab5d","#238b45","#006d2c","#00441b"]
-bluesP   = ["#f7fbff","#deebf7","#c6dbef","#9ecae1","#6baed6","#4292c6","#2171b5","#08519c","#08306b"]
-purplesP = ["#fcfbfd","#efedf5","#dadaeb","#bcbddc","#9e9ac8","#807dba","#6a51a3","#54278f","#3f007d"]
-orangesP = ["#fff5eb","#fee6ce","#fdd0a2","#fdae6b","#fd8d3c","#f16913","#d94801","#a63603","#7f2704"]
-
--- | @#rrggbb@ の相対輝度 (0..1・Rec.601 加重和)。 塗りの明暗で文字色を切替える用。
-luminance :: Text -> Double
-luminance hex =
-  let s = T.dropWhile (== '#') hex
-      hx a b = fromIntegral (16 * hv a + hv b) :: Double
-      hv c | c >= '0' && c <= '9' = fromEnum c - fromEnum '0'
-           | c >= 'a' && c <= 'f' = fromEnum c - fromEnum 'a' + 10
-           | c >= 'A' && c <= 'F' = fromEnum c - fromEnum 'A' + 10
-           | otherwise            = 0
-  in case T.unpack s of
-       (r1:r2:g1:g2:b1:b2:_) ->
-         (0.299 * hx r1 r2 + 0.587 * hx g1 g2 + 0.114 * hx b1 b2) / 255
-       _ -> 1
-
--- ===========================================================================
--- 分類 (Discriminant / NaiveBayes / KNN) — Phase 68 A3
---
--- 代表図は **決定境界** と **confusion 行列**。 いずれも「学習済モデルを評価点で
--- 走らせる」 図ゆえ、 KMeans (A1) と同じく **データ/範囲を取るヘルパ**で提供する
--- (新規 plot mark 不要):
---
---   * 'decisionBoundaryOf' = 2D grid を予測しクラス色で塗る (= 連続軸の散布を
---     四角マーカー・低 alpha で「領域」表現。 ★renderHeatmap はカテゴリ軸なので
---     連続 grid には不適 → 'MScatter' + 'colorBy' (離散色) を採用)。 2 特徴前提。
---   * 'confusionOf' = テストデータの真値×予測の件数を 'MHeatmap' で (カテゴリ軸が適合)。
---
--- 'Plottable' の 'toPlot' (データ非保持で描ける代表 1 枚):
---   * KNN は訓練データ ('knnCX'/'knnCY') を保持 → **ラベル色の訓練点散布**。
---   * Discriminant / NaiveBayes(Gaussian) は **クラス平均散布** (✚)、
---     NaiveBayes(Multinomial) は **クラス事前確率 bar**。
--- ===========================================================================
-
-
-instance ClassPredict DiscriminantFit where
-  predictClasses fit m = V.toList (fst (predictDiscriminant fit m))
-
-instance ClassPredict NBModel where
-  predictClasses nb m = VU.toList (predictNB nb m)
-  classNamesOf (NBGaussian g)    = gnbClassNames g
-  classNamesOf (NBMultinomial g) = mnbClassNames g
-
-instance ClassPredict KNNClassifier where
-  predictClasses knn m = VU.toList (predictKNNC knn m)
-  classNamesOf = knnCClassNames
-
--- Phase 75.5: 分類 NN も同様に decisionBoundaryOf / confusionOf 対応。
-instance ClassPredict MLPFit where
-  predictClasses fit m = V.toList (predictMLPClass fit m)
-  classNamesOf = mlpClassNames
-
--- Phase 75.12: カーネル SVM (真の SV) も decisionBoundaryOf (非線形境界) / confusionOf 対応。
-instance ClassPredict SVM where
-  predictClasses m x = VU.toList (predictSVM m x)
-
-instance ClassPredict SVMMulti where
-  predictClasses m x = VU.toList (predictSVMMulti m x)
-  classNamesOf = svmmClassNames
-
--- | 決定境界 (2 特徴) の **領域塗り** (Phase 76.A・annotation ベース)。
---
--- @res×res@ の格子セルを中心で予測し、 各セルを予測クラス色の塗り矩形 ('annotRectP')
--- で敷き詰める (sklearn @DecisionBoundaryDisplay@ の pcolormesh 相当)。 点散布でなく
--- **実矩形**をセル境界ぴったりに敷くので、 旧実装 (半透明の四角散布) の**縞模様**が出ない。
---
--- クラス色は 'toPlot' の凡例 (@colorBy@ → ggplot @hue_pal()@) と一致させる。 render が
--- categorical を @sort.nub@ 順に並べ 'ggplotHue' を割り当てるのと同順で再現する。
--- 訓練点・クラス平均は呼び出し側で上に重ねる (@decisionBoundaryOf c xr yr res \<\> toPlot c@)。
---
--- ⚠ **annotation の制約**: 塗りは 'annotRectP' 固定の @fill-opacity=0.2@ (薄塗り)。
--- また annotation は layer の**後**に描かれるため、 塗りは重ねた訓練点の**上**に来る
--- (0.2 の薄塗りなので点は透けて見える)。 「点が上・塗りが下」 の厳密な重ね順や
--- 半透明でない濃淡は将来 plot 正式 mark ('MTile'/'MRaster') 移譲時に対応する。
--- クラス色は既定 hue パレット前提 (theme series palette を差し替えた場合、 塗り色は
--- 追従しない — annotation 色は spec 時に確定するため)。
-decisionBoundaryOf
-  :: ClassPredict c => c -> (Double, Double) -> (Double, Double) -> Int -> VisualSpec
-decisionBoundaryOf c (x0, x1) (y0, y1) res
-  | res <= 0 || x1 <= x0 || y1 <= y0 = mempty
-  | otherwise =
-      -- 軸ドメインをグリッド範囲へ正確に固定 (expand=FALSE)。 annotation は軸を駆動しない
-      -- ため、 これが無いと軸がデータ点範囲に縮み塗りがフレーム外へはみ出す (sklearn は
-      -- 軸 = グリッド範囲)。 範囲外の重畳点は panel に clip される。
-      coordCartesian x0 x1 y0 y1 <> mconcat
-      [ annotRectP (PNative cx0) (PNative cy0) (PNative cx1) (PNative cy1) (colorFor k)
-      | (idx, k) <- zip [0 :: Int ..] preds
-      , let (i, j) = idx `divMod` res
-            cx0 = x0 + fromIntegral i * dx
-            cx1 = cx0 + dx
-            cy0 = y0 + fromIntegral j * dy
-            cy1 = cy0 + dy ]
-  where
-    dx = (x1 - x0) / fromIntegral res
-    dy = (y1 - y0) / fromIntegral res
-    -- セル中心 (行 = i*res + j・列 = [x, y]) をまとめて 1 回でバッチ予測する。
-    centers = [ [ x0 + (fromIntegral i + 0.5) * dx, y0 + (fromIntegral j + 0.5) * dy ]
-              | i <- [0 .. res - 1], j <- [0 .. res - 1] ]
-    preds = predictClasses c (LA.fromLists centers)
-    -- クラス色の対応: render は colorBy の categorical を sort.nub 順に並べ ggplotHue を
-    -- 割り当てる。 同じ手順を再現し、 予測クラス k → クラス名 → cats 内 index → 色。
-    names     = classNamesOf c
-    labelOf k = classNameByIx names k
-    classK    = if null names then sort (nub preds) else [0 .. length names - 1]
-    cmap      = hueColorMap (map labelOf classK)
-    colorFor k = Map.findWithDefault "#cccccc" (labelOf k) cmap
-
--- | confusion 行列のヒートマップ: テストデータ @X@ を予測し、 真値 @yTrue@ との件数を
---   x=予測 / y=真値 のセルに集計する ('MHeatmap'・色 = 件数)。
--- | クラス番号 k → 名前 (levels があれば @names !! k@・範囲外/空なら整数 show)。
---   分類 toPlot / confusion がクラス名を出す共通ヘルパ。
-classNameByIx :: [Text] -> Int -> Text
-classNameByIx names k
-  | k >= 0 && k < length names = names !! k
-  | otherwise                  = T.pack (show k)
-
-confusionOf :: ClassPredict c => c -> LA.Matrix Double -> [Int] -> VisualSpec
-confusionOf c x yTrue =
-  let yPred   = predictClasses c x
-      classes = sort (nub (yTrue ++ yPred))
-      -- クラス番号 → クラス名 (levels があれば名前・無ければ整数)。 対角は t==p→同名→
-      -- 同 index ゆえ、 名前順が整数順とずれても混同行列は正しい (対角=正解が保たれる)。
-      nameOf  = classNameByIx (classNamesOf c)
-      counts  = Map.fromListWith (+) [ ((t, p), 1 :: Int) | (t, p) <- zip yTrue yPred ]
-      cells   = [ (t, p, Map.findWithDefault 0 (t, p) counts) | t <- classes, p <- classes ]
-      xs = [ nameOf p | (_, p, _) <- cells ]
-      ys = [ nameOf t | (t, _, _) <- cells ]
-      vs = [ fromIntegral nC | (_, _, nC) <- cells ] :: [Double]
-      -- セル件数の数値注釈 (sklearn ConfusionMatrixDisplay 同型)。 heatmap の categorical
-      -- 軸は label を 'orderedCats' (= sort.nub) の index 位置に置くので、 text は同じ
-      -- index 位置 (数値座標) に重ねる (任意クラス数で整合)。 背景 box 付き ('label') ゆえ
-      -- viridis のどのセル色 (暗紫〜黄) でも読める。
-      axisLabels = sort (nub xs)                       -- x/y 同 classes ゆえ共通・軸順と一致
-      idxOf lbl  = maybe 0 fromIntegral (elemIndex lbl axisLabels) :: Double
-      txIdx  = [ idxOf (nameOf p) | (_, p, _) <- cells ]
-      tyIdx  = [ idxOf (nameOf t) | (t, _, _) <- cells ]
-      cntTxt = [ T.pack (show nC) | (_, _, nC) <- cells ]
-  in layer (heatmap (inlineCat xs) (inlineCat ys) (inline vs))
-       <> layer (label (inline txIdx) (inline tyIdx) (inlineCat cntTxt))
-       <> xLabel "predicted" <> yLabel "true"
-
--- ===========================================================================
--- MDS 埋め込み (モデル型 'MDSResult' + 群色オプション) — Phase 75.21
---
--- 'MDSResult' は @df |-> mds cfg cols@ の結果 (PCAResult 同格のモデル型)。
--- 既定は単色散布 ('Plottable' 'MDSResult' の @toPlot m@)、 群色は元データの列名を
--- 指定する 'mdsGroupBy' を @<>@ で合成する (regression の @statModel <> statColor@ と
--- 同形。 ただし 'statColor' は 'Color' 専用ゆえ「列名で群色」は別オプション)。
---
--- > m = df |-> mds defaultMDS ["x1","x2","x3"]
--- > noDf |>> toPlot m                              -- 単色
--- > noDf |>> toPlot (mdsView m <> mdsGroupBy "species")  -- species で群色
---
--- MDS は反転・回転自由度があるので軸の向きは本質でない (相対配置を見る)。
--- ===========================================================================
-
--- | MDS 埋め込みの描画オプション束 (Monoid)。 'mdsView' で結果を載せ、
--- 'mdsGroupBy' で群色列を足して @<>@ で合成する。
-data MDSView = MDSView
-  { mvResult   :: !(Maybe MDSResult)  -- ^ 描く埋め込み (後勝ち)。
-  , mvGroupCol :: !(Maybe Text)       -- ^ 群色に使う元データの列名 (後勝ち)。
-  }
-
-instance Semigroup MDSView where
-  a <> b = MDSView (orElse (mvResult b) (mvResult a))
-                   (orElse (mvGroupCol b) (mvGroupCol a))
-    where orElse (Just x) _ = Just x
-          orElse Nothing  y = y
-
-instance Monoid MDSView where
-  mempty = MDSView Nothing Nothing
-
--- | MDS 結果を描画オプションに載せる (@<>@ の起点)。
-mdsView :: MDSResult -> MDSView
-mdsView m = mempty { mvResult = Just m }
-
--- | 元データの列名で群色を付ける (factor/数値どちらでも categorical 色に)。
--- @toPlot (mdsView m <> mdsGroupBy "species")@。
-mdsGroupBy :: Text -> MDSView
-mdsGroupBy c = mempty { mvGroupCol = Just c }
-
-instance Plottable MDSResult where
-  -- 単色の埋め込み散布。
-  toPlot m = toPlot (mdsView m)
-
-instance Plottable MDSView where
-  toPlot v = case mvResult v of
-    Nothing -> mempty
-    Just m  ->
-      let cols = LA.toColumns (mdsEmbedding m)
-          xs   = if not (null cols)  then LA.toList (head cols) else []
-          ys   = if length cols >= 2 then LA.toList (cols !! 1) else replicate (length xs) 0
-          base = scatter (inline xs) (inline ys)
-          withColor = case mvGroupCol v >>= \gc -> groupLabels gc (mdsSourceFrame m) of
-            Just labs -> base <> colorBy (inlineCat labs)
-            Nothing   -> base
-      in layer withColor <> xLabel "MDS1" <> yLabel "MDS2"
-
--- | 元データの列を categorical な群ラベル ('[Text]') に変換する。 text 列
--- ('getTextVec') を優先し、 無ければ数値列 ('getDoubleVec') を整数寄せで文字列化。
-groupLabels :: Text -> DXD.DataFrame -> Maybe [Text]
-groupLabels gc frame =
-  case getTextVec gc frame of
-    Just tv -> Just (V.toList tv)
-    Nothing -> case getDoubleVec gc frame of
-      Just dv -> Just (map numLabel (V.toList dv))
-      Nothing -> Nothing
-  where
-    -- 整数値は小数点を出さない (0.0 → "0")。
-    numLabel x = let r = round x :: Int
-                 in if fromIntegral r == x then T.pack (show r) else T.pack (show x)
-
--- | NN 学習損失曲線 (Phase 75.5)。 'mlpLossHist' (エポックごとの損失) を epoch (x) 対
--- loss (y) の line で描く。 損失が単調減少して平坦化すれば収束 (keras @history@ 同型)。
-nnLossOf :: MLPFit -> VisualSpec
-nnLossOf fit =
-  let losses = mlpLossHist fit
-      epochs = [ fromIntegral i | i <- [1 .. length losses] ] :: [Double]
-  in layer (line (inline epochs) (inline losses))
-       <> xLabel "epoch" <> yLabel "loss"
-
--- | カーネル SVM のサポートベクタ (α>0 の点) を強調散布する (Phase 75.12)。 第 0/1 特徴を
--- **そのクラスの色のまま ✚ (cross) マーカー**で打つ (通常点 ○ と形で区別・色はクラスで一致)。
--- 決定境界に重ねて「SV が境界を定義する」 様子を見る。 凡例は通常点散布側に任せる
--- ('legendOff')。 SV が無い/1 次元なら空。
-svmSupportVectorsOf :: SVM -> VisualSpec
-svmSupportVectorsOf m =
-  let cols = LA.toColumns (svmSVx m)
-      xs   = if not (null cols)  then LA.toList (head cols) else []
-      ys   = if length cols >= 2 then LA.toList (cols !! 1) else []
-      -- svmSVy は ±1 (+1 = 正クラス=1・-1 = クラス 0)。 散布の colorBy "cls" と同綴りに
-      -- "0"/"1" の categorical 色で合わせる (= 同グループ同色)。
-      labs = [ if y > 0 then "1" else "0" | y <- VU.toList (svmSVy m) ] :: [Text]
-  in if null xs || null ys then mempty
-     else layer ( scatter (inline xs) (inline ys)
-                  <> colorBy (inlineCat labs) <> shape MShCross )
-
--- | 連続な決定スコアを持つ分類器 (decisionLineOf 用)。 score ≥ 0 が片クラス、 < 0 が他。
-class ScorePredict c where
-  decisionScore :: c -> LA.Matrix Double -> [Double]
-
-instance ScorePredict SVM where
-  decisionScore m x = VU.toList (predictSVMScore m x)
-
--- | 決定境界を **線 (等高線)** で描く (Phase 75.13b)。 'decisionBoundaryOf' が領域を色で
--- 塗り分けるのに対し、 こちらは決定スコア = 0 の等値線を marching squares で引く
--- (sklearn の @contour(…, levels=[0])@ 相当)。 スコアベースなので滑らかな曲線になる。
--- @res@ = grid 解像度 (大きいほど滑らか)。 2 特徴前提。
-decisionLineOf :: ScorePredict c
-               => c -> (Double, Double) -> (Double, Double) -> Int -> VisualSpec
-decisionLineOf c (xlo, xhi) (ylo, yhi) res0 =
-  let res = max 2 res0
-      ax i = xlo + (xhi - xlo) * fromIntegral i / fromIntegral (res - 1)
-      ay j = ylo + (yhi - ylo) * fromIntegral j / fromIntegral (res - 1)
-      xsV  = V.generate res ax
-      ysV  = V.generate res ay
-      grid = LA.fromLists [ [xsV V.! i, ysV V.! j] | j <- [0 .. res - 1], i <- [0 .. res - 1] ]
-      zV   = V.fromList (decisionScore c grid)     -- row-major: index = j*res + i
-      z i j = zV V.! (j * res + i)
-      lvl = 0 :: Double
-      straddle a b = (a < lvl) /= (b < lvl)
-      interp (px, py) (qx, qy) va vb =
-        let t = (lvl - va) / (vb - va) in (px + t * (qx - px), py + t * (qy - py))
-      cellSegs i j =
-        let p00 = (xsV V.! i, ysV V.! j);       v00 = z i j
-            p10 = (xsV V.! (i+1), ysV V.! j);   v10 = z (i+1) j
-            p01 = (xsV V.! i, ysV V.! (j+1));   v01 = z i (j+1)
-            p11 = (xsV V.! (i+1), ysV V.! (j+1)); v11 = z (i+1) (j+1)
-            cross = concat
-              [ [ interp p00 p10 v00 v10 | straddle v00 v10 ]
-              , [ interp p10 p11 v10 v11 | straddle v10 v11 ]
-              , [ interp p01 p11 v01 v11 | straddle v01 v11 ]
-              , [ interp p00 p01 v00 v01 | straddle v00 v01 ] ]
-        in case cross of
-             [a, b]       -> [(a, b)]
-             [a, b, d, e] -> [(a, b), (d, e)]   -- saddle (近似ペアリング)
-             _            -> []
-      segs = concat [ cellSegs i j | i <- [0 .. res - 2], j <- [0 .. res - 2] ]
-  in mconcat
-       [ layer ( line (inline [x1, x2]) (inline [y1, y2])
-                 <> color (fromHex "#333333") )
-       | ((x1, y1), (x2, y2)) <- segs ]
-
-
-
--- ===========================================================================
--- 部分従属図 (PDP / ICE) — Phase 75.27
---
--- 純粋エンジン 'partialDependence' ('Model.PartialDependence') を VisualSpec に落とす
--- 玄関。 回帰モデルは 'RegPredict' instance で短く (@pdpPlot rf trainX 0 "age"@)、 未対応の
--- モデルや分類確率は predict 閉包を直接渡す escape hatch (@partialDependencePlot@) で描く。
--- R @pdp::partial@ / sklearn @PartialDependenceDisplay@ 相当。
--- ===========================================================================
-
--- | 学習済モデルを評価点行列で走らせ、 各行の **連続予測値** を返す共通インターフェース
---   (回帰モデルの PDP を種に依らず組むための薄い抽象)。 分類確率など instance の無い
---   ものは 'partialDependencePlot' に predict 閉包を直接渡す。
-class RegPredict m where
-  predictReg :: m -> LA.Matrix Double -> [Double]
-
-instance RegPredict RandomForest where
-  predictReg rf x = map (RF.predictRF rf) (LA.toLists x)
-
-instance RegPredict GBRegressor where
-  predictReg gb x = VU.toList (predictGBR gb x)
-
--- | 高レベル PDP: 訓練 df ('ColumnSource') と**列名**で部分従属図を描く。
---   @featCols@ = fit に使った特徴列 (順序込み)、 @target@ = 部分従属を見る列。 注目特徴を
---   観測範囲の grid で振り、 他特徴は訓練分布のまま各行予測して平均した曲線を描く
---   (R pdp / sklearn @kind='average'@ 相当)。 列が引けない / target が featCols に無いときは空図。
-pdpOf :: (RegPredict m, ColumnSource d) => m -> d -> [Text] -> Text -> VisualSpec
-pdpOf model d featCols target =
-  case (reqColsM featCols d, elemIndex target featCols) of
-    (Right x, Just j) -> partialDependencePlot x (predictReg model) j target
-    _                 -> mempty
-
--- | 高レベル PDP + ICE 重畳 (sklearn @kind='both'@)。 個体条件付き期待 (ICE) を薄灰で観測数
---   ぶん重ね、 平均 (PDP) を上描きする。 'pdpOf' の ICE 版。
-pdpIceOf :: (RegPredict m, ColumnSource d) => m -> d -> [Text] -> Text -> VisualSpec
-pdpIceOf model d featCols target =
-  case (reqColsM featCols d, elemIndex target featCols) of
-    (Right x, Just j) -> partialDependenceIcePlot x (predictReg model) j target
-    _                 -> mempty
-
--- | 低レベル PDP: 訓練特徴**行列**と列 index を直接取る ('pdpOf' の実体)。
-pdpPlot :: RegPredict m => m -> LA.Matrix Double -> Int -> Text -> VisualSpec
-pdpPlot m x j name = partialDependencePlot x (predictReg m) j name
-
--- | 低レベル PDP + ICE (行列・列 index 版)。
-pdpIcePlot :: RegPredict m => m -> LA.Matrix Double -> Int -> Text -> VisualSpec
-pdpIcePlot m x j name = partialDependenceIcePlot x (predictReg m) j name
-
--- | 任意モデル用 PDP。 predict 閉包 (行列 → 予測値) を直接受ける escape hatch。
---   分類の部分従属 (あるクラスの予測確率) 等、 'RegPredict' instance の無いモデルに使う。
-partialDependencePlot
-  :: LA.Matrix Double -> (LA.Matrix Double -> [Double]) -> Int -> Text -> VisualSpec
-partialDependencePlot x predict j name =
-  let r = partialDependence x predict j 40
-  in if null (pdpGrid r)
-       then mempty
-       else layer ( line (inline (pdpGrid r)) (inline (pdpMean r))
-                    <> color (fromHex "#1f77b4") )
-            <> xLabel name <> yLabel "partial dependence"
-
--- | 任意モデル用 PDP+ICE。 'partialDependencePlot' の ICE 重畳版 (predict 閉包版)。
-partialDependenceIcePlot
-  :: LA.Matrix Double -> (LA.Matrix Double -> [Double]) -> Int -> Text -> VisualSpec
-partialDependenceIcePlot x predict j name =
-  let r  = partialDependence x predict j 40
-      g  = pdpGrid r
-  in if null g
-       then mempty
-       else mconcat
-              [ layer ( line (inline g) (inline curve)
-                        <> color (fromHex "#bbbbbb") <> alpha 0.35 )
-              | curve <- PD.pdpIce r ]
-            <> layer ( line (inline g) (inline (pdpMean r))
-                       <> color (fromHex "#1f77b4") )
-            <> xLabel name <> yLabel "partial dependence"
-
--- ---------------------------------------------------------------------------
--- Phase 76.D: PDP を HBM 抽出子と同型に (Plottable 中間型 + toPlot・<> で合成)
---
--- @pdpOf model d featCols target@ は 'VisualSpec' を直に返すが、 demo は @[] |>> (…)@ の
--- ダミー束ねが要り不格好だった。 HBM の @forestOf@/@epred@ と同じく **Plottable 中間型**
--- ('PDPView') にし、 @toPlot@ で描画・@<>@ で装飾を合成する:
---
--- > noDf |>> (toPlot (pdp rf trainDf featCols target) <> title \"…\")
---
--- ★HBM 抽出子は fit が事後分布を内包し自己完結だが、 RF/GBM は訓練データを保持しないため
---   PDP は訓練 df ('ColumnSource') を受け取る (周辺化に訓練分布が要る)。 予測は 'RegPredict'。
--- ---------------------------------------------------------------------------
-
-data PDPKind = PDPAverage | PDPBoth
-
--- | PDP の Plottable 中間型 (Phase 76.D)。 特徴行列・予測子・注目列 index を捕捉し、
---   'toPlot' で PDP (平均) / PDP+ICE 曲線に描く。 'pdp' / 'pdpIce' で作る。
-data PDPView = PDPView
-  { pvX       :: !(LA.Matrix Double)              -- 訓練特徴行列 (周辺化の分布)
-  , pvPredict :: LA.Matrix Double -> [Double]     -- モデルの連続予測 (RegPredict 由来)
-  , pvJ       :: !Int                             -- 注目特徴の列 index
-  , pvName    :: !Text                            -- 注目特徴名 (x 軸)
-  , pvKind    :: !PDPKind
-  }
-
--- | 訓練 df + 特徴列から (特徴行列, 注目列 index) を解く。 引けない / target が featCols に
---   無いときは 0×0 行列 (toPlot が 'mempty' にする)。
-pdpXJ :: ColumnSource d => d -> [Text] -> Text -> (LA.Matrix Double, Int)
-pdpXJ d feats target =
-  case (reqColsM feats d, elemIndex target feats) of
-    (Right x, Just j) -> (x, j)
-    _                 -> (LA.fromLists [], 0)
-
--- | 平均部分従属 (PDP)。 @noDf |>> (toPlot (pdp model trainDf featCols target) <> …)@。
-pdp :: (RegPredict m, ColumnSource d) => m -> d -> [Text] -> Text -> PDPView
-pdp model d feats target =
-  let (x, j) = pdpXJ d feats target in PDPView x (predictReg model) j target PDPAverage
-
--- | PDP + ICE 重畳 (sklearn @kind='both'@)。 個体曲線 (薄灰) + 平均 (青)。
-pdpIce :: (RegPredict m, ColumnSource d) => m -> d -> [Text] -> Text -> PDPView
-pdpIce model d feats target =
-  let (x, j) = pdpXJ d feats target in PDPView x (predictReg model) j target PDPBoth
-
-instance Plottable PDPView where
-  toPlot (PDPView x predict j name k)
-    | LA.rows x == 0 = mempty
-    | otherwise = case k of
-        PDPAverage -> partialDependencePlot    x predict j name
-        PDPBoth    -> partialDependenceIcePlot x predict j name
-
-instance Plottable KNNClassifier where
-  -- 訓練データをラベル色で散布 (第 0/1 特徴)。 KNN は X/Y を保持するので data-rich。
-  -- 凡例は df|-> が載せた knnCClassNames があればクラス名・無ければ整数 ('classNameByIx')。
-  toPlot knn =
-    let cols = LA.toColumns (knnCX knn)
-        xs   = if not (null cols)      then LA.toList (head cols)   else []
-        ys   = if length cols >= 2     then LA.toList (cols !! 1)   else []
-        labs = map (classNameByIx (knnCClassNames knn)) (VU.toList (knnCY knn))
-    in layer (scatter (inline xs) (inline ys) <> colorBy (inlineCat labs))
-
-instance Plottable DiscriminantFit where
-  toPlot fit =
-    classMeansScatter (LA.toLists (dfMeans fit))
-                      (map round (LA.toList (dfClasses fit)))
-
-instance Plottable NBModel where
-  toPlot (NBGaussian m)    =
-    classMeansScatterNamed (map LA.toList (gnbMeans m)) (gnbClasses m) (gnbClassNames m)
-  toPlot (NBMultinomial m) =
-    let labels = [ classNameByIx (mnbClassNames m) cl | cl <- mnbClasses m ]
-    in layer (bar (inlineCat labels) (inline (map exp (mnbLogPrior m))))
-
--- ===========================================================================
--- 次元圧縮 (PLS / MultiGP) — Phase 68 A4
---
--- どちらも結果が自己完結 ('PCAResult' 同様) なので外部データ不要で 'Plottable':
---
---   * 'PLSFit' = 潜在空間の **score plot** (標本 T) を代表図に、 'loading plot' (変数 P)
---     と **VIP bar** を診断図束に。 いずれも既存 'MScatter'/'bar'。
---   * 'MultiGPResult' = **多出力の予測曲線 + 95% band** (出力ごとに色分け・x=index)。
---     'MLine' + 'MBand' を出力数ぶん重畳。
---
--- ※ 'Hanalyze.Model.MultiOutput' は変換+メトリクスの **ユーティリティ**で
--- fit 結果型を持たないため 'Plottable' 対象外 (多出力の「相関」図は既存
--- 'MultiFit' = 残差相関 heatmap が担当)。 新規 plot mark は不要。
--- ===========================================================================
-
-
--- | PLS 診断ビューの種別 (score / loading / VIP)。
-data PLSViewKind = ScoreView | LoadingView | VipView
-  deriving (Show, Eq)
-
--- | PLS の中間 Plottable Spec (HBM 式統一 — Phase 70.B)。 終端 'VisualSpec' を
--- 直返ししていた旧 @plsScorePlot@ 系を、 forest/trace 等と同じく **'Plottable' な
--- 中間 Spec** に揃える ('toPlot' 境界でオプション合成可・診断束を型で表現)。
-data PLSView = PLSView !PLSFit !PLSViewKind
-
--- | score ビュー: 標本を潜在空間の第 1/2 成分 (T[:,0] vs T[:,1]) で散布。
-scoreView :: PLSFit -> PLSView
-scoreView fit = PLSView fit ScoreView
-
--- | loading ビュー: 変数を潜在空間の第 1/2 成分 (P[:,0] vs P[:,1]) で散布。
-loadingView :: PLSFit -> PLSView
-loadingView fit = PLSView fit LoadingView
-
--- | VIP ビュー: 変数重要度 (Variable Importance in Projection) bar。
-vipView :: PLSFit -> PLSView
-vipView fit = PLSView fit VipView
-
-instance Plottable PLSView where
-  toPlot (PLSView fit ScoreView) =
-    let (xs, ys) = matCols2 (plsScoresT fit) 0 1
-    in layer (scatter (inline xs) (inline ys))
-         <> xLabel "comp 1" <> yLabel "comp 2"
-  toPlot (PLSView fit LoadingView) =
-    let (xs, ys) = matCols2 (plsLoadingsP fit) 0 1
-    in layer (scatter (inline xs) (inline ys))
-         <> xLabel "loading 1" <> yLabel "loading 2"
-  toPlot (PLSView fit VipView) =
-    let vips   = LA.toList (plsVIP fit)
-        labels = [ "f" <> T.pack (show k) | k <- [1 .. length vips] ]
-    in layer (bar (inlineCat labels) (inline vips))
-
-instance Plottable PLSFit where
-  -- 代表図 = score ビュー (標本の潜在空間布置)。
-  toPlot = toPlot . scoreView
-  -- 診断束 = score / loading / VIP の 3 枚。
-  diagnosticPlots fit = map toPlot [ scoreView fit, loadingView fit, vipView fit ]
-
--- ===========================================================================
--- 時系列・生存・FDA (GARCH / AFT / FDA) — Phase 68 A5
---
--- 新規 plot mark は不要 (既存 line/band の重畳):
---
---   * 'GARCHFit'      = 系列 (μ + ε_t) + 条件付き volatility 帯 (μ ± 2σ_t) の帯付き線。
---   * 'AFTFit'        = パラメトリック生存曲線 S(t|x)。 fit は観測時刻を持たないので
---                       代表図 ('toPlot') は **基準共変量** (intercept のみ) の曲線、
---                       任意共変量は 'aftSurvivalAt' ヘルパ。 t 範囲は予測平均寿命から導出。
---   * 'FunctionalPCA' = 平均関数 + 上位固有関数を grid 上に重畳 (x = grid index)。
---   * 'FLMResult'     = 関数回帰係数 β(t) の曲線。
--- ===========================================================================
-
--- | GARCH の条件付き volatility 帯付き線: 系列 @y_t = μ + ε_t@ の line に、
---   @μ ± 2σ_t@ (σ_t = √σ²_t) の帯を重ねる。 x = 時刻 index。
-garchVolatility :: GARCHFit -> VisualSpec
-garchVolatility fit =
-  let eps   = LA.toList (gResiduals fit)
-      s2    = LA.toList (gSigma2 fit)
-      mu    = gMu fit
-      n     = min (length eps) (length s2)
-      xs    = [ fromIntegral i | i <- [1 .. n] ] :: [Double]
-      ys    = [ mu + e | e <- take n eps ]
-      sig   = [ sqrt (max 0 v) | v <- take n s2 ]
-      lo    = zipWith (\_ s -> mu - 2 * s) xs sig
-      hi    = zipWith (\_ s -> mu + 2 * s) xs sig
-  in layer (band (inline xs) (inline lo) (inline hi) <> alpha 0.25)
-       <> layer (line (inline xs) (inline ys))
-       <> xLabel "t" <> yLabel "y"
-
-instance Plottable GARCHFit where
-  toPlot = garchVolatility
-
--- | AFT 生存曲線 S(t|x): 共変量 @x@ の線形予測子 @lp = x·β@ から
---   @z(t) = (log t − lp)/σ@・@S = exp(logS dist z)@ を t-grid 上で評価する。
---   t 範囲は予測平均寿命の @(0.01, 3×mean)@、 grid 120 点。
-aftSurvivalAt :: AFTFit -> [Double] -> VisualSpec
-aftSurvivalAt fit x =
-  let beta   = LA.toList (aftBeta fit)
-      lp     = sum (zipWith (*) x beta)
-      sigma  = aftScale fit
-      dist   = aftDistribution fit
-      meanL  = let v = predictAFT fit (LA.fromLists [x]) in head (LA.toList v)
-      tMax   = if meanL > 0 && not (isInfinite meanL) then 3 * meanL else 10
-      tMin   = max 1e-3 (tMax / 200)
-      ts     = linspace tMin tMax 120
-      surv t = exp (logS dist ((log t - lp) / sigma))
-      ss     = map surv ts
-  in layer (line (inline ts) (inline ss))
-       <> xLabel "t" <> yLabel "S(t)"
-
-instance Plottable AFTFit where
-  -- 代表図 = 基準共変量 (intercept 列のみ = [1,0,…,0]) の生存曲線。
-  toPlot fit =
-    let p = LA.size (aftBeta fit)
-        xRef = if p <= 0 then [] else 1 : replicate (p - 1) 0
-    in aftSurvivalAt fit xRef
-
-
-instance Plottable FunctionalPCA where
-  -- 平均関数 + 上位 (最大 3) 固有関数を grid 上に重畳。
-  toPlot fpca =
-    let meanFn = LA.toList (fpcaMeanFn fpca)
-        eigs   = LA.toRows (fpcaEigenfn fpca)
-        eigNs  = [ ("PC" <> T.pack (show k), LA.toList e)
-                 | (k, e) <- zip [1 :: Int ..] (take 3 eigs) ]
-    in gridCurves (("mean", meanFn) : eigNs)
-
-instance Plottable FLMResult where
-  -- 関数回帰係数 β(t) の曲線 (x = grid index)。
-  toPlot flm =
-    let betaFn = LA.toList (flmBetaFn flm)
-        xs     = [ fromIntegral i | i <- [1 .. length betaFn] ] :: [Double]
-    in layer (line (inline xs) (inline betaFn))
-         <> xLabel "t" <> yLabel "beta(t)"
-
--- ===========================================================================
--- 罰則回帰・因果探索 (Regularized / LiNGAM) — Phase 68 A6
---
--- 新規 plot mark は不要:
---
---   * 'RegFit'          = 単一 λ の係数 ('rfBeta') を bar (代表図)。
---   * 'regPathPlot'     = 正則化パス @[(λ, [β_j])]@ ('regularizationPath' 出力) を、
---                         係数ごとに 1 本の line で λ-横軸に重畳 (= LASSO 係数パス図)。
---   * 'DirectLiNGAMFit' = 推定した因果構造を **MDAG** で描く (B 行列 → node/edge、
---                         決定木と同じ MDAG 再利用)。 edge j→i は @|adjacency[i,j]|>0@。
--- ===========================================================================
-
-instance Plottable RegFit where
-  -- 係数 bar (b1, b2, … = rfBeta)。 intercept 含む並びをそのまま描く。
-  toPlot fit =
-    let bs     = LA.toList (rfBeta fit)
-        labels = [ "b" <> T.pack (show k) | k <- [0 .. length bs - 1] ]
-    in layer (bar (inlineCat labels) (inline bs))
-
--- | 正則化パス図: @[(λ, [β_j])]@ を係数ごとに 1 本の line で重畳。 横軸は **log₁₀λ**
---   (glmnet の係数パス図と同じ慣例・小 λ=full model が左、 大 λ=sparse が右)。 色=係数 index。
---   λ は正を仮定する (パスの λ グリッドは常に @> 0@)。
-regPathPlot :: [(Double, [Double])] -> VisualSpec
-regPathPlot path
-  | null path = mempty
-  | otherwise =
-      let logLams = map (logBase 10 . fst) path   -- x = log₁₀λ
-          rows = map snd path           -- λ ごとの [β_j]
-          p    = minimum (map length rows)
-          mkCoef j =
-            let ys  = [ r !! j | r <- rows ]
-                lbl = "b" <> T.pack (show j)
-            in layer ( line (inline logLams) (inline ys)
-                     <> colorBy (inlineCat (replicate (length logLams) lbl)) )
-      in mconcat [ mkCoef j | j <- [0 .. p - 1] ]
-           <> xLabel "log10(lambda)" <> yLabel "coef"
-
--- | 隣接行列 + 変数名から因果 DAG (MDAG) を描く低レベル (Phase 77.A で切り出し)。
---   edge @j→i@ は @|adj[i,j]| > 0@ (= x_i が x_j に依存)。 @names@ が列数と一致しなければ
---   @x0..@ フォールバック。 全 LiNGAM variant の Plottable が共有する。
-lingamDagNamed :: [Text] -> LA.Matrix Double -> VisualSpec
-lingamDagNamed rawNames adj =
-  let p     = LA.rows adj
-      names = if length rawNames == p && p > 0
-                then rawNames
-                else [ "x" <> T.pack (show j) | j <- [0 .. p - 1] ]
-      dnodes = [ DAGNode { dnId = nm, dnLabel = nm, dnKind = NodeObserved
-                         , dnDist = Nothing, dnX = 0, dnY = 0 } | nm <- names ]
-      dedges = [ DAGEdge (names !! j) (names !! i) Nothing Nothing
-               | i <- [0 .. p - 1], j <- [0 .. p - 1]
-               , abs (adj `LA.atIndex` (i, j)) > 0 ]
-      (positioned, routed) = layoutHierarchicalFullWithPlates dnodes dedges []
-  in bakeDAGRoutesInSpec $
-       layer (dagFromListsWithPlates positioned routed LayoutHierarchical [])
-
--- | 推定因果構造 (DirectLiNGAM) を MDAG で描く。 ノード = @x0..x_{p-1}@ (変数名は
---   高レベル @df |-> directLingam@ 経由で付く・'LiNGAMFitted' の Plottable 参照)。
-lingamDag :: DirectLiNGAMFit -> VisualSpec
-lingamDag fit = lingamDagNamed [] (dlAdjacency fit)
-
-instance Plottable DirectLiNGAMFit where
-  toPlot = lingamDag
-
--- | 高レベル @df |-> directLingam cols@ の結果 = **実変数名**の因果 DAG (Phase 77.A)。
-instance Plottable (LiNGAMFitted DirectLiNGAMFit) where
-  toPlot (LiNGAMFitted fit names) = lingamDagNamed names (dlAdjacency fit)
-
--- | ParceLiNGAM の名前付き DAG (Phase 77.B・pcAdjacency)。
-instance Plottable (LiNGAMFitted ParceFit) where
-  toPlot (LiNGAMFitted fit names) = lingamDagNamed names (pcAdjacency fit)
-
--- | MultiGroupLiNGAM の**共通** DAG (Phase 77.B・多数決 mgCommonAdj・名前付き)。
-instance Plottable (LiNGAMFitted MultiGroupFit) where
-  toPlot (LiNGAMFitted fit names) = lingamDagNamed names (mgCommonAdj fit)
-
--- | VARLiNGAM の**時間ラグ DAG** (Phase 77.B)。 ノード = 各変数の @name[t]@ / @name[t-l]@、
---   辺 = 同時刻 (@B0@: x_j[t]→x_i[t]) + ラグ (@structuralLags[l]@: x_j[t-l]→x_i[t])。
---   @thr@ 未満の係数は辺を出さない。 孤立したラグノード (辺に現れない) は省く。
-varLagDagNamed :: [Text] -> LA.Matrix Double -> [LA.Matrix Double] -> Double -> VisualSpec
-varLagDagNamed rawNames b0 lags thr =
-  let k    = LA.rows b0
-      base = if length rawNames == k && k > 0
-               then rawNames else [ "x" <> T.pack (show j) | j <- [0 .. k - 1] ]
-      p    = length lags
-      nm i 0 = base !! i <> "[t]"
-      nm i l = base !! i <> "[t-" <> T.pack (show l) <> "]"
-      contempEdges = [ DAGEdge (nm j 0) (nm i 0) Nothing Nothing
-                     | i <- [0 .. k - 1], j <- [0 .. k - 1]
-                     , abs (b0 `LA.atIndex` (i, j)) > thr ]
-      lagEdges = [ DAGEdge (nm j l) (nm i 0) Nothing Nothing
-                 | l <- [1 .. p], i <- [0 .. k - 1], j <- [0 .. k - 1]
-                 , abs ((lags !! (l - 1)) `LA.atIndex` (i, j)) > thr ]
-      dedges = contempEdges ++ lagEdges
-      refIds = concatMap (\(DAGEdge a b _ _) -> [a, b]) dedges
-      allNodes = [ (i, l) | l <- [0 .. p], i <- [0 .. k - 1] ]
-      keep (i, l) = l == 0 || nm i l `elem` refIds        -- 現時刻は常に・ラグは辺があるものだけ
-      dnodes = [ DAGNode { dnId = nm i l, dnLabel = nm i l, dnKind = NodeObserved
-                         , dnDist = Nothing, dnX = 0, dnY = 0 }
-               | (i, l) <- allNodes, keep (i, l) ]
-      (positioned, routed) = layoutHierarchicalFullWithPlates dnodes dedges []
-  in bakeDAGRoutesInSpec $
-       layer (dagFromListsWithPlates positioned routed LayoutHierarchical [])
-
--- | VARLiNGAM の高レベル結果 = 時間ラグ DAG (辺閾値 0.1・同時刻 + ラグ)。
-instance Plottable (LiNGAMFitted VARLiNGAMFit) where
-  toPlot (LiNGAMFitted fit names) =
-    varLagDagNamed names (vlB0 fit) (vlStructuralLags fit) 0.1
-
--- | PairwiseLiNGAM の 2 変数向き図 (Phase 77.B)。 検出向きの矢印 1 本 (Inconclusive は無向)。
---   2×2 隣接に落として 'lingamDagNamed' を再利用する。
-instance Plottable (LiNGAMFitted PairwiseResult) where
-  toPlot (LiNGAMFitted r names) =
-    let adj = case prDirection r of
-          XtoY         -> LA.fromLists [[0, 0], [1, 0]]   -- x(0) → y(1): adj[1,0]=1
-          YtoX         -> LA.fromLists [[0, 1], [0, 0]]   -- y(1) → x(0)
-          Inconclusive -> LA.fromLists [[0, 0], [0, 0]]   -- 無向 (2 ノードのみ)
-    in lingamDagNamed names adj
-
--- | ICA-LiNGAM の名前付き DAG (Phase 77.C・ilAdjacency)。
-instance Plottable (LiNGAMFitted ICALiNGAMFit) where
-  toPlot (LiNGAMFitted fit names) = lingamDagNamed names (ilAdjacency fit)
-
--- | 相関ネットワークのグラフ (Phase 77)。 @|r| > cgThreshold@ の対を辺にする (無向・向きは
---   index 順の便宜配置で**因果でない**)。 LiNGAM DAG と対比すると間接相関の過剰さが分かる。
---   下三角のみ辺にして重複/自己ループを避ける (相関は対称ゆえ)。
-instance Plottable CorrelationGraph where
-  toPlot (CorrelationGraph corr names thr) =
-    let p   = LA.rows corr
-        adj = LA.build (p, p)
-                (\i j -> let (ii, jj) = (round i, round j)
-                         in if ii > jj && abs (corr `LA.atIndex` (ii, jj)) > thr
-                              then 1 else 0 :: Double)
-    in lingamDagNamed names adj
-
--- | BootstrapLiNGAM の**確信度 DAG** (Phase 77.C)。 出現確率 ≥ 0.5 のエッジだけ描く
---   (= 過半数の bootstrap で現れた信頼できる因果構造)。 全確率は 'bootstrapEdgeProbOf' で。
-instance Plottable (LiNGAMFitted BootstrapResult) where
-  toPlot (LiNGAMFitted res names) =
-    let prob = brEdgeProbability res
-        p    = LA.rows prob
-        adj  = LA.build (p, p)
-                 (\i j -> if prob `LA.atIndex` (round i, round j) >= 0.5 then 1 else 0)
-    in lingamDagNamed names adj
-
--- | BootstrapLiNGAM の**エッジ出現確率ヒートマップ** (Phase 77.C)。 行=結果 i・列=原因 j、
---   セル = P(j→i) (0..1)。 確信度の全体像を DAG と別に見せる (python lingam の確率行列相当)。
-bootstrapEdgeProbOf :: LiNGAMFitted BootstrapResult -> VisualSpec
-bootstrapEdgeProbOf (LiNGAMFitted res rawNames) =
-  let prob  = brEdgeProbability res
-      p     = LA.rows prob
-      names = if length rawNames == p && p > 0
-                then rawNames else [ "x" <> T.pack (show j) | j <- [0 .. p - 1] ]
-      cells = [ (names !! j, names !! i, prob `LA.atIndex` (i, j))
-              | i <- [0 .. p - 1], j <- [0 .. p - 1] ]
-      xs = [ c | (c, _, _) <- cells ]      -- 原因 j (x 軸)
-      ys = [ r | (_, r, _) <- cells ]      -- 結果 i (y 軸)
-      vs = [ v | (_, _, v) <- cells ]
-  in layer (heatmap (inlineCat xs) (inlineCat ys) (inline vs))
-       <> xLabel "cause (j)" <> yLabel "effect (i)"
-
--- ===========================================================================
--- DOE prediction profiler — Phase 78.C/D/F
---
--- JMP の Prediction Profiler 相当 = **応答 × 各因子**のパネルをグリッドに並べる
--- (行=応答・列=因子)。 各パネル = 予測線 + 95% CI 帯 (他因子は中央値固定) + 打点。
--- 打点は 'Raw' (実測 y) か 'Partial' (偏残差 = 部分効果 + 全モデル残差) を @<>@ で選ぶ。
--- 既存 effect plot ('statModelMulti' + 'along' + 'holdAt') を再利用する。
---
--- 中間 Plottable 型 ('ProfilerSpec') にして @toPlot@ で描画・@<>@ でオプション合成
--- (HBM @epred@ / 'PDPView' と同じ流儀)。 打点はモデル ('mvFrame') の観測値から算出
--- するので @noDf@ で束ねられる。 複数応答は @df |-> 'multiOutput' ys (designModel plan)@
--- が返す @[(応答名, モデル)]@ をそのまま渡す。
---
--- > let model = df |-> multiOutput ["strength","yield"] (designModel plan)
--- > noDf |>> toPlot (profiler model ["temp","time"] <> profilerResidual Partial)
--- ===========================================================================
-
--- | 打点の種別。 'Raw' = 実測 y (他因子が動くぶん予測線から縦に散る = 多変量の正しい挙動)。
---   'Partial' = **偏残差** @fⱼ(xⱼ) + (全モデル残差)@ で他因子の寄与を除き点を予測線に乗せる
---   (R @termplot(partial.resid=TRUE)@ / @car::crPlots@ 相当)。
-data ResidualMode = Raw | Partial
-  deriving (Eq, Show)
-
--- | prediction profiler の中間 Plottable Spec (Phase 78.F)。 @(応答名, モデル)@ のリスト
---   (複数応答)・因子名・打点モード ('ResidualMode') を捕捉し、 'toPlot' で「行=応答 ×
---   列=因子」 のグリッドに描く。 'profiler' で作り、 @<> 'profilerResidual' Partial@ で
---   モードを合成する。
-data ProfilerSpec m = ProfilerSpec
-  { psModels   :: [(Text, m)]        -- ^ (応答ラベル, 学習済モデル)。 行になる。
-  , psFactors  :: [Text]             -- ^ 説明因子名。 列になる。
-  , psResidual :: Maybe ResidualMode -- ^ 打点モード (合成後 'Nothing' は 'Raw' 既定)。
-  }
-
--- | 右バイアス合成 (option-only 片は models\/factors が空)。 mode は後勝ち。
-instance Semigroup (ProfilerSpec m) where
-  a <> b = ProfilerSpec
-    { psModels   = psModels a  <> psModels b
-    , psFactors  = if null (psFactors b) then psFactors a else psFactors b
-    , psResidual = psResidual b <|> psResidual a }
-
-instance Monoid (ProfilerSpec m) where
-  mempty = ProfilerSpec [] [] Nothing
-
--- | @profiler models factors@ — 応答×因子の profiler。 @models@ は
---   @df |-> 'multiOutput' ys (designModel plan)@ が返す @[(応答名, モデル)]@。 既定は 'Raw'。
-profiler :: [(Text, m)] -> [Text] -> ProfilerSpec m
-profiler models factors = ProfilerSpec models factors Nothing
-
--- | 打点モードを差す option (@<>@ で合成)。 @profiler … <> profilerResidual Partial@。
-profilerResidual :: ResidualMode -> ProfilerSpec m
-profilerResidual mode = mempty { psResidual = Just mode }
-
-instance MultiVarModel m => Plottable (ProfilerSpec m) where
-  toPlot (ProfilerSpec models factors mMode)
-    | null models || null factors = mempty
-    | otherwise =
-        subplots [ panel lbl m f | (lbl, m) <- models, f <- factors ]
-          <> subplotCols (length factors)
-    where
-      mode = fromMaybe Raw mMode
-      -- 1 パネル = 予測線 + CI + 打点 (Raw: 実測 y / Partial: 偏残差)。他因子は中央値固定。
-      panel lbl m f =
-        let mf    = mvFrame m
-            contOf nm = case lookup nm (mfRoles mf) of
-              Just (RoleContinuous xs) -> V.toList xs
-              _                        -> []
-            xsf   = contOf f
-            (pts, ylab) = case mode of
-              Raw ->
-                let ysObs = case [ v | (_, RoleResponse v) <- mfRoles mf ] of
-                              (v : _) -> V.toList v
-                              []      -> []
-                in (ysObs, lbl)
-              Partial ->
-                let ysObs = case [ v | (_, RoleResponse v) <- mfRoles mf ] of
-                              (v : _) -> V.toList v
-                              []      -> []
-                    (muFull, _) = mvEvalFrame m 0.95 mf
-                    resid       = zipWith (-) ysObs muFull
-                    -- 部分効果 fⱼ(xⱼ): f=観測値・他因子=中央値固定 (予測線と同じ hold)。
-                    ef          = evalFrame mf f Median [] xsf
-                    (muPart, _) = mvEvalFrame m 0.95 ef
-                in (zipWith (+) muPart resid, "partial: " <> lbl)
-        in layer (scatter (inline xsf) (inline pts))
-             <> toPlot (statModelMulti m (along f) <> holdAt Median <> grid 60)
-             <> xLabel f <> yLabel ylab
-
--- | RSM **等高線 / 応答曲面** (Phase 78.E)。 2 因子 (v1, v2) を grid で動かし他因子を
---   中央値固定して応答 μ̂ を評価し、 **塗り等値帯 ('contourFilled') + 等高線 ('contour')** で
---   描く (R @rsm::contour@ / matplotlib @contourf+contour@ 相当・応答面を平面で俯瞰)。
---   3D の応答曲面は 'surfaceOf' (別途 @saveSVG3D@)。 評価はモデル観測範囲なので
---   @noDf |>> contourOf model "temp" "time"@ で描ける。
-contourOf :: MultiVarModel m => m -> Text -> Text -> VisualSpec
-contourOf m v1 v2 =
-  let (gxs, gys, grid') = surfaceGrid m v1 v2 (defaultSurfaceOpts { soHoldAt = Median })
-      -- grid' !! j !! i = μ̂(gxs!!i, gys!!j)。 (x, y, z) へ平坦化。
-      pts = concat (zipWith (\gy row -> zipWith (\gx z -> (gx, gy, z)) gxs row) gys grid')
-      xs  = [ x | (x, _, _) <- pts ]
-      ys  = [ y | (_, y, _) <- pts ]
-      zs  = [ z | (_, _, z) <- pts ]
-  in layer (contourFilled (inline xs) (inline ys) (inline zs))
-       <> layer (contour (inline xs) (inline ys) (inline zs) <> contourLevels 10)
-       <> xLabel v1 <> yLabel v2
-
--- ===========================================================================
--- 記述統計・検定 (Stat.*) — Phase 68 A7
---
--- 新規 plot mark は不要:
---
---   * 'TestResult'  = 効果量 + 95% CI の **forest** (検定パラメータの区間 + 0 基準線)。
---                     代表図 ('toPlot') は 1 行 forest、 複数検定は 'testForest'。
---   * 'describeBox' = 生データ列の **box plot** (= describe の分布図・5 数要約を可視化)。
--- ===========================================================================
-
--- | 検定結果の forest plot: 各検定の 95% CI ('trCI') を区間、 中心を点推定として
---   1 行に並べ、 0 の基準線を引く。 CI を持たない検定は除外する。 行ラベルは
---   'trMethod'。 同種検定を群間で並べるなど **ラベルを区別したい場合は
---   'testForestLabeled'** を使う。
---
--- ⚠ 0 基準線は **平均差・効果量** (null = 0) 向け。 生の平均など null ≠ 0 の量を
--- 混在させると軸ドメインが歪むので、 同一スケールの量だけを 1 枚に並べること。
-testForest :: [TestResult] -> VisualSpec
-testForest = testForestLabeled . map (\r -> (trMethod r, r))
-
--- | ラベル指定版 'testForest' (= 行ラベルを呼び出し側で与える)。 同じ検定種を
---   群ごとに並べる (= 同名衝突を避ける) 用途に使う。
-testForestLabeled :: [(Text, TestResult)] -> VisualSpec
-testForestLabeled labeled =
-  let rows  = [ (nm, lo, hi) | (nm, r) <- labeled, Just (lo, hi) <- [trCI r] ]
-      names = [ nm            | (nm, _,  _ ) <- rows ]
-      ests  = [ (lo + hi) / 2 | (_,  lo, hi) <- rows ]
-      errs  = [ (hi - lo) / 2 | (_,  lo, hi) <- rows ]
-  in if null rows
-       then mempty
-       else layer (forest (inlineCat names) (inline ests) (inline errs) <> forestNull 0)
-
-instance Plottable TestResult where
-  -- 代表図 = 単一検定の 1 行 forest (effect/CI)。
-  toPlot r = testForest [r]
-
--- | describe の分布図: 生データ列の box plot (5 数要約を可視化)。
-describeBox :: [Double] -> VisualSpec
-describeBox xs = layer (boxplot (inline xs))
-
--- ===========================================================================
--- 次元圧縮 (PLS effect plot) — Phase 70.B2/B3
--- ===========================================================================
-
-instance MultiVarModel PLSModel where
-  mvFrame = plsmFrame
-  -- PLS は閉形式 CI を持たない → band 非提供 (曲線のみ・GAM と同じ honest 方針)。
-  mvEvalFrame m _level ef =
-    let n      = mfNRows ef
-        colOf nm = case lookup nm (mfRoles ef) of
-          Just (RoleContinuous v) -> LA.fromList (V.toList v)
-          _                       -> LA.fromList (replicate n 0)
-        xMat  = LA.fromColumns (map colOf (plsmXNames m))   -- n × p (xNames 順)
-        yPred = predictPLS (plsmFit m) xMat                 -- n × q
-        ycols = LA.toColumns yPred
-        idx   = plsmOutIdx m
-        mu    = if idx < length ycols then LA.toList (ycols !! idx)
-                                      else replicate n 0
-    in (mu, Nothing)
-
--- Phase 78.G-e: 多変量カーネル回帰 (GP/RFF) を effect plot / profiler / contour で使う
--- (DOE の非 LM 化)。 mvEvalFrame は ef から予測子を 'gprnNames' 順に取り 'gprnPredict'
--- に渡す ('PLSModel' と同型)。 帯 = **事後予測帯** (潜在分散 + 観測 noise σ_n²) で、
--- 分布あり象限 (Gp/GpRff) のみ Just、 mean のみ象限 (Krr/KrrRff) は帯なし。 'gpmvVar' は
--- σ_n² を含まない ('GP.hs' の diagKss=σ_f²) ので noise を足して予測帯にする。
-instance MultiVarModel GPRegModelN where
-  mvFrame m =
-    let n     = LA.size (gprnYraw m)
-        roles = ("__gp_resp", RoleResponse (V.fromList (LA.toList (gprnYraw m))))
-              : [ (nm, RoleContinuous (V.fromList (LA.toList xv)))
-                | (nm, xv) <- zip (gprnNames m) (gprnXraws m) ]
-    in ModelFrame { mfRoles = roles, mfNRows = n }
-  mvEvalFrame m level ef =
-    let n        = mfNRows ef
-        colOf nm = case lookup nm (mfRoles ef) of
-          Just (RoleContinuous v) -> V.toList v
-          _                       -> replicate n 0
-        xMat        = LA.fromColumns (map (LA.fromList . colOf) (gprnNames m))  -- n × p
-        (mu, mbVar) = gprnPredict m xMat
-        z           = quantileNormal (1 - (1 - level) / 2)
-        sn2         = max 0 (gpNoiseVar (gprnParams m))
-    in case mbVar of
-         Just vs -> let sds = map (\v -> sqrt (max 0 v + sn2)) vs
-                    in ( mu, Just ( zipWith (\u s -> u - z * s) mu sds
-                                  , zipWith (\u s -> u + z * s) mu sds ) )
-         Nothing -> (mu, Nothing)
-
--- | DOE 階層ベイズ fit の effect plot 開通 (Phase 78.G-f)。固定効果 β の事後 draw で
---   評価点の μ を計算し、事後予測帯 (μ の分散 + 観測 noise σ²) を CI slot に載せる。
---   ランダム効果は集団平均で marginalize (profiler = 代表条件の予測)。
-instance MultiVarModel DesignHBMFit where
-  mvFrame = dhfFrame
-  mvEvalFrame m level ef =
-    case designMatrixF (dhfFormula m) ef of
-      Left _          -> ([], Nothing)
-      Right (xMat, _) ->
-        let rows  = map LA.toList (LA.toRows xMat)   -- 評価点 × p
-            draws = dhfBetaDraws m                   -- draws × p
-            muAt row = [ sum (zipWith (*) row bd) | bd <- draws ]
-            perPoint = map muAt rows                 -- 評価点ごとの draw 列
-            z     = quantileNormal (1 - (1 - level) / 2)
-            s2bar = let ss = dhfSigmaDraws m
-                    in if null ss then 0 else sum (map (^ (2::Int)) ss) / fromIntegral (length ss)
-            center = map mean0L perPoint
-            sds    = map (\ds -> sqrt (varL ds + s2bar)) perPoint
-        in ( center
-           , Just ( zipWith (\c s -> c - z * s) center sds
-                  , zipWith (\c s -> c + z * s) center sds ) )
-    where
-      mean0L xs = if null xs then 0 else sum xs / fromIntegral (length xs)
-      varL   xs = let mu = mean0L xs
-                  in if null xs then 0 else sum (map (\x -> (x - mu) ^ (2::Int)) xs) / fromIntegral (length xs)
-
diff --git a/src/Hanalyze/Plot/Robust.hs b/src/Hanalyze/Plot/Robust.hs
deleted file mode 100644
--- a/src/Hanalyze/Plot/Robust.hs
+++ /dev/null
@@ -1,207 +0,0 @@
--- |
--- Module      : Hanalyze.Plot.Robust
--- Description : hgg 連携層 — ロバスト・分位点回帰族の図化 instance
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- hgg 連携層 — **ロバスト・分位点回帰族** の図化 instance (Phase 71.5)。
---
--- ⚠ 親 'Hanalyze.Plot' と同じ cabal flag @plot-integration@ (既定 off) を
--- on にしたときのみ build される。 共通基盤 (class / ModelSpec / grid 評価核) は
--- 'Hanalyze.Plot.Core' を import して取り込む (orphan instance を許容)。
---
--- 担当する型 (= M 推定ロバスト回帰・分位点回帰):
---   RobustModel / MultiRobustModel / QuantileModel / MultiQuantileModel。
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE FlexibleContexts #-}
-module Hanalyze.Plot.Robust
-  ( robustBand
-  ) where
-
-import           Data.List             (sortBy, minimumBy)
-import           Data.Ord              (comparing)
-import qualified Data.Vector           as V
-import qualified Numeric.LinearAlgebra as LA
-
-import           Hgg.Plot.Spec     ( layer, inline, fromHex
-                                       , scatter, line, band
-                                       , sizeBy, color )
-
-import           Hanalyze.Model.Wrappers
-import           Hanalyze.Plot.Core
-import           Hanalyze.Model.LM       (designMatrix)
-import           Hanalyze.Model.Robust   (RobustFit (..), robustCovBeta)
-import           Hanalyze.Model.Weibull  (quantileNormal)
-import           Hanalyze.Model.Quantile (QRFit (..))
-import           Hanalyze.Model.Formula.Design  (designMatrixF)
-
--- ===========================================================================
--- 多変量ロバスト回帰 (effect plot + 係数サマリ) — Phase 70.D
---
--- ロバスト回帰は formula 経路を持たない (単回帰 'RobustModel' のみだった) ので、
--- 'MultiLMModel' と同型の frame-carrying ラッパを新設する。 設計行列は
--- 'additiveFormula' 由来 ('designMatrixF' で @[1, x1,…,xp]@)、 fit は 'fitRobustLM'、
--- CI 帯は M 推定量サンドイッチ共分散 ('robustCovBeta'・statsmodels RLM 一致)。
--- ===========================================================================
-
-instance MultiVarModel MultiRobustModel where
-  mvFrame = mrmFrame
-  mvEvalFrame m level ef =
-    case designMatrixF (mrmFormula m) ef of
-      Left _        -> ([], Nothing)
-      Right (xe, _) ->
-        let fit  = mrmFit m
-            beta = rfCoef fit
-            cov  = robustCovBeta (rfEstimator fit) (rfScale fit)
-                                 (rfResiduals fit) (mrmDesign m)
-            z    = quantileNormal ((1 + level) / 2)
-            rows = LA.toRows xe
-            mu   = [ r `LA.dot` beta | r <- rows ]
-            se   = [ sqrt (max 0 (r `LA.dot` (cov LA.#> r))) | r <- rows ]
-        in ( mu, Just ( zipWith (\mu' s -> mu' - z * s) mu se
-                      , zipWith (\mu' s -> mu' + z * s) mu se ) )
-
--- ===========================================================================
--- ロバスト回帰 (描画可能)
---
--- 'RobustFit' (Hanalyze.Model.Robust) は M-estimator IRLS の係数 'rfCoef' / fitted
--- 'rfFitted' / 最終重み 'rfWeights' (≤ 1、 外れ値ほど小) を持つ。 代表図 ('toPlot') は
--- ロバスト直線 + サンドイッチ CI 帯。 「どの点がダウンウェイトされたか」 は
--- 'diagnosticPlots' 側で **点サイズ = IRLS 重み** の散布図に encode して見せる。
--- ===========================================================================
-
-instance Plottable RobustModel where
-  -- ロバスト直線 + CI 帯 ('robustBand' = M 推定量サンドイッチ共分散)。 LM と揃え、
-  -- 訓練点の ŷ='rfFitted' を x 昇順に結ぶ (= 単回帰なので直線) + 帯を重ねる。
-  toPlot m =
-    let fit        = rmFit m
-        xs         = LA.toList (rmXraw m)
-        yhat       = LA.toList (rfFitted fit)
-        sorted     = sortBy (comparing fst) (zip xs yhat)
-        xsS        = map fst sorted
-        yhatS      = map snd sorted
-        (los, his) = robustBand m defaultCILevel xsS
-    in layer (band (inline xsS) (inline los) (inline his))
-         <> layer (line (inline xsS) (inline yhatS))
-
-  -- 診断束: ロバスト直線 + 残差 vs fitted + **重み encode 散布図** (点サイズ = IRLS
-  -- 重み、 小さい点 = ダウンウェイトされた外れ値)。 y は ŷ + 残差で復元。
-  diagnosticPlots m =
-    let fit  = rmFit m
-        xs   = LA.toList (rmXraw m)
-        yhat = LA.toList (rfFitted fit)
-        resd = LA.toList (rfResiduals fit)
-        ys   = zipWith (+) yhat resd
-        ws   = LA.toList (rfWeights fit)
-    in [ toPlot m
-       , layer (scatter (inline yhat) (inline resd))
-       , layer (scatter (inline xs) (inline ys) <> sizeBy (inline ws))
-       ]
-
--- | ロバスト回帰の CI 帯。 M 推定量 β̂ の漸近共分散 ('robustCovBeta'・サンドイッチ・
--- statsmodels RLM 一致) から、 評価点 x での @se(ŷ) = √([1,x]·Cov·[1,x]ᵀ)@、
--- 帯 = @μ̂ ∓ z·se@ (z = 正規分位点・RLM は正規で Wald CI)。
-robustBand :: RobustModel -> Double -> [Double] -> ([Double], [Double])
-robustBand m level gxs =
-  let fit  = rmFit m
-      xd   = designMatrix (V.fromList (LA.toList (rmXraw m)))   -- [1, x]
-      cov  = robustCovBeta (rfEstimator fit) (rfScale fit) (rfResiduals fit) xd
-      z    = quantileNormal ((1 + level) / 2)
-      beta = rfCoef fit
-      b0   = LA.atIndex beta 0
-      b1   = if LA.size beta > 1 then LA.atIndex beta 1 else 0
-      muAt gx = b0 + b1 * gx
-      seAt gx = let v = LA.fromList [1, gx]
-                in sqrt (max 0 (v `LA.dot` (cov LA.#> v)))
-  in ( [ muAt gx - z * seAt gx | gx <- gxs ]
-     , [ muAt gx + z * seAt gx | gx <- gxs ] )
-
--- | grid 評価 (Phase 16 C1)。 grid x で β̂·[1, x] を評価しロバスト直線を滑らかに描く。
--- band は 'robustBand' (サンドイッチ CI) を返す (LM と揃えた)。
-instance SingleVarModel RobustModel where
-  svRange m = let xs = LA.toList (rmXraw m) in (minimum xs, maximum xs)
-  svGrid m level gxs =
-    let beta = rfCoef (rmFit m)
-        mu   = [ LA.atIndex beta 0
-                 + (if LA.size beta > 1 then LA.atIndex beta 1 * gx else 0)
-               | gx <- gxs ]
-        (los, his) = robustBand m level gxs
-    in (mu, Just (los, his))
-  -- ブートストラップ: 加法誤差 (残差再標本化)。 refit は同じ estimator で再 fit。
-  svBootKit m =
-    let fit = rmFit m
-    in Just BootKit
-       { bkX = LA.toList (rmXraw m)
-       , bkY = zipWith (+) (LA.toList (rfFitted fit)) (LA.toList (rfResiduals fit))
-       , bkRefit = \xs ys -> robustModel (rfEstimator fit) (LA.fromList xs) (LA.fromList ys)
-       , bkObsDist = Nothing }
-
--- ===========================================================================
--- 分位点回帰 (描画可能)
---
--- 'QRFit' (Hanalyze.Model.Quantile) は 1 つの分位 τ に対する係数 + fitted 'qfYHat' を
--- 持つ。 複数の τ (例 0.1/0.5/0.9) の fit を重ねると **予測区間そのものを線群で** 表現
--- できる (= heteroscedastic データで帯より直接的)。 各線は 'color' ('fromHex') で固定色。
--- ===========================================================================
-
-instance Plottable QuantileModel where
-  -- 各 τ-fit を x 昇順に結んだ折れ線を、 固定色で重畳 (分位ごとに 1 layer)。
-  toPlot m =
-    let xs = LA.toList (qmXraw m)
-        mkLine (i, (_tau, fit)) =
-          let yhat   = LA.toList (qfYHat fit)
-              sorted = sortBy (comparing fst) (zip xs yhat)
-              col    = quantilePalette !! (i `mod` length quantilePalette)
-          in layer (line (inline (map fst sorted)) (inline (map snd sorted))
-                      <> color (fromHex col))
-    in foldMap mkLine (zip [0 ..] (qmFits m))
-
--- | 多変量分位点回帰の代表図 = **第 1 予測子に沿った effect plot** (他予測子は訓練平均に
---   固定)。 各 τ を 1 本の線で色分け重畳する (単変量 'QuantileModel' の τ 別線群の一般化)。
---   分位点回帰は閉形式 CI を持たないため帯はなし。
-instance Plottable MultiQuantileModel where
-  toPlot m =
-    case LA.toColumns (mqmX m) of                    -- [1, x₁, …, xₚ]
-      (_ : x1 : rest) ->
-        let xs1   = LA.toList x1
-            means = [ LA.sumElements c / fromIntegral (max 1 (LA.size c)) | c <- rest ]  -- x₂..xₚ の平均
-            (lo, hi) = (minimum xs1, maximum xs1)
-            gn    = 100 :: Int
-            grid' = [ lo + (hi - lo) * fromIntegral i / fromIntegral (gn - 1) | i <- [0 .. gn - 1] ]
-            evalX = LA.fromRows [ LA.fromList (1 : gx : means) | gx <- grid' ]
-            mkLine (i, (_t, fit)) =
-              let yhat = LA.toList (evalX LA.#> qfBeta fit)
-                  col  = quantilePalette !! (i `mod` length quantilePalette)
-              in layer (line (inline grid') (inline yhat) <> color (fromHex col))
-        in foldMap mkLine (zip [0 :: Int ..] (mqmFits m))
-      _ -> mempty   -- 予測子が無い (設計行列が intercept のみ) = 描画不能
-
--- ===========================================================================
--- 実測 vs 予測 (HasObsPred) — Phase 72.4
---
--- ロバスト/分位点 fit は ŷ と残差を直接持つので 実測 = ŷ + residual。 分位点回帰は
--- 0.5 (中央値) に最も近い τ の fit を代表予測に使う (中央値回帰 = 条件付き中央値)。
--- ===========================================================================
-
-instance HasObsPred RobustModel where
-  obsPredPairs m =
-    let f = LA.toList (rfFitted (rmFit m))
-        e = LA.toList (rfResiduals (rmFit m))
-    in (zipWith (+) f e, f)
-
-instance HasObsPred MultiRobustModel where
-  obsPredPairs m =
-    let f = LA.toList (rfFitted (mrmFit m))
-        e = LA.toList (rfResiduals (mrmFit m))
-    in (zipWith (+) f e, f)
-
-instance HasObsPred QuantileModel where
-  obsPredPairs m =
-    case qmFits m of
-      [] -> ([], [])
-      fs ->
-        let (_, fit) = minimumBy (comparing (\(t, _) -> abs (t - 0.5))) fs
-            f = LA.toList (qfYHat fit)
-            e = LA.toList (qfResid fit)
-        in (zipWith (+) f e, f)
diff --git a/src/Hanalyze/Plot/Smooth.hs b/src/Hanalyze/Plot/Smooth.hs
deleted file mode 100644
--- a/src/Hanalyze/Plot/Smooth.hs
+++ /dev/null
@@ -1,323 +0,0 @@
--- |
--- Module      : Hanalyze.Plot.Smooth
--- Description : hgg 連携層 — 平滑化・カーネル法族の図化 instance
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- hgg 連携層 — **平滑化・カーネル法族** の図化 instance (Phase 71.5)。
---
--- ⚠ 親 'Hanalyze.Plot' と同じ cabal flag @plot-integration@ (既定 off) を
--- on にしたときのみ build される。 共通基盤 (class / ModelSpec / grid 評価核) は
--- 'Hanalyze.Plot.Core' を import して取り込む (orphan instance を許容)。
---
--- 担当する型 (= spline / GAM / GP / kernel 法):
---   SplineModel / GAMModel / GAMModelN / GPResult / GPRegModel / GPRegModelN /
---   MultiGPResult。
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE FlexibleContexts #-}
-module Hanalyze.Plot.Smooth
-  ( splineBasisAt
-  , gamGridCI
-  , multiGpCurves
-  ) where
-
-import           Data.List             (sortBy)
-import           Data.Ord              (comparing)
-import qualified Data.Vector           as V
-import qualified Numeric.LinearAlgebra as LA
-
-import           Data.Text             (Text)
-import qualified Data.Text             as T
-
-import           Hgg.Plot.Spec     ( VisualSpec, layer, inline, inlineCat
-                                       , scatter, line, band
-                                       , colorBy, alpha )
-
-import           Hanalyze.Model.Wrappers
-import           Hanalyze.Plot.Core
-import           Hanalyze.Model.Core     (fittedV, residualsV)
-import           Hanalyze.Model.GP       (GPResult (..), gpNoiseVar)
-import           Hanalyze.Model.LM       ( CIBand (..), confidenceBand, confidenceBandAt
-                                                , predictionBandAt )
-import           Hanalyze.Model.Spline   ( SplineKind (..), SplineFit (..)
-                                                , bsplineBasis, naturalSplineBasis )
-import           Hanalyze.Model.GAM      (GAMFit (..), predictGAMSE)
-import           Hanalyze.Model.Weibull  (quantileNormal)
-import           Hanalyze.Model.MultiGP  (MultiGPResult (..))
-import qualified Statistics.Distribution         as SD
-import           Statistics.Distribution.StudentT (studentT)
-
--- ===========================================================================
--- ガウス過程 (描画可能)
---
--- 'GPResult' (Hanalyze.Model.GP) は予測 grid (gpTestX) + 事後平均 (gpMean) +
--- credible band (gpLower/gpUpper) を **自己完結** で保持する。 ゆえに LMModel の
--- ように X を別途束ねる必要がなく、 結果型をそのまま 'Plottable' にできる。
--- ===========================================================================
-
-instance Plottable GPResult where
-  -- 事後平均 (曲線) + credible band。 予測 grid をソートして 'line' に渡せば GP の曲線が
-  -- そのまま描ける。 band 半幅は対称 (mean ± 2σ) ゆえ es = gpUpper − gpMean とし、
-  -- 'band' に [mean−es, mean+es] を、 'line' に mean を載せる。
-  toPlot res =
-    let triples = sortBy (comparing (\(x, _, _) -> x))
-                    (zip3 (gpTestX res) (gpMean res)
-                          (zipWith (-) (gpUpper res) (gpMean res)))
-        xs = [ x | (x, _, _) <- triples ]
-        ys = [ y | (_, y, _) <- triples ]
-        es = [ e | (_, _, e) <- triples ]
-    in layer (band (inline xs) (inline (zipWith (-) ys es)) (inline (zipWith (+) ys es)))
-         <> layer (line (inline xs) (inline ys))
-
--- ===========================================================================
--- スプライン回帰 (描画可能)
---
--- 'SplineFit' (Hanalyze.Model.Spline) は基底係数 'sfBeta' と、 基底行列で fit した
--- 線形モデル核 'sfResult' (= 'FitResult') を保持する。 ゆえに **基底行列を設計行列と
--- みなせば** LMModel と同じ 'confidenceBand' (= X (XᵀX)⁻¹ Xᵀ の対角) がそのまま使える。
--- 違いは「曲線」 である点だけ: 単回帰の直線でなく、 訓練点を x 昇順に結ぶと基底展開に
--- よる平滑曲線になる。 帯は LM と同じ **線形モデルの対称 Wald CI** (基底空間での予測分散)。
--- ===========================================================================
-
--- | 'SplineFit' を訓練 x で評価したときの基底行列 (= confidenceBand の設計行列)。
-splineBasisAt :: SplineFit -> LA.Vector Double -> LA.Matrix Double
-splineBasisAt fit xs =
-  let xsV = V.fromList (LA.toList xs)
-  in case sfKind fit of
-       BSpline k    -> bsplineBasis k (sfKnots fit) xsV
-       NaturalCubic -> naturalSplineBasis (sfKnots fit) xsV
-
-instance Plottable SplineModel where
-  -- 平滑曲線 + CI band。 基底行列を設計行列とみなして 'confidenceBand' を訓練点で
-  -- 評価し (LMModel と同じ Wald CI)、 x 昇順にソートして折れ線で結ぶ (= 平滑曲線)。
-  -- ± 半幅 errorY = se (帯は基底空間の予測分散 = 対称)。
-  toPlot m =
-    let fit    = splFit m
-        res    = sfResult fit
-        xs     = LA.toList (splXraw m)
-        yhat   = LA.toList (fittedV res)
-        basis  = splineBasisAt fit (splXraw m)
-        cib    = confidenceBand basis res defaultCILevel
-        se     = zipWith (-) (upperBound cib) yhat   -- upper - ŷ = 片側半幅
-        sorted = sortBy (comparing (\(x, _, _) -> x)) (zip3 xs yhat se)
-        xsS    = [ x | (x, _, _) <- sorted ]
-        yhatS  = [ y | (_, y, _) <- sorted ]
-        seS    = [ e | (_, _, e) <- sorted ]
-    in layer (band (inline xsS) (inline (zipWith (-) yhatS seS)) (inline (zipWith (+) yhatS seS)))
-         <> layer (line (inline xsS) (inline yhatS))
-
-  -- 残差診断 (平滑曲線 + 残差 vs fitted)。
-  diagnosticPlots m =
-    let res  = sfResult (splFit m)
-        yhat = LA.toList (fittedV res)
-        resd = LA.toList (residualsV res)
-    in [ toPlot m
-       , layer (scatter (inline yhat) (inline resd))
-       ]
-
--- | grid 評価 (Phase 16 C1)。 grid x で基底行列を再構築し、 それを設計行列とみなして
--- 'confidenceBandAt' を評価する (基底空間の対称 Wald CI = 訓練 'confidenceBand' と同核)。
-instance SingleVarModel SplineModel where
-  svRange m = let xs = LA.toList (splXraw m) in (minimum xs, maximum xs)
-  svGrid m level gxs =
-    let fit        = splFit m
-        basisTrain = splineBasisAt fit (splXraw m)
-        basisGrid  = splineBasisAt fit (LA.fromList gxs)
-        cib        = confidenceBandAt basisTrain (sfResult fit) level basisGrid
-        los        = lowerBound cib
-        his        = upperBound cib
-        mu         = zipWith (\l h -> (l + h) / 2) los his
-    in (mu, Just (los, his))
-  -- PI = closed form σ̂²(1 + xᵀ(XᵀX)⁻¹x) (基底空間 OLS ゆえ LM と同型・statsmodels obs_ci 相当)。
-  svGridPI m level gxs =
-    let fit        = splFit m
-        basisTrain = splineBasisAt fit (splXraw m)
-        basisGrid  = splineBasisAt fit (LA.fromList gxs)
-        pib        = predictionBandAt basisTrain (sfResult fit) level basisGrid
-    in Just (lowerBound pib, upperBound pib)
-  -- ブートストラップ: 加法誤差。 refit は同じ kind/knots で再 fit。
-  svBootKit m =
-    let fit = splFit m
-        res = sfResult fit
-    in Just BootKit
-       { bkX = LA.toList (splXraw m)
-       , bkY = zipWith (+) (LA.toList (fittedV res)) (LA.toList (residualsV res))
-       , bkRefit = \xs ys -> splineModel (sfKind fit) (sfKnots fit) (LA.fromList xs) (LA.fromList ys)
-       , bkObsDist = Nothing }
-
--- ===========================================================================
--- 一般化加法モデル (描画可能)
---
--- 'GAMFit' (Hanalyze.Model.GAM) は各特徴の基底係数 + fitted 'gamYHat' を保持する。
--- 本 Phase では mgcv 流 Bayesian CI を実装した平滑曲線 + CI 帯を描く。
--- ===========================================================================
-
-instance Plottable GAMModel where
-  -- 平滑曲線 + CI 帯 (Phase 70.6 G で mgcv 流 Bayesian CI を実装)。 grid 経路
-  -- ('statModel') に固定し、 LM/spline と同様 band + line を出す。
-  toPlot = toPlot . statModel
-
-  -- 残差診断 (平滑曲線 + 残差 vs fitted)。
-  diagnosticPlots m =
-    let fit  = gamFit m
-        yhat = LA.toList (gamYHat fit)
-        resd = LA.toList (gamResid fit)
-    in [ toPlot m
-       , layer (scatter (inline yhat) (inline resd))
-       ]
-
--- | GAM の grid 評価 (中心 μ̂ + **mgcv 流 Bayesian 信頼帯**)。 'predictGAMSE' の
---   pointwise se に t_{n−edf} 臨界値を掛けて帯にする (Vβ='gamCov')。
-gamGridCI :: GAMFit -> Double -> [V.Vector Double] -> ([Double], Maybe ([Double], [Double]))
-gamGridCI fit level cols =
-  let (muV, seV) = predictGAMSE fit cols
-      mu   = V.toList muV
-      se   = V.toList seV
-      df   = fromIntegral (LA.size (gamResid fit)) - gamEdf fit
-      tVal = SD.quantile (studentT (max 1 df)) ((1 + level) / 2)
-      lo   = zipWith (\u s -> u - tVal * s) mu se
-      hi   = zipWith (\u s -> u + tVal * s) mu se
-  in (mu, Just (lo, hi))
-
--- | grid 評価 (Phase 16 C1)。 grid x を 'predictGAMSE' に通し平滑曲線 + CI 帯を評価
--- (Phase 70.6 G: mgcv 流 Bayesian CI を実装)。
-instance SingleVarModel GAMModel where
-  svRange m = let xs = LA.toList (gamXraw m) in (minimum xs, maximum xs)
-  svGrid m level gxs = gamGridCI (gamFit m) level [V.fromList gxs]
-
-
--- | 第1予測子を描画軸に、 他予測子は訓練平均に固定して偏依存曲線を評価する。
-instance SingleVarModel GAMModelN where
-  svRange m = case gamNXraws m of
-    (x:_) -> let xs = LA.toList x in (minimum xs, maximum xs)
-    []    -> (0, 1)
-  svGrid m level gxs =
-    let n          = length gxs
-        others     = drop 1 (gamNXraws m)
-        holdMean v = V.replicate n (LA.sumElements v / fromIntegral (LA.size v))
-        cols       = V.fromList gxs : map holdMean others
-    in gamGridCI (gamNFit m) level cols
-  svCoefR2 m = Just ([gamIntercept (gamNFit m)], gamR2 (gamNFit m))
-
-instance Plottable GAMModelN where
-  -- 平滑曲線 + CI 帯 (Phase 70.6 G)。 grid 経路 ('statModel') に固定。 多予測子では
-  -- 第1予測子を軸に他を訓練平均で固定した偏依存曲線 + その点の CI。
-  toPlot = toPlot . statModel
-
--- ===========================================================================
--- カーネル回帰 (GP / KRR / RFF) の描画可能ラッパ
--- ===========================================================================
-
--- | grid 評価 (E2)。 予測子 'gprPredict' を grid x に当て、 分布あり象限 (Gp/GpRff) は
--- 事後分散→正規 credible 帯 (μ̂ ± z·σ)、 点象限 (Ridge/RidgeRff) は帯なし ('Nothing')。
--- 信頼水準 @level@ → @z = Φ⁻¹(1 − (1−level)/2)@ ('quantileNormal')。 'WeightedLMModel'
--- と同じく 'toPlot' を grid 経路 ('statModel') に固定する (元データ散布図と整合)。
-instance SingleVarModel GPRegModel where
-  svRange m = let xs = LA.toList (gprXraw m) in (minimum xs, maximum xs)
-  svGrid m level gxs =
-    let (mu, mbVar) = gprPredict m gxs
-        z           = quantileNormal (1 - (1 - level) / 2)
-    in case mbVar of
-         Just vs -> let sds = map (sqrt . max 0) vs
-                        los = zipWith (\u s -> u - z * s) mu sds
-                        his = zipWith (\u s -> u + z * s) mu sds
-                    in (mu, Just (los, his))
-         Nothing -> (mu, Nothing)               -- Ridge 系 = 帯なし
-  -- 予測区間 (PI) = 事後予測分散 (f の分散 + 観測ノイズ σ_n²) の正規帯。 分布あり象限のみ。
-  svGridPI m level gxs =
-    let (mu, mbVar) = gprPredict m gxs
-        z           = quantileNormal (1 - (1 - level) / 2)
-        sn2         = max 0 (gpNoiseVar (gprParams m))
-    in case mbVar of
-         Just vs -> let sds = map (\v -> sqrt (max 0 v + sn2)) vs
-                    in Just ( zipWith (\u s -> u - z * s) mu sds
-                            , zipWith (\u s -> u + z * s) mu sds )
-         Nothing -> Nothing
-  -- カーネル回帰は β₀+β₁x の線形「式」を持たないため式/R² 注釈は出さない。
-  svCoefR2 _ = Nothing
-
--- | ★訓練点経路ではなく grid 経路 ('statModel') に固定 (元データ散布図と整合)。
--- 分布あり象限は曲線 + credible 帯、 点象限は曲線のみ。
-instance Plottable GPRegModel where
-  toPlot = toPlot . statModel
-
-
--- | 第1予測子を描画軸に、 他予測子を訓練平均に固定した偏依存曲線 (band は分布あり象限のみ)。
-instance SingleVarModel GPRegModelN where
-  svRange m = case gprnXraws m of
-    (x:_) -> let xs = LA.toList x in (minimum xs, maximum xs)
-    []    -> (0, 1)
-  svGrid m level gxs =
-    let n          = length gxs
-        others     = drop 1 (gprnXraws m)
-        holdMean v = LA.konst (LA.sumElements v / fromIntegral (LA.size v)) n
-        testX      = LA.fromColumns (LA.fromList gxs : map holdMean others)
-        (mu, mbVar) = gprnPredict m testX
-        z          = quantileNormal (1 - (1 - level) / 2)
-    in case mbVar of
-         Just vs -> let sds = map (sqrt . max 0) vs
-                    in (mu, Just ( zipWith (\u s -> u - z * s) mu sds
-                                 , zipWith (\u s -> u + z * s) mu sds ))
-         Nothing -> (mu, Nothing)
-  svGridPI m level gxs =
-    let n          = length gxs
-        others     = drop 1 (gprnXraws m)
-        holdMean v = LA.konst (LA.sumElements v / fromIntegral (LA.size v)) n
-        testX      = LA.fromColumns (LA.fromList gxs : map holdMean others)
-        (mu, mbVar) = gprnPredict m testX
-        z          = quantileNormal (1 - (1 - level) / 2)
-        sn2        = max 0 (gpNoiseVar (gprnParams m))
-    in case mbVar of
-         Just vs -> let sds = map (\v -> sqrt (max 0 v + sn2)) vs
-                    in Just ( zipWith (\u s -> u - z * s) mu sds
-                            , zipWith (\u s -> u + z * s) mu sds )
-         Nothing -> Nothing
-  svCoefR2 _ = Nothing
-
-instance Plottable GPRegModelN where
-  toPlot = toPlot . statModel
-
--- ===========================================================================
--- 多出力 GP (描画可能)
--- ===========================================================================
-
--- | 多出力 GP の予測曲線 + 95% band (出力ごとに色分け・x = 予測点 index)。
-multiGpCurves :: MultiGPResult -> VisualSpec
-multiGpCurves res =
-  let outs = zip3 (mgpMean res) (mgpLower res) (mgpUpper res)
-      mkOut k (m, lo, hi) =
-        let xs  = [ fromIntegral i | i <- [1 .. length m] ] :: [Double]
-            lbl = "y" <> T.pack (show (k :: Int))
-            grp = inlineCat (replicate (length m) lbl)
-        in layer (band (inline xs) (inline lo) (inline hi) <> colorBy grp <> alpha 0.2)
-             <> layer (line (inline xs) (inline m) <> colorBy grp)
-  in mconcat (zipWith mkOut [0 ..] outs)
-
-instance Plottable MultiGPResult where
-  toPlot = multiGpCurves
-
--- ===========================================================================
--- 実測 vs 予測 (HasObsPred) — Phase 72.4
---
--- spline は内側の線形 fit ('sfResult') から、 GAM は保持する ŷ/残差から復元する。
--- ===========================================================================
-
-instance HasObsPred SplineModel where
-  obsPredPairs m =
-    let r = sfResult (splFit m)
-        f = LA.toList (fittedV r)
-        e = LA.toList (residualsV r)
-    in (zipWith (+) f e, f)
-
-instance HasObsPred GAMModel where
-  obsPredPairs m =
-    let f = LA.toList (gamYHat (gamFit m))
-        e = LA.toList (gamResid (gamFit m))
-    in (zipWith (+) f e, f)
-
-instance HasObsPred GAMModelN where
-  obsPredPairs m =
-    let f = LA.toList (gamYHat (gamNFit m))
-        e = LA.toList (gamResid (gamNFit m))
-    in (zipWith (+) f e, f)
diff --git a/src/Hanalyze/Plot/Wrappers.hs b/src/Hanalyze/Plot/Wrappers.hs
deleted file mode 100644
--- a/src/Hanalyze/Plot/Wrappers.hs
+++ /dev/null
@@ -1,264 +0,0 @@
--- |
--- Module      : Hanalyze.Plot.Wrappers
--- Description : hgg 連携層 — 汎用ラッパの Plottable / SingleVarModel 連携 instance
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- hgg 連携層 — **汎用ラッパ (どの族にも属さない) の Plottable / SingleVarModel**
--- 連携 instance + 専用 helper (Phase 71.7)。
---
--- ⚠ 親 'Hanalyze.Plot' と同じ cabal flag @plot-integration@ (既定 off) を
--- on にしたときのみ build される。 共通基盤 (class / ModelSpec / grid 評価核) は
--- 'Hanalyze.Plot.Core' を import して取り込む (orphan instance を許容:
--- クラス=Core・instance=ここ・型=Wrappers/各 Model module)。
---
--- 担当する型・ヘルパ (= 特定の ML / ベイズ族に属さない汎用ラッパ):
---   多出力線形回帰 'MultiFit' の残差相関 heatmap・k-NN 回帰の単変量描画
---   ('KNNRegressor')・透過標準化ラッパ 'StandardizedModel'・罰則回帰結果
---   'RegModel' の係数 bar・群別フィット 'GroupedFit' の N 曲線重畳・plot ColData
---   源の 'ColumnSource'・LM 係数診断アクセサ ('lmDiag' / 'groupedLmDiag')。
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE UndecidableInstances #-}
-module Hanalyze.Plot.Wrappers
-  ( -- ** 係数診断の薄アクセサ — Phase 52.A9
-    lmDiag
-  , groupedLmDiag
-    -- ** 群別フィットの fullrange レンダラ — Phase 52.A4 / A7
-  , groupedFullrange
-  ) where
-
-import qualified Data.Map.Strict       as Map
-import qualified Data.Vector           as V
-import qualified Data.Vector.Unboxed    as VU
-import qualified Numeric.LinearAlgebra as LA
-
-import           Data.Text             (Text)
-import qualified Data.Text             as T
-import qualified DataFrame.Internal.Column    as DX
-import qualified DataFrame.Internal.DataFrame  as DX
-
-import           Hanalyze.Data.ColumnSource     (ColumnSource (..))
-
-import           Hgg.Plot.Spec     ( VisualSpec, layer, inline, inlineCat
-                                       , ColData (..)
-                                       , scatter, line
-                                       , heatmap, colorBy
-                                       , scaleColorManual, legend
-                                       , bar, title )
-
-import           Hanalyze.Model.Wrappers
-import           Hanalyze.Plot.Core
-import           Hanalyze.Fit
-import           Hanalyze.Model.LM.Diagnostics (CoefStats (..), lmCoefStats)
-import           Hanalyze.Model.LM     (linspace)
-import           Hanalyze.Model.MultiLM (MultiFit (..))
-import           Hanalyze.Stat.Standardize
-                   ( Standardizer (..)
-                   , applyStandardizerCol )
-import           Hanalyze.Model.KNN (KNNRegressor (..), predictKNNR)
-
--- ===========================================================================
--- 多出力線形回帰 (描画可能)
---
--- 'MultiFit' (Hanalyze.Model.MultiLM) は q 個の応答を共通の予測子で同時回帰し、
--- 固有の成果物として **出力間の残差相関 'mfResidCor' (q×q)** を保持する。 q 本の回帰
--- 関係を単一図に素直に載せる方法は一意でない (出力ごとスケールが異なり得る) ため、
--- 代表図 ('toPlot') は **残差相関 heatmap** とする (= 多出力回帰固有の図。 user 決定
--- 2026-06-04)。 'MultiFit' は heatmap に必要な相関行列を自己完結で持つので、 'GPResult'
--- 同様 X を別途束ねず結果型をそのまま 'Plottable' にできる。 個別の出力 j の回帰線は
--- 'predictMultiLM' で別途描ける (本 instance の対象外)。
---
--- ⚠ 'heatmap' (geom_tile) は **categorical 軸専用** (renderHeatmap が x/y をラベルとして
--- カテゴリ軸の index に引く。 実測: Render/Statistical.hs)。 ゆえに格子座標は数値でなく
--- **出力名ラベル** ("y1", "y2", …) を 'inlineCat' で渡す (数値だとカテゴリ軸が立たず
--- 全セルが drop されてタイルが描かれない = 計測で確認)。
--- ===========================================================================
-
-instance Plottable MultiFit where
-  -- 残差相関 q×q を heatmap に。 行 i・列 j のセル (x=yⱼ, y=yᵢ) に相関値 mfResidCor[i,j]
-  -- を割り当てて 'heatmap' (= geom_tile) layer を 1 枚返す。 軸は出力名ラベル (categorical)。
-  toPlot mf =
-    let cor   = LA.toLists (mfResidCor mf)
-        q     = length cor
-        lbl k = "y" <> T.pack (show (k + 1 :: Int))   -- 出力名ラベル
-        cells = [ (lbl j, lbl i, (cor !! i) !! j)
-                | i <- [0 .. q - 1], j <- [0 .. q - 1] ]
-        xs = [ x | (x, _, _) <- cells ]
-        ys = [ y | (_, y, _) <- cells ]
-        vs = [ v | (_, _, v) <- cells ]
-    in layer (heatmap (inlineCat xs) (inlineCat ys) (inline vs))
-
--- C2: 元スケール逆変換 instance (Phase 70.3 項目 C) -------------------------
---
--- 内側モデルは標準化空間で学習されている。 ここで予測子 x を入力時に標準化し、
--- ('standardizedY' なら) 応答 y を出力時に逆変換することで、 図・予測を**元スケール**で
--- 返す。 単変量 (1 特徴) 描画が対象 (smXStd の 0 次元を使う)。
-
--- | k-NN 回帰の単変量描画 (透過標準化の内側として要る)。 1 特徴 ('knnRX' が 1 列) を
---   仮定し grid 点を予測曲線にする。 局所平均ゆえ band は持たない (Nothing)。
-instance SingleVarModel KNNRegressor where
-  svRange m =
-    let c0 = LA.toList (head (LA.toColumns (knnRX m)))
-    in (minimum c0, maximum c0)
-  svGrid m _ gxs =
-    let xEval = LA.fromColumns [LA.fromList gxs]    -- n × 1 (単一特徴)
-    in (VU.toList (predictKNNR m xEval), Nothing)   -- 帯なし
-
--- | 0 次元 (単変量描画の予測子) の (μ, σ)。
-stMu1, stSd1 :: Standardizer -> Double
-stMu1 = head . stMu
-stSd1 = head . stSd
-
--- | 応答 y の逆変換 (@smYStd = Just@ のみ実施。 @Nothing@ は元 y スケールのまま)。
-unstdY :: Maybe (Double, Double) -> Double -> Double
-unstdY (Just (muY, sdY)) v = v * sdY + muY
-unstdY Nothing           v = v
-
--- | 透過標準化ラッパの単変量描画 (元スケール)。 入力 x を標準化 → 内側を評価 →
---   ('standardizedY' なら) 出力 y を逆変換する。 内側 'svRange' (標準化空間) は
---   smXStd の 0 次元で元スケールへ戻す。 band/PI も同様に y を逆変換。
-instance SingleVarModel m => SingleVarModel (StandardizedModel m) where
-  svRange (StandardizedModel inner sx _ _) =
-    let (zlo, zhi) = svRange inner
-        unX z = z * stSd1 sx + stMu1 sx
-    in (unX zlo, unX zhi)
-  svGrid (StandardizedModel inner sx mY _) level xs =
-    let zs            = map (applyStandardizerCol sx 0) xs
-        (muZ, mbBand) = svGrid inner level zs
-    in ( map (unstdY mY) muZ
-       , fmap (\(lo, hi) -> (map (unstdY mY) lo, map (unstdY mY) hi)) mbBand )
-  svGridPI (StandardizedModel inner sx mY _) level xs =
-    let zs = map (applyStandardizerCol sx 0) xs
-    in fmap (\(lo, hi) -> (map (unstdY mY) lo, map (unstdY mY) hi))
-            (svGridPI inner level zs)
-  -- 線形内側のみ式注釈 (係数を元スケールへ逆変換・R² はスケール不変で透過)。
-  --   X のみ標準化: y = β₀ + β₁·(x−μₓ)/σₓ = (β₀ − β₁μₓ/σₓ) + (β₁/σₓ)·x。
-  --   X+y 標準化:   y = σ_y·(β₀ + β₁·(x−μₓ)/σₓ) + μ_y。
-  svCoefR2 (StandardizedModel inner sx mY _) =
-    case svCoefR2 inner of
-      Just ([b0, b1], r2) ->
-        let mux = stMu1 sx; sdx = stSd1 sx
-            (a0, a1) = case mY of
-              Nothing         -> (b0 - b1 * mux / sdx, b1 / sdx)
-              Just (muY, sdY) -> (b0 * sdY + muY - b1 * sdY * mux / sdx, b1 * sdY / sdx)
-        in Just ([a0, a1], r2)
-      _ -> Nothing   -- 非線形 (kNN 等) は式注釈なし
-
--- | 透過標準化ラッパの代表図 = 元スケールの予測曲線 (+ 単変量散布 'smTrain')。
---   内側 'toPlot' (標準化軸) には依存せず、 ラッパ自身の 'SingleVarModel' を
---   'statModel' grid 機構へ流す。
-instance SingleVarModel m => Plottable (StandardizedModel m) where
-  toPlot sm = case smTrain sm of
-    Just (xs, ys) -> layer (scatter (inline xs) (inline ys)) <> toPlot (statModel sm)
-    Nothing       -> toPlot (statModel sm)
-
--- | 係数 bar (特徴名ラベル・元スケール) を代表図に。 CV パスがあれば診断束に λ-MSE 図。
-instance Plottable RegModel where
-  toPlot m =
-    layer (bar (inlineCat (rmgNames m)) (inline (rmgCoefs m)))
-      <> title (regMethodName (rmgMethod m) <> " coefficients (\955="
-                <> T.pack (show (roundTo 4 (rmgLambda m))) <> ")")
-  diagnosticPlots m = toPlot m : case rmgCVPath m of
-    Just (lams, scores) ->
-      [ layer (line (inline lams) (inline scores)) <> title "CV/LOOCV score path" ]
-    Nothing -> []
-
--- | RegMethod の表示名 (図タイトル用)。
-regMethodName :: RegMethod -> Text
-regMethodName Ridge            = "Ridge"
-regMethodName Lasso            = "Lasso"
-regMethodName (ElasticNet _)   = "Elastic Net"
-regMethodName (MCP _)          = "MCP"
-regMethodName (SCAD _)         = "SCAD"
-regMethodName (AdaptiveLasso _) = "Adaptive Lasso"
-regMethodName (GroupLasso _)   = "Group Lasso"
-
--- | 小数 n 桁丸め (タイトル表示用)。
-roundTo :: Int -> Double -> Double
-roundTo n v = let f = 10 ^^ n in fromIntegral (round (v * f) :: Integer) / f
-
--- | A9: 'LMModel' の係数診断 (SE / t値 / p値) を一発取得する薄アクセサ。
---   数値核は 'Hanalyze.Model.LM.Diagnostics.lmCoefStats'。 描画用に X を束ねた
---   'LMModel' から設計行列 ('lmDesign') と fit 結果 ('lmResult') を渡すだけ。
---   返りは係数順 (@[(Intercept), x]@) の 'CoefStats' リスト。
-lmDiag :: LMModel -> [CoefStats]
-lmDiag m = lmCoefStats (lmDesign m) (lmResult m)
-
--- | A9: 群別 LM フィット ('grouped "g" (lm …)' の結果) の各群係数診断を取り出す。
---   @[(群ラベル, [係数の CoefStats])]@。 群間で傾き SE/有意性を比較する用途。
---   ★@Fitted spec ~ LMModel@ に特殊化 (LM 群フィット専用)。
-groupedLmDiag :: (Fitted spec ~ LMModel) => GroupedFit spec -> [(Text, [CoefStats])]
-groupedLmDiag = map (fmap lmDiag) . groupModels
-
--- | 群別フィットを N 曲線で重畳する ('toPlot' = 各群 'svGrid' の μ̂ 曲線・群色 + 凡例)。
---   ★A3 の凡例機構を N 群へ一般化: 各曲線を 'ColorByCol' (群ラベル) に載せ
---   'scaleColorManual' で群色を固定し 'legend' を出す (固定色だと凡例が出ない罠を回避)。
---   grid 点数 100・帯なし (A1 既定 OFF) 固定。 群色は 'effectPalette' の循環。
-instance SingleVarModel (Fitted spec) => Plottable (GroupedFit spec) where
-  toPlot = renderGrouped
-
--- | 群別フィットを **各群の x 範囲のみ**で描く (既定。 'toPlot' = これ)。
-renderGrouped :: SingleVarModel (Fitted spec) => GroupedFit spec -> VisualSpec
-renderGrouped = renderGroupedWith False
-
--- | 群別フィットを **データ全幅** (全群 x の union 範囲) へ延ばして描く (A7 fullrange)。
---   ggplot @geom_smooth(fullrange = TRUE)@ 相当: 各群の回帰線を、 その群の x 範囲だけでなく
---   **全群を合わせた x の min/max** まで延長して評価する (群間の傾き差を全域で比較しやすい)。
---   ★単一モデルでは「データ全幅 = 訓練 x」 ゆえ意味を持たない (range 拡張は grouped 固有)。
---   'toPlot' とは別経路 (結果型 'GroupedFit' に描画 flag を持たせない・別レンダラとして提供)。
-groupedFullrange :: SingleVarModel (Fitted spec) => GroupedFit spec -> VisualSpec
-groupedFullrange = renderGroupedWith True
-
--- | 群別フィットの共通レンダラ。 @full@ で評価 x 範囲を切替える
---   (@False@ = 各群自範囲、 @True@ = 全群 union 範囲 = A7 fullrange)。
-renderGroupedWith :: SingleVarModel (Fitted spec) => Bool -> GroupedFit spec -> VisualSpec
-renderGroupedWith full gf =
-  let pairs   = zip [0 :: Int ..] (gfGroups gf)
-      n       = 100
-      colOf i = effectPalette !! (i `mod` length effectPalette)
-      -- fullrange = 全群 svRange の union (lo = 最小, hi = 最大)。 群が無ければ使われない。
-      ranges  = [ svRange m | (_, (_, m)) <- pairs ]
-      unionLo = minimum (map fst ranges)
-      unionHi = maximum (map snd ranges)
-      curveOf (_, (lbl, m)) =
-        let (lo, hi) = if full then (unionLo, unionHi) else svRange m
-            gxs      = linspace lo hi n
-            (mu, _)  = svGrid m defaultCILevel gxs
-        in layer (line (inline gxs) (inline mu)
-                    <> colorBy (inlineCat (replicate n lbl)))
-      legendSpec
-        | null pairs = mempty
-        | otherwise  = scaleColorManual [ (lbl, colOf i) | (i, (lbl, _)) <- pairs ]
-                         <> legend
-  in foldMap curveOf pairs <> legendSpec
-
--- --- plot ColData 源の ColumnSource instance (flag 配下・非 portable) -------
---
--- hgg の @[(Text, ColData)]@ (= df 中立表現)。 'NumData' は数値列、
--- 'TxtData' は factor 列。 'lookupCol' は数値列のみ返し、 'toFrame' は
--- 数値・文字列の両方を 'DX.DataFrame' に詰めて formula 経路で factor を温存する。
-instance ColumnSource [(Text, ColData)] where
-  lookupCol n cs = case lookup n cs of
-    Just (NumData v) -> Just (V.toList v)
-    _                -> Nothing
-  columnNames = map fst
-  toFrame cs  = DX.fromNamedColumns (concatMap toCol cs)
-    where
-      toCol (n, NumData v) = [(n, DX.fromList (V.toList v))]
-      toCol (n, TxtData v) = [(n, DX.fromList (V.toList v))]
-
--- ===========================================================================
--- 実測 vs 予測 (HasObsPred) — Phase 72.4
---
--- 罰則回帰 ('RegModel') は元スケール係数 (rmgIntercept + rmgCoefs) と生設計
--- 'rmgXraw' から予測を再構成し、 実測は生応答 'rmgYraw' を使う。
--- ===========================================================================
-
-instance HasObsPred RegModel where
-  obsPredPairs m =
-    let beta = LA.fromList (rmgCoefs m)
-        prd  = map (+ rmgIntercept m) (LA.toList (rmgXraw m LA.#> beta))
-    in (LA.toList (rmgYraw m), prd)
diff --git a/src/Hanalyze/Stat/AD.hs b/src/Hanalyze/Stat/AD.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/AD.hs
+++ /dev/null
@@ -1,294 +0,0 @@
-{-# LANGUAGE RankNTypes #-}
--- |
--- Module      : Hanalyze.Stat.AD
--- Description : automatic differentiation (AD) による正確な勾配計算 (HMC 連携)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Exact gradient computation via automatic differentiation (AD), with HMC
--- integration.
---
--- Uses reverse-mode AD from @Numeric.AD@ (ekmett/ad) to compute gradients.
--- More accurate than central-difference numerical differentiation, and runs
--- at comparable speed when the parameter count is small (< 100).
---
--- == Usage
---
--- The user writes @log p(θ, y)@ as a /Floating-polymorphic/ function. Fixed
--- observation values are lifted via @realToFrac@:
---
--- @
--- import Hanalyze.Stat.AD
--- import Hanalyze.Stat.Distribution (Transform (..))
---
--- -- θ = [mu, sigma]
--- myLogJoint :: [Double] -> LogJointF
--- myLogJoint obs [mu, sigma] =
---   logNormalF 0 10 mu                          -- prior: μ ~ N(0,10)
---   + logExpF 1 sigma                           -- prior: σ ~ Exp(1)
---   + sum [ logNormalObsF y mu sigma | y <- obs ] -- lik
---
--- chain <- hmcAD (myLogJoint myData)
---                [UnconstrainedT, PositiveT]
---                defaultHMCConfig
---                ["mu","sigma"]
---                (Map.fromList [("mu",0),("sigma",1)])
---                gen
--- @
-module Hanalyze.Stat.AD
-  ( -- * 多相対数密度関数 (log-joint 記述用)
-    LogJointF
-  , Params
-  , logNormalF
-  , logNormalObsF
-  , logExpF
-  , logGammaF
-  , logBetaF
-  , logPoissonObsF
-  , logBernoulliObsF
-    -- * AD-gradient computation
-  , gradAD
-  , gradADU
-    -- * HMC (AD variant)
-  , hmcAD
-  , hmcADChains
-  ) where
-
-import Control.Concurrent.Async (mapConcurrently)
-import Control.Monad (forM, replicateM)
-import Data.IORef
-import qualified Data.Map.Strict as Map
-import Data.Text (Text)
-import Numeric.AD.Mode.Reverse.Double (grad)
-import System.Random.MWC (GenIO, uniform)
-import System.Random.MWC.Distributions (standard)
-
-import Hanalyze.MCMC.Core (Chain (..), spawnGen)
-import Hanalyze.MCMC.HMC (HMCConfig (..), leapfrogWith, kinetic)
-import Hanalyze.Stat.Distribution (Transform (..), toUnconstrained, fromUnconstrained)
-
--- | Named parameter map (parameter name → constrained-space value).
-type Params = Map.Map Text Double
-
--- | Type alias for a 'Floating'-polymorphic log-joint function. The
--- argument @[a]@ is the constrained-space parameter vector.
-type LogJointF = forall a. Floating a => [a] -> a
-
--- ---------------------------------------------------------------------------
--- 多相対数密度関数
--- ---------------------------------------------------------------------------
-
--- | @log N(x; μ₀, σ₀)@ where @μ₀@ and @σ₀@ are fixed @Double@
--- hyperparameters and @x@ is differentiable.
-logNormalF :: Floating a => Double -> Double -> a -> a
-logNormalF mu0 sig0 x =
-  let mu  = realToFrac mu0
-      sig = realToFrac sig0
-  in negate (0.5 * log (2 * pi)) - log sig - 0.5 * ((x - mu) / sig) ^ (2::Int)
-{-# INLINE logNormalF #-}
-
--- | @log N(y_obs; μ, σ)@ where @y_obs@ is a fixed observation and @μ@,
--- @σ@ are differentiable.
-logNormalObsF :: Floating a => Double -> a -> a -> a
-logNormalObsF y_obs mu sig =
-  let y = realToFrac y_obs
-  in negate (0.5 * log (2 * pi)) - log sig - 0.5 * ((y - mu) / sig) ^ (2::Int)
-{-# INLINE logNormalObsF #-}
-
--- | @log Exp(x; rate)@ with fixed rate.
-logExpF :: Floating a => Double -> a -> a
-logExpF rate0 x =
-  let r = realToFrac rate0
-  in log r - r * x
-{-# INLINE logExpF #-}
-
--- | @log Gamma(x; shape, rate)@ with fixed shape and rate.
---
--- @log p(x) = (α-1) log x − β x + α log β − log Γ(α)@.
--- @log Γ(α)@ is Stirling's approximation (treated as a fixed constant).
-logGammaF :: Floating a => Double -> Double -> a -> a
-logGammaF shape0 rate0 x =
-  let a   = realToFrac shape0
-      b   = realToFrac rate0
-      lgA = realToFrac (stirlingLogGamma shape0)
-  in (a - 1) * log x - b * x + a * log b - lgA
-{-# INLINE logGammaF #-}
-
--- | @log Beta(x; α, β)@ with fixed shape parameters.
--- @log p(x) = (α-1) log x + (β-1) log(1-x) − log B(α,β)@.
-logBetaF :: Floating a => Double -> Double -> a -> a
-logBetaF alpha0 beta0 x =
-  let a   = realToFrac alpha0
-      b   = realToFrac beta0
-      lbB = realToFrac (stirlingLogGamma alpha0 + stirlingLogGamma beta0
-                        - stirlingLogGamma (alpha0 + beta0))
-  in (a - 1) * log x + (b - 1) * log (1 - x) - lbB
-{-# INLINE logBetaF #-}
-
--- | @log Poisson(k | λ)@ with @k@ a fixed (rounded) observation and @λ@
--- differentiable.
-logPoissonObsF :: Floating a => Double -> a -> a
-logPoissonObsF y_obs lam =
-  let k  = fromIntegral (round y_obs :: Int) :: Double
-      lf = realToFrac (logFactorial (round y_obs :: Int))
-  in realToFrac k * log lam - lam - lf
-{-# INLINE logPoissonObsF #-}
-
--- | @log Bernoulli(y | p)@ with @y ∈ {0, 1}@ a fixed observation and @p@
--- differentiable.
-logBernoulliObsF :: Floating a => Double -> a -> a
-logBernoulliObsF y_obs p
-  | y_obs > 0.5 = log p
-  | otherwise   = log (1 - p)
-{-# INLINE logBernoulliObsF #-}
-
--- ---------------------------------------------------------------------------
--- AD 勾配計算
--- ---------------------------------------------------------------------------
-
--- | Compute the gradient of a constrained-space log-joint via AD.
---
--- @
--- gradAD logJoint [1.0, 0.5]  -- [∂/∂θ₁, ∂/∂θ₂]
--- @
-gradAD :: LogJointF -> [Double] -> [Double]
-gradAD f xs = grad f xs
-
--- | AD gradient of the log-joint in unconstrained space (with constraint
--- transforms and Jacobian correction applied automatically).
-gradADU :: LogJointF -> [Transform] -> [Double] -> [Double]
-gradADU logJointC transforms us =
-  grad (logJointUF transforms logJointC) us
-
--- ---------------------------------------------------------------------------
--- 制約変換 (Floating 多相版)
--- ---------------------------------------------------------------------------
-
--- | Map an unconstrained value to its constrained image
--- (Floating-polymorphic).
-invTransformF :: Floating a => Transform -> a -> a
-invTransformF UnconstrainedT u = u
-invTransformF PositiveT      u = exp u
-invTransformF UnitIntervalT  u = 1 / (1 + exp (-u))  -- sigmoid
-{-# INLINE invTransformF #-}
-
--- | Log-Jacobian @log |∂θ/∂u|@ for one parameter (Floating-polymorphic).
-logJacF :: Floating a => Transform -> a -> a
-logJacF UnconstrainedT _ = 0
-logJacF PositiveT      u = u                     -- log(exp u) = u
-logJacF UnitIntervalT  u =
-  let p = 1 / (1 + exp (-u))
-  in log p + log (1 - p)                         -- log σ(u)(1−σ(u))
-{-# INLINE logJacF #-}
-
--- | Log-joint in unconstrained space, including constraint transforms
--- and the Jacobian correction.
-logJointUF :: Floating a => [Transform] -> LogJointF -> [a] -> a
-logJointUF transforms logJointC us =
-  let thetas = zipWith invTransformF transforms us
-      logJac  = sum (zipWith logJacF transforms us)
-  in logJointC thetas + logJac
-
--- ---------------------------------------------------------------------------
--- HMC AD 版サンプラー
--- ---------------------------------------------------------------------------
-
--- | HMC sampler using AD gradients.
---
--- Same algorithm as 'Hanalyze.MCMC.HMC.hmc', but gradients are computed exactly
--- with 'Numeric.AD.grad'. The user writes the log-joint in 'LogJointF'
--- form (i.e. @Floating@-polymorphic).
-hmcAD
-  :: LogJointF    -- ^ @log p(θ, y)@ as a 'LogJointF' (constrained space).
-  -> [Transform]  -- ^ Per-parameter constraint kind (same order as the
-                  --   parameter-name list).
-  -> HMCConfig
-  -> [Text]       -- ^ Parameter names (matches the initial-value @Params@ keys).
-  -> Params       -- ^ Initial values (constrained space).
-  -> GenIO
-  -> IO Chain
-hmcAD logJointC transforms cfg names initC gen = do
-  let total   = hmcBurnIn cfg + hmcIterations cfg
-      -- Unconstrained log-joint
-      logJU u = logJointUF transforms logJointC
-                  [Map.findWithDefault 0 n u | n <- names]
-      -- AD gradient function: leapfrogWith の規約は ∇U = -∇logπ なので符号を反転
-      gradFn ns paramsU =
-        let xs = [Map.findWithDefault 0 n paramsU | n <- ns]
-        in map negate (grad (logJointUF transforms logJointC) xs)
-      -- Initial unconstrained params
-      initU = Map.fromList
-        [ (n, toUnconstrained t v)
-        | (n, t) <- zip names transforms
-        , Just v <- [Map.lookup n initC]
-        ]
-
-  samplesRef  <- newIORef []
-  acceptedRef <- newIORef (0 :: Int)
-
-  let step currentU = do
-        r <- forM names (\_ -> standard gen)
-        let (proposedU, rFinal) =
-              leapfrogWith gradFn names
-                           (hmcStepSize cfg) (hmcLeapfrogSteps cfg)
-                           currentU r
-            logAlpha = (logJU proposedU - kinetic rFinal)
-                     - (logJU currentU  - kinetic r)
-        u <- uniform gen
-        if log (u :: Double) < logAlpha
-          then do modifyIORef' acceptedRef (+1); return proposedU
-          else return currentU
-
-  let loop 0 currentU = return currentU
-      loop i currentU = do
-        nextU <- step currentU
-        when (i <= hmcIterations cfg) $
-          modifyIORef' samplesRef
-            (Map.fromList
-               [ (n, fromUnconstrained t (Map.findWithDefault 0 n nextU))
-               | (n, t) <- zip names transforms
-               ] :)
-        loop (i - 1) nextU
-
-  _ <- loop total initU
-  samples  <- fmap reverse (readIORef samplesRef)
-  accepted <- readIORef acceptedRef
-  return Chain
-    { chainSamples  = samples
-    , chainAccepted = accepted
-    , chainTotal    = total
-    , chainEnergy   = []
-    , chainDivergences = []
-    }
-  where
-    when True  action = action
-    when False _      = return ()
-
--- | Run 'hmcAD' on @numChains@ parallel chains.
-hmcADChains
-  :: LogJointF
-  -> [Transform]
-  -> HMCConfig
-  -> Int
-  -> [Text]
-  -> Params
-  -> GenIO
-  -> IO [Chain]
-hmcADChains logJointC transforms cfg numChains names initC baseGen = do
-  gens <- replicateM numChains (spawnGen baseGen)
-  mapConcurrently (\g -> hmcAD logJointC transforms cfg names initC g) gens
-
--- ---------------------------------------------------------------------------
--- 数値ユーティリティ
--- ---------------------------------------------------------------------------
-
--- Stirling 近似による log Γ(z) — z は固定 Double ハイパーパラメータ用
-stirlingLogGamma :: Double -> Double
-stirlingLogGamma z
-  | z < 0.5   = log pi - log (sin (pi * z)) - stirlingLogGamma (1 - z)
-  | z < 12    = stirlingLogGamma (z + 1) - log z
-  | otherwise = (z - 0.5) * log z - z + 0.5 * log (2 * pi)
-                + 1/(12*z) - 1/(360*z^(3::Int))
-
-logFactorial :: Int -> Double
-logFactorial n = sum (map log [2 .. fromIntegral n])
diff --git a/src/Hanalyze/Stat/AdaptiveGrid.hs b/src/Hanalyze/Stat/AdaptiveGrid.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/AdaptiveGrid.hs
+++ /dev/null
@@ -1,179 +0,0 @@
--- |
--- Module      : Hanalyze.Stat.AdaptiveGrid
--- Description : 複数 id 間で変化の急な領域に点を集中させる適応的 1D グリッド生成
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Adaptive 1D grid generation.
---
--- Builds a common grid that concentrates grid points in regions where the
--- function changes rapidly across multiple ids.
---
--- Algorithm:
---
--- 1. Interpolate each id's @(z, y)@ via 'Hanalyze.Stat.Interpolate' and evaluate on a
---    common coarse grid (e.g. 200 points).
--- 2. For each z, compute @|dy/dz|@ across all ids and take the **maximum**
---    (peak) as @density(z)@.
--- 3. Add @ε = 0.05 × max(density)@ to avoid division by zero on flat regions.
--- 4. Build the cumulative integral @F(z) = ∫ (density(z) + ε) dz@.
--- 5. Divide the range of @F@ into @N-1@ equal parts and invert to obtain
---    @N@ z-coordinates.
---
--- When @N < 'minAdaptiveN'@ (= 10), the request silently falls back to a
--- uniform grid.
-module Hanalyze.Stat.AdaptiveGrid
-  ( GridKind (..)
-  , GridSpec (..)
-  , defaultGridSpec
-  , makeGrid
-  , uniformGrid
-  , minAdaptiveN
-  ) where
-
-import qualified Data.Vector.Unboxed as U
-import           Hanalyze.Stat.Interpolate    (InterpKind (..), interp1d)
-
--- | Grid kind.
-data GridKind
-  = Uniform     -- ^ Equally spaced @N@ points on @[zmin, zmax]@.
-  | Adaptive    -- ^ @N@ points concentrated where @|dy/dz|@ peaks.
-  deriving (Show, Eq)
-
--- | Specification used to build a grid.
-data GridSpec = GridSpec
-  { gsKind        :: !GridKind   -- ^ Uniform or adaptive.
-  , gsN           :: !Int        -- ^ Number of grid points.
-  , gsInterpKind  :: !InterpKind -- ^ Per-id interpolant used to evaluate the density.
-  , gsCoarseN     :: !Int        -- ^ Size of the coarse density grid (default 200).
-  , gsEpsRatio    :: !Double     -- ^ Floor on density on flat regions (default 0.05).
-  } deriving (Show, Eq)
-
--- | Recommended defaults: adaptive grid, linear interpolant, coarse grid
--- of 200 points, @ε = 0.05 × max(density)@.
-defaultGridSpec :: Int -> GridSpec
-defaultGridSpec n = GridSpec
-  { gsKind       = Adaptive
-  , gsN          = n
-  , gsInterpKind = Linear
-  , gsCoarseN    = 200
-  , gsEpsRatio   = 0.05
-  }
-
--- | Smallest @N@ for which adaptive grids are honored. Below this, an
--- adaptive request falls back to uniform.
-minAdaptiveN :: Int
-minAdaptiveN = 10
-
--- | Build a common grid.
---
--- Inputs: per-id observation lists @[[(z, y)]]@, the @(zmin, zmax)@
--- range, and a 'GridSpec'. The result is an ascending list of @N@ grid
--- points whose endpoints are exactly @zmin@ and @zmax@.
-makeGrid :: [[(Double, Double)]] -> (Double, Double) -> GridSpec -> [Double]
-makeGrid _      (zmin, zmax) spec
-  | gsN spec < 2 = [zmin, zmax]
-  | gsKind spec == Uniform || gsN spec < minAdaptiveN
-                 = uniformGrid (gsN spec) zmin zmax
-makeGrid perId  (zmin, zmax) spec =
-  let n       = gsN spec
-      coarseN = gsCoarseN spec
-      coarse  = uniformGrid coarseN zmin zmax
-      -- 各 id を補間し coarse grid 上で y を評価
-      ysPerId = [ map (interp1d (gsInterpKind spec) pts) coarse
-                | pts <- perId
-                , length pts >= 2 ]
-      -- 各 id の |dy/dz| 中央差分 → coarseN 長の Vector
-      slopesPerId = map (slopeAbs coarse) ysPerId
-      -- ピーク密度: 各 z 点で全 id の最大 |slope|
-      peak    = U.fromList
-                  [ if null slopesPerId
-                      then 1.0
-                      else maximum [ s U.! i | s <- slopesPerId ]
-                  | i <- [0 .. coarseN - 1] ]
-      mx      = U.maximum peak
-      eps     = gsEpsRatio spec * (if mx > 0 then mx else 1.0)
-      density = U.map (+ eps) peak
-      -- 累積積分 (台形則)
-      czs     = U.fromList coarse
-      cumF    = trapezoidalCDF czs density
-      total   = U.last cumF
-      -- N-1 等分点に対応する z を逆写像
-      targets = [ (fromIntegral k / fromIntegral (n - 1)) * total
-                | k <- [0 .. n - 1] ]
-      gridZ   = map (invMap czs cumF) targets
-  in -- 端点を保証 + monotone 化 (浮動小数誤差で僅かに非単調になることがある)
-     ensureMonotone zmin zmax gridZ
-
--- | Equally spaced @N@-point grid on @[zmin, zmax]@. With @N < 2@ the
--- result is @[zmin, zmax]@.
---
--- >>> uniformGrid 5 0 1
--- [0.0,0.25,0.5,0.75,1.0]
-uniformGrid :: Int -> Double -> Double -> [Double]
-uniformGrid n zmin zmax
-  | n < 2     = [zmin, zmax]
-  | otherwise =
-      let step = (zmax - zmin) / fromIntegral (n - 1)
-      in [ zmin + step * fromIntegral i | i <- [0 .. n - 1] ]
-
--- ---------------------------------------------------------------------------
-
--- | 中央差分での |dy/dz|。両端は片側差分。
-slopeAbs :: [Double] -> [Double] -> U.Vector Double
-slopeAbs zs ys =
-  let zV = U.fromList zs
-      yV = U.fromList ys
-      n  = U.length zV
-  in U.generate n $ \i ->
-       if n < 2 then 0
-       else if i == 0
-              then abs ((yV U.! 1 - yV U.! 0) / (zV U.! 1 - zV U.! 0))
-       else if i == n - 1
-              then abs ((yV U.! (n-1) - yV U.! (n-2)) / (zV U.! (n-1) - zV U.! (n-2)))
-       else
-         abs ((yV U.! (i+1) - yV U.! (i-1)) / (zV U.! (i+1) - zV U.! (i-1)))
-
--- | 累積分布 F[i] = ∫_{z_0}^{z_i} ρ dz (台形則)。F[0] = 0。
-trapezoidalCDF :: U.Vector Double -> U.Vector Double -> U.Vector Double
-trapezoidalCDF zs rho =
-  let n = U.length zs
-  in U.scanl' (+) 0 $
-       U.generate (n - 1) $ \i ->
-         let dz = zs U.! (i + 1) - zs U.! i
-             r  = (rho U.! i + rho U.! (i + 1)) / 2
-         in dz * r
-
--- | 累積 F の逆写像: target に対応する z を線形内挿で求める。
-invMap :: U.Vector Double -> U.Vector Double -> Double -> Double
-invMap zs cum target =
-  let n  = U.length cum
-      -- 二分探索で cum[i] <= target <= cum[i+1] の i を見つける
-      go lo hi
-        | hi - lo <= 1 = lo
-        | otherwise =
-            let mid = (lo + hi) `div` 2
-            in if cum U.! mid > target then go lo mid else go mid hi
-      i  = max 0 (min (n - 2) (go 0 (n - 1)))
-      c0 = cum U.! i
-      c1 = cum U.! (i + 1)
-      z0 = zs  U.! i
-      z1 = zs  U.! (i + 1)
-      t  = if c1 > c0 then (target - c0) / (c1 - c0) else 0
-  in z0 + t * (z1 - z0)
-
--- | 端点を [zmin, zmax] にスナップ + 単調化 (重複は微小 ε ずつシフト)。
-ensureMonotone :: Double -> Double -> [Double] -> [Double]
-ensureMonotone zmin zmax xs0 =
-  let xs = case xs0 of
-             []     -> [zmin, zmax]
-             [_]    -> [zmin, zmax]
-             (_:rs) -> zmin : init rs ++ [zmax]
-      -- 単調化 (前進方向で max を取り、僅かに ε を加算)
-      go prev (x:rest) =
-        let x' = max x (prev + 1e-12 * (zmax - zmin + 1))
-        in x' : go x' rest
-      go _    []       = []
-  in case xs of
-       (x0:rest) -> x0 : go x0 rest
-       []        -> []
diff --git a/src/Hanalyze/Stat/BayesFactor.hs b/src/Hanalyze/Stat/BayesFactor.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/BayesFactor.hs
+++ /dev/null
@@ -1,101 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes        #-}
--- |
--- Module      : Hanalyze.Stat.BayesFactor
--- Description : Bridge Sampling による Bayes Factor (Kass & Raftery 1995) 計算
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Bayes Factor (Kass & Raftery 1995) via Bridge Sampling.
---
--- @
---   BF_{10} = p(y | M_1) / p(y | M_0)
--- @
---
--- 計算は 2 モデルそれぞれに対し
--- 'Hanalyze.Stat.BridgeSampling.bridgeSampling' を呼び、 log marginal
--- 同士の差を取る。 解釈表は Kass-Raftery (1995) Table 1。
---
--- Reference: Kass & Raftery (1995) "Bayes factors". JASA 90:773-795.
-module Hanalyze.Stat.BayesFactor
-  ( BayesFactorResult (..)
-  , bayesFactor
-  , BFInterpretation (..)
-  , interpretBF
-  ) where
-
-import           System.Random.MWC         (GenIO)
-
-import           Hanalyze.Model.HBM        (ModelP)
-import           Hanalyze.MCMC.Core        (Chain)
-import           Hanalyze.Stat.BridgeSampling
-                   (BridgeConfig, BridgeResult (..), bridgeSampling)
-
--- ---------------------------------------------------------------------------
--- Bayes Factor
--- ---------------------------------------------------------------------------
-
-data BayesFactorResult = BayesFactorResult
-  { bfLog10        :: !Double  -- ^ log_10 BF_{10}
-  , bfLogE         :: !Double  -- ^ log_e BF_{10} = log p(y|M_1) - log p(y|M_0)
-  , bfLogMarginal0 :: !Double  -- ^ log p(y | M_0)
-  , bfLogMarginal1 :: !Double  -- ^ log p(y | M_1)
-  , bfConverged0   :: !Bool
-  , bfConverged1   :: !Bool
-  } deriving (Show)
-
--- | 2 モデル間の Bayes Factor BF_{10} = p(y|M_1) / p(y|M_0)。
--- 各モデルに対し Bridge Sampling で log marginal を推定、 差を取る。
-bayesFactor
-  :: forall r0 r1.
-     ModelP r0 -> Chain     -- ^ M_0 + posterior chain
-  -> ModelP r1 -> Chain     -- ^ M_1 + posterior chain
-  -> BridgeConfig
-  -> GenIO
-  -> IO BayesFactorResult
-bayesFactor m0 ch0 m1 ch1 cfg gen = do
-  r0 <- bridgeSampling m0 cfg ch0 gen
-  r1 <- bridgeSampling m1 cfg ch1 gen
-  let logE   = brLogMarginal r1 - brLogMarginal r0
-      log10v = logE / log 10
-  pure BayesFactorResult
-    { bfLog10        = log10v
-    , bfLogE         = logE
-    , bfLogMarginal0 = brLogMarginal r0
-    , bfLogMarginal1 = brLogMarginal r1
-    , bfConverged0   = brConverged r0
-    , bfConverged1   = brConverged r1
-    }
-
--- ---------------------------------------------------------------------------
--- Kass-Raftery 解釈表
--- ---------------------------------------------------------------------------
-
--- | Bayes Factor の強度区分 (Kass & Raftery 1995 Table 1)。
--- 区分の境界は @log_e BF@ で定義 (= log_10 ≈ /2.303):
---
--- @
---   0 < log_e BF < 1   (1 < BF < 2.7)    : Negligible
---   1 ≤ log_e BF < 3   (2.7 ≤ BF < 20)   : Positive (substantial)
---   3 ≤ log_e BF < 5   (20 ≤ BF < 150)   : Strong
---   5 ≤ log_e BF       (BF ≥ 150)        : Very strong (decisive)
--- @
---
--- 負側は対称 (M_0 寄り)。
-data BFInterpretation
-  = BFNegligible
-  | BFPositive          -- substantial evidence
-  | BFStrong
-  | BFVeryStrong
-  deriving (Show, Eq)
-
--- | log_e BF 値から強度区分を返す。 符号で方向 (M_0 / M_1 どちらに寄与) は
--- 呼び出し側が判定する想定 (@abs logE@ を渡しても OK)。
-interpretBF :: Double -> BFInterpretation
-interpretBF logE
-  | a < 1     = BFNegligible
-  | a < 3     = BFPositive
-  | a < 5     = BFStrong
-  | otherwise = BFVeryStrong
-  where
-    a = abs logE
diff --git a/src/Hanalyze/Stat/BayesianModelAveraging.hs b/src/Hanalyze/Stat/BayesianModelAveraging.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/BayesianModelAveraging.hs
+++ /dev/null
@@ -1,88 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Stat.BayesianModelAveraging
--- Description : Bridge Sampling の log marginal を用いた真の Bayesian Model Averaging (BMA)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- True Bayesian Model Averaging (BMA) via Bridge Sampling log marginals.
---
--- @
---   p(θ | y) = Σ_k p(θ | y, M_k) · p(M_k | y)
---   p(M_k | y) ∝ p(y | M_k) · p(M_k)
--- @
---
--- 入力: 各モデルの **Bridge Sampling 推定 log marginal likelihood** +
--- prior model weights (省略時 uniform 1/K)。
---
--- 出力: posterior model weights + 重み付き予測の helper。
---
--- ## 既存 pseudo-BMA との位置付け
---
--- 既存 'Hanalyze.Stat.ModelSelect' の pseudo-BMA (= PSIS-LOO ベース近似) は
--- 軽量だが marginal likelihood を正しく計算していない (= LOO予測精度を代理
--- 指標として使う)。 本 module は Bridge Sampling 経由で **真の log marginal**
--- を使う BMA で、 解釈が一貫している (= Bayes Factor / 仮説検定と同じ基盤)。
---
--- Reference: Hoeting, Madigan, Raftery, Volinsky (1999) "Bayesian Model
--- Averaging: A Tutorial". Statistical Science 14(4):382-417.
-module Hanalyze.Stat.BayesianModelAveraging
-  ( BMAResult (..)
-  , bayesianModelAveraging
-  , averagePredictions
-  ) where
-
-import qualified Numeric.LinearAlgebra    as LA
-
--- ---------------------------------------------------------------------------
--- BMA
--- ---------------------------------------------------------------------------
-
-data BMAResult = BMAResult
-  { bmaWeights      :: ![Double]   -- ^ posterior model weights @p(M_k|y)@、 Σ = 1
-  , bmaLogMarginals :: ![Double]   -- ^ 入力された per-model @log p(y|M_k)@ (引き継ぎ)
-  , bmaLogPriors    :: ![Double]   -- ^ 入力された per-model @log p(M_k)@ (引き継ぎ)
-  } deriving (Show)
-
--- | log marginal + log prior weights (省略時 uniform) から posterior model
--- weights を計算 (softmax 安定化)。
---
--- @
---   p(M_k | y) ∝ exp(log p(y|M_k) + log p(M_k))
--- @
---
--- 引数の長さは同じである必要 (異なる場合は短い方に合わせる)。 全 log
--- marginal が -∞ なら uniform fallback。
-bayesianModelAveraging
-  :: [Double]          -- ^ log marginals (Bridge Sampling 推定値 等)
-  -> Maybe [Double]    -- ^ optional log prior weights (Nothing = uniform)
-  -> BMAResult
-bayesianModelAveraging logMs mPriors =
-  let k = length logMs
-      logPriors = case mPriors of
-        Just ps | length ps == k -> ps
-        _                        -> replicate k (- log (fromIntegral k))
-      logUnnorm = zipWith (+) logMs logPriors
-      ws = if all isInfinite logUnnorm
-             then replicate k (1 / fromIntegral k)   -- fallback uniform
-             else
-               let m  = maximum logUnnorm
-                   es = map (\x -> exp (x - m)) logUnnorm
-                   s  = sum es
-               in if s == 0 then replicate k (1 / fromIntegral k)
-                            else map (/ s) es
-  in BMAResult
-       { bmaWeights      = ws
-       , bmaLogMarginals = logMs
-       , bmaLogPriors    = logPriors
-       }
-
--- | per-model 予測ベクトル (= 各モデルから出した y* の posterior mean 等) を
--- BMA weights で加重平均。 全ベクトルは同じ長さである必要。
-averagePredictions :: BMAResult -> [LA.Vector Double] -> LA.Vector Double
-averagePredictions bma preds
-  | null preds = LA.fromList []
-  | length preds /= length (bmaWeights bma) =
-      error "averagePredictions: number of predictions ≠ number of weights"
-  | otherwise =
-      foldr1 (+) [ LA.scale w v | (w, v) <- zip (bmaWeights bma) preds ]
diff --git a/src/Hanalyze/Stat/Bootstrap.hs b/src/Hanalyze/Stat/Bootstrap.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/Bootstrap.hs
+++ /dev/null
@@ -1,303 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Stat.Bootstrap
--- Description : ブートストラップ再標本化と置換検定
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Bootstrap resampling and permutation tests.
---
--- @
--- import Hanalyze.Stat.Bootstrap
--- import qualified System.Random.MWC as MWC
---
--- gen <- MWC.createSystemRandom
--- mean_ci <- bootstrapCI 10000 0.95 sampleMean xs gen
--- @
---
--- Provides:
---
---   * 'bootstrap' — generic resampling, returns a list of statistics.
---   * 'bootstrapCI' — percentile interval.
---   * 'bootstrapBcaCI' — bias-corrected & accelerated (BCa) interval.
---   * 'permutationTest' — permutation test for two-sample location.
-module Hanalyze.Stat.Bootstrap
-  ( -- * Generic resampling
-    bootstrap
-  , bootstrapCI
-  , bootstrapBcaCI
-    -- * Specialised fast paths
-  , bootstrapMeanCI
-    -- * Permutation tests
-  , permutationTest
-    -- * Statistics
-  , sampleMean
-  , sampleVar
-  , sampleMedian
-  ) where
-
-import qualified Numeric.LinearAlgebra            as LA
-import qualified Statistics.Distribution          as SD
-import qualified Statistics.Distribution.Normal   as Normal
-import qualified System.Random.MWC                as MWC
-import qualified Data.Vector                      as V
-import qualified Data.Vector.Mutable              as VM
-import qualified Data.Vector.Storable             as VS
-import qualified Data.Vector.Storable.Mutable     as MVS
-import qualified Data.Vector.Algorithms.Intro     as VAI
-import qualified Data.Word
-import           Control.Monad                    (replicateM, forM)
-import           Data.List                        (sort)
-
--- ---------------------------------------------------------------------------
--- Bootstrap
--- ---------------------------------------------------------------------------
-
--- | Bootstrap @n@ resamples and apply the statistic. Returns the list
--- of @n@ statistic values.
-bootstrap
-  :: Int                              -- ^ Number of resamples.
-  -> (LA.Vector Double -> Double)     -- ^ Statistic.
-  -> LA.Vector Double                 -- ^ Sample.
-  -> MWC.GenIO
-  -> IO [Double]
-bootstrap nReps stat xs gen = do
-  -- LA.Vector Double = Storable.Vector Double under the hood, so we can
-  -- fill a Storable.Mutable buffer and freeze it directly to an
-  -- LA.Vector. The previous implementation used [Double] + (!!), giving
-  -- O(n) per index → O(n²·B) total; this is O(n·B).
-  let n = LA.size xs
-  forM [1 .. nReps] $ \_ -> do
-    mv <- MVS.unsafeNew n
-    let go i
-          | i >= n    = pure ()
-          | otherwise = do
-              j <- MWC.uniformR (0, n - 1) gen
-              MVS.unsafeWrite mv i (xs `LA.atIndex` j)
-              go (i + 1)
-    go 0
-    frozen <- VS.unsafeFreeze mv
-    pure (stat frozen)
-
--- | Percentile bootstrap CI: @[(α/2)-quantile, (1-α/2)-quantile]@ of
--- the resampled statistic distribution.
-bootstrapCI
-  :: Int                              -- ^ Number of resamples.
-  -> Double                           -- ^ Confidence level (0 < c < 1).
-  -> (LA.Vector Double -> Double)     -- ^ Statistic.
-  -> LA.Vector Double                 -- ^ Sample.
-  -> MWC.GenIO
-  -> IO (Double, Double)
-bootstrapCI nReps conf stat xs gen = do
-  bs <- bootstrap nReps stat xs gen
-  let alpha = 1 - conf
-      sorted = sort bs
-      lo = quantile (alpha / 2) sorted
-      hi = quantile (1 - alpha / 2) sorted
-  pure (lo, hi)
-
--- | Specialised mean-bootstrap CI. Statistically equivalent to
--- @bootstrapCI nReps conf sampleMean xs gen@ but markedly faster:
---
---   * All @B × n@ resampled values are written into a /single/
---     contiguous Storable buffer (one allocation, one freeze) instead
---     of @B@ separate length-@n@ vectors with @B@ allocations / freezes.
---   * The @B@ row sums are computed in one BLAS GEMV
---     (@buf · 1_n@), giving @B@ resample means without the @B@-fold
---     per-row 'LA.sumElements' dispatch overhead.
---   * The bootstrap distribution is sorted in place via
---     @vector-algorithms@ Intro sort on a Storable.Vector — no
---     @[Double]@ list materialisation, no @!!@ indexing in @quantile@.
---
--- Numerical result is identical to the generic path on the same RNG
--- stream.
-bootstrapMeanCI
-  :: Int                              -- ^ Number of resamples @B@.
-  -> Double                           -- ^ Confidence level (0 < c < 1).
-  -> LA.Vector Double                 -- ^ Sample (length @n@).
-  -> MWC.GenIO
-  -> IO (Double, Double)
-bootstrapMeanCI nReps conf xs gen = do
-  let !n     = LA.size xs
-      !total = nReps * n
-      !invN  = 1.0 / fromIntegral n
-      !nW    = fromIntegral n :: Data.Word.Word64
-  -- P40 (2026-05-07): uniformR per element costs 14 ns on mwc-random
-  -- and dominated this bench (15.8 ms / 22 ms total). Batch the
-  -- @B × n@ Word64 draws into a single @uniformVector@ call (~7 ns
-  -- per element, no per-call dispatch overhead), then convert to
-  -- @[0, n-1]@ indices via modular reduction. Bias from @w `mod` n@
-  -- is bounded by @n / 2^64 ≤ 1e-16@ for any n ≤ 10⁶ — far below
-  -- the bootstrap's intrinsic Monte-Carlo variance.
-  ws <- MWC.uniformVector gen total :: IO (VS.Vector Data.Word.Word64)
-  buf <- MVS.unsafeNew total :: IO (MVS.IOVector Double)
-  let go !i
-        | i >= total = pure ()
-        | otherwise  = do
-            let !w = VS.unsafeIndex ws i
-                !j = fromIntegral (w `mod` nW) :: Int
-            MVS.unsafeWrite buf i (xs `LA.atIndex` j)
-            go (i + 1)
-  go 0
-  flat <- VS.unsafeFreeze buf
-  let !mat   = LA.reshape n flat                          -- B × n
-      !ones  = LA.konst 1 n :: LA.Vector Double
-      !means = LA.scale invN (mat LA.#> ones)             -- B-vector
-  -- In-place sort of the resample means.
-  mvSorted <- VS.thaw means
-  VAI.sort mvSorted
-  sortedMeans <- VS.unsafeFreeze mvSorted
-  let alpha = 1 - conf
-      lo    = quantileVS (alpha / 2)       sortedMeans
-      hi    = quantileVS (1 - alpha / 2)   sortedMeans
-  pure (lo, hi)
-
--- | Bias-corrected & accelerated (BCa) bootstrap CI (Efron 1987).
--- Improves on percentile CI when the bootstrap distribution is biased
--- or skewed.
-bootstrapBcaCI
-  :: Int
-  -> Double
-  -> (LA.Vector Double -> Double)
-  -> LA.Vector Double
-  -> MWC.GenIO
-  -> IO (Double, Double)
-bootstrapBcaCI nReps conf stat xs gen = do
-  bs <- bootstrap nReps stat xs gen
-  let alpha   = 1 - conf
-      theta0  = stat xs
-      sorted  = sort bs
-      -- z0: bias correction.
-      pBelow  = fromIntegral (length [b | b <- bs, b < theta0])
-                / fromIntegral nReps
-      z0      = SD.quantile Normal.standard (clip pBelow)
-      clip p  = max 1e-10 (min (1 - 1e-10) p)
-      -- a: acceleration via jackknife.
-      n       = LA.size xs
-      xsList  = LA.toList xs
-      jackVals = [ stat (LA.fromList (omit i xsList))
-                 | i <- [0 .. n - 1] ]
-      jMean   = sum jackVals / fromIntegral n
-      jDiffs  = [(jMean - jv) | jv <- jackVals]
-      num     = sum [d^(3::Int) | d <- jDiffs]
-      den     = 6 * (sum [d^(2::Int) | d <- jDiffs] ** 1.5)
-      a       = if den == 0 then 0 else num / den
-      -- Adjusted alphas.
-      zL      = SD.quantile Normal.standard (alpha / 2)
-      zU      = SD.quantile Normal.standard (1 - alpha / 2)
-      alphaLo = SD.cumulative Normal.standard
-                  (z0 + (z0 + zL) / (1 - a * (z0 + zL)))
-      alphaHi = SD.cumulative Normal.standard
-                  (z0 + (z0 + zU) / (1 - a * (z0 + zU)))
-      lo      = quantile alphaLo sorted
-      hi      = quantile alphaHi sorted
-  pure (lo, hi)
-
--- | Permutation test for difference in means between two samples.
--- Returns @(observed diff, p-value)@.
-permutationTest
-  :: Int                              -- ^ Number of permutations.
-  -> LA.Vector Double                 -- ^ Sample 1.
-  -> LA.Vector Double                 -- ^ Sample 2.
-  -> MWC.GenIO
-  -> IO (Double, Double)
-permutationTest nPerms xs ys gen = do
-  let xsL = LA.toList xs
-      ysL = LA.toList ys
-      n1  = length xsL
-      _n2 = length ysL
-      pooled = xsL ++ ysL
-      meanOf vs = sum vs / fromIntegral (length vs)
-      observedDiff = meanOf xsL - meanOf ysL
-  permDiffs <- forM [1 .. nPerms] $ \_ -> do
-    shuffled <- shuffleList pooled gen
-    let g1 = take n1 shuffled
-        g2 = drop n1 shuffled
-    pure (meanOf g1 - meanOf g2)
-  let p = fromIntegral (length [d | d <- permDiffs, abs d >= abs observedDiff])
-          / fromIntegral nPerms
-  pure (observedDiff, p)
-
--- ---------------------------------------------------------------------------
--- Statistics
--- ---------------------------------------------------------------------------
-
--- | Sample mean.
-sampleMean :: LA.Vector Double -> Double
-sampleMean v = LA.sumElements v / fromIntegral (LA.size v)
-
--- | Unbiased sample variance.
-sampleVar :: LA.Vector Double -> Double
-sampleVar v =
-  let n = fromIntegral (LA.size v) :: Double
-      m = sampleMean v
-  in LA.sumElements ((v - LA.scalar m) ^ (2 :: Int)) / (n - 1)
-
--- | Sample median.
-sampleMedian :: LA.Vector Double -> Double
-sampleMedian v =
-  let xs = sort (LA.toList v)
-      n  = length xs
-  in if even n
-       then (xs !! (n `div` 2 - 1) + xs !! (n `div` 2)) / 2
-       else xs !! (n `div` 2)
-
--- ---------------------------------------------------------------------------
--- Helpers
--- ---------------------------------------------------------------------------
-
--- | Linear-interpolation quantile from a sorted Storable Vector.
--- Vector-native form of @quantile@; avoids the @sorted !! lo@
--- (O(n)) list indexing in the @[Double]@ version.
-quantileVS :: Double -> VS.Vector Double -> Double
-quantileVS q sorted
-  | VS.null sorted = 0
-  | q <= 0         = VS.unsafeIndex sorted 0
-  | q >= 1         = VS.unsafeIndex sorted (VS.length sorted - 1)
-  | otherwise      =
-      let !n  = VS.length sorted
-          !h  = q * fromIntegral (n - 1)
-          !lo = floor h    :: Int
-          !hi = ceiling h  :: Int
-          !fr = h - fromIntegral lo
-      in if lo == hi
-           then VS.unsafeIndex sorted lo
-           else VS.unsafeIndex sorted lo * (1 - fr)
-              + VS.unsafeIndex sorted hi * fr
-
--- | Linear-interpolation quantile from a sorted list.
-quantile :: Double -> [Double] -> Double
-quantile q sorted
-  | null sorted = 0
-  | q <= 0      = head sorted
-  | q >= 1      = last sorted
-  | otherwise   =
-      let n  = length sorted
-          h  = q * fromIntegral (n - 1)
-          lo = floor h
-          hi = ceiling h
-          fr = h - fromIntegral lo
-      in if lo == hi
-           then sorted !! lo
-           else sorted !! lo * (1 - fr) + sorted !! hi * fr
-
--- | Omit element at index i.
-omit :: Int -> [a] -> [a]
-omit i xs = take i xs ++ drop (i + 1) xs
-
--- | Shuffle a list (Fisher-Yates) via mutable Vector.
-shuffleList :: [a] -> MWC.GenIO -> IO [a]
-shuffleList xs gen = do
-  let n = length xs
-  v <- V.thaw (V.fromList xs)
-  let loop i
-        | i <= 0 = pure ()
-        | otherwise = do
-            j <- MWC.uniformR (0, i) gen
-            a <- VM.read v i
-            b <- VM.read v j
-            VM.write v i b
-            VM.write v j a
-            loop (i - 1)
-  loop (n - 1)
-  V.toList <$> V.freeze v
diff --git a/src/Hanalyze/Stat/BridgeSampling.hs b/src/Hanalyze/Stat/BridgeSampling.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/BridgeSampling.hs
+++ /dev/null
@@ -1,233 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes        #-}
-{-# LANGUAGE BangPatterns      #-}
--- |
--- Module      : Hanalyze.Stat.BridgeSampling
--- Description : Bridge Sampling による周辺尤度 log p(y) 推定 (Meng & Wong 1996)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Bridge Sampling estimator of the marginal likelihood @log p(y)@
--- (Meng & Wong 1996).
---
--- Reference:
---
---   * Meng & Wong (1996) "Simulating ratios of normalising constants
---     via a simple identity: a theoretical exploration". Statistica
---     Sinica 6:831-860.
---   * Gronau, Sarafoglou, Matzke, Ly, Boehm, Marsman, Leslie, Forster,
---     Wagenmakers, Steingroever (2017) "A tutorial on bridge sampling".
---     Journal of Mathematical Psychology 81:80-97.
---
--- ## アルゴリズム (Phase 29-A2)
---
--- 目的: 周辺尤度 @log p(y) = log ∫ p(y|θ) p(θ) dθ@ を、 既存 MCMC chain
--- (posterior samples) と diagonal Gaussian proposal @g(θ)@ から推定する。
---
--- Bridge identity (Meng-Wong):
---
--- @
---   p(y) = E_g[α(θ) q(θ)] / E_p[α(θ) g(θ)]
--- @
---
--- 最適 bridge function @α*(θ) = 1 / (s_1 q(θ) + s_2 r g(θ))@ を使った
--- iterative scheme で @r̂@ を求める:
---
--- @
---   r̂_{t+1} = [(1/N_2) Σ_i q(θ̃_2,i) / (s_1 q(θ̃_2,i) + s_2 r̂_t g(θ̃_2,i))]
---           / [(1/N_1) Σ_j g(θ̃_1,j) / (s_1 q(θ̃_1,j) + s_2 r̂_t g(θ̃_1,j))]
--- @
---
--- ここで:
---   * @θ̃_1@ は proposal @g@ から (本実装では Gaussian fit-to-chain)
---   * @θ̃_2@ は posterior chain サンプル
---   * @s_1 = N_1/(N_1+N_2)@、 @s_2 = N_2/(N_1+N_2)@
---   * @q(θ) = p(y|θ)·p(θ)@ = 'logJoint' の exp 化
---
--- 全計算は **log space** で行い (log-sum-exp 安定化)、 浮動小数 underflow を回避。
---
--- ## SMC との関係 (Phase 29-A1/A2 統合)
---
--- SMC は副産物として log marginal を推定する (= temperature schedule の
--- incremental log-mean-weight 累積)。 Bridge Sampling は MCMC chain + proposal
--- から **独立な推定経路** で求めるので、 両者が 5% 以内で一致すれば妥当性が裏付け。
--- 不一致なら chain の収束不足 / SMC schedule 粗さ / proposal 不適切のサイン。
-module Hanalyze.Stat.BridgeSampling
-  ( BridgeConfig (..)
-  , defaultBridgeConfig
-  , BridgeResult (..)
-  , bridgeSampling
-  ) where
-
-import           Control.Monad             (replicateM, forM)
-import qualified Data.Map.Strict           as Map
-import           Data.Text                 (Text)
-import           System.Random.MWC         (GenIO)
-import           System.Random.MWC.Distributions (normal)
-
-import           Hanalyze.Model.HBM        (ModelP, Params, logJoint, sampleNames)
-import           Hanalyze.MCMC.Core        (Chain (..), chainVals)
-
--- ---------------------------------------------------------------------------
--- Configuration
--- ---------------------------------------------------------------------------
-
--- | Bridge Sampling 設定。
-data BridgeConfig = BridgeConfig
-  { bcNProposal :: !Int     -- ^ N_1: proposal samples 数 (典型 chain サンプル数と同等)
-  , bcMaxIter   :: !Int     -- ^ 反復解の最大回数 (典型 100、 通常 < 20 で収束)
-  , bcTolerance :: !Double  -- ^ 反復収束判定 |Δ log r̂| < tol (典型 1e-6)
-  } deriving (Show)
-
-defaultBridgeConfig :: BridgeConfig
-defaultBridgeConfig = BridgeConfig
-  { bcNProposal = 500
-  , bcMaxIter   = 100
-  , bcTolerance = 1e-6
-  }
-
--- | Bridge Sampling 結果。
-data BridgeResult = BridgeResult
-  { brLogMarginal :: !Double   -- ^ 推定 @log p(y)@
-  , brIterations  :: !Int      -- ^ 収束に要した反復数
-  , brConverged   :: !Bool     -- ^ tol 以内で収束したか
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- 公開 API
--- ---------------------------------------------------------------------------
-
--- | Bridge Sampling で @log p(y)@ を推定。
---
--- 入力:
---   * モデル (logJoint = log q(θ) = log p(y|θ) + log p(θ))
---   * posterior chain (既存 NUTS / MH / SMC 等の結果)
---   * proposal は **diagonal Gaussian fit** to chain (各パラメータの sample
---     mean / SD から構築)
---
--- 出力: log marginal likelihood 推定値 + 収束情報。
-bridgeSampling
-  :: forall r. ModelP r
-  -> BridgeConfig
-  -> Chain                     -- ^ posterior chain
-  -> GenIO
-  -> IO BridgeResult
-bridgeSampling model cfg chain gen = do
-  let names      = sampleNames model
-      posterior  = chainSamples chain
-      n2         = length posterior
-      (mus, sds) = fitDiagGaussian names chain
-  -- 1. Sample N_1 from proposal g (diagonal Gaussian)
-  proposal <- replicateM (bcNProposal cfg) (sampleProposal names mus sds gen)
-  let n1   = length proposal
-      s1   = fromIntegral n1 / fromIntegral (n1 + n2)
-      s2   = fromIntegral n2 / fromIntegral (n1 + n2)
-      -- 2. Precompute log q (logJoint) and log g (proposal log-density)
-      logq2 = map (logJoint model) posterior
-      logq1 = map (logJoint model) proposal
-      logg2 = map (logProposal names mus sds) posterior
-      logg1 = map (logProposal names mus sds) proposal
-  -- 3. Iterative solve for log r̂
-  let (logR, niter, converged) =
-        iterateBridge cfg logq1 logg1 logq2 logg2 s1 s2 0.0
-  pure BridgeResult
-    { brLogMarginal = logR
-    , brIterations  = niter
-    , brConverged   = converged
-    }
-
--- | Meng-Wong iterative formula in log space.
-iterateBridge
-  :: BridgeConfig
-  -> [Double] -> [Double]   -- ^ logq1, logg1 (proposal samples)
-  -> [Double] -> [Double]   -- ^ logq2, logg2 (posterior samples)
-  -> Double                 -- ^ s_1
-  -> Double                 -- ^ s_2
-  -> Double                 -- ^ 初期 log r̂
-  -> (Double, Int, Bool)
-iterateBridge cfg logq1 logg1 logq2 logg2 s1 s2 logR0 = go 0 logR0
-  where
-    ls1 = log s1
-    ls2 = log s2
-    go !it !logR
-      | it >= bcMaxIter cfg = (logR, it, False)
-      | otherwise =
-          let -- Numerator: posterior 側の logq2 - logSumExp(s1·q2, s2·r·g2)
-              numTerms =
-                [ lq - logSumExp2 (ls1 + lq) (ls2 + logR + lg)
-                | (lq, lg) <- zip logq2 logg2 ]
-              -- Denominator: proposal 側の logg1 - logSumExp(s1·q1, s2·r·g1)
-              denTerms =
-                [ lg - logSumExp2 (ls1 + lq) (ls2 + logR + lg)
-                | (lq, lg) <- zip logq1 logg1 ]
-              num = logMeanExp numTerms
-              den = logMeanExp denTerms
-              logR' = num - den
-              diff  = abs (logR' - logR)
-          in if diff < bcTolerance cfg
-               then (logR', it + 1, True)
-               else go (it + 1) logR'
-
--- ---------------------------------------------------------------------------
--- Diagonal Gaussian proposal (fit-to-chain)
--- ---------------------------------------------------------------------------
-
--- | chain から各パラメータの sample mean / SD を抽出。 SD = 0 になりうる
--- (定数推定) 場合は 1e-6 で下駄を履かせる (g(θ) 評価で除算 0 を避ける safety)。
-fitDiagGaussian
-  :: [Text] -> Chain -> (Map.Map Text Double, Map.Map Text Double)
-fitDiagGaussian names chain =
-  let mus = Map.fromList
-        [ (n, mean (chainVals n chain)) | n <- names ]
-      sds = Map.fromList
-        [ (n, max 1e-6 (stddev (chainVals n chain))) | n <- names ]
-  in (mus, sds)
-  where
-    mean xs = sum xs / fromIntegral (length xs)
-    stddev xs =
-      let mu = mean xs
-          n  = fromIntegral (length xs) :: Double
-      in if n <= 1 then 0
-                   else sqrt (sum [(x - mu) ^ (2 :: Int) | x <- xs] / (n - 1))
-
--- | Diagonal Gaussian proposal からサンプル抽出。
-sampleProposal
-  :: [Text] -> Map.Map Text Double -> Map.Map Text Double -> GenIO
-  -> IO Params
-sampleProposal names mus sds gen =
-  fmap Map.fromList $ forM names $ \n -> do
-    let mu = Map.findWithDefault 0 n mus
-        sd = Map.findWithDefault 1 n sds
-    x <- normal mu sd gen
-    pure (n, x)
-
--- | log density of diagonal Gaussian proposal at θ。
-logProposal
-  :: [Text] -> Map.Map Text Double -> Map.Map Text Double -> Params
-  -> Double
-logProposal names mus sds theta =
-  sum
-    [ let mu = Map.findWithDefault 0 n mus
-          sd = Map.findWithDefault 1 n sds
-          x  = Map.findWithDefault 0 n theta
-          z  = (x - mu) / sd
-      in -0.5 * log (2 * pi) - log sd - 0.5 * z * z
-    | n <- names ]
-
--- ---------------------------------------------------------------------------
--- log-sum-exp helpers
--- ---------------------------------------------------------------------------
-
-logSumExp2 :: Double -> Double -> Double
-logSumExp2 a b
-  | a > b     = a + log (1 + exp (b - a))
-  | otherwise = b + log (1 + exp (a - b))
-
-logMeanExp :: [Double] -> Double
-logMeanExp xs
-  | null xs   = -1 / 0
-  | otherwise =
-      let m  = maximum xs
-          s  = sum [ exp (x - m) | x <- xs ]
-          n  = fromIntegral (length xs) :: Double
-      in m + log (s / n)
diff --git a/src/Hanalyze/Stat/CV.hs b/src/Hanalyze/Stat/CV.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/CV.hs
+++ /dev/null
@@ -1,274 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Stat.CV
--- Description : クロスバリデーションのフレームワーク (fold 分割 + 汎用 crossValidate)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Cross-validation framework.
---
--- Provides train/validation splits and a generic 'crossValidate'
--- function that runs a user-supplied @fit@ + @score@ on each fold.
---
--- @
--- import Hanalyze.Stat.CV
--- import qualified System.Random.MWC as MWC
---
--- gen <- MWC.createSystemRandom
--- folds <- kFold 5 (LA.rows x) gen
--- scores <- crossValidate folds fitFn scoreFn (x, y)
--- let mean = sum scores / fromIntegral (length scores)
--- @
---
--- == Available split strategies
---
---   * 'kFold' (random k-fold)
---   * 'stratifiedKFold' (preserves class balance for classification)
---   * 'leaveOneOut'
---   * 'shuffleSplit' (random repeated train/test)
---   * 'timeSeriesSplit' (forward-chaining for time series)
---
--- All return @[Fold]@ where each 'Fold' is a pair @(trainIdx, testIdx)@.
-module Hanalyze.Stat.CV
-  ( -- * Fold types
-    Fold
-    -- * Split strategies
-  , kFold
-  , stratifiedKFold
-  , leaveOneOut
-  , shuffleSplit
-  , timeSeriesSplit
-    -- * Cross-validation
-  , crossValidate
-  , crossValidateScores
-    -- * Hyperparameter search
-  , gridSearchCV
-  , GridSearchResult (..)
-  ) where
-
-import qualified Data.Map.Strict       as Map
-import qualified Data.Vector           as V
-import qualified Data.Vector.Mutable   as VM
-import           Control.Monad         (forM, forM_)
-import           Control.Monad.Primitive (PrimMonad, PrimState)
-import           Data.List             (sortBy)
-import           Data.Ord              (comparing)
-import qualified System.Random.MWC     as MWC
-
--- ---------------------------------------------------------------------------
--- Fold types
--- ---------------------------------------------------------------------------
-
--- | A single train / test split: @(trainIdx, testIdx)@. Indices are
--- 0-based row numbers into the original data.
-type Fold = ([Int], [Int])
-
--- ---------------------------------------------------------------------------
--- Split strategies
--- ---------------------------------------------------------------------------
-
--- | Random k-fold split. 'PrimMonad' 汎用 (mwc は 'PrimMonad' 汎用) ゆえ ST/IO 両経路で
--- 同コード。 IO 呼び出しは従来どおり。 純粋 (seed) 経路は呼び出し側で
--- @runST (MWC.initialize (V.singleton seed) >>= kFold k n)@ で完結 (Phase 70.7 = 罰則回帰
--- の λ CV 純粋化に使う・[[selectLambdaCV]])。
-kFold
-  :: PrimMonad m
-  => Int            -- ^ Number of folds @k@.
-  -> Int            -- ^ Total sample count @n@.
-  -> MWC.Gen (PrimState m)
-  -> m [Fold]
-kFold k n gen
-  | k < 2     = pure [(allIdx n, [])]
-  | k > n     = leaveOneOut n
-  | otherwise = do
-      perm <- shuffleIndices n gen
-      let foldSize  = n `div` k
-          remainder = n `mod` k
-          -- Fold sizes: first 'remainder' folds get 1 extra.
-          sizes = [foldSize + (if i < remainder then 1 else 0) | i <- [0..k-1]]
-          starts = scanl (+) 0 sizes
-          ranges = [(s, s + sz) | (s, sz) <- zip starts sizes]
-          allRows = take n perm
-      pure [ let testIdx  = take (e - s) (drop s allRows)
-                 trainIdx = take s allRows ++ drop e allRows
-             in (trainIdx, testIdx)
-           | (s, e) <- ranges ]
-
--- | Stratified k-fold: preserves class proportions in each fold.
-stratifiedKFold
-  :: Int            -- ^ Number of folds @k@.
-  -> [Int]          -- ^ Class labels (length @n@).
-  -> MWC.GenIO
-  -> IO [Fold]
-stratifiedKFold k labels gen
-  | k < 2 = pure [(allIdx (length labels), [])]
-  | otherwise = do
-      let n         = length labels
-          byClass   = Map.fromListWith (++)
-                        [(l, [i]) | (i, l) <- zip [0..] labels]
-      -- For each class, shuffle its indices and split into k folds.
-      classFolds <- forM (Map.toList byClass) $ \(_, idxs) -> do
-        shuffled <- shuffleList idxs gen
-        let m         = length shuffled
-            foldSize  = m `div` k
-            remainder = m `mod` k
-            sizes     = [foldSize + (if i < remainder then 1 else 0)
-                        | i <- [0..k-1]]
-            starts    = scanl (+) 0 sizes
-            ranges    = [(s, s + sz) | (s, sz) <- zip starts sizes]
-        pure [take (e - s) (drop s shuffled) | (s, e) <- ranges]
-      -- Combine: fold i = concat of i-th sub-fold from each class.
-      let testIdxByFold =
-            [ concat [classFolds !! ci !! fi | ci <- [0 .. length classFolds - 1]]
-            | fi <- [0 .. k - 1] ]
-          allI = [0 .. n - 1]
-      pure [ let testIdx  = sortBy compare ti
-                 trainIdx = filter (`notElem` testIdx) allI
-             in (trainIdx, testIdx)
-           | ti <- testIdxByFold ]
-
--- | Leave-one-out cross-validation: @n@ folds, each test set is a
--- single row.
-leaveOneOut :: Applicative f => Int -> f [Fold]
-leaveOneOut n =
-  pure [ ([j | j <- [0 .. n - 1], j /= i], [i]) | i <- [0 .. n - 1] ]
-
--- | Repeated random train/test split (Monte-Carlo CV).
-shuffleSplit
-  :: Int            -- ^ Number of repetitions.
-  -> Double         -- ^ Test fraction (0 < t < 1).
-  -> Int            -- ^ Total samples @n@.
-  -> MWC.GenIO
-  -> IO [Fold]
-shuffleSplit nReps testFrac n gen = do
-  let testN = max 1 (round (fromIntegral n * testFrac))
-  forM [1 .. nReps] $ \_ -> do
-    perm <- shuffleIndices n gen
-    let testIdx  = take testN perm
-        trainIdx = drop testN perm
-    pure (trainIdx, testIdx)
-
--- | Time-series forward-chaining split. Fold @i@ uses the first
--- @initial + i × step@ samples for train and the next @step@ for test.
--- Useful for evaluating models on time-ordered data.
-timeSeriesSplit
-  :: Int            -- ^ Initial training set size.
-  -> Int            -- ^ Step size (samples per test fold).
-  -> Int            -- ^ Total samples.
-  -> [Fold]
-timeSeriesSplit initial step n =
-  [ ([0 .. initial + (i - 1) * step - 1],
-     [initial + (i - 1) * step .. initial + i * step - 1])
-  | i <- [1 .. (n - initial) `div` step]
-  ]
-
--- ---------------------------------------------------------------------------
--- Cross-validation
--- ---------------------------------------------------------------------------
-
--- | Run a fit / score loop over folds. Returns a score per fold.
---
--- The user provides:
---
---   * a function that takes (trainIdx, testIdx) and the dataset, fits
---     a model on the train indices, and returns predictions on the
---     test indices,
---   * a score function that compares true and predicted values.
---
--- For type generality the dataset and predictions are user-defined.
-crossValidate
-  :: [Fold]
-  -> (([Int], [Int]) -> data_ -> IO pred_)  -- ^ fit + predict
-  -> (data_ -> [Int] -> pred_ -> IO Double) -- ^ scoring fn (true vs pred)
-  -> data_
-  -> IO [Double]
-crossValidate folds fitPredict scoreFn d =
-  forM folds $ \fold@(_train, testIdx) -> do
-    pred_ <- fitPredict fold d
-    scoreFn d testIdx pred_
-
--- | Convenience: returns @(mean, std)@ of fold scores.
-crossValidateScores
-  :: [Fold]
-  -> (([Int], [Int]) -> data_ -> IO pred_)
-  -> (data_ -> [Int] -> pred_ -> IO Double)
-  -> data_
-  -> IO (Double, Double)
-crossValidateScores folds fp sf d = do
-  scores <- crossValidate folds fp sf d
-  let n     = fromIntegral (length scores) :: Double
-      mean  = sum scores / n
-      var   = sum [(s - mean) ^ (2 :: Int) | s <- scores]
-              / max 1 (n - 1)
-  pure (mean, sqrt var)
-
--- ---------------------------------------------------------------------------
--- Grid search
--- ---------------------------------------------------------------------------
-
--- | Result of a grid search.
-data GridSearchResult hp = GridSearchResult
-  { gsBestParams :: hp
-  , gsBestScore  :: !Double
-  , gsAllResults :: ![(hp, Double, Double)]
-    -- ^ (params, mean score, std of fold scores) for each grid point.
-  } deriving (Show)
-
--- | Grid search over hyperparameters with k-fold CV. The user
--- provides:
---
---   * the list of HP values to try
---   * a function to fit/predict given an HP and a fold
---   * a scoring function (higher = better)
---
--- Returns the best HP plus full grid results.
-gridSearchCV
-  :: [Fold]
-  -> [hp]                                              -- ^ HP grid
-  -> (hp -> ([Int], [Int]) -> data_ -> IO pred_)       -- ^ fit/predict
-  -> (data_ -> [Int] -> pred_ -> IO Double)            -- ^ score
-  -> data_
-  -> IO (GridSearchResult hp)
-gridSearchCV folds grid fp sf d = do
-  results <- forM grid $ \hp -> do
-    (mean, std) <- crossValidateScores folds (fp hp) sf d
-    pure (hp, mean, std)
-  let (bestHp, bestScore, _) = head (sortBy (comparing (\(_, s, _) -> negate s)) results)
-  pure GridSearchResult
-    { gsBestParams = bestHp
-    , gsBestScore  = bestScore
-    , gsAllResults = results
-    }
-
--- ---------------------------------------------------------------------------
--- Helpers
--- ---------------------------------------------------------------------------
-
-allIdx :: Int -> [Int]
-allIdx n = [0 .. n - 1]
-
--- | Fisher-Yates shuffle producing a list of indices. 'PrimMonad' 汎用 (ST/IO 両用)。
-shuffleIndices :: PrimMonad m => Int -> MWC.Gen (PrimState m) -> m [Int]
-shuffleIndices n gen = do
-  v <- V.thaw (V.fromList [0 .. n - 1])
-  forM_ [n - 1, n - 2 .. 1] $ \i -> do
-    j <- MWC.uniformR (0, i) gen
-    a <- VM.read v i
-    b <- VM.read v j
-    VM.write v i b
-    VM.write v j a
-  V.toList <$> V.freeze v
-
--- | Shuffle an arbitrary list. 'PrimMonad' 汎用 (ST/IO 両用)。
-shuffleList :: PrimMonad m => [a] -> MWC.Gen (PrimState m) -> m [a]
-shuffleList xs gen = do
-  let n = length xs
-  v <- V.thaw (V.fromList xs)
-  forM_ [n - 1, n - 2 .. 1] $ \i -> do
-    j <- MWC.uniformR (0, i) gen
-    a <- VM.read v i
-    b <- VM.read v j
-    VM.write v i b
-    VM.write v j a
-  V.toList <$> V.freeze v
-
diff --git a/src/Hanalyze/Stat/Causal/CATE.hs b/src/Hanalyze/Stat/Causal/CATE.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/Causal/CATE.hs
+++ /dev/null
@@ -1,197 +0,0 @@
--- |
--- Module      : Hanalyze.Stat.Causal.CATE
--- Description : Künzel et al. (2019) の S/T/X-Learner による CATE meta-learner 実装
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Conditional Average Treatment Effect (CATE) meta-learners (Phase 30-A4)。
---
--- Künzel et al. (2019) の 3 meta-learner を実装:
---
--- - 'SLearner': 単一モデル @μ̂(X, T)@、 @τ̂(X) = μ̂(X, 1) - μ̂(X, 0)@
--- - 'TLearner': 2 モデル @μ̂_1(X)@ / @μ̂_0(X)@、 @τ̂(X) = μ̂_1(X) - μ̂_0(X)@
--- - 'XLearner': T-learner の残差を再帰回帰、 PS で重み付け平均
---
--- base learner は 'CATELM' (= 'Hanalyze.Model.LM') と 'CATERF' (=
--- 'Hanalyze.Model.RandomForest') から選択。 将来 Causal Forest 等を追加する
--- ときは新 constructor を加える。
---
--- ## 使い方
---
--- @
---   gen <- MWC.create
---   r   <- fitCATE TLearner CATELM x t y gen
---   print (cateATE r)   -- average of cateEstimates
--- @
---
--- Reference:
---   Künzel, Sekhon, Bickel, Yu (2019) "Metalearners for estimating
---   heterogeneous treatment effects using machine learning".
---   PNAS 116:4156-4165.
-module Hanalyze.Stat.Causal.CATE
-  ( CATEBaseLearner (..)
-  , CATELearner (..)
-  , CATEResult (..)
-  , fitCATE
-  ) where
-
-import qualified Numeric.LinearAlgebra      as LA
-import qualified Data.Vector.Storable       as VS
-import qualified Data.Vector.Unboxed        as VU
-import qualified Hanalyze.Model.LM          as LM
-import qualified Hanalyze.Model.RandomForest as RF
-import           Hanalyze.Model.Core         (coefficientsV)
-import           Hanalyze.Stat.Causal.PropensityScore
-                   (PropensityScore (..), propensityScore, trimPropensity)
-import           Hanalyze.Stat.Causal.IPW   (defaultPSTrim)
-import qualified System.Random.MWC          as MWC
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
--- | base learner 選択。 LM は OLS、 RF は Random Forest。
-data CATEBaseLearner = CATELM | CATERF RF.RFConfig
-  deriving (Show)
-
--- | meta-learner 選択。
-data CATELearner = SLearner | TLearner | XLearner
-  deriving (Show, Eq)
-
-data CATEResult = CATEResult
-  { cateEstimates :: !(LA.Vector Double)  -- ^ τ̂(X_i) for each unit
-  , cateMethod    :: !CATELearner
-  , cateATE       :: !Double               -- ^ mean of cateEstimates
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- Base learner abstraction
--- ---------------------------------------------------------------------------
-
--- | Train a base learner on (X, y) and return a predictor for new X.
--- Random forest path threads through @MWC.GenIO@; LM is pure but is
--- wrapped in @IO@ for uniform signature.
-fitPredict :: CATEBaseLearner
-           -> LA.Matrix Double -> LA.Vector Double -> MWC.GenIO
-           -> IO (LA.Matrix Double -> LA.Vector Double)
-fitPredict CATELM x y _ = do
-  let beta = coefficientsV (LM.fitLMVec x y)
-  pure (\xNew -> LM.predictLMVec beta xNew)
-fitPredict (CATERF cfg) x y gen = do
-  rf <- RF.fitRFV cfg x (VS.convert y :: VU.Vector Double)
-                  gen
-  pure (\xNew ->
-          let rows = LA.toRows xNew
-          in LA.fromList [RF.predictRF rf (LA.toList r) | r <- rows])
-
--- ---------------------------------------------------------------------------
--- fitCATE
--- ---------------------------------------------------------------------------
-
-fitCATE :: CATELearner -> CATEBaseLearner
-        -> LA.Matrix Double -> LA.Vector Double -> LA.Vector Double
-        -> MWC.GenIO -> IO CATEResult
-fitCATE method base x t y gen = case method of
-  SLearner -> sLearner base x t y gen
-  TLearner -> tLearner base x t y gen
-  XLearner -> xLearner base x t y gen
-
--- ---------------------------------------------------------------------------
--- S-learner: 単一モデル on (X, T)
--- ---------------------------------------------------------------------------
-
-sLearner :: CATEBaseLearner
-         -> LA.Matrix Double -> LA.Vector Double -> LA.Vector Double
-         -> MWC.GenIO -> IO CATEResult
-sLearner base x t y gen = do
-  let xt  = LA.fromBlocks [[x, LA.asColumn t]]
-      n   = LA.rows x
-      x1  = LA.fromBlocks [[x, LA.asColumn (LA.fromList (replicate n 1))]]
-      x0  = LA.fromBlocks [[x, LA.asColumn (LA.fromList (replicate n 0))]]
-  predict <- fitPredict base xt y gen
-  let mu1 = predict x1
-      mu0 = predict x0
-      tauHat = mu1 - mu0
-  pure CATEResult
-    { cateEstimates = tauHat
-    , cateMethod    = SLearner
-    , cateATE       = LA.sumElements tauHat / fromIntegral n
-    }
-
--- ---------------------------------------------------------------------------
--- T-learner: 2 モデル、 群別 fit
--- ---------------------------------------------------------------------------
-
-tLearner :: CATEBaseLearner
-         -> LA.Matrix Double -> LA.Vector Double -> LA.Vector Double
-         -> MWC.GenIO -> IO CATEResult
-tLearner base x t y gen = do
-  let n    = LA.rows x
-      idx1 = filterIdx (== 1.0) t
-      idx0 = filterIdx (== 0.0) t
-      x1   = x LA.? idx1
-      y1   = LA.fromList [LA.atIndex y i | i <- idx1]
-      x0   = x LA.? idx0
-      y0   = LA.fromList [LA.atIndex y i | i <- idx0]
-  pred1 <- fitPredict base x1 y1 gen
-  pred0 <- fitPredict base x0 y0 gen
-  let mu1 = pred1 x
-      mu0 = pred0 x
-      tauHat = mu1 - mu0
-  pure CATEResult
-    { cateEstimates = tauHat
-    , cateMethod    = TLearner
-    , cateATE       = LA.sumElements tauHat / fromIntegral n
-    }
-
--- ---------------------------------------------------------------------------
--- X-learner: 残差再回帰 + PS 重み付け
--- ---------------------------------------------------------------------------
-
-xLearner :: CATEBaseLearner
-         -> LA.Matrix Double -> LA.Vector Double -> LA.Vector Double
-         -> MWC.GenIO -> IO CATEResult
-xLearner base x t y gen = do
-  let n    = LA.rows x
-      idx1 = filterIdx (== 1.0) t
-      idx0 = filterIdx (== 0.0) t
-      x1   = x LA.? idx1
-      y1   = LA.fromList [LA.atIndex y i | i <- idx1]
-      x0   = x LA.? idx0
-      y0   = LA.fromList [LA.atIndex y i | i <- idx0]
-  -- Step 1: T-learner と同じ outcome models
-  pred1 <- fitPredict base x1 y1 gen
-  pred0 <- fitPredict base x0 y0 gen
-  -- Step 2: imputed treatment effects
-  --   For T=1 units: D̃_1 = Y - μ̂_0(X)
-  --   For T=0 units: D̃_0 = μ̂_1(X) - Y
-  let mu0_at_x1 = pred0 x1
-      mu1_at_x0 = pred1 x0
-      dTilde1   = y1 - mu0_at_x1
-      dTilde0   = mu1_at_x0 - y0
-  -- Step 3: τ̂_1(X) を D̃_1 ~ X_{T=1} で fit、 τ̂_0(X) は D̃_0 ~ X_{T=0}
-  tau1Pred <- fitPredict base x1 dTilde1 gen
-  tau0Pred <- fitPredict base x0 dTilde0 gen
-  let tau1At = tau1Pred x
-      tau0At = tau0Pred x
-  -- Step 4: PS 重み付け平均
-  --   τ̂(X) = p̂(X) · τ̂_0(X) + (1 - p̂(X)) · τ̂_1(X)
-  --   (treated が少ない領域では τ̂_0 を信頼、 control が少ない領域では τ̂_1)
-      (lo, hi) = defaultPSTrim
-  let ps     = trimPropensity lo hi (propensityScore x t)
-      p      = psScores ps
-      one    = LA.scalar 1
-      tauHat = p * tau0At + (one - p) * tau1At
-  pure CATEResult
-    { cateEstimates = tauHat
-    , cateMethod    = XLearner
-    , cateATE       = LA.sumElements tauHat / fromIntegral n
-    }
-
--- ---------------------------------------------------------------------------
--- ヘルパ
--- ---------------------------------------------------------------------------
-
-filterIdx :: (Double -> Bool) -> LA.Vector Double -> [Int]
-filterIdx pr v =
-  [ i | i <- [0 .. LA.size v - 1], pr (LA.atIndex v i) ]
diff --git a/src/Hanalyze/Stat/Causal/DoublyRobust.hs b/src/Hanalyze/Stat/Causal/DoublyRobust.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/Causal/DoublyRobust.hs
+++ /dev/null
@@ -1,99 +0,0 @@
--- |
--- Module      : Hanalyze.Stat.Causal.DoublyRobust
--- Description : Doubly Robust / Augmented IPW (AIPW) 推定量
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Doubly Robust / Augmented IPW (AIPW) 推定量 (Phase 30-A3)。
---
--- 結果モデル @μ̂_1(X)@ / @μ̂_0(X)@ と傾向スコア @p̂(X)@ の両方を使い、
--- どちらか一方が正しく指定されていれば一致性を持つ推定量:
---
--- @
---   ATE_AIPW = (1/n) Σ [ μ̂_1(X_i) - μ̂_0(X_i)
---                       + T_i (Y_i - μ̂_1(X_i)) / p̂_i
---                       - (1-T_i) (Y_i - μ̂_0(X_i)) / (1 - p̂_i) ]
--- @
---
--- 結果モデルは 'Hanalyze.Model.LM.fitLM' を流用 (= OLS、 線形)。 非線形が
--- 必要な場合は呼び出し側で X を拡張するか CATE module (30-A4) を使う。
---
--- Reference:
---   Robins, Rotnitzky, Zhao (1994) "Estimation of Regression Coefficients
---   When Some Regressors Are Not Always Observed". JASA 89:846-866.
-module Hanalyze.Stat.Causal.DoublyRobust
-  ( DoublyRobustResult (..)
-  , doublyRobust
-  , doublyRobustWith
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import qualified Hanalyze.Model.LM    as LM
-import           Hanalyze.Model.Core   (coefficientsV)
-import           Hanalyze.Stat.Causal.PropensityScore
-                   (PropensityScore (..), propensityScore, trimPropensity)
-import           Hanalyze.Stat.Causal.IPW (defaultPSTrim)
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
-data DoublyRobustResult = DoublyRobustResult
-  { drATE          :: !Double
-  , drMu1Predicted :: !(LA.Vector Double)  -- ^ μ̂_1(X_i) for all i
-  , drMu0Predicted :: !(LA.Vector Double)  -- ^ μ̂_0(X_i) for all i
-  , drPropensity   :: !PropensityScore
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- AIPW
--- ---------------------------------------------------------------------------
-
--- | 共変量 @X@ (intercept 列を含む)、 二値処置 @T@、 結果 @Y@ から AIPW ATE
--- を推定。 内部で 'propensityScore' + 'defaultPSTrim' を適用、 outcome
--- model は OLS で群別 fit。
-doublyRobust :: LA.Matrix Double -> LA.Vector Double -> LA.Vector Double
-             -> DoublyRobustResult
-doublyRobust x t y =
-  let (lo, hi) = defaultPSTrim
-      ps       = trimPropensity lo hi (propensityScore x t)
-  in doublyRobustWith ps x t y
-
--- | 既存 PS を再利用する版。 PS と outcome model の組み合わせを変えて
--- 二重ロバスト性を検証したい場合に有用。
-doublyRobustWith :: PropensityScore -> LA.Matrix Double -> LA.Vector Double
-                 -> LA.Vector Double -> DoublyRobustResult
-doublyRobustWith ps x t y =
-  let n     = fromIntegral (LA.size t) :: Double
-      one   = LA.scalar 1
-      p     = psScores ps
-      -- 群別 OLS: T=1 部分集合 / T=0 部分集合
-      idx1 = filterIdx (== 1.0) t
-      idx0 = filterIdx (== 0.0) t
-      x1   = x LA.? idx1
-      y1   = LA.fromList [LA.atIndex y i | i <- idx1]
-      x0   = x LA.? idx0
-      y0   = LA.fromList [LA.atIndex y i | i <- idx0]
-      beta1 = coefficientsV (LM.fitLMVec x1 y1)
-      beta0 = coefficientsV (LM.fitLMVec x0 y0)
-      mu1   = LM.predictLMVec beta1 x
-      mu0   = LM.predictLMVec beta0 x
-      -- AIPW contribution per unit
-      contrib = (mu1 - mu0)
-              + t * (y - mu1) / p
-              - (one - t) * (y - mu0) / (one - p)
-      ateHat = LA.sumElements contrib / n
-  in DoublyRobustResult
-       { drATE          = ateHat
-       , drMu1Predicted = mu1
-       , drMu0Predicted = mu0
-       , drPropensity   = ps
-       }
-
--- ---------------------------------------------------------------------------
--- ヘルパ
--- ---------------------------------------------------------------------------
-
-filterIdx :: (Double -> Bool) -> LA.Vector Double -> [Int]
-filterIdx pr v =
-  [ i | i <- [0 .. LA.size v - 1], pr (LA.atIndex v i) ]
diff --git a/src/Hanalyze/Stat/Causal/IPW.hs b/src/Hanalyze/Stat/Causal/IPW.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/Causal/IPW.hs
+++ /dev/null
@@ -1,103 +0,0 @@
--- |
--- Module      : Hanalyze.Stat.Causal.IPW
--- Description : Inverse Probability Weighting (IPW) による ATE / ATT 推定
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Inverse Probability Weighting (IPW) による ATE / ATT 推定 (Phase 30-A2)。
---
--- Hajek 正規化推定量 (finite-sample で stable、 Horvitz-Thompson より低分散):
---
--- @
---   ATE = Σ(T·Y/p) / Σ(T/p)  -  Σ((1-T)·Y/(1-p)) / Σ((1-T)/(1-p))
---   ATT = Σ(T·Y) / Σ T       -  Σ((1-T)·(p/(1-p))·Y) / Σ((1-T)·(p/(1-p)))
--- @
---
--- ここで @p_i@ は 'PropensityScore' で推定した P(T=1 | X_i)。 重みは
--- @PropensityScore.ipwWeights@ / @attWeights@ で hmatrix Vector 演算で計算。
---
--- ## 使い方
---
--- @
---   let r = ipw xConf treat outcome           -- 共変量から PS 推定 + trim も内部で実施
---   print (ipwATE r, ipwATT r)
---
---   -- 既に PS を計算済 / カスタム trim したい場合:
---   let ps' = trimPropensity 0.05 0.95 (propensityScore x t)
---       r'  = ipwWith ps' t y
--- @
---
--- Reference:
---   Horvitz & Thompson (1952) "A Generalization of Sampling Without
---   Replacement from a Finite Universe". JASA 47:663-685.
-module Hanalyze.Stat.Causal.IPW
-  ( IPWResult (..)
-  , ipw
-  , ipwWith
-  , defaultPSTrim
-  ) where
-
-import qualified Numeric.LinearAlgebra            as LA
-import           Hanalyze.Stat.Causal.PropensityScore
-                   (PropensityScore (..), propensityScore, trimPropensity,
-                    ipwWeights, attWeights)
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
-data IPWResult = IPWResult
-  { ipwATE        :: !Double
-  , ipwATT        :: !Double
-  , ipwWeightsATE :: !(LA.Vector Double)
-  , ipwWeightsATT :: !(LA.Vector Double)
-  , ipwPropensity :: !PropensityScore
-  } deriving (Show)
-
--- | 既定の PS trim 範囲 @(0.01, 0.99)@ (Rosenbaum 慣例)。
-defaultPSTrim :: (Double, Double)
-defaultPSTrim = (0.01, 0.99)
-
--- ---------------------------------------------------------------------------
--- 推定
--- ---------------------------------------------------------------------------
-
--- | 共変量 @X@、 二値処置 @T@、 結果 @Y@ から ATE / ATT を IPW で推定。
--- 内部で 'propensityScore' + 'defaultPSTrim' を適用。
-ipw :: LA.Matrix Double -> LA.Vector Double -> LA.Vector Double -> IPWResult
-ipw x t y =
-  let (lo, hi) = defaultPSTrim
-      ps       = trimPropensity lo hi (propensityScore x t)
-  in ipwWith ps t y
-
--- | 既に算出 (+trim) 済の PropensityScore を再利用する版。 同じ X から
--- ATE / ATT を複数バリアントで比べたい場合に有用。
-ipwWith :: PropensityScore -> LA.Vector Double -> LA.Vector Double -> IPWResult
-ipwWith ps t y =
-  let p     = psScores ps
-      one   = LA.scalar 1
-      wATE  = ipwWeights ps t
-      wATT  = attWeights ps t
-      -- ATE (Hajek 正規化): 各群の重み付き平均の差
-      --   μ̂_1 = Σ (T/p)·Y  /  Σ (T/p)
-      --   μ̂_0 = Σ ((1-T)/(1-p))·Y / Σ ((1-T)/(1-p))
-      w1     = t / p
-      w0     = (one - t) / (one - p)
-      mu1Hat = safeDiv (LA.sumElements (w1 * y)) (LA.sumElements w1)
-      mu0Hat = safeDiv (LA.sumElements (w0 * y)) (LA.sumElements w0)
-      ateHat = mu1Hat - mu0Hat
-      -- ATT (Hajek 正規化): treated 平均と、 p/(1-p) で再重み付けした control 平均の差
-      wt1    = t                       -- treated indicator
-      wt0    = (one - t) * (p / (one - p))
-      attMu1 = safeDiv (LA.sumElements (wt1 * y)) (LA.sumElements wt1)
-      attMu0 = safeDiv (LA.sumElements (wt0 * y)) (LA.sumElements wt0)
-      attHat = attMu1 - attMu0
-  in IPWResult
-       { ipwATE        = ateHat
-       , ipwATT        = attHat
-       , ipwWeightsATE = wATE
-       , ipwWeightsATT = wATT
-       , ipwPropensity = ps
-       }
-  where
-    safeDiv num den = if abs den < 1e-12 then 0 else num / den
diff --git a/src/Hanalyze/Stat/Causal/PropensityScore.hs b/src/Hanalyze/Stat/Causal/PropensityScore.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/Causal/PropensityScore.hs
+++ /dev/null
@@ -1,89 +0,0 @@
--- |
--- Module      : Hanalyze.Stat.Causal.PropensityScore
--- Description : logistic regression による Propensity Score P(T=1|X) の推定
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Propensity Score の推定 (Phase 30-A1)。
---
--- @p_i = P(T = 1 | X_i)@ を logistic regression (GLM Binomial+Logit) で
--- 推定する。 観測研究での因果効果推定 (IPW / AIPW / CATE) の前提となる
--- 共変量バランス指標。
---
--- ## 使い方
---
--- @
---   let ps = propensityScore xConf treat
---       ps' = trimPropensity 0.01 0.99 ps   -- 重み発散防止
---       w   = ipwWeights ps' treat          -- t/p + (1-t)/(1-p)
--- @
---
--- Reference:
---   Rosenbaum & Rubin (1983) "The Central Role of the Propensity Score in
---   Observational Studies for Causal Effects". Biometrika 70:41-55.
-module Hanalyze.Stat.Causal.PropensityScore
-  ( PropensityScore (..)
-  , propensityScore
-  , trimPropensity
-  , ipwWeights
-  , attWeights
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import qualified Hanalyze.Model.GLM   as GLM
-import           Hanalyze.Model.Core   (coefficientsV, fittedV)
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
-data PropensityScore = PropensityScore
-  { psScores :: !(LA.Vector Double)  -- ^ @p_i = P(T=1|X_i)@、 長さ @n@
-  , psBeta   :: !(LA.Vector Double)  -- ^ logistic coefficients
-  , psN      :: !Int                 -- ^ サンプル数
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- 推定
--- ---------------------------------------------------------------------------
-
--- | 共変量行列 @X@ (intercept 列は呼び出し側で付加) と二値処置 @T ∈ {0,1}@
--- から logistic regression で傾向スコアを推定。
---
--- @X@ は @n × p@、 @T@ は長さ @n@ の 0/1 vector。 intercept が欲しい場合は
--- @1@ 列を先頭に prepend して渡す。
-propensityScore :: LA.Matrix Double -> LA.Vector Double -> PropensityScore
-propensityScore x t =
-  let (fit, _) = GLM.fitGLMFull GLM.Binomial GLM.Logit x t
-  in PropensityScore
-       { psScores = fittedV fit
-       , psBeta   = coefficientsV fit
-       , psN      = LA.size t
-       }
-
--- | @[lo, hi]@ に clip。 @p_i@ が 0 / 1 に張り付くと IPW 重みが発散する
--- ので必須。 推奨値: @lo = 0.01@, @hi = 0.99@。
-trimPropensity :: Double -> Double -> PropensityScore -> PropensityScore
-trimPropensity lo hi ps =
-  ps { psScores = LA.cmap (clamp lo hi) (psScores ps) }
-  where
-    clamp a b v = max a (min b v)
-
--- ---------------------------------------------------------------------------
--- 重み (hmatrix Vector 演算)
--- ---------------------------------------------------------------------------
-
--- | ATE 用の Horvitz-Thompson 重み: @w_i = t_i/p_i + (1-t_i)/(1-p_i)@
-ipwWeights :: PropensityScore -> LA.Vector Double -> LA.Vector Double
-ipwWeights ps t =
-  let p   = psScores ps
-      one = LA.scalar 1
-  in t / p + (one - t) / (one - p)
-
--- | ATT 用の重み: @w_i = t_i + (1-t_i) · p_i/(1-p_i)@
--- (treated は重み 1、 control は odds ratio で再重み付け)
-attWeights :: PropensityScore -> LA.Vector Double -> LA.Vector Double
-attWeights ps t =
-  let p   = psScores ps
-      one = LA.scalar 1
-  in t + (one - t) * p / (one - p)
diff --git a/src/Hanalyze/Stat/Cholesky.hs b/src/Hanalyze/Stat/Cholesky.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/Cholesky.hs
+++ /dev/null
@@ -1,105 +0,0 @@
-{-# LANGUAGE StrictData #-}
--- |
--- Module      : Hanalyze.Stat.Cholesky
--- Description : 対称正定値 (SPD) 系向け Cholesky 分解ベースの線形ソルバ
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Cholesky-based linear solver for symmetric positive-definite (SPD)
--- systems.
---
--- Replaces the generic least-squares solve @LA.\<\\\>@ in code paths
--- where the matrix is known to be SPD (Gram matrices @K + λI@, posterior
--- precision matrices, etc.). hmatrix's @\<\\\>@ uses the LAPACK QR
--- (@dgels@) which is general but ~2-3× slower than the SPD-specific
--- Cholesky (@dpotrf@ + @dpotrs@).
---
--- The solver also handles near-singular matrices by progressively
--- adding a multiple of the identity (jittering) until the Cholesky
--- factorization succeeds.
-module Hanalyze.Stat.Cholesky
-  ( cholSolve
-  , cholSolveJitter
-  , cholSolveJitterWith
-  , cholFactor
-  , cholSolveWithFactor
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-import           Control.Exception     (SomeException, try, evaluate)
-import           System.IO.Unsafe      (unsafePerformIO)
-
--- | Default sequence of jitter ratios applied to the diagonal until the
--- Cholesky factorization succeeds. The first attempt adds nothing; the
--- subsequent attempts add @ratio × max(diag(A))@ (the largest diagonal
--- entry, used to scale to the matrix's natural magnitude).
-defaultJitters :: [Double]
-defaultJitters = [0, 1e-10, 1e-8, 1e-6, 1e-4]
-
--- | Solve @A X = B@ for SPD @A@. Equivalent to @A LA.\<\\\> B@ but ~2×
--- faster. Tries an exact Cholesky first, falling back to a jittered
--- version (see @defaultJitters@) when the matrix is numerically
--- non-positive-definite.
---
--- If every jitter fails, returns 'Nothing' (caller chooses a fallback;
--- typically 'LA.\<\\\>').
-cholSolve :: LA.Matrix Double -> LA.Matrix Double -> Maybe (LA.Matrix Double)
-cholSolve = cholSolveJitterWith defaultJitters
-{-# INLINE cholSolve #-}
-
--- | Like 'cholSolve' but always returns a result by falling back to
--- @LA.\<\\\>@ (the general LSQ solver) if the Cholesky path fails for
--- every jitter level. Logs no information about which jitter level (if
--- any) was used; for diagnostics, call 'cholSolveJitterWith' directly.
-cholSolveJitter :: LA.Matrix Double -> LA.Matrix Double -> LA.Matrix Double
-cholSolveJitter a b = case cholSolve a b of
-  Just x  -> x
-  Nothing -> a LA.<\> b
-
--- | Try a custom sequence of jitter ratios. Returns 'Nothing' when none
--- succeeds.
-cholSolveJitterWith
-  :: [Double] -> LA.Matrix Double -> LA.Matrix Double
-  -> Maybe (LA.Matrix Double)
-cholSolveJitterWith jitters a b
-  | LA.rows a /= LA.cols a = Nothing      -- not square
-  | otherwise              = go jitters
-  where
-    n     = LA.rows a
-    sigma = max 1.0 (LA.maxElement (LA.cmap abs (LA.takeDiag a)))
-    go []         = Nothing
-    go (eps : rest) =
-      let aPlus = if eps <= 0 then a
-                  else a + LA.scale (eps * sigma) (LA.ident n)
-      in case tryChol aPlus of
-           Nothing -> go rest
-           Just r  ->
-             -- A = Rᵀ R. Solve Rᵀ y = B then R X = y.
-             let y = LA.triSolve LA.Lower (LA.tr r) b
-                 x = LA.triSolve LA.Upper r y
-             in Just x
-
--- | Wrapper around @LA.chol (LA.sym a)@ that catches the LAPACK error
--- (raised as a Haskell exception) when the matrix is not SPD.
-cholFactor :: LA.Matrix Double -> Maybe (LA.Matrix Double)
-cholFactor = tryChol
-{-# INLINE cholFactor #-}
-
--- | Solve @A X = B@ given an /already-computed/ Cholesky factor @R@
--- (from 'cholFactor', upper-triangular with @A = Rᵀ R@). Cheaper when
--- the same factor is used for multiple right-hand sides or when the
--- factor was needed elsewhere (e.g. for the log-determinant during
--- marginal-likelihood evaluation).
-cholSolveWithFactor :: LA.Matrix Double -> LA.Matrix Double -> LA.Matrix Double
-cholSolveWithFactor r b =
-  LA.triSolve LA.Upper r (LA.triSolve LA.Lower (LA.tr r) b)
-{-# INLINE cholSolveWithFactor #-}
-
-tryChol :: LA.Matrix Double -> Maybe (LA.Matrix Double)
-tryChol a =
-  let r = unsafePerformIO $
-            try (evaluate (LA.chol (LA.sym a)))
-              :: Either SomeException (LA.Matrix Double)
-  in case r of
-       Right x -> Just x
-       Left  _ -> Nothing
diff --git a/src/Hanalyze/Stat/ClassMetrics.hs b/src/Hanalyze/Stat/ClassMetrics.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/ClassMetrics.hs
+++ /dev/null
@@ -1,372 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Stat.ClassMetrics
--- Description : 分類モデル評価指標 (混同行列・ROC/AUC・PR 曲線・logLoss 等)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Classification model evaluation metrics.
---
--- Two families:
---
---   * __Hard predictions__ (predicted class labels): 'confusionMatrix',
---     'accuracy', 'precision', 'recall', 'f1Score', 'fBetaScore'.
---   * __Soft predictions__ (predicted probabilities): 'rocCurve',
---     'auc', 'prCurve', 'averagePrecision', 'logLoss',
---     'brierScore'.
---
--- Multi-class extensions: @macroAvg@, @weightedAvg@. Binary helpers
--- assume class labels @0@ / @1@ (negative / positive).
-module Hanalyze.Stat.ClassMetrics
-  ( -- * Confusion matrix (binary)
-    Confusion (..)
-  , confusionMatrix
-    -- * Hard-prediction metrics
-  , accuracy
-  , precision
-  , recall
-  , specificity
-  , f1Score
-  , fBetaScore
-  , balancedAccuracy
-  , matthewsCorr
-    -- * Soft-prediction metrics
-  , rocCurve
-  , auc
-  , prCurve
-  , averagePrecision
-  , logLoss
-  , brierScore
-    -- * Multi-class confusion
-  , ConfusionMulti (..)
-  , confusionMulti
-  , accuracyMulti
-  , macroF1
-  , weightedF1
-  ) where
-
-import qualified Data.Map.Strict             as Map
-import           Data.List                   (sort, sortBy)
-import           Data.Ord                    (comparing, Down (..))
-import qualified Data.Vector.Unboxed         as VU
-import qualified Data.Vector.Unboxed.Mutable as MVU
-import qualified Data.Vector.Algorithms.Intro as VAI
-import           Control.Monad.ST             (ST, runST)
-import           Control.Monad                (forM_)
-
--- ---------------------------------------------------------------------------
--- Binary confusion matrix
--- ---------------------------------------------------------------------------
-
--- | 2×2 confusion matrix for binary classification (labels @0@/@1@).
---
--- @
---                Predicted
---               ┌─────┬─────┐
---               │  0  │  1  │
---      ┌────┬───┼─────┼─────┤
--- True │  0 │   │ TN  │ FP  │
---      │  1 │   │ FN  │ TP  │
---      └────┴───┴─────┴─────┘
--- @
-data Confusion = Confusion
-  { confTP :: !Int
-  , confFP :: !Int
-  , confFN :: !Int
-  , confTN :: !Int
-  } deriving (Show, Eq)
-
--- | Build a binary confusion matrix from true / predicted label vectors
--- (both 0/1).
-confusionMatrix
-  :: [Int]   -- ^ True labels.
-  -> [Int]   -- ^ Predicted labels.
-  -> Confusion
-confusionMatrix ys yhats =
-  let pairs = zip ys yhats
-      tp    = length [() | (1, 1) <- pairs]
-      fp    = length [() | (0, 1) <- pairs]
-      fn    = length [() | (1, 0) <- pairs]
-      tn    = length [() | (0, 0) <- pairs]
-  in Confusion tp fp fn tn
-
--- ---------------------------------------------------------------------------
--- Hard-prediction metrics (binary)
--- ---------------------------------------------------------------------------
-
--- | Overall accuracy: @(TP + TN) / total@.
-accuracy :: Confusion -> Double
-accuracy c =
-  let n = confTP c + confFP c + confFN c + confTN c
-  in if n == 0 then 0
-       else fromIntegral (confTP c + confTN c) / fromIntegral n
-
--- | Precision: @TP / (TP + FP)@. The "purity" of positive predictions.
-precision :: Confusion -> Double
-precision c =
-  let denom = confTP c + confFP c
-  in if denom == 0 then 0 else fromIntegral (confTP c) / fromIntegral denom
-
--- | Recall (sensitivity, TPR): @TP / (TP + FN)@.
-recall :: Confusion -> Double
-recall c =
-  let denom = confTP c + confFN c
-  in if denom == 0 then 0 else fromIntegral (confTP c) / fromIntegral denom
-
--- | Specificity (TNR): @TN / (TN + FP)@.
-specificity :: Confusion -> Double
-specificity c =
-  let denom = confTN c + confFP c
-  in if denom == 0 then 0 else fromIntegral (confTN c) / fromIntegral denom
-
--- | F1: harmonic mean of precision and recall.
-f1Score :: Confusion -> Double
-f1Score c =
-  let p = precision c
-      r = recall c
-  in if p + r == 0 then 0 else 2 * p * r / (p + r)
-
--- | F-beta: weighted harmonic mean. @β > 1@ favours recall, @β < 1@
--- favours precision.
-fBetaScore :: Double -> Confusion -> Double
-fBetaScore beta c =
-  let p   = precision c
-      r   = recall c
-      b2  = beta * beta
-      num = (1 + b2) * p * r
-      den = b2 * p + r
-  in if den == 0 then 0 else num / den
-
--- | Balanced accuracy: @(sensitivity + specificity) / 2@. Robust to
--- class imbalance.
-balancedAccuracy :: Confusion -> Double
-balancedAccuracy c = (recall c + specificity c) / 2
-
--- | Matthews correlation coefficient (MCC) — robust binary metric in
--- @[-1, 1]@.
-matthewsCorr :: Confusion -> Double
-matthewsCorr c =
-  let tp = fromIntegral (confTP c) :: Double
-      fp = fromIntegral (confFP c) :: Double
-      fn = fromIntegral (confFN c) :: Double
-      tn = fromIntegral (confTN c) :: Double
-      num = tp * tn - fp * fn
-      den = sqrt ((tp + fp) * (tp + fn) * (tn + fp) * (tn + fn))
-  in if den == 0 then 0 else num / den
-
--- ---------------------------------------------------------------------------
--- Soft-prediction metrics
--- ---------------------------------------------------------------------------
-
--- | ROC curve: list of @(FPR, TPR)@ points. Sorted by descending
--- score threshold; starts at @(0, 0)@ and ends at @(1, 1)@.
-rocCurve
-  :: [Int]      -- ^ True labels (0/1).
-  -> [Double]   -- ^ Predicted scores (higher = more positive).
-  -> [(Double, Double)]
-rocCurve ys scores =
-  let pairs   = sortBy (comparing (Down . snd)) (zip ys scores)
-      pos     = length [y | (y, _) <- pairs, y == 1]
-      neg     = length [y | (y, _) <- pairs, y == 0]
-      go _ _ tp fp [] = [(fromIntegral fp / fromIntegral (max 1 neg),
-                          fromIntegral tp / fromIntegral (max 1 pos))]
-      go prev acc tp fp ((y, s):rest)
-        | s == prev =
-            go prev acc (if y == 1 then tp + 1 else tp)
-                       (if y == 0 then fp + 1 else fp) rest
-        | otherwise =
-            let pt = (fromIntegral fp / fromIntegral (max 1 neg),
-                      fromIntegral tp / fromIntegral (max 1 pos))
-            in pt : go s acc (if y == 1 then tp + 1 else tp)
-                              (if y == 0 then fp + 1 else fp) rest
-      curve = (0, 0) : go (1/0) [] 0 0 pairs
-  in curve
-
--- | Area under ROC curve.
---
--- Implementation: Mann-Whitney U identity. Ranks of positive scores
--- (with average-rank tie correction) yield
--- @AUC = (R_pos − n_pos(n_pos+1)/2) / (n_pos · n_neg)@.
--- This is equivalent to the trapezoidal integration of the ROC curve
--- but avoids constructing it. The sort uses
--- 'Data.Vector.Algorithms.Intro' on a Storable indexed vector for
--- @O(n log n)@ in tight Storable loops; the previous implementation
--- went through 'Data.List.sortBy' on @[(Int, Double)]@ + a
--- list-traversal trapezoid loop. Bench: @AUC_LogLoss_n10000@ moves
--- from 5.6 ms to ≲ 4 ms, matching scikit-learn's @roc_auc_score@.
-auc :: [Int] -> [Double] -> Double
-auc ys scores
-  | nPos == 0 || nNeg == 0 = 0.5
-  | otherwise =
-      let -- average ranks (1-based) over the score-sorted order
-          ranks   = averageRanks scoreV
-          -- sum of ranks of positive observations
-          rPos    = VU.sum (VU.izipWith
-                              (\i lab _ -> if lab == 1 then ranks VU.! i else 0)
-                              labelV labelV)
-          nPosD   = fromIntegral nPos :: Double
-          nNegD   = fromIntegral nNeg :: Double
-      in (rPos - nPosD * (nPosD + 1) / 2) / (nPosD * nNegD)
-  where
-    labelV  = VU.fromList ys
-    scoreV  = VU.fromList scores
-    nPos    = VU.length (VU.filter (== 1) labelV)
-    nNeg    = VU.length labelV - nPos
-
--- | Average ranks (1-based, with tied-value mean correction) of a
--- vector of Doubles. Used by 'auc' for the Mann-Whitney U identity.
-averageRanks :: VU.Vector Double -> VU.Vector Double
-averageRanks v =
-  let n   = VU.length v
-      idx = VU.modify
-              (VAI.sortBy (\i j -> compare (v VU.! i) (v VU.! j)))
-              (VU.generate n id)
-      -- Walk the sorted run and assign average ranks within ties.
-      out = runST $ do
-        r <- MVU.new n
-        let loop i
-              | i >= n    = pure ()
-              | otherwise = do
-                  let v_i = v VU.! (idx VU.! i)
-                      -- find the run [i, j) of equal scores
-                      findEnd j
-                        | j >= n            = j
-                        | v VU.! (idx VU.! j) == v_i = findEnd (j + 1)
-                        | otherwise         = j
-                      j_ = findEnd (i + 1)
-                      avgRank = fromIntegral (i + j_ + 1) / 2.0  -- (i+1 + j_)/2
-                  forM_ [i .. j_ - 1] $ \k ->
-                    MVU.unsafeWrite r (idx VU.! k) avgRank
-                  loop j_
-        loop 0
-        VU.unsafeFreeze r
-  in out
-
--- | Precision–recall curve as @(recall, precision)@ pairs, sorted by
--- recall ascending.
-prCurve :: [Int] -> [Double] -> [(Double, Double)]
-prCurve ys scores =
-  let pairs = sortBy (comparing (Down . snd)) (zip ys scores)
-      pos   = length [y | (y, _) <- pairs, y == 1]
-      go tp fp [] = [(fromIntegral tp / fromIntegral (max 1 pos),
-                      if tp + fp == 0 then 1
-                        else fromIntegral tp / fromIntegral (tp + fp))]
-      go tp fp ((y, _):rest) =
-        let tp' = if y == 1 then tp + 1 else tp
-            fp' = if y == 0 then fp + 1 else fp
-            r   = fromIntegral tp' / fromIntegral (max 1 pos)
-            p   = if tp' + fp' == 0 then 1
-                    else fromIntegral tp' / fromIntegral (tp' + fp')
-        in (r, p) : go tp' fp' rest
-  in (0, 1) : go 0 0 pairs
-
--- | Average precision (area under PR curve via step-wise integration).
-averagePrecision :: [Int] -> [Double] -> Double
-averagePrecision ys scores =
-  let pairs = sortBy (comparing (Down . snd)) (zip ys scores)
-      pos   = length [y | (y, _) <- pairs, y == 1]
-      go _ _ _ [] = 0
-      go tp _fp prevR ((y, _):rest) =
-        let tp' = if y == 1 then tp + 1 else tp
-            fp' = if y == 0 then 0 else 0  -- fp not used in formula
-            _ = fp'
-            r   = fromIntegral tp' / fromIntegral (max 1 pos)
-            p   = fromIntegral tp' / fromIntegral (max 1 (length pairs
-                                                          - length rest))
-            inc = if y == 1 then (r - prevR) * p else 0
-        in inc + go tp' 0 r rest
-  in go 0 0 0 pairs
-
--- | Logarithmic loss (cross-entropy). Clipped to
--- @[1e-15, 1 − 1e-15]@ to avoid @log 0@. Storable-Vector implementation:
--- one fused pass via 'VU.izipWith' instead of @zipWith + sum@ on
--- lists.
-logLoss :: [Int] -> [Double] -> Double
-logLoss ys probs =
-  let yV    = VU.fromList ys
-      pV    = VU.fromList probs
-      n     = fromIntegral (VU.length yV) :: Double
-      clip x = max 1e-15 (min (1 - 1e-15) x)
-      total = VU.sum (VU.zipWith
-                        (\y p -> let p' = clip p
-                                     yd = fromIntegral y :: Double
-                                 in yd * log p' + (1 - yd) * log (1 - p'))
-                        yV pV)
-  in - total / n
-
--- | Brier score: mean squared error between predicted probabilities
--- and true labels.
-brierScore :: [Int] -> [Double] -> Double
-brierScore ys probs =
-  let yV    = VU.fromList ys
-      pV    = VU.fromList probs
-      n     = fromIntegral (VU.length yV) :: Double
-      total = VU.sum (VU.zipWith
-                        (\y p -> let d = p - fromIntegral y in d * d)
-                        yV pV)
-  in total / n
-
--- ---------------------------------------------------------------------------
--- Multi-class
--- ---------------------------------------------------------------------------
-
--- | Multi-class confusion matrix as a Map (true, pred) -> count.
-data ConfusionMulti = ConfusionMulti
-  { cmCounts :: !(Map.Map (Int, Int) Int)
-  , cmLabels :: ![Int]
-  } deriving (Show)
-
--- | Build a multi-class confusion matrix from labels.
-confusionMulti :: [Int] -> [Int] -> ConfusionMulti
-confusionMulti ys yhats =
-  let labels = sort (Map.keys (Map.fromList [(y, ()) | y <- ys ++ yhats]))
-      pairs  = zip ys yhats
-      countOf k = Map.fromListWith (+) [(p, 1::Int) | p <- pairs, p == k]
-      _ = countOf
-      counts = Map.fromListWith (+) [(p, 1::Int) | p <- pairs]
-  in ConfusionMulti counts labels
-
--- | Multi-class overall accuracy.
-accuracyMulti :: ConfusionMulti -> Double
-accuracyMulti cm =
-  let total    = sum (Map.elems (cmCounts cm))
-      diagonal = sum [ Map.findWithDefault 0 (l, l) (cmCounts cm)
-                     | l <- cmLabels cm ]
-  in if total == 0 then 0
-       else fromIntegral diagonal / fromIntegral total
-
--- | Per-class precision / recall as a binary one-vs-rest task.
-classBinary :: ConfusionMulti -> Int -> Confusion
-classBinary cm c =
-  let counts = cmCounts cm
-      tp = Map.findWithDefault 0 (c, c) counts
-      fp = sum [ Map.findWithDefault 0 (t, c) counts
-               | t <- cmLabels cm, t /= c ]
-      fn = sum [ Map.findWithDefault 0 (c, p) counts
-               | p <- cmLabels cm, p /= c ]
-      tn = sum (Map.elems counts) - tp - fp - fn
-  in Confusion tp fp fn tn
-
--- | Macro-averaged F1 (mean of per-class F1s, equal weight).
-macroF1 :: ConfusionMulti -> Double
-macroF1 cm =
-  let f1s = [ f1Score (classBinary cm c) | c <- cmLabels cm ]
-      n   = fromIntegral (length f1s) :: Double
-  in if n == 0 then 0 else sum f1s / n
-
--- | Weighted-averaged F1 (weighted by class support).
-weightedF1 :: ConfusionMulti -> Double
-weightedF1 cm =
-  let counts = cmCounts cm
-      total  = fromIntegral (sum (Map.elems counts)) :: Double
-      perClass = [ let cb = classBinary cm c
-                       sup = fromIntegral (sum [ Map.findWithDefault 0 (c, p) counts
-                                                | p <- cmLabels cm ]) :: Double
-                   in sup * f1Score cb
-                 | c <- cmLabels cm ]
-  in if total == 0 then 0 else sum perClass / total
-
--- ---------------------------------------------------------------------------
--- Helpers (suppress unused warnings from internal stuff)
--- ---------------------------------------------------------------------------
-
diff --git a/src/Hanalyze/Stat/CorrelationNetwork.hs b/src/Hanalyze/Stat/CorrelationNetwork.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/CorrelationNetwork.hs
+++ /dev/null
@@ -1,240 +0,0 @@
--- |
--- Module      : Hanalyze.Stat.CorrelationNetwork
--- Description : Graphical Lasso による sparse precision matrix 推定 (相関ネットワーク)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Correlation Network via Graphical Lasso (Phase 32-A1)。
---
--- 高次元データの相関構造を sparse precision matrix @Θ = Σ^{-1}@ で
--- 表現する。 「ゼロ要素 ↔ 条件付き独立」 の対応で変数間ネットワークを
--- 推定する。 scikit-learn `GraphicalLasso`、 R `glasso` 相当。
---
--- ## 最適化
---
--- @
---   max_{Θ ≻ 0}  log det Θ - tr(SΘ) - λ ‖Θ‖_{1,off}
--- @
---
--- ここで @S@ は経験共分散行列、 @λ@ は L1 罰則。 対角は罰しない (FHT 2008
--- 慣例)。
---
--- ## アルゴリズム (Friedman-Hastie-Tibshirani 2008、 block CD)
---
--- 1. @Σ ← S + λI@ で初期化 (対角に λ shrinkage)
--- 2. 各列 @j@ について部分問題:
---    - @W_{11}@ = @Σ@ の row j / col j を除いた部分 (p-1 × p-1)
---    - @s_{12}@ = @S@ の列 j (行 j を除く)
---    - 内部 Lasso: @argmin_β (1/2) β^T W_{11} β - s_{12}^T β + λ |β|_1@
---    - @Σ_{:j} = W_{11} β@ で列を更新 (対角は @S_{jj} + λ@)
--- 3. @Σ@ が収束するまで全列 sweep を反復
--- 4. @Θ = Σ^{-1}@ を計算
---
--- Reference:
---   Friedman, Hastie, Tibshirani (2008) "Sparse inverse covariance
---   estimation with the graphical lasso". Biostatistics 9(3):432-441.
-module Hanalyze.Stat.CorrelationNetwork
-  ( GLassoFit (..)
-  , graphicalLasso
-  , graphicalLassoFromCov
-  , empiricalCov
-  , nonZeroPrecision
-    -- * Pearson 相関ネットワーク (Phase 77・df|-> correlationOf 用)
-  , correlationMatrix
-  , CorrelationGraph (..)
-  ) where
-
-import           Data.Text             (Text)
-import qualified Numeric.LinearAlgebra as LA
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
-data GLassoFit = GLassoFit
-  { glPrecision  :: !(LA.Matrix Double)   -- ^ 推定された Θ (precision)
-  , glCovariance :: !(LA.Matrix Double)   -- ^ 推定された Σ = Θ⁻¹
-  , glIterations :: !Int                   -- ^ 外側 sweep の反復数
-  , glConverged  :: !Bool                  -- ^ tol 内収束したか
-  , glLambda     :: !Double                -- ^ 使用した λ
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- API
--- ---------------------------------------------------------------------------
-
--- | 経験共分散行列 (= 中央化 + scale 1/(n-1))。
-empiricalCov :: LA.Matrix Double -> LA.Matrix Double
-empiricalCov x =
-  let n    = LA.rows x
-      ones = LA.konst 1 n :: LA.Vector Double
-      mu   = LA.scale (1 / fromIntegral n) (LA.tr x LA.#> ones)
-      xc   = x - LA.asRow mu
-      m    = max 1 (n - 1)
-  in LA.scale (1 / fromIntegral m) (LA.tr xc LA.<> xc)
-
--- | Pearson 相関行列 (@X@ n×p → p×p)。 'empiricalCov' を対角の標準偏差で正規化する
---   (@r_ij = Σ_ij / (σ_i σ_j)@)。 分散 0 の列は 0 除算回避で 0 相関扱い。
-correlationMatrix :: LA.Matrix Double -> LA.Matrix Double
-correlationMatrix x =
-  let cov  = empiricalCov x
-      p    = LA.rows cov
-      sds  = [ sqrt (cov `LA.atIndex` (i, i)) | i <- [0 .. p - 1] ]
-      dInv = LA.diag (LA.fromList [ if s > 1e-12 then 1 / s else 0 | s <- sds ])
-  in dInv LA.<> cov LA.<> dInv
-
--- | 相関ネットワーク (Pearson 相関 + 閾値) の結果 (Phase 77・@df |-> correlationOf thr cols@)。
---   'Plottable' (@Hanalyze.Plot.ML@) が @|r| > cgThreshold@ の対を辺にしたグラフを描く
---   (無向・向きは便宜上の配置。 因果でない)。 LiNGAM DAG と対比すると間接相関の過剰さが分かる。
-data CorrelationGraph = CorrelationGraph
-  { cgCorr      :: !(LA.Matrix Double)   -- ^ p × p Pearson 相関行列
-  , cgNames     :: ![Text]               -- ^ 変数名 (列順)
-  , cgThreshold :: !Double               -- ^ |r| > この値で辺を張る
-  } deriving (Show)
-
--- | データ行列 @X@ (n × p) から graphical Lasso 推定。 内部で
--- 'empiricalCov' を計算してから 'graphicalLassoFromCov' を呼ぶ。
-graphicalLasso
-  :: LA.Matrix Double      -- ^ X (n × p)
-  -> Double                -- ^ λ
-  -> Int                   -- ^ max outer sweeps (推奨 100)
-  -> Double                -- ^ tolerance (推奨 1e-4)
-  -> GLassoFit
-graphicalLasso x lambda maxOuter tol =
-  graphicalLassoFromCov (empiricalCov x) lambda maxOuter tol
-
--- | 経験共分散行列から直接推定 (= 既に共分散を持っているとき向け)。
-graphicalLassoFromCov
-  :: LA.Matrix Double      -- ^ S (p × p)
-  -> Double                -- ^ λ
-  -> Int -> Double
-  -> GLassoFit
-graphicalLassoFromCov s lambda maxOuter tol =
-  let p = LA.rows s
-      -- 初期化: Σ = S + λI (対角 shrinkage)
-      sigma0 = s + LA.scale lambda (LA.ident p)
-      -- 外側 sweep
-      sweep sigma =
-        foldl
-          (\sigCur j -> updateColumn sigCur s lambda j)
-          sigma
-          [0 .. p - 1]
-      loop !k !sigma
-        | k >= maxOuter = (sigma, k, False)
-        | otherwise     =
-            let sigmaN = sweep sigma
-                d      = LA.maxElement (LA.cmap abs (sigmaN - sigma))
-            in if d < tol
-                 then (sigmaN, k + 1, True)
-                 else loop (k + 1) sigmaN
-      (sigmaFinal, iters, conv) = loop 0 sigma0
-      -- 対角を S + λ にリセット (FHT 慣例)
-      sigmaDiag = setDiag sigmaFinal (LA.takeDiag s + LA.konst lambda p)
-      theta     = LA.inv sigmaDiag
-  in GLassoFit
-       { glPrecision  = theta
-       , glCovariance = sigmaDiag
-       , glIterations = iters
-       , glConverged  = conv
-       , glLambda     = lambda
-       }
-
--- | 1 列の更新: 内部 Lasso を解いて @Σ@ の j 列 / j 行を上書き。
-updateColumn :: LA.Matrix Double -> LA.Matrix Double -> Double -> Int
-             -> LA.Matrix Double
-updateColumn sigma s lambda j =
-  let p   = LA.rows sigma
-      ids = [i | i <- [0 .. p - 1], i /= j]
-      w11 = sigma LA.? ids LA.¿ ids
-      s12 = LA.fromList [LA.atIndex s (i, j) | i <- ids]
-      beta = innerLassoQuad w11 s12 lambda 200 1e-5
-      newCol = w11 LA.#> beta
-      sigma' = updateOffDiagColumn sigma j ids (LA.toList newCol)
-  in sigma'
-
--- | 内部 Lasso (quadratic form):
--- @argmin_β (1/2) β^T W β - s^T β + λ |β|_1@
--- coord update: @β_k ← S(s_k - Σ_{l≠k} W_{kl} β_l, λ) / W_{kk}@。
-innerLassoQuad
-  :: LA.Matrix Double -> LA.Vector Double -> Double -> Int -> Double
-  -> LA.Vector Double
-innerLassoQuad w sVec lambda maxIter tol =
-  let m  = LA.size sVec
-      diagW = LA.takeDiag w
-      sweep beta =
-        foldl
-          (\(bAcc, mDelta) k ->
-              let wkk = LA.atIndex diagW k
-                  wRow = LA.flatten (w LA.? [k])
-                  pred_k = wRow LA.<.> bAcc - wkk * LA.atIndex bAcc k
-                  rho = LA.atIndex sVec k - pred_k
-                  bk' = if wkk <= 0
-                          then 0
-                          else softT rho lambda / wkk
-                  bk  = LA.atIndex bAcc k
-                  d   = abs (bk' - bk)
-                  bAcc' = updateAt bAcc k bk'
-              in (bAcc', max mDelta d))
-          (beta, 0)
-          [0 .. m - 1]
-      loop !k !beta
-        | k >= maxIter = beta
-        | otherwise    =
-            let (betaN, d) = sweep beta
-            in if d < tol
-                 then betaN
-                 else loop (k + 1) betaN
-  in loop 0 (LA.konst 0 m)
-
--- ---------------------------------------------------------------------------
--- ヘルパ
--- ---------------------------------------------------------------------------
-
-softT :: Double -> Double -> Double
-softT z g
-  | z > g     = z - g
-  | z < -g    = z + g
-  | otherwise = 0
-
-setDiag :: LA.Matrix Double -> LA.Vector Double -> LA.Matrix Double
-setDiag m d =
-  let p = LA.rows m
-      xs = LA.toLists m
-      ds = LA.toList d
-      rewrite (i, row) =
-        [ if i == j then ds !! i else (xs !! i) !! j | j <- [0 .. p - 1] ]
-  in LA.fromLists [rewrite (i, xs !! i) | i <- [0 .. p - 1]]
-
-updateAt :: LA.Vector Double -> Int -> Double -> LA.Vector Double
-updateAt v i nv =
-  LA.fromList [ if k == i then nv else LA.atIndex v k
-              | k <- [0 .. LA.size v - 1] ]
-
--- | Σ の列 j / 行 j を新値で上書き (対角は触らない、 残り対角は別 step で
--- 設定)。 @ids@ は j を除いた行 index、 @vals@ は @ids@ 順の長さ p-1。
-updateOffDiagColumn
-  :: LA.Matrix Double -> Int -> [Int] -> [Double] -> LA.Matrix Double
-updateOffDiagColumn sigma j ids vals =
-  let p   = LA.rows sigma
-      pairs = zip ids vals
-      lookupV i = case lookup i pairs of
-        Just v -> v
-        Nothing -> 0
-      rows = LA.toLists sigma
-      newRow i
-        | i == j    = [ if k == j then (rows !! i) !! k else lookupV k
-                      | k <- [0 .. p - 1] ]
-        | otherwise = [ if k == j then lookupV i
-                                  else (rows !! i) !! k
-                      | k <- [0 .. p - 1] ]
-  in LA.fromLists [newRow i | i <- [0 .. p - 1]]
-
--- | precision matrix の非零要素数 (対角を除く上三角)。 @threshold@ で
--- 「ゼロ」 とみなす絶対値の閾値を指定。
-nonZeroPrecision :: Double -> LA.Matrix Double -> Int
-nonZeroPrecision threshold theta =
-  let p = LA.rows theta
-  in length [ ()
-            | i <- [0 .. p - 1]
-            , j <- [i + 1 .. p - 1]
-            , abs (LA.atIndex theta (i, j)) > threshold ]
diff --git a/src/Hanalyze/Stat/Descriptive.hs b/src/Hanalyze/Stat/Descriptive.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/Descriptive.hs
+++ /dev/null
@@ -1,149 +0,0 @@
-{-# LANGUAGE FlexibleContexts  #-}
-{-# LANGUAGE ScopedTypeVariables #-}
--- |
--- Module      : Hanalyze.Stat.Descriptive
--- Description : 一次元記述統計 (mean/quantile/variance 等) の単一の正 (single source of truth)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- 一次元の記述統計 (descriptive statistics) の公開 API。
---
--- hanalyze の記述統計の **単一の正 (single source of truth)**。 従来は
--- @mean@ / @median@ / @quantile@ / @variance@ が 'Stat.GroupComparison' /
--- 'Stat.ModelSelect' / 'Stat.Effect' / 'Model.Quantile' / 'Stat.Bootstrap' 等に
--- 私的 helper として散在 (シグネチャ @[Double]@ / @[Int]@ / @LA.Vector@ 混在・
--- ほぼ未 export) していたのを、 ここに集約する (Phase 65)。
---
--- === 正準型 = 'Data.Vector.Generic.Vector' v Double
--- 'statistics' パッケージ自身と同じく @G.Vector v Double@ で多相。 これにより
--- Storable (= hmatrix @LA.Vector@)・Unboxed・boxed (@V.Vector@・DataFrame 列) の
--- いずれも **ゼロ変換**で渡せる (速度経路は list 化を挟まない)。 素の @[Double]@
--- 利用には末尾の @*L@ wrapper を用意する。
---
--- === 実装方針
--- @mean@ / @variance@ (n-1) / @sd@ は 'Statistics.Sample' を再利用。 @quantile@ は
--- R 既定の **type-7** (線形補間) を自前実装し R 一致を保証する (@median@ / @iqr@ /
--- @percentile@ はこれを呼ぶ)。 ソートは 'Data.Vector.Algorithms.Intro'。
---
--- === NA
--- 本モジュールは NA を扱わない (total・純粋)。 R の @na.rm = TRUE@ 相当は呼び手が
--- @mapMaybe id@ で除去してから 'Data.Vector.Generic.fromList' する。
-module Hanalyze.Stat.Descriptive
-  ( -- * 中心
-    mean, median
-    -- * 位置
-  , quantile, percentile, minimum', maximum'
-    -- * 散布
-  , variance, sd, iqr, range'
-    -- * [Double] 便宜 wrapper
-  , meanL, medianL, quantileL, sdL, varianceL, iqrL
-  ) where
-
-import qualified Data.Vector.Generic            as G
-import qualified Data.Vector.Storable           as VS
-import qualified Data.Vector.Algorithms.Intro   as Intro
-import qualified Statistics.Sample              as S
-
--- ===========================================================================
--- 中心
--- ===========================================================================
-
--- | 算術平均。 空なら NaN (R @mean(numeric(0))@)。
-mean :: G.Vector v Double => v Double -> Double
-mean v | G.null v  = nan
-       | otherwise = S.mean v
-{-# INLINE mean #-}
-
--- | 中央値 (= type-7 の 0.5 分位点・偶数長は中央 2 点の平均)。
-median :: G.Vector v Double => v Double -> Double
-median = quantile 0.5
-{-# INLINE median #-}
-
--- ===========================================================================
--- 位置 (分位点は R 既定 type-7)
--- ===========================================================================
-
--- | R 既定 (type-7) の分位点。 確率を第 1 引数に取る (@quantile 0.95 v@)。
---
--- ソート済 0-index 列 @x[0..n-1]@・@h = (n-1) p@ として
--- @x[⌊h⌋] + (h - ⌊h⌋)(x[⌊h⌋+1] - x[⌊h⌋])@。 空なら NaN。
-quantile :: G.Vector v Double => Double -> v Double -> Double
-quantile p v
-  | n == 0    = nan
-  | n == 1    = G.head v
-  | otherwise =
-      let sorted = G.modify Intro.sort v
-          h      = fromIntegral (n - 1) * p
-          lo     = floor h
-          lo'    = max 0 (min (n - 1) lo)
-          hi'    = min (n - 1) (lo' + 1)
-          frac   = h - fromIntegral lo'
-          xlo    = G.unsafeIndex sorted lo'
-          xhi    = G.unsafeIndex sorted hi'
-      in xlo + frac * (xhi - xlo)
-  where n = G.length v
-
--- | パーセンタイル (= @quantile (p/100)@)。
-percentile :: G.Vector v Double => Double -> v Double -> Double
-percentile p = quantile (p / 100)
-{-# INLINE percentile #-}
-
--- | 最小値 (空なら NaN)。
-minimum' :: G.Vector v Double => v Double -> Double
-minimum' v | G.null v  = nan
-           | otherwise = G.minimum v
-{-# INLINE minimum' #-}
-
--- | 最大値 (空なら NaN)。
-maximum' :: G.Vector v Double => v Double -> Double
-maximum' v | G.null v  = nan
-           | otherwise = G.maximum v
-{-# INLINE maximum' #-}
-
--- ===========================================================================
--- 散布
--- ===========================================================================
-
--- | 標本分散 (n-1 で割る・R @var()@)。 n<2 なら NaN。
-variance :: G.Vector v Double => v Double -> Double
-variance v | G.length v < 2 = nan
-           | otherwise       = S.varianceUnbiased v
-{-# INLINE variance #-}
-
--- | 標準偏差 (= sqrt . variance・R @sd()@)。
-sd :: G.Vector v Double => v Double -> Double
-sd v | G.length v < 2 = nan
-     | otherwise       = S.stdDev v
-{-# INLINE sd #-}
-
--- | 四分位範囲 (= type-7 の 0.75 分位点 - 0.25 分位点・R @IQR()@)。
-iqr :: G.Vector v Double => v Double -> Double
-iqr v = quantile 0.75 v - quantile 0.25 v
-{-# INLINE iqr #-}
-
--- | 範囲 (= 最大 - 最小)。
-range' :: G.Vector v Double => v Double -> Double
-range' v = maximum' v - minimum' v
-{-# INLINE range' #-}
-
--- ===========================================================================
--- [Double] 便宜 wrapper (= f . VS.fromList)
--- ===========================================================================
-
-meanL     :: [Double] -> Double
-meanL      = mean     . VS.fromList
-medianL   :: [Double] -> Double
-medianL    = median   . VS.fromList
-quantileL :: Double -> [Double] -> Double
-quantileL p = quantile p . VS.fromList
-sdL       :: [Double] -> Double
-sdL        = sd       . VS.fromList
-varianceL :: [Double] -> Double
-varianceL  = variance . VS.fromList
-iqrL      :: [Double] -> Double
-iqrL       = iqr      . VS.fromList
-
--- ===========================================================================
-
-nan :: Double
-nan = 0 / 0
diff --git a/src/Hanalyze/Stat/Distribution.hs b/src/Hanalyze/Stat/Distribution.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/Distribution.hs
+++ /dev/null
@@ -1,275 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Stat.Distribution
--- Description : ライブラリ全体で使う確率分布 27 種と HMC/NUTS 用の制約変換
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Probability distributions used throughout the library.
---
--- Provides 27 named distributions (Normal, Beta, Gamma, StudentT, LKJ,
--- Truncated, Censored, ...) with @density@ / @logDensity@ / @supportRange@
--- and a constraint-transform mechanism ('Transform') for unconstrained
--- HMC/NUTS sampling. Distributions are tagged via the 'Distribution' sum
--- type so they can be passed as first-class values (used by the
--- 'Hanalyze.Model.HBM' DSL and the variational layer 'Hanalyze.Stat.VI').
-module Hanalyze.Stat.Distribution
-  ( Distribution (..)
-  , density
-  , logDensity
-  , isContinuous
-  , supportRange
-  , distributionName
-  , parseDistribution
-    -- * Constraint transforms (for HMC/NUTS unconstrained sampling)
-  , Transform (..)
-  , distTransform
-  , toUnconstrained
-  , fromUnconstrained
-  , logJacobianAdj
-  ) where
-
-import Data.Text (Text)
-import qualified Data.Text as T
-
--- ---------------------------------------------------------------------------
--- Types
--- ---------------------------------------------------------------------------
-
--- | First-class probability distribution.
-data Distribution
-  = Normal     Double Double   -- ^ @Normal μ σ@.
-  | Binomial   Int    Double   -- ^ @Binomial n p@.
-  | Poisson    Double          -- ^ @Poisson λ@.
-  | Exponential Double         -- ^ @Exponential rate@.
-  | Gamma      Double Double   -- ^ @Gamma shape rate@.
-  | Beta       Double Double   -- ^ @Beta α β@.
-  deriving (Show, Eq)
-
--- ---------------------------------------------------------------------------
--- Density / PMF
--- ---------------------------------------------------------------------------
-
--- | Probability density (continuous distributions) or PMF (discrete).
-density :: Distribution -> Double -> Double
-density (Normal mu sig) x
-  | sig <= 0  = 0
-  | otherwise = exp (negate ((x - mu)^(2::Int) / (2 * sig^(2::Int))))
-              / (sig * sqrt (2 * pi))
-
-density (Binomial n p) x
-  | p < 0 || p > 1      = 0
-  | x < 0 || x > fromIntegral n = 0
-  | otherwise =
-      let k = round x :: Int
-      in fromIntegral (choose n k) * p ^ k * (1 - p) ^ (n - k)
-
-density (Poisson lam) x
-  | lam <= 0  = 0
-  | x < 0     = 0
-  | otherwise =
-      let k = round x :: Int
-      in exp (negate lam) * lam ^ k / fromIntegral (factorial k)
-
-density (Exponential lam) x
-  | lam <= 0 = 0
-  | x < 0    = 0
-  | otherwise = lam * exp (negate lam * x)
-
-density (Gamma alpha beta_) x
-  | alpha <= 0 || beta_ <= 0 = 0
-  | x <= 0                    = 0
-  | otherwise =
-      beta_ ** alpha * x ** (alpha - 1) * exp (negate beta_ * x)
-      / gammaFn alpha
-
-density (Beta alpha beta_) x
-  | alpha <= 0 || beta_ <= 0 = 0
-  | x <= 0 || x >= 1         = 0
-  | otherwise =
-      x ** (alpha - 1) * (1 - x) ** (beta_ - 1)
-      / betaFn alpha beta_
-
--- | Log density. For Binomial and Poisson the result is computed
--- directly in log-space to avoid overflow at large @n@ or @λ@.
-logDensity :: Distribution -> Double -> Double
-logDensity (Binomial n p) x
-  | p <= 0 || p >= 1                = -1/0
-  | x < 0 || x > fromIntegral n    = -1/0
-  | otherwise =
-      let k = round x :: Int
-      in lgChoose n k
-       + fromIntegral k * log p
-       + fromIntegral (n - k) * log (1 - p)
-  where
-    lgChoose a b = sum [log (fromIntegral i) | i <- [a - b + 1 .. a]]
-                 - sum [log (fromIntegral i) | i <- [1 .. b]]
-
-logDensity (Poisson lam) x
-  | lam <= 0 = -1/0
-  | x < 0    = -1/0
-  | otherwise =
-      let k = round x :: Int
-      in fromIntegral k * log lam - lam - logFactorial k
-  where
-    logFactorial m = sum (map (log . fromIntegral) [1..m])
-
-logDensity d x =
-  let p = density d x
-  in if p <= 0 then -1/0 else log p
-
--- ---------------------------------------------------------------------------
--- Properties
--- ---------------------------------------------------------------------------
-
--- | True for continuous distributions, False for discrete ones.
-isContinuous :: Distribution -> Bool
-isContinuous (Binomial  _ _) = False
-isContinuous (Poisson   _  ) = False
-isContinuous _               = True
-
--- | Suggested x-axis range for plotting.
--- Continuous: mean ± k*sd; discrete: [0, mean + k*sd].
-supportRange :: Distribution -> (Double, Double)
-supportRange (Normal mu sig)      = (mu - 4*sig,     mu + 4*sig)
-supportRange (Binomial n _)       = (0, fromIntegral n)
-supportRange (Poisson lam)        = (0, max 20 (lam + 4 * sqrt lam))
-supportRange (Exponential lam)    = (0, 6 / lam)
-supportRange (Gamma alpha beta_)  = let m = alpha / beta_
-                                        s = sqrt (alpha / (beta_*beta_))
-                                    in (0, m + 4*s)
-supportRange (Beta _ _)           = (0, 1)
-
--- | Human-readable name with parameter values, e.g. @\"Normal(0.00, 1.00)\"@.
-distributionName :: Distribution -> Text
-distributionName (Normal     mu sig ) = "Normal(" <> fmt mu <> ", " <> fmt sig <> ")"
-distributionName (Binomial   n  p   ) = "Binomial(" <> T.pack (show n) <> ", " <> fmt p <> ")"
-distributionName (Poisson    lam    ) = "Poisson(" <> fmt lam <> ")"
-distributionName (Exponential lam   ) = "Exponential(" <> fmt lam <> ")"
-distributionName (Gamma  a b        ) = "Gamma(" <> fmt a <> ", " <> fmt b <> ")"
-distributionName (Beta   a b        ) = "Beta(" <> fmt a <> ", " <> fmt b <> ")"
-
-fmt :: Double -> Text
-fmt v = T.pack (show (fromIntegral (round (v * 100) :: Int) / 100.0 :: Double))
-
--- | Parse "normal", "binomial", "poisson", "exponential", "gamma", "beta".
-parseDistribution :: String -> [Double] -> Either String Distribution
-parseDistribution name params = case map toLowerAscii name of
-  "normal"      -> case params of
-    [mu, sig] | sig > 0  -> Right (Normal mu sig)
-    [_, sig]             -> Left ("Normal: σ must be > 0, got " ++ show sig)
-    _                    -> Left "Normal requires params: mean sd"
-  "binomial"    -> case params of
-    [n, p] | p >= 0, p <= 1, n >= 1 ->
-      Right (Binomial (round n) p)
-    _ -> Left "Binomial requires params: n p  (n≥1, 0≤p≤1)"
-  "poisson"     -> case params of
-    [lam] | lam > 0 -> Right (Poisson lam)
-    _               -> Left "Poisson requires params: lambda (>0)"
-  "exponential" -> case params of
-    [lam] | lam > 0 -> Right (Exponential lam)
-    _               -> Left "Exponential requires params: rate (>0)"
-  "gamma"       -> case params of
-    [a, b] | a > 0, b > 0 -> Right (Gamma a b)
-    _                      -> Left "Gamma requires params: shape rate (both >0)"
-  "beta"        -> case params of
-    [a, b] | a > 0, b > 0 -> Right (Beta a b)
-    _                      -> Left "Beta requires params: alpha beta (both >0)"
-  other -> Left ("Unknown distribution: " ++ other
-              ++ ". Available: normal, binomial, poisson, exponential, gamma, beta")
-
--- ---------------------------------------------------------------------------
--- 制約変換
--- ---------------------------------------------------------------------------
-
--- | Constraint transform corresponding to a parameter's domain.
---
--- HMC and NUTS run leapfrog in the unconstrained space @ℝ@ and map
--- samples back to the constrained space, preventing excursions outside
--- the support.
-data Transform
-  = UnconstrainedT   -- ^ @(-∞, ∞)@: identity transform (e.g. Normal mean).
-  | PositiveT        -- ^ @(0, ∞)@: log transform, @θ = exp(u)@.
-  | UnitIntervalT    -- ^ @(0, 1)@: logit transform, @θ = sigmoid(u)@.
-  deriving (Show, Eq)
-
--- | Pick the appropriate 'Transform' from the parameter's prior.
-distTransform :: Distribution -> Transform
-distTransform (Normal _ _)    = UnconstrainedT
-distTransform (Exponential _) = PositiveT
-distTransform (Gamma _ _)     = PositiveT
-distTransform (Beta _ _)      = UnitIntervalT
-distTransform (Binomial _ _)  = UnconstrainedT  -- 離散; HMC/NUTS 非推奨
-distTransform (Poisson _)     = UnconstrainedT  -- 離散; HMC/NUTS 非推奨
-
--- | Map @θ@ in constrained space to @u@ in unconstrained space.
-toUnconstrained :: Transform -> Double -> Double
-toUnconstrained UnconstrainedT x = x
-toUnconstrained PositiveT      x = log x
-toUnconstrained UnitIntervalT  x = log x - log (1 - x)  -- logit
-
--- | Map @u@ in unconstrained space back to @θ@ in constrained space.
-fromUnconstrained :: Transform -> Double -> Double
-fromUnconstrained UnconstrainedT u = u
-fromUnconstrained PositiveT      u = exp u
-fromUnconstrained UnitIntervalT  u = 1 / (1 + exp (-u))  -- sigmoid
-
--- | Jacobian log-det @log |dθ/du|@ to add to the log-joint when working
--- in unconstrained space.
---
--- * @PositiveT@:     @θ = exp(u)     → log|J| = u@.
--- * @UnitIntervalT@: @θ = sigmoid(u) → log|J| = log σ(u) + log(1-σ(u))@.
-logJacobianAdj :: Transform -> Double -> Double
-logJacobianAdj UnconstrainedT _ = 0
-logJacobianAdj PositiveT      u = u
-logJacobianAdj UnitIntervalT  u =
-  let s = 1 / (1 + exp (-u))
-  in log s + log (1 - s)
-
-toLowerAscii :: Char -> Char
-toLowerAscii c
-  | c >= 'A' && c <= 'Z' = toEnum (fromEnum c + 32)
-  | otherwise             = c
-
--- ---------------------------------------------------------------------------
--- Math helpers
--- ---------------------------------------------------------------------------
-
-factorial :: Int -> Int
-factorial n = product [1 .. n]
-
--- | 二項係数: 乗算公式 O(min(k, n-k))
-choose :: Int -> Int -> Int
-choose n k
-  | k < 0 || k > n = 0
-  | k == 0 || k == n = 1
-  | k > n - k = choose n (n - k)
-  | otherwise = foldl (\acc i -> acc * (n + 1 - i) `div` i) 1 [1..k]
-
--- Lanczos approximation for Γ(z), z > 0
-gammaFn :: Double -> Double
-gammaFn z
-  | z < 0.5   = pi / (sin (pi * z) * gammaFn (1 - z))
-  | otherwise =
-      let z'  = z - 1
-          x   = lanczosC !! 0
-              + sum [ lanczosC !! i / (z' + fromIntegral i)
-                    | i <- [1 .. length lanczosC - 1] ]
-          t   = z' + fromIntegral (length lanczosC) - 0.5
-      in sqrt (2*pi) * t ** (z' + 0.5) * exp (negate t) * x
-
-lanczosC :: [Double]
-lanczosC =
-  [ 0.99999999999980993
-  , 676.5203681218851
-  , -1259.1392167224028
-  , 771.32342877765313
-  , -176.61502916214059
-  , 12.507343278686905
-  , -0.13857109526572012
-  , 9.9843695780195716e-6
-  , 1.5056327351493116e-7
-  ]
-
-betaFn :: Double -> Double -> Double
-betaFn a b = gammaFn a * gammaFn b / gammaFn (a + b)
diff --git a/src/Hanalyze/Stat/Effect.hs b/src/Hanalyze/Stat/Effect.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/Effect.hs
+++ /dev/null
@@ -1,323 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Stat.Effect
--- Description : 効果量 (Cohen's d 等) と検出力分析
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Effect sizes and power analysis.
---
--- Effect-size measures complement p-values by quantifying the
--- magnitude of an effect, not just its statistical significance.
--- Power analysis lets the user pick sample sizes a priori or assess
--- post-hoc power.
---
--- == Effect-size summary
---
---   * 'cohenD' — standardised mean difference (two-sample).
---   * 'hedgesG' — small-sample-corrected Cohen's d.
---   * 'cohensF' — for ANOVA / regression.
---   * 'eta2' / 'omega2' — variance explained in ANOVA.
---   * 'cramerV' — for chi-square contingency tables.
---   * 'oddsRatio' — for 2×2 tables.
---
--- == Power analysis
---
--- Each test family provides @powerXxx@ (compute power given n / α /
--- effect) and @sampleSizeXxx@ (compute n given power / α / effect).
-module Hanalyze.Stat.Effect
-  ( -- * Effect-size measures (location)
-    cohenD
-  , cohenDCI
-  , cohenDPaired
-  , hedgesG
-    -- * Effect-size (ANOVA / regression)
-  , cohensF
-  , eta2
-  , eta2CI
-  , omega2
-    -- * Effect-size (categorical)
-  , cramerV
-  , phiCoeff
-  , oddsRatio
-    -- * Power analysis (t-test)
-  , powerTTest
-  , sampleSizeTTest
-    -- * Power analysis (one-way ANOVA)
-  , powerANOVA
-  , sampleSizeANOVA
-    -- * Power analysis (correlation)
-  , powerCorrelation
-  ) where
-
-import qualified Numeric.LinearAlgebra            as LA
-import qualified Statistics.Distribution          as SD
-import qualified Statistics.Distribution.FDistribution as FDist
-import qualified Statistics.Distribution.Normal   as Normal
-import qualified Statistics.Distribution.StudentT as StuT
-
--- ---------------------------------------------------------------------------
--- Effect sizes (location)
--- ---------------------------------------------------------------------------
-
--- | Cohen's d for two independent samples (pooled SD denominator).
--- Conventional interpretation: small = 0.2, medium = 0.5, large = 0.8.
-cohenD :: LA.Vector Double -> LA.Vector Double -> Double
-cohenD xs ys =
-  let n1 = fromIntegral (LA.size xs) :: Double
-      n2 = fromIntegral (LA.size ys) :: Double
-      m1 = mean xs
-      m2 = mean ys
-      v1 = variance xs
-      v2 = variance ys
-      pooledV = ((n1 - 1) * v1 + (n2 - 1) * v2) / (n1 + n2 - 2)
-  in if pooledV <= 0 then 0 else (m1 - m2) / sqrt pooledV
-
--- | Cohen's d with (1-α) confidence interval (Hedges-Olkin SE approximation).
---
--- > SE(d) ≈ √( (n1+n2)/(n1·n2) + d² / (2(n1+n2)) )
--- > CI    = d ± z_{1-α/2} · SE(d)
---
--- 厳密な非中心 t 分布の逆変換ではないが、 サンプルサイズ ≥ 20 程度で
--- 十分実用的 (Cumming 2012)。
-cohenDCI :: LA.Vector Double -> LA.Vector Double -> Double
-         -> (Double, (Double, Double))
-cohenDCI xs ys alpha =
-  let d   = cohenD xs ys
-      n1  = fromIntegral (LA.size xs) :: Double
-      n2  = fromIntegral (LA.size ys) :: Double
-      se  = sqrt ((n1 + n2) / (n1 * n2) + d * d / (2 * (n1 + n2)))
-      z   = SD.quantile Normal.standard (1 - alpha / 2)
-  in (d, (d - z * se, d + z * se))
-
--- | Cohen's d for paired samples (uses SD of differences).
-cohenDPaired :: LA.Vector Double -> LA.Vector Double -> Double
-cohenDPaired xs ys =
-  let diffs = xs - ys
-      m     = mean diffs
-      s     = sqrt (variance diffs)
-  in if s <= 0 then 0 else m / s
-
--- | Hedges' g — Cohen's d corrected for small-sample bias.
--- @g = d × (1 − 3 / (4(n1 + n2) − 9))@.
-hedgesG :: LA.Vector Double -> LA.Vector Double -> Double
-hedgesG xs ys =
-  let d  = cohenD xs ys
-      n1 = LA.size xs
-      n2 = LA.size ys
-      df = fromIntegral (n1 + n2) - 2
-      j  = 1 - 3 / (4 * df - 1)
-  in d * j
-
--- ---------------------------------------------------------------------------
--- Effect sizes (ANOVA / regression)
--- ---------------------------------------------------------------------------
-
--- | Cohen's f for ANOVA: @sqrt(η² / (1 − η²))@.
--- Conventional: small = 0.10, medium = 0.25, large = 0.40.
-cohensF :: Double -> Double
-cohensF e2 = sqrt (e2 / max 1e-15 (1 - e2))
-
--- | η² (eta-squared): @SS_between / SS_total@.
--- Range @[0, 1]@; biased upward, especially with small @n@.
-eta2 :: [LA.Vector Double] -> Double
-eta2 groups
-  | null groups = 0
-  | otherwise =
-      let ns    = map (fromIntegral . LA.size) groups :: [Double]
-          n     = sum ns
-          means = map mean groups
-          grand = sum (zipWith (*) ns means) / n
-          ssB   = sum [ ni * (mi - grand)^(2::Int) | (ni, mi) <- zip ns means ]
-          ssT   = sum [ LA.sumElements ((g - LA.scalar grand)^(2::Int))
-                      | g <- groups ]
-      in if ssT <= 0 then 0 else ssB / ssT
-
--- | η² with (1-α) confidence interval from F-statistic + df via the
---   noncentrality parameter inversion.
---
---   F-statistic, df_between, df_within を入力に取り、 η² の (lo, hi) CI を
---   返す。 信頼区間は noncentrality parameter λ の (lo, hi) を二分探索で
---   求め、 そこから η² = λ / (λ + df_total + 1) に変換する近似版。
---
---   既存 @anovaOneWay@ 等で得た F 値を入れて使う。
-eta2CI :: Double            -- ^ F statistic
-       -> (Int, Int)        -- ^ (df_between, df_within)
-       -> Double            -- ^ α (例: 0.05)
-       -> (Double, (Double, Double))
-eta2CI fStat (dfB, dfW) alpha =
-  let dfBd = fromIntegral dfB :: Double
-      dfWd = fromIntegral dfW :: Double
-      dfTotal = dfBd + dfWd + 1
-      eta = (fStat * dfBd) / (fStat * dfBd + dfWd)
-      -- noncentrality parameter from observed F (point estimate)
-      lambdaHat = max 0 (fStat * dfBd - dfBd)
-      -- crude symmetric CI on λ via Patnaik / Helmert approximation:
-      seL = sqrt (2 * (2 * lambdaHat + dfBd + dfWd))
-      z   = SD.quantile Normal.standard (1 - alpha / 2)
-      lamLo = max 0 (lambdaHat - z * seL)
-      lamHi = max 0 (lambdaHat + z * seL)
-      toEta l = l / (l + dfTotal)
-  in (eta, (toEta lamLo, toEta lamHi))
-
--- | ω² (omega-squared): unbiased version of η².
--- @ω² = (SS_between − (k − 1) × MS_within) / (SS_total + MS_within)@.
-omega2 :: [LA.Vector Double] -> Double
-omega2 groups
-  | length groups < 2 = 0
-  | otherwise =
-      let k     = length groups
-          ns    = map (fromIntegral . LA.size) groups :: [Double]
-          n     = sum ns
-          means = map mean groups
-          grand = sum (zipWith (*) ns means) / n
-          ssB   = sum [ ni * (mi - grand)^(2::Int) | (ni, mi) <- zip ns means ]
-          ssW   = sum [ LA.sumElements ((g - LA.scalar mi)^(2::Int))
-                      | (g, mi) <- zip groups means ]
-          ssT   = ssB + ssW
-          msW   = ssW / (n - fromIntegral k)
-      in if ssT + msW <= 0 then 0
-           else (ssB - fromIntegral (k - 1) * msW) / (ssT + msW)
-
--- ---------------------------------------------------------------------------
--- Effect sizes (categorical)
--- ---------------------------------------------------------------------------
-
--- | Cramér's V from a chi-square statistic and table dimensions.
--- Range @[0, 1]@; > 0.5 = strong association.
-cramerV :: Double -> Int -> Int -> Int -> Double
-cramerV chi2 n r c =
-  sqrt (chi2 / (fromIntegral n * fromIntegral (min r c - 1)))
-
--- | φ (phi) coefficient for 2×2 tables. @φ = sqrt(χ² / n)@. Same as
--- 'cramerV' for 2×2.
-phiCoeff :: Double -> Int -> Double
-phiCoeff chi2 n = sqrt (chi2 / fromIntegral n)
-
--- | Odds ratio for a 2×2 table @((a, b), (c, d))@.
-oddsRatio :: ((Int, Int), (Int, Int)) -> Double
-oddsRatio ((a, b), (c, d))
-  | b * c == 0 = 1 / 0
-  | otherwise  = fromIntegral (a * d) / fromIntegral (b * c)
-
--- ---------------------------------------------------------------------------
--- Power analysis — t-test
--- ---------------------------------------------------------------------------
-
--- | Power of a two-sided two-sample t-test.
---
--- @power(n, α, d) = 1 − β@ where @β@ is the type-II error rate.
--- Computed via the noncentral t-distribution; we approximate with a
--- normal approximation good for moderate-to-large @n@.
---
--- Inputs:
---
---   * @nPerGroup@: sample size per group.
---   * @alpha@: significance level (e.g. 0.05).
---   * @effect@: Cohen's d.
-powerTTest :: Int -> Double -> Double -> Double
-powerTTest nPerGroup alpha d =
-  let n      = fromIntegral nPerGroup :: Double
-      df     = 2 * n - 2
-      tCrit  = SD.quantile (StuT.studentT df) (1 - alpha / 2)
-      ncp    = d * sqrt (n / 2)
-      -- P(T > tCrit | non-centrality = ncp), approximated via Normal:
-      -- z ≈ (T − ncp) / 1; P(T > tCrit) ≈ 1 - Φ(tCrit - ncp)
-      pUpper = 1 - SD.cumulative Normal.standard (tCrit - ncp)
-      pLower = SD.cumulative Normal.standard (-tCrit - ncp)
-  in pUpper + pLower
-
--- | Required sample size per group for a target power on a two-sample
--- t-test (two-sided). Solved by binary search over @powerTTest@.
-sampleSizeTTest
-  :: Double  -- ^ Target power (e.g. 0.80).
-  -> Double  -- ^ Significance level @α@.
-  -> Double  -- ^ Cohen's d.
-  -> Int
-sampleSizeTTest tgtPower alpha d
-  | d <= 0    = 0
-  | otherwise = binSearch 4 100000
-  where
-    binSearch lo hi
-      | hi - lo <= 1 = hi
-      | otherwise    =
-          let mid = (lo + hi) `div` 2
-              p   = powerTTest mid alpha d
-          in if p >= tgtPower then binSearch lo mid else binSearch mid hi
-
--- ---------------------------------------------------------------------------
--- Power analysis — one-way ANOVA
--- ---------------------------------------------------------------------------
-
--- | Power of a one-way ANOVA F-test.
---
---   * @nPerGroup@: cells per group.
---   * @k@: number of groups.
---   * @f@: Cohen's f effect size.
-powerANOVA :: Int -> Int -> Double -> Double -> Double
-powerANOVA nPerGroup k alpha f =
-  let n     = fromIntegral nPerGroup * fromIntegral k :: Double
-      df1   = fromIntegral (k - 1) :: Double
-      df2   = n - fromIntegral k
-      fCrit = SD.quantile (FDist.fDistribution (k - 1)
-                                                (round df2)) (1 - alpha)
-      ncp   = f * f * n   -- non-centrality parameter
-      -- Approximation: shift the F crit by ncp/df1.
-      adjustedF = fCrit / (1 + ncp / df1)
-      _ = adjustedF
-      -- A better approximation uses the noncentral F directly. We use
-      -- a simple normal approximation on the test statistic.
-      mu = (1 + ncp / df1) * df2 / (df2 - 2)
-      sd = sqrt (2 * (df2 / (df2 - 2))^(2::Int) * (df1 + ncp)
-                 / (df1 * (df2 - 4)))
-      _ = sd
-  in 1 - SD.cumulative Normal.standard ((fCrit - mu) / max 1e-9 sd)
-
--- | Required cells per group for a target power on one-way ANOVA.
-sampleSizeANOVA
-  :: Double  -- ^ Target power.
-  -> Int     -- ^ Number of groups.
-  -> Double  -- ^ Significance level @α@.
-  -> Double  -- ^ Cohen's f.
-  -> Int
-sampleSizeANOVA tgtPower k alpha f
-  | f <= 0    = 0
-  | otherwise = binSearch 4 100000
-  where
-    binSearch lo hi
-      | hi - lo <= 1 = hi
-      | otherwise    =
-          let mid = (lo + hi) `div` 2
-              p   = powerANOVA mid k alpha f
-          in if p >= tgtPower then binSearch lo mid else binSearch mid hi
-
--- ---------------------------------------------------------------------------
--- Power analysis — correlation
--- ---------------------------------------------------------------------------
-
--- | Power of testing @H0: ρ = 0@ via Fisher z transform.
---
---   * @n@: sample size.
---   * @r@: target correlation effect size.
-powerCorrelation :: Int -> Double -> Double -> Double
-powerCorrelation n alpha r =
-  let nn   = fromIntegral n :: Double
-      zr   = 0.5 * log ((1 + r) / (1 - r))  -- Fisher z transform
-      seZ  = 1 / sqrt (nn - 3)
-      zCrit = SD.quantile Normal.standard (1 - alpha / 2)
-      pUpper = 1 - SD.cumulative Normal.standard (zCrit - zr / seZ)
-      pLower = SD.cumulative Normal.standard (-zCrit - zr / seZ)
-  in pUpper + pLower
-
--- ---------------------------------------------------------------------------
--- Internal helpers
--- ---------------------------------------------------------------------------
-
-mean :: LA.Vector Double -> Double
-mean v = LA.sumElements v / fromIntegral (LA.size v)
-
-variance :: LA.Vector Double -> Double
-variance v =
-  let n = fromIntegral (LA.size v) :: Double
-      m = mean v
-  in LA.sumElements ((v - LA.scalar m) ^ (2 :: Int)) / max 1 (n - 1)
diff --git a/src/Hanalyze/Stat/GroupComparison.hs b/src/Hanalyze/Stat/GroupComparison.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/GroupComparison.hs
+++ /dev/null
@@ -1,119 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Stat.GroupComparison
--- Description : 2 群間の多変量比較ランキング (Spotfire 風 "Good vs Bad")
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- 2 群間の多変量比較ランキング (Spotfire 風 "Good vs Bad")。
---
--- 「良品 vs 不良品」 を二値ラベルで分け、 各説明変数について
--- (i) 平均差、 (ii) Cohen's d 効果量、 (iii) Welch t-test p 値 を計算し、
--- 効果量の絶対値降順にランク付けして返す。 半導体品質解析等で頻出。
---
--- 単独検定ではなく **複数変数の並列比較に最適化**された helper。
--- 多重比較補正は呼び出し側で `Hanalyze.Stat.MultipleTesting` を使う。
-module Hanalyze.Stat.GroupComparison
-  ( -- * 結果型
-    GroupCompResult (..)
-    -- * 比較
-  , goodVsBad
-  ) where
-
-import qualified Data.Vector           as V
-import qualified Numeric.LinearAlgebra as LA
-import           Data.List             (sortBy)
-import           Data.Ord              (comparing, Down (..))
-import           Data.Text             (Text)
-import           Data.Vector           (Vector)
-
-import qualified Hanalyze.Stat.Test    as ST
-import qualified Hanalyze.Stat.Effect  as Eff
-
--- ===========================================================================
--- 型
--- ===========================================================================
-
--- | 1 変数の Good vs Bad 比較結果。
-data GroupCompResult = GroupCompResult
-  { gcrVarName  :: !Text     -- ^ 変数名
-  , gcrMeanG    :: !Double   -- ^ Good 群 (label = True) の平均
-  , gcrMeanB    :: !Double   -- ^ Bad  群 (label = False) の平均
-  , gcrMeanDiff :: !Double   -- ^ Mean(Bad) − Mean(Good)
-  , gcrEffect   :: !Double   -- ^ Cohen's d (signed; |gcrEffect| でランク)
-  , gcrPValue   :: !Double   -- ^ Welch's two-sided t-test の p 値
-  , gcrNG       :: !Int      -- ^ Good 群サイズ
-  , gcrNB       :: !Int      -- ^ Bad  群サイズ
-  } deriving (Show, Eq)
-
--- ===========================================================================
--- 公開関数
--- ===========================================================================
-
--- | 各説明変数について 2 群間の差を計算し、 効果量絶対値降順でランク付け。
---
--- 入力契約:
---
---   * 変数リストは非空 (1 変数以上)
---   * 各変数の Vector 長 = labels の長さ (一致しないと 'Left')
---   * 両群とも 2 個以上の観測必須 (Welch t-test の前提)
-goodVsBad
-  :: [(Text, Vector Double)]   -- ^ (変数名, 値ベクトル) のリスト
-  -> Vector Bool               -- ^ 群ラベル (True = Good、 False = Bad)
-  -> Either Text [GroupCompResult]
-goodVsBad vars labels
-  | null vars               = Left "goodVsBad: empty variable list"
-  | V.null labels           = Left "goodVsBad: empty labels"
-  | any (\(_, v) -> V.length v /= V.length labels) vars
-                            = Left "goodVsBad: variable length mismatch with labels"
-  | nG < 2 || nB < 2        = Left "goodVsBad: each group needs at least 2 observations"
-  | otherwise =
-      let results = map (compareOne labels) vars
-      in Right (sortBy (comparing (Down . absEffect)) results)
-  where
-    nG = V.length (V.filter id labels)
-    nB = V.length labels - nG
-    absEffect = abs . gcrEffect
-
--- ---------------------------------------------------------------------------
--- 1 変数の比較
--- ---------------------------------------------------------------------------
-
-compareOne :: Vector Bool -> (Text, Vector Double) -> GroupCompResult
-compareOne labels (name, vals) =
-  let (goodList, badList) = partitionByLabels labels vals
-      gVec = LA.fromList goodList
-      bVec = LA.fromList badList
-      tr   = ST.tTestWelch gVec bVec ST.TwoSided
-      pVal = ST.trPValue tr
-      d    = Eff.cohenD bVec gVec   -- Mean(Bad) − Mean(Good) 方向
-      mG   = mean goodList
-      mB   = mean badList
-  in GroupCompResult
-       { gcrVarName  = name
-       , gcrMeanG    = mG
-       , gcrMeanB    = mB
-       , gcrMeanDiff = mB - mG
-       , gcrEffect   = d
-       , gcrPValue   = pVal
-       , gcrNG       = length goodList
-       , gcrNB       = length badList
-       }
-
--- | label が True の要素を good、 False を bad として分割。
-partitionByLabels :: Vector Bool -> Vector Double -> ([Double], [Double])
-partitionByLabels labels vals = go 0 ([], [])
-  where
-    n = V.length vals
-    go !i (gs, bs)
-      | i >= n = (reverse gs, reverse bs)
-      | otherwise =
-          let v = vals V.! i
-              l = labels V.! i
-          in if l then go (i + 1) (v : gs, bs)
-                  else go (i + 1) (gs, v : bs)
-
-mean :: [Double] -> Double
-mean [] = 0
-mean xs = sum xs / fromIntegral (length xs)
diff --git a/src/Hanalyze/Stat/Interpolate.hs b/src/Hanalyze/Stat/Interpolate.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/Interpolate.hs
+++ /dev/null
@@ -1,256 +0,0 @@
--- |
--- Module      : Hanalyze.Stat.Interpolate
--- Description : 一次元補間 (線形 / 自然三次スプライン / PCHIP)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- One-dimensional interpolation (Linear / natural cubic spline / PCHIP).
---
--- Builds a continuous @Double -> Double@ function from observed points
--- @[(x_i, y_i)]@ (sorted ascending, distinct in x). Out-of-range queries
--- (@x < x_0@ or @x > x_{n-1}@) are handled by linearly extrapolating the
--- end segments.
---
--- Primary use: as the per-id interpolant inside
--- 'Hanalyze.DataIO.Preprocess.regridLong', which resamples jagged long-form data
--- onto a common grid.
-module Hanalyze.Stat.Interpolate
-  ( InterpKind (..)
-  , interp1d
-  ) where
-
-import           Data.List (sortBy)
-import           Data.Ord  (comparing)
-import qualified Data.Vector.Unboxed         as U
-import qualified Data.Vector.Unboxed.Mutable as MU
-
--- | Interpolation method.
-data InterpKind
-  = Linear         -- ^ Piecewise linear. Most robust, never diverges on
-                   --   extrapolation.
-  | NaturalSpline  -- ^ Natural cubic spline (zero second derivative at
-                   --   the endpoints). Smooth but may overshoot.
-  | PCHIP          -- ^ Piecewise Cubic Hermite Interpolating Polynomial,
-                   --   monotone-preserving (Fritsch-Carlson 1980); avoids
-                   --   spline overshoot.
-  deriving (Show, Eq)
-
--- | Build an interpolant from observed points. The input is sorted and
--- de-duplicated internally.
---
--- Edge cases: with fewer than two points the result is constant
--- (@y_0@ for one point, @0@ for none).
---
--- >>> let f = interp1d Linear [(0,0),(1,2),(2,4)]
--- >>> f 0.5
--- 1.0
--- >>> f 1.5
--- 3.0
-interp1d :: InterpKind -> [(Double, Double)] -> (Double -> Double)
-interp1d _    []         = const 0
-interp1d _    [(_, y)]   = const y
-interp1d kind pts0       =
-  let pts = dedupe (sortBy (comparing fst) pts0)
-      xs  = U.fromList (map fst pts)
-      ys  = U.fromList (map snd pts)
-  in case kind of
-       Linear        -> linearAt xs ys
-       NaturalSpline -> naturalSplineAt xs ys
-       PCHIP         -> pchipAt xs ys
-  where
-    -- 同一 x の重複は y を平均化して 1 点にまとめる。
-    dedupe :: [(Double, Double)] -> [(Double, Double)]
-    dedupe []     = []
-    dedupe (z:zs) = go z 1 [snd z] zs
-      where
-        go (x, _) n acc [] = [(x, sum acc / fromIntegral (n :: Int))]
-        go (x, _) n acc ((x', y'):rest)
-          | abs (x' - x) < 1e-15 = go (x, 0) (n + 1) (y' : acc) rest
-          | otherwise            = (x, sum acc / fromIntegral n)
-                                 : go (x', y') 1 [y'] rest
-
--- ---------------------------------------------------------------------------
--- 共通: x が含まれる区間 [x_i, x_{i+1}] の i を二分探索
--- ---------------------------------------------------------------------------
-
--- | x の挿入位置を返す。範囲外は端 (0 or n-2) にクランプ。
-findSegment :: U.Vector Double -> Double -> Int
-findSegment xs x =
-  let n = U.length xs
-      go lo hi
-        | hi - lo <= 1 = lo
-        | otherwise    =
-            let mid = (lo + hi) `div` 2
-            in if xs U.! mid > x then go lo mid else go mid hi
-  in max 0 (min (n - 2) (go 0 (n - 1)))
-
--- ---------------------------------------------------------------------------
--- Linear
--- ---------------------------------------------------------------------------
-
-linearAt :: U.Vector Double -> U.Vector Double -> Double -> Double
-linearAt xs ys x =
-  let i  = findSegment xs x
-      x0 = xs U.! i
-      x1 = xs U.! (i + 1)
-      y0 = ys U.! i
-      y1 = ys U.! (i + 1)
-      t  = (x - x0) / (x1 - x0)
-  in y0 + t * (y1 - y0)
-
--- ---------------------------------------------------------------------------
--- Natural cubic spline (端点で y'' = 0)
--- ---------------------------------------------------------------------------
-
--- | 端点で 2 階導関数 0 の自然スプラインの 2 階導関数 m を Thomas algorithm で解く。
-naturalSplineAt :: U.Vector Double -> U.Vector Double -> Double -> Double
-naturalSplineAt xs ys =
-  let n = U.length xs
-      h = U.generate (n - 1) (\i -> xs U.! (i + 1) - xs U.! i)
-      -- 三重対角系: 内部点 i = 1 .. n-2 で
-      --   h_{i-1} m_{i-1} + 2 (h_{i-1}+h_i) m_i + h_i m_{i+1}
-      --     = 6 ( (y_{i+1}-y_i)/h_i - (y_i-y_{i-1})/h_{i-1} )
-      -- m_0 = m_{n-1} = 0 (自然境界)
-      m = solveNatural h ys
-  in \x ->
-       let i  = findSegment xs x
-           x0 = xs U.! i
-           x1 = xs U.! (i + 1)
-           y0 = ys U.! i
-           y1 = ys U.! (i + 1)
-           hi = x1 - x0
-           m0 = m U.! i
-           m1 = m U.! (i + 1)
-           a  = (x1 - x) / hi
-           b  = (x - x0) / hi
-       in a * y0 + b * y1
-        + ((a*a*a - a) * m0 + (b*b*b - b) * m1) * (hi * hi) / 6
-
--- | n 次元 m を Thomas で解く (端 m_0 = m_{n-1} = 0)。
-solveNatural :: U.Vector Double -> U.Vector Double -> U.Vector Double
-solveNatural h ys =
-  let n = U.length ys
-  in if n < 3
-       then U.replicate n 0
-       else
-         let -- 内部 (n-2) 元連立、行 i = 1..n-2 (1-indexed; 配列 indices 0..n-3)
-             k = n - 2
-             a = U.generate k (\i -> if i == 0      then 0 else h U.! i)
-             b = U.generate k (\i -> 2 * (h U.! i + h U.! (i + 1)))
-             c = U.generate k (\i -> if i == k - 1 then 0 else h U.! (i + 1))
-             d = U.generate k (\i ->
-                    let i'  = i + 1
-                        hi  = h U.! i'
-                        him = h U.! (i' - 1)
-                    in 6 * ( (ys U.! (i' + 1) - ys U.! i') / hi
-                           - (ys U.! i'       - ys U.! (i' - 1)) / him))
-             mInner = thomas a b c d
-         in U.fromList (0 : U.toList mInner ++ [0])
-
--- | 三重対角線形系 (Thomas algorithm)。
---
--- P38 (2026-05-07): the previous implementation rebuilt the @cp@ and
--- @dp@ vectors each iteration with @U.// [(i, x)]@, which is a
--- full-copy update. The forward sweep therefore ran in O(n²) — for
--- n=1000 that is 1M ops on top of the algorithm's intrinsic O(n).
--- This dominated the n=1000 NaturalSpline bench (1.72 ms vs scipy
--- LAPACK DPTSV at 0.18 ms).
---
--- Now uses a mutable Storable Vector (allowed under the project's
--- "algorithmically essential" rule for in-place updates) restoring the
--- algorithm's true O(n) complexity. Forward and backward sweeps each
--- carry the previous iteration's value through the recursion's
--- accumulator instead of indexing into the partially-built array, so
--- we only read from @a, b, c, d@ (immutable inputs) and write each
--- output cell once.
-thomas :: U.Vector Double -> U.Vector Double -> U.Vector Double
-       -> U.Vector Double -> U.Vector Double
-thomas a b c d = U.create $ do
-  let !n = U.length b
-  cp <- MU.unsafeNew n
-  dp <- MU.unsafeNew n
-  x  <- MU.unsafeNew n
-  -- Forward sweep: cp[i] = c[i] / m_i, dp[i] = (d[i] - a[i] dp[i-1]) / m_i
-  -- where m_i = b[i] - a[i] cp[i-1]. The (cprev, dprev) accumulator
-  -- lets us avoid re-reading from the mutable vectors we just wrote.
-  let forward !i !cprev !dprev
-        | i >= n    = pure ()
-        | otherwise = do
-            let !ai  = U.unsafeIndex a i
-                !bi  = U.unsafeIndex b i
-                !ci  = U.unsafeIndex c i
-                !di  = U.unsafeIndex d i
-                !m   = bi - ai * cprev
-                !cp' = ci / m
-                !dp' = (di - ai * dprev) / m
-            MU.unsafeWrite cp i cp'
-            MU.unsafeWrite dp i dp'
-            forward (i + 1) cp' dp'
-  forward 0 0 0
-  -- Backward substitution: x[n-1] = dp[n-1]; x[i] = dp[i] - cp[i] x[i+1].
-  let backward !i !xnext
-        | i < 0     = pure ()
-        | otherwise = do
-            cpi <- MU.unsafeRead cp i
-            dpi <- MU.unsafeRead dp i
-            let !xi = if i == n - 1 then dpi else dpi - cpi * xnext
-            MU.unsafeWrite x i xi
-            backward (i - 1) xi
-  backward (n - 1) 0
-  pure x
-
--- ---------------------------------------------------------------------------
--- PCHIP (Fritsch-Carlson 1980; monotone cubic Hermite)
--- ---------------------------------------------------------------------------
-
--- | PCHIP の傾き m_i を Fritsch-Carlson 法で計算してから区間ごとの 3 次 Hermite で評価。
-pchipAt :: U.Vector Double -> U.Vector Double -> Double -> Double
-pchipAt xs ys =
-  let n  = U.length xs
-      h  = U.generate (n - 1) (\i -> xs U.! (i + 1) - xs U.! i)
-      d  = U.generate (n - 1) (\i -> (ys U.! (i + 1) - ys U.! i) / (h U.! i))
-      m  = U.generate n (slopeAt h d n)
-  in \x ->
-       let i  = findSegment xs x
-           x0 = xs U.! i
-           x1 = xs U.! (i + 1)
-           y0 = ys U.! i
-           y1 = ys U.! (i + 1)
-           hi = x1 - x0
-           t  = (x - x0) / hi
-           h00 = (1 + 2*t) * (1 - t) * (1 - t)
-           h10 = t * (1 - t) * (1 - t)
-           h01 = t * t * (3 - 2*t)
-           h11 = t * t * (t - 1)
-       in h00 * y0 + h10 * hi * (m U.! i)
-        + h01 * y1 + h11 * hi * (m U.! (i + 1))
-
--- | Fritsch-Carlson 単調保存スロープ。
-slopeAt :: U.Vector Double -> U.Vector Double -> Int -> Int -> Double
-slopeAt h d n i
-  | n < 2     = 0
-  | i == 0    = endpointSlope (d U.! 0) (d U.! (min 1 (U.length d - 1)))
-                              (h U.! 0) (h U.! (min 1 (U.length h - 1)))
-  | i == n - 1 = endpointSlope (d U.! (n - 2)) (d U.! (max 0 (n - 3)))
-                               (h U.! (n - 2)) (h U.! (max 0 (n - 3)))
-  | otherwise  =
-      let dPrev = d U.! (i - 1)
-          dCur  = d U.! i
-      in if dPrev * dCur <= 0
-           then 0
-           else
-             let hPrev = h U.! (i - 1)
-                 hCur  = h U.! i
-                 w1 = 2 * hCur + hPrev
-                 w2 = hCur + 2 * hPrev
-             in (w1 + w2) / (w1 / dPrev + w2 / dCur)
-
--- | 端点の 3 点 quadratic estimate + Fritsch-Carlson の符号調整。
-endpointSlope :: Double -> Double -> Double -> Double -> Double
-endpointSlope d0 d1 h0 h1 =
-  let m = ((2 * h0 + h1) * d0 - h0 * d1) / (h0 + h1)
-  in if m * d0 <= 0
-       then 0
-       else if d0 * d1 < 0 && abs m > 3 * abs d0
-              then 3 * d0
-              else m
diff --git a/src/Hanalyze/Stat/Interpret.hs b/src/Hanalyze/Stat/Interpret.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/Interpret.hs
+++ /dev/null
@@ -1,217 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Stat.Interpret
--- Description : モデル解釈ツール (permutation importance / partial dependence / ICE)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Model interpretability tools.
---
--- Model-agnostic explanations of predictions:
---
---   * 'permutationImportance' — feature importance by random shuffling
---     (Breiman 2001).
---   * 'partialDependence' — marginal effect of a feature on predictions
---     (Friedman 2001).
---   * 'icePlot' — individual conditional expectation curves (Goldstein
---     et al. 2015).
---
--- These work on any black-box model exposed as a function
--- @predict :: [Double] -> Double@ or @[[Double]] -> [Double]@; the
--- caller is responsible for plumbing in their fitted model.
-module Hanalyze.Stat.Interpret
-  ( -- * Permutation feature importance
-    PermutationConfig (..)
-  , defaultPermutationConfig
-  , PermutationImportance (..)
-  , permutationImportance
-    -- * Partial dependence
-  , PDPResult (..)
-  , partialDependence
-    -- * Individual conditional expectation
-  , ICEResult (..)
-  , icePlot
-  ) where
-
-import qualified System.Random.MWC     as MWC
-import qualified Data.Vector           as V
-import qualified Data.Vector.Mutable   as VM
-import           Control.Monad         (forM, forM_)
-
--- ---------------------------------------------------------------------------
--- Permutation feature importance
--- ---------------------------------------------------------------------------
-
--- | Configuration for permutation importance.
-data PermutationConfig = PermutationConfig
-  { pcNRepeats :: !Int
-    -- ^ Number of times to shuffle each feature (Breiman recommends 10-30).
-  } deriving (Show, Eq)
-
--- | Default: 30 repeats.
-defaultPermutationConfig :: PermutationConfig
-defaultPermutationConfig = PermutationConfig { pcNRepeats = 30 }
-
--- | Result of permutation importance.
-data PermutationImportance = PermutationImportance
-  { piMeanImportance :: ![Double]   -- ^ Per-feature mean drop in score.
-  , piStdImportance  :: ![Double]   -- ^ Per-feature std dev across repeats.
-  , piBaselineScore  :: !Double     -- ^ Score on un-shuffled data.
-  } deriving (Show)
-
--- | Compute permutation importance for each feature.
---
--- For each feature @j@:
---
---   1. Shuffle column @j@ across rows.
---   2. Predict and compute score.
---   3. Importance @= baseline_score − shuffled_score@.
---
--- A higher score means the feature was more important.
---
--- The user supplies:
---
---   * a predict function @[[Double]] -> [Double]@,
---   * a score function comparing true vs predicted (e.g. accuracy,
---     R²; higher is better).
-permutationImportance
-  :: PermutationConfig
-  -> ([[Double]] -> [Double])     -- ^ Predict.
-  -> ([Double] -> [Double] -> Double)  -- ^ Score (true, pred -> Double).
-  -> [[Double]]                   -- ^ Test X.
-  -> [Double]                     -- ^ True y.
-  -> MWC.GenIO
-  -> IO PermutationImportance
-permutationImportance cfg predict score xs ys gen =
-  let nFeat = if null xs then 0 else length (head xs)
-      nReps = pcNRepeats cfg
-      baseline = score ys (predict xs)
-  in do
-    perFeat <- forM [0 .. nFeat - 1] $ \j -> do
-      drops <- forM [1 .. nReps] $ \_ -> do
-        xsShuffled <- shuffleColumn j xs gen
-        let predShuf = predict xsShuffled
-            scoreShuf = score ys predShuf
-        pure (baseline - scoreShuf)
-      let n     = fromIntegral nReps :: Double
-          mean  = sum drops / n
-          var   = sum [(d - mean) ^ (2 :: Int) | d <- drops]
-                  / max 1 (n - 1)
-      pure (mean, sqrt var)
-    pure PermutationImportance
-      { piMeanImportance = map fst perFeat
-      , piStdImportance  = map snd perFeat
-      , piBaselineScore  = baseline
-      }
-
--- | Shuffle column @j@ of a 2D feature matrix.
-shuffleColumn :: Int -> [[Double]] -> MWC.GenIO -> IO [[Double]]
-shuffleColumn j xs gen = do
-  let column = [row !! j | row <- xs]
-  shuffled <- shuffleList column gen
-  pure [ [if k == j then shuffled !! i else row !! k
-         | k <- [0 .. length row - 1]]
-       | (i, row) <- zip [0 ..] xs ]
-
--- ---------------------------------------------------------------------------
--- Partial dependence
--- ---------------------------------------------------------------------------
-
--- | Partial dependence plot result.
-data PDPResult = PDPResult
-  { pdpFeatureValues :: ![Double]     -- ^ Grid points for the chosen feature.
-  , pdpMeanPredict   :: ![Double]     -- ^ Mean prediction at each grid point.
-  } deriving (Show)
-
--- | Partial dependence: marginal effect of feature @j@ on prediction.
---
--- For each value @v@ on the grid:
---
---   1. Replace column @j@ with @v@ in every row of the dataset.
---   2. Predict on the modified dataset.
---   3. Average predictions to get @PD(v)@.
---
--- @
--- PD_j(v) = (1/n) Σ_i predict(replaceCol(x_i, j, v))
--- @
-partialDependence
-  :: ([[Double]] -> [Double])    -- ^ Predict.
-  -> [[Double]]                  -- ^ Background X.
-  -> Int                         -- ^ Feature index j.
-  -> [Double]                    -- ^ Grid of values for feature j.
-  -> PDPResult
-partialDependence predict xs j grid =
-  let pdAt v =
-        let xsModified = [replaceAt j v row | row <- xs]
-            preds = predict xsModified
-        in sum preds / fromIntegral (length preds)
-      means = [pdAt v | v <- grid]
-  in PDPResult
-       { pdpFeatureValues = grid
-       , pdpMeanPredict   = means
-       }
-
--- ---------------------------------------------------------------------------
--- Individual conditional expectation (ICE)
--- ---------------------------------------------------------------------------
-
--- | ICE plot result: one curve per row in the input, plus the average
--- (= partial dependence).
-data ICEResult = ICEResult
-  { iceFeatureValues :: ![Double]
-  , iceCurves        :: ![[Double]]   -- ^ Per-sample prediction curves.
-  , iceMean          :: ![Double]     -- ^ Average curve (= partial dep).
-  } deriving (Show)
-
--- | Compute ICE curves: per-sample partial-dependence-style plots.
---
--- Same as partial dependence, but instead of averaging across samples
--- we keep each sample's curve. Useful for detecting heterogeneous
--- effects (interactions).
-icePlot
-  :: ([[Double]] -> [Double])    -- ^ Predict.
-  -> [[Double]]                  -- ^ Samples (each gets its own curve).
-  -> Int                         -- ^ Feature index j.
-  -> [Double]                    -- ^ Grid of values.
-  -> ICEResult
-icePlot predict xs j grid =
-  let -- For each grid value, predict for ALL samples (with feature j replaced).
-      predsByGrid =
-        [ predict [replaceAt j v row | row <- xs]
-        | v <- grid ]
-      -- Reshape: predsByGrid[g][i] → curves[i] is [predsByGrid[g][i] for g].
-      curves =
-        [ [ predsByGrid !! g !! i | g <- [0 .. length grid - 1] ]
-        | i <- [0 .. length xs - 1] ]
-      meanCurve =
-        [ sum [predsByGrid !! g !! i | i <- [0 .. length xs - 1]]
-          / fromIntegral (length xs)
-        | g <- [0 .. length grid - 1] ]
-  in ICEResult
-       { iceFeatureValues = grid
-       , iceCurves        = curves
-       , iceMean          = meanCurve
-       }
-
--- ---------------------------------------------------------------------------
--- Internal helpers
--- ---------------------------------------------------------------------------
-
--- | Replace element at position @i@ in a list.
-replaceAt :: Int -> a -> [a] -> [a]
-replaceAt _ _ []     = []
-replaceAt 0 v (_:xs) = v : xs
-replaceAt i v (x:xs) = x : replaceAt (i - 1) v xs
-
--- | Shuffle a list (Fisher-Yates).
-shuffleList :: [a] -> MWC.GenIO -> IO [a]
-shuffleList xs gen = do
-  let n = length xs
-  v <- V.thaw (V.fromList xs)
-  forM_ [n - 1, n - 2 .. 1] $ \i -> do
-    j <- MWC.uniformR (0, i) gen
-    a <- VM.read v i
-    b <- VM.read v j
-    VM.write v i b
-    VM.write v j a
-  V.toList <$> V.freeze v
diff --git a/src/Hanalyze/Stat/KernelDist.hs b/src/Hanalyze/Stat/KernelDist.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/KernelDist.hs
+++ /dev/null
@@ -1,173 +0,0 @@
-{-# LANGUAGE StrictData #-}
--- |
--- Module      : Hanalyze.Stat.KernelDist
--- Description : BLAS を使った行列間ペアワイズ距離の高速計算
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- BLAS-backed pairwise distance helpers.
---
--- Computes the @n × n@ (or @m × n@) matrix of squared Euclidean
--- distances between rows of input matrices via the identity
---
--- @
--- ‖x_i − y_j‖² = ‖x_i‖² + ‖y_j‖² − 2 x_iᵀ y_j
--- @
---
--- The cross term @X Yᵀ@ is delegated to BLAS (GEMM via @hmatrix@), so
--- the only non-vectorized work is the per-row squared norm. List
--- traversals over @n²@ pairs are avoided.
-module Hanalyze.Stat.KernelDist
-  ( pairwiseSqDist
-  , pairwiseSqDistXY
-  , rowSqNorms
-  , diagAB
-  , rowDotsAB
-  , mapMatrix
-  , mapVector
-  ) where
-
-import qualified Numeric.LinearAlgebra        as LA
-import qualified Data.Vector.Storable         as VS
-import qualified Data.Vector.Storable.Mutable as VSM
-import           Control.Monad.ST             (runST)
-
--- | Diagonal of the matrix product @A · B@ where @A@ is @m × n@ and
--- @B@ is @n × m@, computed without forming the full @m × m@ product.
---
--- @diag(A·B)[i] = Σ_j A[i, j] · B[j, i] = Σ_j (A ⊙ Bᵀ)[i, j]@,
--- i.e. one element-wise multiply (@m × n@) plus one row-sum (GEMV
--- against a length-@n@ ones vector). Replaces the naive
--- @[A[i,:] @dot@ B[:,i] | i]@ which paid an m-times BLAS-dispatch
--- overhead. Used for GP posterior variance computation
--- (@σ² = sf − diag(K_* · K_y⁻¹ K_*ᵀ)@).
-diagAB :: LA.Matrix Double -> LA.Matrix Double -> LA.Vector Double
-diagAB a b =
-  let n    = LA.cols a
-      ones = LA.konst 1 n :: LA.Vector Double
-  in (a * LA.tr b) LA.#> ones
-{-# INLINE diagAB #-}
-
--- | Per-row dot products of two same-shape matrices.
---
--- @rowDotsAB A B[i] = Σ_j A[i, j] · B[i, j] = (A ⊙ B)[i, :] · 1@.
--- Replaces @[A[i,:] @dot@ B[i,:] | i]@ which paid an m-times BLAS
--- dispatch overhead.
-rowDotsAB :: LA.Matrix Double -> LA.Matrix Double -> LA.Vector Double
-rowDotsAB a b =
-  let n    = LA.cols a
-      ones = LA.konst 1 n :: LA.Vector Double
-  in (a * b) LA.#> ones
-{-# INLINE rowDotsAB #-}
-
--- | Squared Euclidean norm of every row of @X@. Length-@n@ vector.
---
--- Vectorised: @(X ⊙ X) · 1_p@ — one element-wise square (BLAS-friendly
--- per-element multiply) plus one GEMV. Replaces the naive
--- @[row @dot@ row | row <- toRows x]@ which paid an n-times BLAS
--- dispatch overhead on small rows.
-rowSqNorms :: LA.Matrix Double -> LA.Vector Double
-rowSqNorms x =
-  let p    = LA.cols x
-      ones = LA.konst 1 p :: LA.Vector Double
-  in (x * x) LA.#> ones
-{-# INLINE rowSqNorms #-}
-
--- | Pairwise squared distance among rows of one matrix.
---
--- @D[i, j] = ‖X[i,:] − X[j,:]‖²@ for @X@ of shape @n × p@; result is
--- @n × n@ with zeros on the diagonal (exactly).
---
--- Phase 11a (2026-05-06): rewritten with @runST@ + @MVector@. Profile
--- showed the previous massiv-fused version spent 75% of its time in
--- @trivialScheduler_@ overhead. A pure @LA.outer@-based replacement
--- was 6× /slower/ because the two @n × n@ broadcast intermediates
--- dominated allocation. The current version computes the cross term
--- with BLAS GEMM (one alloc) and fills the result @n²@ matrix with
--- a tight @runST + MVector@ loop using flat indices — single alloc,
--- no scheduler dispatch, no per-element function call. Mutable use
--- is justified: immutable was bottleneck (profile evidence) and
--- in-place fill with flat indexing is the algorithmically correct
--- representation.
-pairwiseSqDist :: LA.Matrix Double -> LA.Matrix Double
-pairwiseSqDist x =
-  let n     = LA.rows x
-      sq    = rowSqNorms x                              -- length n
-      cross = x LA.<> LA.tr x                           -- n × n, BLAS GEMM
-      crossF = LA.flatten cross                          -- length n²
-      out = runST $ do
-        v <- VSM.new (n * n)
-        let go i j
-              | i == n = pure ()
-              | j == n = go (i + 1) 0
-              | otherwise = do
-                  let sqi = sq    `VS.unsafeIndex` i
-                      sqj = sq    `VS.unsafeIndex` j
-                      cij = crossF `VS.unsafeIndex` (i * n + j)
-                      d   = if i == j
-                              then 0
-                              else let !s = sqi + sqj - 2 * cij
-                                   in if s < 0 then 0 else s
-                  VSM.unsafeWrite v (i * n + j) d
-                  go i (j + 1)
-        go 0 0
-        VS.unsafeFreeze v
-  in LA.reshape n out
-
--- | Pairwise squared distance between rows of two matrices.
---
--- @D[i, j] = ‖X[i,:] − Y[j,:]‖²@ for @X@ of shape @m × p@ and @Y@ of
--- shape @n × p@; result is @m × n@.
---
--- Phase 11a: same @runST + MVector@ rewrite as 'pairwiseSqDist'. No
--- diagonal special-case (matrices are different sources).
-pairwiseSqDistXY :: LA.Matrix Double -> LA.Matrix Double -> LA.Matrix Double
-pairwiseSqDistXY x y =
-  let m      = LA.rows x
-      n      = LA.rows y
-      sx     = rowSqNorms x
-      sy     = rowSqNorms y
-      cross  = x LA.<> LA.tr y                          -- m × n, BLAS GEMM
-      crossF = LA.flatten cross                          -- length m·n
-      out = runST $ do
-        v <- VSM.new (m * n)
-        let go i j
-              | i == m = pure ()
-              | j == n = go (i + 1) 0
-              | otherwise = do
-                  let sxi = sx     `VS.unsafeIndex` i
-                      syj = sy     `VS.unsafeIndex` j
-                      cij = crossF `VS.unsafeIndex` (i * n + j)
-                      !s  = sxi + syj - 2 * cij
-                      d   = if s < 0 then 0 else s
-                  VSM.unsafeWrite v (i * n + j) d
-                  go i (j + 1)
-        go 0 0
-        VS.unsafeFreeze v
-  in LA.reshape n out
-
--- ---------------------------------------------------------------------------
--- Element-wise helpers
--- ---------------------------------------------------------------------------
-
--- | Element-wise map over a hmatrix Matrix.
---
--- Implementation: flatten + 'VS.map' + reshape. The earlier massiv
--- ('A.map' with @Comp = Seq@) version was ~1.7× faster than 'LA.cmap'
--- on a single 2000×2000 call, but iterative paths (GP HP loop, GLM
--- IRLS) call this many times per fit and the per-call
--- @trivialScheduler_@ overhead dominated — profile attributed
--- 10–16% of GP fit time and 4% of GLM IRLS time to scheduler
--- bookkeeping. Direct 'VS.map' has zero scheduling overhead and is
--- the right default here.
-{-# INLINE mapMatrix #-}
-mapMatrix :: (Double -> Double) -> LA.Matrix Double -> LA.Matrix Double
-mapMatrix f m =
-  let cs = LA.cols m
-  in LA.reshape cs (VS.map f (LA.flatten m))
-
--- | Element-wise map over a hmatrix Vector. Direct 'VS.map'; see
--- 'mapMatrix' for why we no longer route through massiv.
-{-# INLINE mapVector #-}
-mapVector :: (Double -> Double) -> LA.Vector Double -> LA.Vector Double
-mapVector = VS.map
diff --git a/src/Hanalyze/Stat/MCMC.hs b/src/Hanalyze/Stat/MCMC.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/MCMC.hs
+++ /dev/null
@@ -1,316 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Stat.MCMC
--- Description : MCMC チェーンの純粋な後処理 (自己相関・HDI・ESS・R-hat・KDE・BFMI)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Pure post-processing utilities for MCMC chains.
---
--- Provides autocorrelation, highest-density intervals (HDI), effective
--- sample size (Geyer's initial monotone sequence estimator), split-R-hat
--- (Vehtari et al. 2021), kernel density estimation (Silverman bandwidth)
--- and BFMI. Operates on raw @Vector@ samples or on the 'Hanalyze.MCMC.Core.Chain'
--- type from the sampler layer.
-module Hanalyze.Stat.MCMC
-  ( autocorr
-  , hdi
-  , ess
-  , essBulk
-  , rhat
-  , kde
-  , bfmi
-  , rankHist
-  ) where
-
-import Control.Monad (when)
-import Control.Monad.ST (runST)
-import Data.Function (on)
-import Data.List (groupBy, minimumBy, sort, sortBy)
-import Data.Ord  (comparing)
-import qualified Data.Vector as V
-import qualified Data.Vector.Unboxed as VU
-import qualified Data.Vector.Unboxed.Mutable as VUM
-import qualified Statistics.Distribution as SD
-import Statistics.Distribution.Normal (standard)
-
--- | Autocorrelation at lags 0 .. min(maxLag, n-1).
--- Uses O(n × maxLag) time with Vector indexing.
-autocorr :: Int -> [Double] -> [(Int, Double)]
-autocorr maxLag xs =
-  let v   = V.fromList xs
-      n   = V.length v
-      mu  = V.sum v / fromIntegral n
-      var = V.sum (V.map (\x -> (x - mu) ^ (2 :: Int)) v) / fromIntegral n
-      acf k
-        | var == 0 || k >= n = 0
-        | otherwise =
-            V.sum (V.zipWith (\a b -> (a - mu) * (b - mu))
-                             (V.take (n - k) v)
-                             (V.drop k      v))
-            / (fromIntegral (n - k) * var)
-  in [(k, acf k) | k <- [0 .. min maxLag (n - 1)]]
-
--- | Highest density interval: shortest contiguous interval that covers
--- @level@ fraction of the (sorted) samples. Returns (lower, upper).
-hdi :: Double -> [Double] -> (Double, Double)
-hdi level xs
-  | null xs   = (0, 0)
-  | otherwise =
-      let sorted  = V.fromList (sort xs)
-          n       = V.length sorted
-          window  = max 1 (min (n - 1) (floor (level * fromIntegral n) :: Int))
-          (_, i)  = minimumBy (comparing fst)
-                      [ (sorted V.! (i' + window) - sorted V.! i', i')
-                      | i' <- [0 .. n - window - 1] ]
-      in (sorted V.! i, sorted V.! (i + window))
-
--- | Effective sample size via Geyer's initial monotone sequence estimator.
--- Returns n when the chain is too short to estimate.
-ess :: [Double] -> Double
-ess xs
-  | n < 4     = fromIntegral n
-  | otherwise =
-      let acs    = map snd (autocorr (n `div` 2) xs)
-          -- Gamma(k) = rho(2k) + rho(2k+1)
-          gammas = pairSums acs
-          -- Monotone non-increasing sequence of Gamma
-          monoG  = scanl1 min gammas
-          posG   = takeWhile (> 0) monoG
-          tau    = max 1 (-1 + 2 * sum posG)
-      in fromIntegral n / tau
-  where
-    n = length xs
-    pairSums (a : b : rest) = (a + b) : pairSums rest
-    pairSums _              = []
-
--- | arviz / Stan 互換の rank-normalized **bulk ESS** (Vehtari et al. 2021)。
---
--- 引数は 'rhat' と同じ「パラメータ 1 つの chain ごとの sample 列」。手順は
--- arviz の @ess(method="bulk")@ と同一:
---
--- 1. 各 chain を半分に split (奇数長は中央 1 点を落とす) して 2M 本の
---    sub-chain にする
--- 2. 全値プールの平均 rank (同値は平均) を
---    @(r − 3\/8) \/ (S + 1\/4)@ で (0,1) に写し Φ⁻¹ で z 化 (rank 正規化)
--- 3. 多 chain 結合自己相関 @ρ̂_t = 1 − (W − mean acov_t) \/ var⁺@ に
---    Geyer の initial positive + monotone sequence を適用し
---    @τ̂ = −1 + 2Σρ̂@ (下限 @1\/log₁₀(MN)@)、@ESS = MN \/ τ̂@
---
--- 単 chain の 'ess' (Geyer IMSE・τ 下限 1 クランプで n 頭打ち) と異なり
--- 多 chain 情報と rank 正規化で裾の重い分布でも安定し、PyMC / arviz の
--- @ess_bulk@ と数値比較できる (Phase 92 B4: bench の指標非対称の是正)。
--- chain が短すぎるとき (split 後 4 draw 未満・arviz は NaN を返す領域) は
--- フォールバックとして元の総 draw 数を返す。
-essBulk :: [[Double]] -> Double
-essBulk chains
-  | m < 1 || n < 4 = fromIntegral (sum (map length nonEmpty))  -- 元の総 draw 数
-  | otherwise      = essMultiChain (rankNormalize sub)
-  where
-    nonEmpty = filter (not . null) chains
-    -- arviz _split_chains: 前半 floor(n/2) + 後半 floor(n/2) (奇数長は中央落ち)
-    splitOne vs = let h = length vs `div` 2
-                  in [take h vs, drop (length vs - h) vs]
-    sub0 = concatMap splitOne nonEmpty
-    n    = if null sub0 then 0 else minimum (map length sub0)
-    sub  = map (take n) sub0
-    m    = length sub
-
--- | rank 正規化 (arviz @_z_scale@): 全 chain プールの平均 rank →
--- @(r − 3\/8)\/(S + 1\/4)@ → 標準正規の分位関数。chain 構造は保存する。
-rankNormalize :: [[Double]] -> [[Double]]
-rankNormalize chains = rechunk (map length chains) (map z ranks)
-  where
-    flat  = concat chains
-    s     = fromIntegral (length flat) :: Double
-    ranks = averageRanks flat
-    z r   = SD.quantile standard ((r - 3 / 8) / (s + 0.25))
-    rechunk []           _  = []
-    rechunk (len : lens) xs = let (h, t) = splitAt len xs in h : rechunk lens t
-
--- | 同値を平均 rank (scipy @rankdata(method="average")@ 相当) にした
--- 1-based rank を入力順で返す。
-averageRanks :: [Double] -> [Double]
-averageRanks xs = map snd (sortBy (comparing fst) ranked)
-  where
-    byVal  = sortBy (comparing snd) (zip [0 :: Int ..] xs)
-    groups = groupBy ((==) `on` snd) byVal
-    ranked = go 0 groups
-    go _ [] = []
-    go pos (g : gs) =
-      let k   = length g
-          -- ranks pos+1 .. pos+k の平均
-          avg = fromIntegral (2 * pos + k + 1) / 2 :: Double
-      in [ (i, avg) | (i, _) <- g ] ++ go (pos + k) gs
-
--- | 多 chain 結合 ESS (arviz @_ess@ の忠実な移植)。入力 = z 化済み等長
--- sub-chain 群。
-essMultiChain :: [[Double]] -> Double
-essMultiChain sub
-  | isNaN varPlus || varPlus <= 0 = sTotal
-  | otherwise = runST $ do
-      rhoT <- VUM.replicate n 0
-      VUM.write rhoT 0 1
-      let rho1 = rho 1
-      VUM.write rhoT 1 rho1
-      -- Geyer initial positive sequence (ペア和が正の間だけ採用)
-      let goPos t rhoEven rhoOdd
-            | t < n - 3 && rhoEven + rhoOdd > 0 = do
-                let re = rho (t + 1)
-                    ro = rho (t + 2)
-                when (re + ro >= 0) $ do
-                  VUM.write rhoT (t + 1) re
-                  VUM.write rhoT (t + 2) ro
-                goPos (t + 2) re ro
-            | otherwise = pure (t, rhoEven)
-      (tEnd, lastEven) <- goPos 1 1.0 rho1
-      let maxT = tEnd - 2
-      when (lastEven > 0 && maxT + 1 < n) $
-        VUM.write rhoT (maxT + 1) lastEven
-      -- Geyer initial monotone sequence (ペア和を非増加に均す)
-      let goMono t
-            | t <= maxT - 2 = do
-                a <- VUM.read rhoT (t - 1)
-                b <- VUM.read rhoT t
-                c <- VUM.read rhoT (t + 1)
-                d <- VUM.read rhoT (t + 2)
-                when (c + d > a + b) $ do
-                  VUM.write rhoT (t + 1) ((a + b) / 2)
-                  VUM.write rhoT (t + 2) ((a + b) / 2)
-                goMono (t + 2)
-            | otherwise = pure ()
-      goMono 1
-      frozen <- VU.unsafeFreeze rhoT
-      let tauRaw = -1 + 2 * VU.sum (VU.take (maxT + 1) frozen)
-                      + (if maxT + 1 < n then frozen VU.! (maxT + 1) else 0)
-          tau    = max tauRaw (1 / logBase 10 sTotal)
-      pure (sTotal / tau)
-  where
-    m      = length sub
-    n      = length (head sub)
-    sTotal = fromIntegral (m * n)
-    acovs  = map (autocovBiased . V.fromList) sub
-    chainMeans = map (\vs -> sum vs / fromIntegral n) sub
-    meanAcov t = sum (map (V.! t) acovs) / fromIntegral m
-    meanVar = meanAcov 0 * fromIntegral n / fromIntegral (n - 1)
-    varPlus = meanVar * fromIntegral (n - 1) / fromIntegral n
-            + (if m > 1 then sampleVar chainMeans else 0)
-    rho t   = 1 - (meanVar - meanAcov t) / varPlus
-    sampleVar vs =
-      let mu = sum vs / fromIntegral (length vs)
-      in sum [ (x - mu) ^ (2 :: Int) | x <- vs ] / fromIntegral (length vs - 1)
-
--- | biased 自己共分散 (分母 n・arviz @_autocov@ と同じ規約) を lag 0..n-1 で。
-autocovBiased :: V.Vector Double -> V.Vector Double
-autocovBiased v = V.generate nn at
-  where
-    nn = V.length v
-    mu = V.sum v / fromIntegral nn
-    c  = V.map (subtract mu) v
-    at t = V.sum (V.zipWith (*) (V.take (nn - t) c) (V.drop t c))
-           / fromIntegral nn
-
--- | Split-R-hat convergence diagnostic (Vehtari et al. 2021).
---
--- Splits each chain in half to obtain @2M@ sub-chains, then computes
--- R-hat from the between-chain variance @B@ and within-chain variance
--- @W@. The conventional convergence threshold is @R-hat < 1.01@.
--- The argument is the per-chain sample list for a single parameter.
--- Returns 'Nothing' when there are fewer than 2 chains or fewer than 4
--- samples per chain.
-rhat :: [[Double]] -> Maybe Double
-rhat chains
-  | m < 2 || n < 4 = Nothing
-  | w == 0         = Nothing
-  | otherwise      = Just (sqrt (varPlus / w))
-  where
-    allVals   = filter (not . null) chains
-    splitOne vs = let half = length vs `div` 2
-                  in [take half vs, drop half vs]
-    subchains = concatMap splitOne allVals
-    m         = length subchains
-    n         = minimum (map length subchains)
-    trimmed   = map (take n) subchains
-    mean_ vs  = sum vs / fromIntegral (length vs)
-    chainMeans = map mean_ trimmed
-    grandMean  = mean_ chainMeans
-    b = fromIntegral n / fromIntegral (m - 1)
-        * sum (map (\mu -> (mu - grandMean) ^ (2 :: Int)) chainMeans)
-    chainVars = map (\vs -> let mu = mean_ vs
-                            in sum (map (\x -> (x - mu) ^ (2 :: Int)) vs)
-                               / fromIntegral (n - 1)) trimmed
-    w       = mean_ chainVars
-    varPlus = fromIntegral (n - 1) / fromIntegral n * w + b / fromIntegral n
-
--- | Kernel density estimation (Gaussian kernel, Silverman bandwidth).
---
--- Returns @nPoints@ pairs of @(x, density)@. With fewer than two samples
--- the returned list is empty. The grid spans @[min - 3σ, max + 3σ]@.
-kde :: Int -> [Double] -> [(Double, Double)]
-kde nPoints xs
-  | length xs < 2 = []
-  | sig <= 0      = []
-  | otherwise     = [(x, density x) | x <- grid]
-  where
-    n    = length xs
-    mu   = sum xs / fromIntegral n
-    var  = sum (map (\x -> (x - mu) ^ (2 :: Int)) xs) / fromIntegral (n - 1)
-    sig  = sqrt var
-    h    = 1.06 * sig * fromIntegral n ** (-0.2)   -- Silverman's rule
-    lo   = minimum xs - 3 * sig
-    hi   = maximum xs + 3 * sig
-    step = (hi - lo) / fromIntegral (nPoints - 1)
-    grid = [lo + fromIntegral i * step | i <- [0 .. nPoints - 1 :: Int]]
-    kernel u = exp (-0.5 * u * u) / sqrt (2 * pi)
-    density x = sum [kernel ((x - xi) / h) | xi <- xs]
-                / (fromIntegral n * h)
-
--- | Bayesian Fraction of Missing Information (Betancourt 2016).
---
--- @
--- BFMI = E[(E_n − E_{n−1})²] / Var(E)
--- @
---
--- Computed from the energy sequence (Hamiltonian per iteration) of an
--- HMC/NUTS run. Values below 0.3 indicate that momentum resampling is
--- not exploring the posterior tails (consider reparameterization — the
--- canonical example is Neal's funnel). Values above 0.3 are healthy;
--- PyMC commonly uses 0.5 as a reference threshold.
-bfmi :: [Double] -> Maybe Double
-bfmi es
-  | length es < 4 = Nothing
-  | varE == 0     = Nothing
-  | otherwise     = Just (numer / varE)
-  where
-    n        = length es
-    mu       = sum es / fromIntegral n
-    varE     = sum (map (\x -> (x - mu) ^ (2 :: Int)) es)
-               / fromIntegral (n - 1)
-    diffs    = zipWith (-) (drop 1 es) es
-    numer    = sum (map (\d -> d * d) diffs)
-               / fromIntegral (length diffs)
-
--- | Rank-normalized per-chain histogram counts (PyMC @plot_rank@ の素材・
--- Vehtari et al. 2021)。 全 chain をプールした値に昇順 rank (1..n) を振り、
--- chain ごとに @nBins@ 個のビンへ振り分けた **ビンごとのカウント** を返す。
--- 返り値は chain ごとの長さ @nBins@ のカウント列 (= @[[count]]@・入力 chain 順)。
--- 収束時は各 chain の rank 分布が一様 (= どのビンもほぼ同数) に近づく。
---
--- ビン境界は Viz/Plot 両経路で共有するためここに一元化する (二重実装を避ける)。
-rankHist :: Int -> [[Double]] -> [[Int]]
-rankHist nBins perChain =
-  [ [ length (filter (== b) (chainBins c)) | b <- [0 .. nBins - 1] ]
-  | c <- [0 .. nCh - 1] ]
-  where
-    nCh       = length perChain
-    flat      = [ (cid, v) | (cid, vs) <- zip [0 :: Int ..] perChain, v <- vs ]
-    n         = length flat
-    -- 値昇順に rank 1..n を振り、 元 (flat) 順序へ戻す
-    ranked    = zipWith (\rk (oi, _) -> (oi, rk))
-                        [1 :: Int ..]
-                        (sortBy (comparing (snd . snd)) (zip [0 :: Int ..] flat))
-    rankByIdx = map snd (sortBy (comparing fst) ranked)   -- flat 順の rank
-    binSize   = max 1 (n `div` nBins)
-    binOf r   = min (nBins - 1) ((r - 1) `div` binSize)
-    chainSeq  = map fst flat
-    chainBins c = [ binOf r | (cid, r) <- zip chainSeq rankByIdx, cid == c ]
diff --git a/src/Hanalyze/Stat/MDS.hs b/src/Hanalyze/Stat/MDS.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/MDS.hs
+++ /dev/null
@@ -1,144 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Stat.MDS
--- Description : 多次元尺度構成法 (古典 MDS / Sammon MDS)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Multidimensional Scaling (MDS).
---
--- * Classical MDS (Torgerson) — 距離行列を二重中心化 → 固有分解 → 上位 k 成分。
--- * Sammon MDS — Sammon stress を勾配降下で最小化 (古典 MDS を初期値)。
---
--- @
--- import qualified Hanalyze.Stat.MDS as MDS
--- let d  = MDS.euclideanDist x                -- x :: Matrix Double (n × p)
---     emb = MDS.mdsClassical d 2              -- 2-D 埋め込み (n × 2)
--- @
-module Hanalyze.Stat.MDS
-  ( euclideanDist
-  , mdsClassical
-  , mdsSammon
-  , sammonStress
-  , SammonConfig (..)
-  , defaultSammonConfig
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-
--- ---------------------------------------------------------------------------
--- Distance matrix helper
--- ---------------------------------------------------------------------------
-
--- | n × p のデータ行列から n × n のユークリッド距離行列を作る。
-euclideanDist :: LA.Matrix Double -> LA.Matrix Double
-euclideanDist x =
-  let !n = LA.rows x
-      row i = LA.flatten (x LA.? [i])
-      dij i j = LA.norm_2 (row i - row j)
-  in LA.build (n, n) (\i j -> dij (round i) (round j))
-
--- ---------------------------------------------------------------------------
--- Classical MDS (Torgerson)
--- ---------------------------------------------------------------------------
-
--- | 距離行列 D (n × n) を k 次元埋め込み (n × k) に。
---
--- B = -1/2 · H · D² · H、 H = I - 1/n · 11ᵀ。 B = V Λ Vᵀ から
--- 正の上位 k 成分のみ抽出して X = V_k √Λ_k。
-mdsClassical :: LA.Matrix Double  -- ^ 距離行列 D (n × n)。
-             -> Int                -- ^ 目的次元 k。
-             -> LA.Matrix Double  -- ^ 埋め込み (n × k)。
-mdsClassical d k =
-  let !n   = LA.rows d
-      !d2  = d * d
-      ones = LA.konst 1 (n, n) :: LA.Matrix Double
-      h    = LA.ident n - LA.scale (1 / fromIntegral n) ones
-      b    = LA.scale (-0.5) (h LA.<> d2 LA.<> h)
-      -- 対称化 (数値誤差吸収)
-      bSym = LA.scale 0.5 (b + LA.tr b)
-      (eigVals, eigVecs) = LA.eigSH (LA.trustSym bSym)
-      -- 降順 (hmatrix eigSH)。 正かつ上位 k を採用。
-      lamList = LA.toList eigVals
-      take_   = min k n
-      lamPos  = [ if v > 0 then v else 0 | v <- take take_ lamList ]
-      sqrtL   = LA.diag (LA.fromList (map sqrt lamPos))
-      vK      = eigVecs LA.¿ [0 .. take_ - 1]
-  in vK LA.<> sqrtL
-
--- ---------------------------------------------------------------------------
--- Sammon MDS
--- ---------------------------------------------------------------------------
-
-data SammonConfig = SammonConfig
-  { sammonMaxIter :: !Int
-  , sammonLR      :: !Double   -- ^ 学習率。
-  , sammonTol     :: !Double   -- ^ stress 改善の許容下限。
-  } deriving (Show)
-
-defaultSammonConfig :: SammonConfig
-defaultSammonConfig = SammonConfig
-  { sammonMaxIter = 300
-  , sammonLR      = 0.3
-  , sammonTol     = 1e-6
-  }
-
--- | Sammon stress E = (1/c) Σ_{i<j} (δ_ij - d_ij)² / δ_ij
---   ただし δ_ij は元距離、 d_ij は埋め込み距離、 c = Σ_{i<j} δ_ij。
-sammonStress :: LA.Matrix Double  -- ^ 元距離行列 (n × n)。
-             -> LA.Matrix Double  -- ^ 埋め込み (n × k)。
-             -> Double
-sammonStress d y =
-  let !n = LA.rows d
-      row i = LA.flatten (y LA.? [i])
-      pairs = [ (i, j) | i <- [0 .. n - 1], j <- [i + 1 .. n - 1] ]
-      delta i j = LA.atIndex d (i, j)
-      dij i j = LA.norm_2 (row i - row j)
-      cTot  = sum [ delta i j | (i, j) <- pairs ]
-      num   = sum [ let !del = delta i j
-                        !dd  = dij i j
-                    in if del > 0 then (del - dd)^(2 :: Int) / del
-                                  else 0
-                  | (i, j) <- pairs ]
-  in if cTot > 0 then num / cTot else 0
-
--- | Sammon MDS。 古典 MDS を初期値にして勾配降下。
-mdsSammon :: SammonConfig
-          -> LA.Matrix Double  -- ^ 距離行列 D (n × n)。
-          -> Int                -- ^ 目的次元 k。
-          -> LA.Matrix Double  -- ^ 埋め込み (n × k)。
-mdsSammon cfg d k =
-  let !y0 = mdsClassical d k
-      loop !y !iter !prevE
-        | iter >= sammonMaxIter cfg = y
-        | otherwise =
-            let !grad = sammonGrad d y
-                !y'   = y - LA.scale (sammonLR cfg) grad
-                !e'   = sammonStress d y'
-            in if abs (prevE - e') < sammonTol cfg
-                 then y'
-                 else loop y' (iter + 1) e'
-  in loop y0 0 (sammonStress d y0)
-
--- | Sammon stress の勾配 (n × k)。
-sammonGrad :: LA.Matrix Double -> LA.Matrix Double -> LA.Matrix Double
-sammonGrad d y =
-  let !n = LA.rows d
-      !k = LA.cols y
-      row i = LA.flatten (y LA.? [i])
-      delta i j = LA.atIndex d (i, j)
-      cTot = sum [ delta i j | i <- [0 .. n - 1]
-                             , j <- [i + 1 .. n - 1] ]
-      scl = if cTot > 0 then 2 / cTot else 0
-      gradRow i =
-        let yi = row i
-            contribs = [ let yj = row j
-                             dij = LA.norm_2 (yi - yj)
-                             del = delta i j
-                         in if del > 0 && dij > 0
-                              then LA.scale ((del - dij) / (del * dij))
-                                     (yi - yj)
-                              else LA.konst 0 k
-                       | j <- [0 .. n - 1], j /= i ]
-        in LA.scale (negate scl) (sum contribs)
-  in LA.fromRows [ gradRow i | i <- [0 .. n - 1] ]
diff --git a/src/Hanalyze/Stat/ModelSelect.hs b/src/Hanalyze/Stat/ModelSelect.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/ModelSelect.hs
+++ /dev/null
@@ -1,460 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- |
--- Module      : Hanalyze.Stat.ModelSelect
--- Description : MCMC ベースのモデル比較基準 (WAIC / PSIS-LOO / pseudo-BMA)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- MCMC-based model comparison criteria.
---
--- Provides WAIC (Widely Applicable Information Criterion) and PSIS-LOO
--- (Pareto-Smoothed Importance Sampling LOO-CV), plus a @pm.compare@-style
--- weighting facility (pseudo-BMA / stacking).
---
--- References:
---
--- * Watanabe (2010) — WAIC.
--- * Vehtari, Gelman, Gabry (2017) — PSIS-LOO.
--- * Hosking & Wallis (1987) — generalized Pareto moment estimator.
---
--- @
--- let logLikMat = chainLogLikMatrix model chain  -- [[Double]]
--- print (waic logLikMat)
--- print (loo  logLikMat)
--- @
-module Hanalyze.Stat.ModelSelect
-  ( -- * WAIC
-    WAICResult (..)
-  , waic
-  , chainWAIC
-    -- * LOO-CV (PSIS)
-  , LOOResult (..)
-  , loo
-  , chainLOO
-    -- * Utilities
-  , chainLogLikMatrix
-    -- * LM / GLM posterior sampling (for WAIC / LOO-CV)
-  , lmPosteriorLogLiks
-  , glmPosteriorLogLiks
-  , lmePosteriorLogLiks
-    -- * Model-comparison weights (PyMC @pm.compare@ analogue)
-  , CompareEntry (..)
-  , CompareResult (..)
-  , compareModels
-  ) where
-
-import Control.Monad (replicateM)
-import Data.List (sort, transpose)
-import qualified Numeric.LinearAlgebra as LA
-import qualified Data.Vector.Storable as VS
-import qualified Data.Vector.Algorithms.Intro as VAI
-import System.Random.MWC (GenIO)
-import System.Random.MWC.Distributions (normal)
-
-import Hanalyze.Model.Core (FitResult (..), coefficientsV, residualsV)
-import Hanalyze.Model.GLM  (Family (..), LinkFn (..))
-import Hanalyze.Model.HBM  (ModelP, perObsLogLiks)
-import Hanalyze.MCMC.Core  (Chain, chainSamples)
-import qualified Hanalyze.Stat.Distribution as Dist
-
--- ---------------------------------------------------------------------------
--- 結果型
--- ---------------------------------------------------------------------------
-
--- | WAIC result.
-data WAICResult = WAICResult
-  { waicValue :: Double  -- ^ @WAIC = −2(lppd − p_waic)@; smaller is better.
-  , waicLppd  :: Double  -- ^ Log pointwise predictive density.
-  , waicPwaic :: Double  -- ^ Effective number of parameters @p_waic@.
-  , waicSE    :: Double  -- ^ Estimated standard error of @WAIC@.
-  } deriving (Show)
-
--- | PSIS-LOO result.
-data LOOResult = LOOResult
-  { looValue   :: Double    -- ^ @−2 × elpd_loo@; smaller is better.
-  , looElpd    :: Double    -- ^ @Σᵢ elpd_i@ (expected log predictive density).
-  , looSE      :: Double    -- ^ Standard error of @looValue@.
-  , looKHat    :: [Double]  -- ^ Per-observation Pareto @k̂@; @< 0.5@ good,
-                            --   @0.5–0.7@ acceptable, @> 0.7@ flag.
-  , looKHatBad :: Int       -- ^ Number of observations with @k̂ > 0.7@.
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- WAIC
--- ---------------------------------------------------------------------------
-
--- | Compute WAIC from a log-likelihood matrix.
---
--- @logLikMat !! s !! i = log p(y_i | θ^s)@: rows are @S@ posterior
--- samples, columns are @N@ observations.
---
--- Internally builds an @S × N@ hmatrix matrix once and computes the
--- per-column @logSumExp@ and sample variance via Storable-Vector
--- folds. Replaces the previous @transpose [[Double]] + map@
--- formulation, which allocated @S × N@ list cells just to flip the
--- shape.
-waic :: [[Double]] -> WAICResult
-waic [] = WAICResult 0 0 0 0
-waic logLikMat =
-  let mat  = LA.fromLists logLikMat       -- S × N
-      sN   = LA.rows mat
-      s    = fromIntegral sN :: Double
-      cols = LA.toColumns mat              -- N storable vectors of length S
-      n    = length cols
-
-      lppd_i  = map (\c -> logSumExpVS c - log s) cols
-      lppd    = sum lppd_i
-      pwaic_i = map sampleVarVS cols
-      pwaic   = sum pwaic_i
-      waicVal = -2 * (lppd - pwaic)
-
-      contrib = zipWith (\l p -> -2 * (l - p)) lppd_i pwaic_i
-      se      = sqrt (fromIntegral n * sampleVar contrib)
-
-  in WAICResult waicVal lppd pwaic se
-  -- Note: tested 'LA.tr mat + LA.toRows' to get contiguous Storable
-  -- slices for per-row (= per-observation) folds, but the transpose
-  -- allocation outweighed the cache benefit at @S=1000, N=200@. The
-  -- 'toColumns' path stays ~12 ms; transpose path measured ~13.4 ms.
-  -- arviz's @az.waic@ at 6.3 ms benefits from numpy axis-reductions
-  -- and SIMD @exp@ that we cannot match without FFI.
-
--- | logSumExp over a Storable Vector. @m + log Σ exp(x - m)@ for
--- numerical stability.
-logSumExpVS :: LA.Vector Double -> Double
-logSumExpVS v
-  | VS.null v = -1/0
-  | otherwise =
-      let m = VS.maximum v
-      in m + log (VS.sum (VS.map (\x -> exp (x - m)) v))
-
--- | Sample variance (divisor @n - 1@) over a Storable Vector.
-sampleVarVS :: LA.Vector Double -> Double
-sampleVarVS v
-  | VS.length v < 2 = 0
-  | otherwise =
-      let nD = fromIntegral (VS.length v) :: Double
-          mu = VS.sum v / nD
-          ss = VS.sum (VS.map (\x -> (x - mu) * (x - mu)) v)
-      in ss / (nD - 1)
-
--- ---------------------------------------------------------------------------
--- LOO-CV (PSIS)
--- ---------------------------------------------------------------------------
-
--- | Compute PSIS-LOO from a log-likelihood matrix.
---
--- For each observation, importance weights are smoothed by a Pareto
--- distribution; this returns the truncated-IS LOO estimate together with
--- the diagnostic Pareto @k̂@.
-loo :: [[Double]] -> LOOResult
-loo [] = LOOResult 0 0 0 [] 0
-loo logLikMat =
-  -- Mirrors 'waic': @S × N@ hmatrix matrix once, then per-column
-  -- 'psisElpdV' on Storable Vectors. Avoids the @transpose [[Double]]@
-  -- (S × N list-cell allocation) and the per-column list ops in the
-  -- old 'psisElpd'.
-  let mat     = LA.fromLists logLikMat   -- S × N
-      s       = LA.rows mat
-      cols    = LA.toColumns mat
-      n       = length cols
-      results = map (psisElpdV s) cols
-      elpd_i  = map fst results
-      khat_i  = map snd results
-      elpd    = sum elpd_i
-      looVal  = -2 * elpd
-      se      = sqrt (fromIntegral n * sampleVar elpd_i)
-      nBad    = length (filter (> 0.7) khat_i)
-  in LOOResult looVal elpd se khat_i nBad
-
--- | PSIS estimate for a single observation: @(elpd_i, k̂_i)@.
---
--- Algorithm:
---
---   1. Compute log importance weights @log r_i^s = −log p(y_i|θ^s)@.
---   2. Fit a Pareto @k̂@ to the top @M = min(S/5, 3√S)@ values.
---   3. Truncate weights at @log √S@ and renormalize for stability.
---   4. @elpd_i = logSumExp(log W_s + log p(y_i|θ^s))@.
-psisElpd :: Int -> [Double] -> (Double, Double)
-psisElpd s colLL = psisElpdV s (VS.fromList colLL)
-
--- | Storable-Vector version of 'psisElpd'. Internal hot path used by
--- 'loo'. All steps stay on @VS.Vector Double@: no @[Double]@
--- intermediates, sort via 'Data.Vector.Algorithms.Intro' on a
--- mutable Storable buffer.
-psisElpdV :: Int -> VS.Vector Double -> (Double, Double)
-psisElpdV s colLL =
-  let logR = VS.map negate colLL
-      m    = max 5 (min (s `div` 5)
-                        (floor (3 * sqrt (fromIntegral s :: Double))))
-      sortedLogR = VS.modify VAI.sort logR    -- ascending
-      topM       = VS.drop (s - m) sortedLogR
-      khat       = paretoKhatV topM
-
-      logCap  = 0.5 * log (fromIntegral s :: Double)
-      capped  = VS.map (min logCap) logR
-      logZ    = logSumExpVS capped
-      logW    = VS.map (\r -> r - logZ) capped
-
-      elpdi   = logSumExpVS (VS.zipWith (+) logW colLL)
-  in (elpdi, khat)
-
--- | Estimate the Pareto shape @k̂@ from the top-@M@ log-weights
--- (ascending).
---
--- Uses the Hosking-Wallis (1987) moment estimator:
---
--- @
--- excess = exp(r − u) − 1   (u = lower threshold)
--- k̂      = 0.5 × (1 − μ² / s²)   where  μ = mean excess, s² = Var excess
--- @
-paretoKhat :: [Double] -> Double
-paretoKhat topM = paretoKhatV (VS.fromList topM)
-
--- | Storable-Vector version of 'paretoKhat'.
-paretoKhatV :: VS.Vector Double -> Double
-paretoKhatV topM
-  | VS.length topM < 5 = 0
-  | otherwise =
-      let u      = topM VS.! 0
-          excess = VS.map (\r -> exp (r - u) - 1) topM
-          mu     = VS.sum excess / fromIntegral (VS.length excess)
-          var    = sampleVarVS excess
-      in if var <= 0 || mu <= 0 then 0
-         else 0.5 * (1 - mu ^ (2 :: Int) / var)
-
--- ---------------------------------------------------------------------------
--- Chain との連携
--- ---------------------------------------------------------------------------
-
--- | Build a log-likelihood matrix from a model and a chain.
--- Rows are post-burnin samples, columns are observations.
-chainLogLikMatrix :: ModelP r -> Chain -> [[Double]]
-chainLogLikMatrix model chain = map (perObsLogLiks model) (chainSamples chain)
-
--- | Compute WAIC directly from a model and chain.
-chainWAIC :: ModelP r -> Chain -> WAICResult
-chainWAIC model = waic . chainLogLikMatrix model
-
--- | Compute PSIS-LOO directly from a model and chain.
-chainLOO :: ModelP r -> Chain -> LOOResult
-chainLOO model = loo . chainLogLikMatrix model
-
--- ---------------------------------------------------------------------------
--- LM / GLM 事後サンプリング (WAIC/LOO-CV 用)
--- ---------------------------------------------------------------------------
-
--- | Generate an @S × N@ log-likelihood matrix from a flat-prior LM
--- posterior.
---
--- Sampling scheme:
---
--- @
--- σ² ~ InvGamma((n−p)/2, RSS/2)   (drawn as RSS / χ²_{n-p})
--- β  ~ MVN(β̂,  σ² (X'X)⁻¹)
--- log p(y_i | β^s, σ^s) = log N(y_i; x_i·β^s, σ^s)
--- @
-lmPosteriorLogLiks
-  :: LA.Matrix Double  -- ^ Design matrix @X@ (@n×p@).
-  -> LA.Vector Double  -- ^ Response @y@ (length @n@).
-  -> FitResult         -- ^ OLS fit result.
-  -> Int               -- ^ Number of posterior samples @S@.
-  -> GenIO
-  -> IO [[Double]]
-lmPosteriorLogLiks x y fr s gen = do
-  let n      = LA.rows x
-      p      = LA.cols x
-      df'    = n - p
-      beta0  = coefficientsV fr
-      rss    = let resV = residualsV fr in LA.dot resV resV
-      xtxInv = LA.inv (LA.tr x LA.<> x)
-      rChol  = LA.chol (LA.trustSym xtxInv)
-      lChol  = LA.tr rChol
-  replicateM s $ do
-    chi2Vals <- replicateM df' (normal 0 1 gen)
-    let chi2  = sum (map (^(2::Int)) chi2Vals)
-        sigma = sqrt (rss / chi2)
-    zVec <- fmap LA.fromList (replicateM p (normal 0 1 gen))
-    let betaSamp = beta0 + LA.scale sigma (lChol LA.#> zVec)
-        yHat     = x LA.#> betaSamp
-    -- Phase 12c: VS.zipWith fuses on Storable Vectors and avoids the
-    -- two LA.toList allocations + Haskell list zip (cf. Phase 11c
-    -- glmLogLik change).
-    return (VS.toList (VS.zipWith (\yi yhi -> logNormDensity yi yhi sigma)
-                                  y yHat))
-
--- | Generate an @S × N@ log-likelihood matrix from a Laplace-approximate
--- GLM posterior. For Gaussian-family models prefer 'lmPosteriorLogLiks'.
---
--- @
--- β ~ MVN(β̂,  Fisher⁻¹)
--- log p(y_i | β^s) = family-specific log-density
--- @
-glmPosteriorLogLiks
-  :: Family
-  -> LinkFn
-  -> LA.Matrix Double  -- ^ Design matrix @X@.
-  -> LA.Vector Double  -- ^ Response @y@.
-  -> LA.Matrix Double  -- ^ Inverse Fisher information.
-  -> FitResult
-  -> Int               -- ^ Number of posterior samples @S@.
-  -> GenIO
-  -> IO [[Double]]
-glmPosteriorLogLiks family linkFn x y fisherInv fr s gen = do
-  let p     = LA.rows fisherInv
-      beta0 = coefficientsV fr
-      rChol = LA.chol (LA.trustSym fisherInv)
-      lChol = LA.tr rChol
-  replicateM s $ do
-    zVec <- fmap LA.fromList (replicateM p (normal 0 1 gen))
-    let betaSamp = beta0 + lChol LA.#> zVec
-        eta      = x LA.#> betaSamp
-    -- Phase 12c: same VS.zipWith / no toList pattern as
-    -- 'lmPosteriorLogLiks'.
-    return (VS.toList (VS.zipWith (glmLogDensity family linkFn) y eta))
-
--- | Log-likelihood matrix for the **conditional** WAIC of a Gaussian
--- LME (random intercepts).
---
--- This is not a fully marginal GLMM posterior. It conditions on a point
--- estimate of the BLUPs @û@ and posterior-samples @(β, σ²)@ as if from
--- a residualized LM:
---
---   * @y' := y − Z·û@  (response with BLUP offset removed).
---   * @σ² ~ InvGamma((n−p)/2, RSS_cond/2)@ where @RSS_cond@ is the LME
---     conditional residual sum of squares.
---   * @β ~ MVN(β̂,  σ² (X'X)⁻¹)@.
---   * @log p(y_i | β^s, û_{j(i)}, σ^s) = log N(y_i; X_iβ^s + û_{j(i)}, σ^s)@.
---
--- Because @u@ is held fixed, @p_WAIC@ tends to be smaller than the true
--- value; this is still useful for comparing fixed-effect structures on
--- the same data (see Gelman, Hwang & Vehtari 2014, §3.3).
-lmePosteriorLogLiks
-  :: LA.Matrix Double  -- ^ Fixed-effect design matrix @X@ (@n×p@).
-  -> LA.Vector Double  -- ^ Response @y@ (length @n@).
-  -> [Double]          -- ^ Per-observation BLUP offset @û_{j(i)}@ (length @n@).
-  -> FitResult         -- ^ Fixed-effect LME fit result.
-  -> Int               -- ^ Number of posterior samples @S@.
-  -> GenIO
-  -> IO [[Double]]
-lmePosteriorLogLiks x y offsets fr s gen = do
-  let n      = LA.rows x
-      p      = LA.cols x
-      df'    = n - p
-      beta0  = coefficientsV fr
-      rss    = let resV = residualsV fr in LA.dot resV resV
-      xtxInv = LA.inv (LA.tr x LA.<> x)
-      rChol  = LA.chol (LA.trustSym xtxInv)
-      lChol  = LA.tr rChol
-  replicateM s $ do
-    chi2Vals <- replicateM df' (normal 0 1 gen)
-    let chi2    = sum (map (^(2::Int)) chi2Vals)
-        sigSamp = sqrt (rss / chi2)
-    zVec <- fmap LA.fromList (replicateM p (normal 0 1 gen))
-    let betaSamp = beta0 + LA.scale sigSamp (lChol LA.#> zVec)
-        yFix     = LA.toList (x LA.#> betaSamp)
-        yCond    = zipWith (+) yFix offsets
-        ys       = LA.toList y
-    return [ logNormDensity yi yhi sigSamp | (yi, yhi) <- zip ys yCond ]
-
-logNormDensity :: Double -> Double -> Double -> Double
-logNormDensity y mu sig
-  | sig <= 0  = -1/0
-  | otherwise = let d = (y - mu) / sig
-                in -0.5 * log (2 * pi) - log sig - 0.5 * d * d
-
-glmLogDensity :: Family -> LinkFn -> Double -> Double -> Double
-glmLogDensity family linkFn y eta =
-  let mu = case linkFn of
-              Identity -> eta
-              Log      -> exp eta
-              Logit    -> 1 / (1 + exp (-eta))
-              Sqrt     -> eta * eta
-  in case family of
-       Gaussian -> logNormDensity y mu 1.0
-       Poisson  -> Dist.logDensity (Dist.Poisson (max 1e-10 mu)) y
-       Binomial -> Dist.logDensity (Dist.Binomial 1 (max 1e-8 (min (1-1e-8) mu))) y
-
--- ---------------------------------------------------------------------------
--- 数値ユーティリティ
--- ---------------------------------------------------------------------------
-
-logSumExp :: [Double] -> Double
-logSumExp [] = -1/0
-logSumExp xs =
-  let m = maximum xs
-  in m + log (sum (map (\x -> exp (x - m)) xs))
-
-mean :: [Double] -> Double
-mean [] = 0
-mean xs = sum xs / fromIntegral (length xs)
-
--- | 標本分散 (n-1 で割る)
-sampleVar :: [Double] -> Double
-sampleVar xs
-  | length xs < 2 = 0
-  | otherwise =
-      let mu = mean xs
-      in sum (map (\x -> (x - mu) ^ (2::Int)) xs)
-         / fromIntegral (length xs - 1)
-
--- ---------------------------------------------------------------------------
--- モデル比較の重み (Pseudo-BMA, ArviZ.compare 相当)
--- ---------------------------------------------------------------------------
-
--- | One candidate model for comparison: label and log-likelihood matrix.
-data CompareEntry = CompareEntry
-  { ceLabel    :: String          -- ^ Model label.
-  , ceLogLikMat :: [[Double]]     -- ^ @S × N@ log-likelihood matrix.
-  } deriving (Show)
-
--- | Per-model comparison result.
-data CompareResult = CompareResult
-  { crLabel     :: String          -- ^ Model label.
-  , crWAIC      :: Double          -- ^ WAIC (smaller is better).
-  , crLOO       :: Double          -- ^ LOO  (smaller is better).
-  , crDeltaWAIC :: Double          -- ^ @ΔWAIC@ vs the best model.
-  , crDeltaLOO  :: Double          -- ^ @ΔLOO@  vs the best model.
-  , crSE        :: Double          -- ^ Standard error of @WAIC@.
-  , crKHatBad   :: Int             -- ^ Number of observations with @k̂ > 0.7@.
-  , crWeight    :: Double          -- ^ Pseudo-BMA weight (sums to 1 over models).
-  } deriving (Show)
-
--- | Compare several models by WAIC / LOO and compute Pseudo-BMA weights.
---
--- Algorithm:
---
---   * Compute WAIC and LOO for each model.
---   * Use the best (minimum) model as baseline for @ΔWAIC@ / @ΔLOO@.
---   * Pseudo-BMA weight: @w_i = exp(elpd_i) / Σ exp(elpd_j)@.
---     (実用的には Δ から計算: w_i ∝ exp(-Δelpd_i))
-compareModels :: [CompareEntry] -> [CompareResult]
-compareModels entries =
-  let waicResults = map (\e -> (ceLabel e, waic (ceLogLikMat e))) entries
-      looResults  = map (\e -> (ceLabel e, loo  (ceLogLikMat e))) entries
-      waicVals    = map (waicValue . snd) waicResults
-      looVals     = map (looValue  . snd) looResults
-      -- elpd_loo (= -looValue / 2) 基準で Pseudo-BMA 重みを計算
-      elpds       = map (\v -> -v / 2) looVals
-      maxElpd     = maximum elpds
-      unnorm      = map (\e -> exp (e - maxElpd)) elpds
-      total       = sum unnorm
-      weights     = map (/ total) unnorm
-      bestWaic    = minimum waicVals
-      bestLoo     = minimum looVals
-  in zipWith4 mkRow entries waicResults looResults weights
-  where
-    mkRow entry (lbl, w) (_, l) wt = CompareResult
-      { crLabel     = lbl
-      , crWAIC      = waicValue w
-      , crLOO       = looValue  l
-      , crDeltaWAIC = waicValue w - minimum (map (\e -> waicValue (waic (ceLogLikMat e))) entries)
-      , crDeltaLOO  = looValue  l - minimum (map (\e -> looValue  (loo  (ceLogLikMat e))) entries)
-      , crSE        = waicSE w
-      , crKHatBad   = looKHatBad l
-      , crWeight    = wt
-      }
-    zipWith4 f as bs cs ds = case (as, bs, cs, ds) of
-      (a:as', b:bs', c:cs', d:ds') -> f a b c d : zipWith4 f as' bs' cs' ds'
-      _ -> []
diff --git a/src/Hanalyze/Stat/MultipleTesting.hs b/src/Hanalyze/Stat/MultipleTesting.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/MultipleTesting.hs
+++ /dev/null
@@ -1,173 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Stat.MultipleTesting
--- Description : 多重比較補正 (FWER: Bonferroni/Holm、 FDR: BH/BY)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Multiple-testing correction.
---
--- Adjusts a list of p-values to control either:
---
---   * Family-wise error rate (FWER):
---     'bonferroni', 'holm'
---   * False discovery rate (FDR):
---     'benjaminiHochberg' (BH), 'benjaminiYekutieli' (BY)
---
--- All functions take and return @[Double]@; the order of input
--- p-values is preserved in the output.
-module Hanalyze.Stat.MultipleTesting
-  ( CorrectionMethod (..)
-  , pAdjust
-    -- * Individual methods
-  , bonferroni
-  , holm
-  , benjaminiHochberg
-  , benjaminiYekutieli
-    -- * Storable-vector variants (avoid boxed list ↔ unboxed Vector
-    -- conversions; the same numerical algorithms as the @[Double]@
-    -- versions above, but accepting and returning @VU.Vector Double@).
-  , benjaminiHochbergV
-  , holmV
-  ) where
-
-import qualified Data.Vector.Unboxed         as VU
-import qualified Data.Vector.Unboxed.Mutable as MVU
-import qualified Data.Vector.Algorithms.Intro as VAI
-import           Control.Monad.ST             (runST, ST)
-
--- | Correction method.
-data CorrectionMethod
-  = Bonferroni
-  | Holm
-  | BenjaminiHochberg   -- ^ FDR (BH 1995)
-  | BenjaminiYekutieli  -- ^ FDR under arbitrary dependence (BY 2001)
-  deriving (Show, Eq)
-
--- | Apply a correction by name.
-pAdjust :: CorrectionMethod -> [Double] -> [Double]
-pAdjust Bonferroni         = bonferroni
-pAdjust Holm               = holm
-pAdjust BenjaminiHochberg  = benjaminiHochberg
-pAdjust BenjaminiYekutieli = benjaminiYekutieli
-
--- | Bonferroni: @p_adj = min(1, p · m)@ where @m@ is the number of tests.
--- Most conservative; controls FWER.
-bonferroni :: [Double] -> [Double]
-bonferroni ps =
-  let m = fromIntegral (length ps) :: Double
-  in map (\p -> min 1 (p * m)) ps
-
--- | Holm-Bonferroni step-down: less conservative than 'bonferroni',
--- still controls FWER.
-holm :: [Double] -> [Double]
-holm = VU.toList . holmV . VU.fromList
-
--- | Holm step-down on an unboxed vector — see 'benjaminiHochbergV'
--- for the rationale on bypassing the @[Double]@ API.
-holmV :: VU.Vector Double -> VU.Vector Double
-holmV ps = runST $ do
-  let !m  = VU.length ps
-      !mD = fromIntegral m :: Double
-  if m <= 1
-    then return ps
-    else do
-      idx <- VU.thaw (VU.generate m id) :: ST s (MVU.STVector s Int)
-      VAI.sortBy (\i j -> compare (VU.unsafeIndex ps i) (VU.unsafeIndex ps j)) idx
-      idxV <- VU.unsafeFreeze idx
-      raw  <- MVU.new m
-      let goRaw !k
-            | k >= m    = pure ()
-            | otherwise = do
-                let !p = VU.unsafeIndex ps (VU.unsafeIndex idxV k)
-                    !q = min 1 (p * (mD - fromIntegral k))
-                MVU.unsafeWrite raw k q
-                goRaw (k + 1)
-      goRaw 0
-      let goMax !k
-            | k >= m    = pure ()
-            | otherwise = do
-                a <- MVU.unsafeRead raw (k - 1)
-                b <- MVU.unsafeRead raw k
-                MVU.unsafeWrite raw k (max a b)
-                goMax (k + 1)
-      goMax 1
-      out <- MVU.new m
-      let goSc !k
-            | k >= m    = pure ()
-            | otherwise = do
-                v <- MVU.unsafeRead raw k
-                MVU.unsafeWrite out (VU.unsafeIndex idxV k) v
-                goSc (k + 1)
-      goSc 0
-      VU.unsafeFreeze out
-
--- | Benjamini-Hochberg (BH) FDR control.
-benjaminiHochberg :: [Double] -> [Double]
-benjaminiHochberg = VU.toList . benjaminiHochbergV . VU.fromList
-
--- | BH on an unboxed 'VU.Vector Double'. Equivalent to
--- 'benjaminiHochberg' but skips the @[Double]@↔@VU.Vector Double@
--- conversion, which on the n=1000 bench dominates the @[Double]@
--- API by a 2× factor (boxed-Double allocation + GC pressure).
---
--- Numerical algorithm:
---
---   1. argsort p ascending.
---   2. raw_k = min(1, p_(k) · m / (k+1)).
---   3. Right-to-left prefix-min on @raw@ (step-up monotonisation).
---   4. Scatter back to original positions.
---
--- All steps are written as hand-rolled ST loops (not @forM_ [0..m-1]@)
--- so we avoid the per-iter list-cell allocation that GHC otherwise
--- has to fuse away.
-benjaminiHochbergV :: VU.Vector Double -> VU.Vector Double
-benjaminiHochbergV ps = runST $ do
-  let !m  = VU.length ps
-      !mD = fromIntegral m :: Double
-  if m <= 1
-    then return ps
-    else do
-      idx <- VU.thaw (VU.generate m id) :: ST s (MVU.STVector s Int)
-      VAI.sortBy (\i j -> compare (VU.unsafeIndex ps i) (VU.unsafeIndex ps j)) idx
-      idxV <- VU.unsafeFreeze idx
-      raw  <- MVU.new m
-      -- raw_k = min(1, p_(k) · m / (k+1))
-      let goRaw !k
-            | k >= m    = pure ()
-            | otherwise = do
-                let !p = VU.unsafeIndex ps (VU.unsafeIndex idxV k)
-                    !q = min 1 (p * mD / fromIntegral (k + 1))
-                MVU.unsafeWrite raw k q
-                goRaw (k + 1)
-      goRaw 0
-      -- Right-to-left prefix-min monotonisation.
-      let goMin !k
-            | k < 0     = pure ()
-            | otherwise = do
-                a <- MVU.unsafeRead raw k
-                b <- MVU.unsafeRead raw (k + 1)
-                MVU.unsafeWrite raw k (min a b)
-                goMin (k - 1)
-      goMin (m - 2)
-      -- Scatter back to original positions.
-      out <- MVU.new m
-      let goSc !k
-            | k >= m    = pure ()
-            | otherwise = do
-                v <- MVU.unsafeRead raw k
-                MVU.unsafeWrite out (VU.unsafeIndex idxV k) v
-                goSc (k + 1)
-      goSc 0
-      VU.unsafeFreeze out
-
--- | Benjamini-Yekutieli (BY) FDR control under arbitrary dependence.
--- Multiplies each BH q-value by the harmonic-number factor
--- @c(m) = Σ_{i=1..m} 1/i@.
-benjaminiYekutieli :: [Double] -> [Double]
-benjaminiYekutieli ps =
-  let m  = length ps
-      cM = sum [ 1 / fromIntegral i | i <- [1..m] ] :: Double
-      bh = benjaminiHochberg ps
-  in map (\p -> min 1 (p * cM)) bh
-
diff --git a/src/Hanalyze/Stat/NumberFormat.hs b/src/Hanalyze/Stat/NumberFormat.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/NumberFormat.hs
+++ /dev/null
@@ -1,72 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Stat.NumberFormat
--- Description : レポート/CLI 出力向けの数値フォーマット helper (桁数に応じた固定/指数表記の自動選択)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Number-formatting helpers for reports and CLI output.
---
--- A single function chooses fixed-point or exponential notation based on
--- magnitude:
---
--- >>> fmtNum 0
--- "0.00"
--- >>> fmtNum 0.91
--- "0.91"
--- >>> fmtNum 12.34
--- "12.34"
--- >>> fmtNum 1.10e13
--- "1.10E+13"
--- >>> fmtNum 3.057e-24
--- "3.06E-24"
--- >>> fmtNum 1234.5
--- "1.23E+03"
---
--- Threshold: values with @|x|@ outside @[0.01, 999]@ use exponential
--- notation; inside the range, two decimal digits. Zero and non-finite
--- values (@NaN@ / @Infinity@) get dedicated fallbacks.
-module Hanalyze.Stat.NumberFormat
-  ( fmtNum
-  , fmtNumT
-  , fmtNumWith
-  ) where
-
-import Data.Text (Text)
-import qualified Data.Text as T
-import Text.Printf (printf)
-
--- | Default-threshold numeric formatting (String).
-fmtNum :: Double -> String
-fmtNum = fmtNumWith 0.01 999
-
--- | Default-threshold numeric formatting (Text).
-fmtNumT :: Double -> Text
-fmtNumT = T.pack . fmtNum
-
--- | Custom-threshold formatter.
---
--- @fmtNumWith lo hi x@ formats @x@ with @\"%.2f\"@ when @|x|@ is inside
--- @[lo, hi]@, otherwise @\"%.2E\"@. Zero, @NaN@ and @Infinity@ get
--- dedicated representations.
-fmtNumWith :: Double -> Double -> Double -> String
-fmtNumWith lo hi x
-  | isNaN x         = "NaN"
-  | isInfinite x    = if x > 0 then "+Inf" else "-Inf"
-  | x == 0          = "0.00"
-  | a >= hi || a < lo = formatSci x
-  | otherwise       = printf "%.2f" x
-  where
-    a = abs x
-
--- | "M.MME+NN" / "M.MME-NN" 形式の指数表記。
--- printf "%.2E" は実装依存で "+" の有無が変わるため、自前で組む。
-formatSci :: Double -> String
-formatSci x =
-  let s = if x < 0 then "-" else "" :: String
-      a = abs x
-      e = floor (logBase 10 a) :: Int
-      m = a / (10 ** fromIntegral e)
-      (m', e') = if m >= 10 then (m / 10, e + 1) else (m, e)
-      sign = if e' >= 0 then "+" else "-" :: String
-  in printf "%s%.2fE%s%d" s m' sign (abs e' :: Int)
diff --git a/src/Hanalyze/Stat/PosteriorPredictive.hs b/src/Hanalyze/Stat/PosteriorPredictive.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/PosteriorPredictive.hs
+++ /dev/null
@@ -1,153 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- |
--- Module      : Hanalyze.Stat.PosteriorPredictive
--- Description : 事前/事後予測サンプリング (PyMC の sample_prior/posterior_predictive 相当)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Prior- and posterior-predictive sampling (analogous to PyMC's
--- @sample_prior_predictive@ / @sample_posterior_predictive@).
---
--- @
--- import Hanalyze.Stat.PosteriorPredictive
---
--- chain <- nuts model cfg initP gen
--- ppc   <- posteriorPredictive model chain gen
--- -- ppc :: [Map Text [Double]]   -- predicted observations per sample
--- @
-module Hanalyze.Stat.PosteriorPredictive
-  ( -- * 事後予測サンプリング (chain ベース)
-    posteriorPredictive
-  , posteriorPredictiveSummary
-    -- * Prior predictive sampling (chain not required)
-  , priorPredictive
-    -- * Prior sampling (including latents)
-  , samplePrior
-  ) where
-
-import Control.Monad (replicateM)
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import Data.Text (Text)
-import Data.List (sort)
-import System.Random.MWC (GenIO)
-
-import Hanalyze.MCMC.Core (Chain (..))
-import Hanalyze.Model.HBM
-  ( ModelP, sampleDist, runObserveDists, priorList )
-
--- ---------------------------------------------------------------------------
--- 事後予測サンプリング
--- ---------------------------------------------------------------------------
-
--- | Posterior-predictive samples for every observe node in the model.
---
--- Algorithm:
---
---   1. Walk the chain's latent samples.
---   2. At each sample, evaluate 'runObserveDists' to obtain the
---      conditional distribution at every observe node.
---   3. Draw as many fresh @y@ values from that distribution as the
---      original observation count.
---
--- The returned list has the same length as @chainSamples@; each element
--- is a @Map@ from observe-node name to a fresh predicted-value list of
--- the original length.
-posteriorPredictive
-  :: forall r. ModelP r
-  -> Chain
-  -> GenIO
-  -> IO [Map Text [Double]]
-posteriorPredictive m chain gen =
-  mapM (\ps -> genFromObserves m ps gen) (chainSamples chain)
-
--- | Per-observation posterior-predictive summary statistics
--- (mean and 95 % credible interval).
---
--- Returns: observation name ↦ a list of @(mean, 2.5%, 97.5%)@ triples,
--- one per original observation index.
-posteriorPredictiveSummary
-  :: [Map Text [Double]]                           -- posteriorPredictive の出力
-  -> Map Text [(Double, Double, Double)]
-posteriorPredictiveSummary preds =
-  let names = case preds of
-                []    -> []
-                (m:_) -> Map.keys m
-  in Map.fromList
-       [ (n, summarizePerObs (perSamplePerObs n preds)) | n <- names ]
-  where
-    -- 観測 n: 各サンプルの観測 i 番目を集めて [[Double]] (列ごと)
-    perSamplePerObs :: Text -> [Map Text [Double]] -> [[Double]]
-    perSamplePerObs nm samples =
-      transpose (map (Map.findWithDefault [] nm) samples)
-
-    summarizePerObs :: [[Double]] -> [(Double, Double, Double)]
-    summarizePerObs cols = map oneObs cols
-      where
-        oneObs xs =
-          let s   = sort xs
-              n   = length s
-              mu  = if n == 0 then 0 else sum xs / fromIntegral n
-              q p = if n == 0 then 0
-                              else s !! min (n - 1) (max 0 (floor (p * fromIntegral n) :: Int))
-          in (mu, q 0.025, q 0.975)
-
-    transpose :: [[a]] -> [[a]]
-    transpose [] = []
-    transpose xss
-      | all null xss = []
-      | otherwise =
-          let heads = [h | (h:_) <- xss]
-              tails = [t | (_:t) <- xss]
-          in heads : transpose tails
-
--- ---------------------------------------------------------------------------
--- 事前予測サンプリング (チェーン不要)
--- ---------------------------------------------------------------------------
-
--- | Generate @N@ predictive samples from the prior alone (without any
--- observed data). Useful for sanity-checking what the model predicts
--- /before/ conditioning on observations.
-priorPredictive
-  :: forall r. ModelP r
-  -> Int        -- ^ Number of samples @N@.
-  -> GenIO
-  -> IO [Map Text [Double]]
-priorPredictive m n gen = replicateM n $ do
-  ps <- samplePrior m gen
-  genFromObserves m ps gen
-
--- | Draw one sample of every latent variable from its prior.
---
--- Note: 'priorList' walks the model with placeholder zeros to extract its
--- structure. This function then samples each latent independently from
--- its individual prior. For hierarchical models this does not match
--- PyMC's @sample_prior_predictive@ (which threads downstream dependencies),
--- but it is enough for quick prior sanity checks.
-samplePrior :: forall r. ModelP r -> GenIO -> IO (Map Text Double)
-samplePrior m gen = do
-  let priors = priorList m   -- [(name, Distribution Double)] (placeholder=0 走査)
-  vals <- mapM (\(_, d) -> sampleDist d gen) priors
-  return (Map.fromList (zip (map fst priors) vals))
-
--- ---------------------------------------------------------------------------
--- 内部: 与えられた latent 値で観測を生成
--- ---------------------------------------------------------------------------
-
--- 各 observe ノードについて、元データの個数だけ新しいサンプルを生成。
-genFromObserves
-  :: forall r. ModelP r
-  -> Map Text Double
-  -> GenIO
-  -> IO (Map Text [Double])
-genFromObserves m ps gen = do
-  let observes = runObserveDists m ps   -- [(name, Distribution Double, [Double])]
-  newGroups <- mapM
-    (\(nm, d, ys) -> do
-        let nObs = length ys
-        newYs <- replicateM nObs (sampleDist d gen)
-        return (nm, newYs))
-    observes
-  -- 同名 observe が複数ある場合はリスト連結
-  return $ Map.fromListWith (++) newGroups
diff --git a/src/Hanalyze/Stat/QuasiRandom.hs b/src/Hanalyze/Stat/QuasiRandom.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/QuasiRandom.hs
+++ /dev/null
@@ -1,189 +0,0 @@
--- |
--- Module      : Hanalyze.Stat.QuasiRandom
--- Description : 低不一致準乱数列 (Halton 列・LHS) — ベイズ最適化の初期設計に利用
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Quasi-random number sequences with low discrepancy.
---
--- These sequences cover a multi-dimensional unit hyper-cube more
--- evenly than independent uniform-random samples and are the
--- recommended way to seed Bayesian-optimization initial designs and
--- multi-start global optimizers.
---
--- The 'haltonSequence' implementation uses the first @d@ prime numbers
--- as bases. For @d ≤ 6@ (Branin, Hartmann6, etc.) it is essentially
--- as good as Sobol; for @d ≥ 10@ correlation between dimensions can
--- become visible and Sobol with scrambling is preferred (not
--- implemented here).
-module Hanalyze.Stat.QuasiRandom
-  ( haltonPoint
-  , haltonSequence
-  , haltonSequenceIn
-  , haltonMatrix
-  , radicalInverse
-  , primes
-    -- * Latin Hypercube Sampling
-  , lhsSamples
-  , lhsSamplesIn
-  ) where
-
-import           Control.Monad         (forM)
-import qualified Data.Vector.Mutable   as MV
-import qualified Data.Vector           as V
-import qualified Data.Vector.Storable         as VS
-import qualified Data.Vector.Storable.Mutable as MVS
-import qualified Numeric.LinearAlgebra        as LA
-import           System.Random.MWC     (GenIO, uniformR)
-
--- | Infinite list of prime numbers via a simple Sieve.
-primes :: [Int]
-primes = sieve [2 ..]
-  where
-    sieve (p : xs) = p : sieve [x | x <- xs, x `mod` p /= 0]
-    sieve []       = []
-
--- | Radical-inverse function in base @b@. Maps an integer @i@ into
--- @[0, 1)@.
---
--- P41 inner-loop tweaks:
---
---   * @1 / fromIntegral base@ is computed once; subsequent iterations
---     multiply by @invB@ instead of dividing by @base@ each step.
---     Halton at n=10000 d=5 spends ~500K loop iterations here, each
---     previously paying a Double division.
---   * @divMod@ → @quot@ + @r = n - q*base@: avoids the @(q,r)@ tuple
---     pattern-match alloc, replaces a IDIV with an IMUL+SUB on x86.
-radicalInverse :: Int -> Int -> Double
-radicalInverse base i = go i invB 0
-  where
-    !invB = 1.0 / fromIntegral base
-    go !n !f !acc
-      | n == 0    = acc
-      | otherwise =
-          let !q = n `quot` base
-              !r = n - q * base
-          in go q (f * invB) (acc + fromIntegral r * f)
-{-# INLINE radicalInverse #-}
-
--- | Single Halton point in @d@ dimensions: applies @radicalInverse@
--- with the first @d@ primes.
-haltonPoint :: Int          -- ^ Dimension @d@.
-            -> Int          -- ^ Index @i@ (1-based; @i = 0@ would yield the origin).
-            -> [Double]
-haltonPoint d i = take d [ radicalInverse p i | p <- primes ]
-
--- | First @n@ Halton points in @d@ dimensions, each in @[0, 1)^d@.
--- Indexed from 1 (skipping @i = 0@, which would be at the origin).
---
--- We tried @runST@ + flat Storable Vector + final list-comp slicing,
--- but the cost is dominated by the @n × d@ cons-cell allocations of
--- the @[[Double]]@ boundary representation, not by the kernel of
--- @radicalInverse@. The flat-vector path benchmarked the same as or
--- slightly slower than the direct list comprehension below — the
--- structural ceiling here is the @[[Double]]@ API. Internal-only
--- callers that want the table as a flat Storable can use a future
--- 'haltonMatrix' (TODO).
-haltonSequence :: Int        -- ^ Number of points @n@.
-               -> Int        -- ^ Dimension @d@.
-               -> [[Double]]
-haltonSequence n d =
-  let bases = take d primes
-  in [ map (\b -> radicalInverse b i) bases | i <- [1 .. n] ]
-
--- | First @n@ Halton points returned as a flat @n × d@ matrix
--- (row-major: row @i@ = the @i@-th Halton point in @[0, 1)^d@).
---
--- This is the same numerical sequence as 'haltonSequence', but
--- written into a Storable buffer with no @[[Double]]@ boxing — the
--- scipy.stats.qmc.Halton API returns an @ndarray@ of the same shape,
--- and the @[[Double]]@ form was a 2× allocation tax purely from the
--- API boundary (P41).
---
--- Internal-loop optimisations:
---
---   * Bases are loaded into an unboxed @VS.Vector Int@ once.
---   * Per-cell write goes through a hand-rolled ST loop (@outer@/
---     @inner@) so no @forM_ [0..k]@ list cells are allocated.
---   * @radicalInverse@ is the same kernel as before; the saving is
---     entirely in the boundary representation.
-haltonMatrix :: Int        -- ^ Number of points @n@.
-             -> Int        -- ^ Dimension @d@.
-             -> LA.Matrix Double
-haltonMatrix n d
-  | n <= 0 || d <= 0 = LA.fromLists []
-  | otherwise =
-      let basesV = VS.fromList (take d primes) :: VS.Vector Int
-          total  = n * d
-          flat = VS.create $ do
-            v <- MVS.unsafeNew total
-            let outer !i
-                  | i >= n    = pure ()
-                  | otherwise = do
-                      let !iOne   = i + 1   -- skip i=0 (origin)
-                          !rowBeg = i * d
-                          inner !k
-                            | k >= d    = pure ()
-                            | otherwise = do
-                                let !b   = VS.unsafeIndex basesV k
-                                    !val = radicalInverse b iOne
-                                MVS.unsafeWrite v (rowBeg + k) val
-                                inner (k + 1)
-                      inner 0
-                      outer (i + 1)
-            outer 0
-            pure v
-      in LA.reshape d flat
-
--- | Halton sequence rescaled into a per-dimension box
--- @[lo_k, hi_k)@. @bounds@ must have length @d@.
-haltonSequenceIn :: Int                       -- ^ @n@.
-                 -> [(Double, Double)]        -- ^ @bounds@ (length @d@).
-                 -> [[Double]]
-haltonSequenceIn n bs =
-  let d   = length bs
-      pts = haltonSequence n d
-  in [ zipWith (\u (lo, hi) -> lo + u * (hi - lo)) p bs | p <- pts ]
-
--- ---------------------------------------------------------------------------
--- Latin Hypercube Sampling
--- ---------------------------------------------------------------------------
-
--- | Generate @n@ Latin-Hypercube samples in @[0, 1)^d@.
---
--- Algorithm (McKay-Beckman-Conover 1979):
---
---   1. For each dimension @k@, partition @[0, 1)@ into @n@ equal cells
---      @[i/n, (i+1)/n)@ and pick one stratified-random point per cell:
---      @u_{i,k} = (i + r_{i,k}) / n@ where @r ~ U(0, 1)@.
---   2. Independently for each dimension, randomly permute the @n@ cells.
---   3. Stack the per-dim permutations into @n@ points of @d@ coords.
---
--- The result fills every per-dimension marginal cell exactly once,
--- giving much better coverage than @n@ iid uniform draws while still
--- being random.
-lhsSamples :: Int -> Int -> GenIO -> IO [[Double]]
-lhsSamples n d gen = do
-  -- per-dim stratified samples (length n each)
-  perDim <- forM [1 .. d] $ \_ -> do
-    -- 1) one stratified sample per cell
-    base <- forM [0 .. n - 1] $ \i -> do
-      r <- uniformR (0, 1) gen :: IO Double
-      pure ((fromIntegral i + r) / fromIntegral n)
-    -- 2) random permutation (Fisher-Yates)
-    mv <- V.thaw (V.fromList base)
-    let nLast = n - 1
-    mapM_ (\i -> do
-              j <- uniformR (i, nLast) gen
-              MV.swap mv i j) [0 .. nLast - 1]
-    V.toList <$> V.unsafeFreeze mv
-  -- transpose: perDim is d × n, want n × d
-  pure [ [ (perDim !! k) !! i | k <- [0 .. d - 1] ] | i <- [0 .. n - 1] ]
-
--- | LHS samples rescaled into the per-dimension box @[lo_k, hi_k)@.
--- @bounds@ must have length @d@.
-lhsSamplesIn :: Int -> [(Double, Double)] -> GenIO -> IO [[Double]]
-lhsSamplesIn n bs gen = do
-  let d = length bs
-  pts <- lhsSamples n d gen
-  pure [ zipWith (\u (lo, hi) -> lo + u * (hi - lo)) p bs | p <- pts ]
diff --git a/src/Hanalyze/Stat/SPC.hs b/src/Hanalyze/Stat/SPC.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/SPC.hs
+++ /dev/null
@@ -1,749 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Stat.SPC
--- Description : 統計的工程管理 (SPC) — 管理図 (X̄-R/I-MR/p/np/c/u) + 判定ルール
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- 統計的工程管理 (Statistical Process Control) — 管理図 + 判定ルール。
---
--- 変数管理図 (X̄-R / I-MR) と属性管理図 (p / np / c / u) を共通 API で扱う。
--- 判定ルール (Western Electric / Nelson) は fit と分離した pure 関数。
---
--- ===  公開 API
---
--- * 'SPCChart' / 'SPCInput' / 'SPCChartResult'
--- * 'fitSPC'
--- * 'westernElectricRules' / 'nelsonRules' / 'checkRules'
---
--- ===  典型的な使い方
---
--- > case fitSPC XR (VarSubgroups subs) of
--- >   Left err -> ...
--- >   Right [xbar, rChart] -> do
--- >     let viols = checkRules westernElectricRules xbar
--- >     ...
-module Hanalyze.Stat.SPC
-  ( -- * chart 種別
-    SPCChart (..)
-  , SPCInput  (..)
-  , SPCChartResult (..)
-    -- * fit
-  , fitSPC
-    -- * 判定ルール
-  , SPCRule (..)
-  , SPCViolation (..)
-  , westernElectricRules
-  , nelsonRules
-  , checkRules
-  ) where
-
-import qualified Data.Text     as T
-import qualified Data.Vector   as V
-import           Data.Text     (Text)
-import           Data.Vector   (Vector)
-
--- ===========================================================================
--- 型定義
--- ===========================================================================
-
--- | 管理図の種別。
-data SPCChart
-  = XR    -- ^ X̄-R chart (subgroup 平均 + range)
-  | IMR   -- ^ I-MR chart (individual + moving range)
-  | P     -- ^ p chart (不良率、 subgroup size 可変)
-  | NP    -- ^ np chart (不良数、 subgroup size 一定)
-  | C     -- ^ c chart (単位あたり欠陥数、 unit size 一定)
-  | U     -- ^ u chart (単位あたり欠陥率、 unit size 可変)
-  | EWMAChart    -- ^ EWMA (Exponentially Weighted Moving Average) chart (Phase 11)
-  | CUSUMChart   -- ^ CUSUM (Cumulative Sum) chart 両側 (Phase 11)
-  deriving (Show, Eq)
-
--- | 管理図入力。 chart 種別に対応した構成のみ受け付ける。
-data SPCInput
-  = -- | 変数管理図 (X̄-R) 用。 各 subgroup の観測値ベクトル。
-    --   subgroup サイズ (内側 Vector の長さ) は全 subgroup で同一であること。
-    VarSubgroups   !(Vector (Vector Double))
-  | -- | I-MR 用。 個別観測値の系列。
-    VarIndividual  !(Vector Double)
-  | -- | p chart 用。 (不良数, sample size) の系列。
-    AttrProportion !(Vector Int) !(Vector Int)
-  | -- | np chart 用。 (不良数の系列, 一定 sample size)。
-    AttrCount      !(Vector Int) !Int
-  | -- | c chart 用。 欠陥数の系列 (unit size は一定と仮定)。
-    AttrDefects    !(Vector Int)
-  | -- | u chart 用。 (欠陥数, unit size) の系列。
-    AttrDefectRate !(Vector Int) !(Vector Int)
-  | -- | EWMA 用。 (個別観測値 xs, λ ∈ (0,1], L (sigma 倍数), μ₀ target, σ₀ baseline σ)。
-    --   σ₀ ≤ 0 を渡すと xs の標本標準偏差で代用。
-    EWMAInput      !(Vector Double) !Double !Double !Double !Double
-  | -- | CUSUM 用。 (個別観測値 xs, μ₀ target, σ₀ baseline σ, k (allowance, σ単位), h (decision interval, σ単位))。
-    --   σ₀ ≤ 0 を渡すと xs の標本標準偏差で代用。 両側 CUSUM (C+, C-) を返す。
-    CUSUMInput     !(Vector Double) !Double !Double !Double !Double
-  deriving (Show, Eq)
-
--- | 1 つの管理図の fit 結果。 X̄-R / I-MR では 2 つ並んで返る。
---
--- 不変条件:
---
---   * @V.length spcPoints == V.length spcUCL == V.length spcLCL@
---   * 固定 limit chart (X̄-R / I-MR / np / c) では UCL/LCL は全要素同値
---   * 変動 limit chart (p / u) では UCL/LCL が点ごとに異なる
-data SPCChartResult = SPCChartResult
-  { spcPoints    :: !(Vector Double)
-    -- ^ 点ごとにプロットする統計量 (X̄、 R、 個別値、 MR、 p̂、 np、 c、 u 等)
-  , spcCenter    :: !Double
-    -- ^ 中心線 (CL)
-  , spcUCL       :: !(Vector Double)
-    -- ^ 上方管理限界 (点ごと)
-  , spcLCL       :: !(Vector Double)
-    -- ^ 下方管理限界 (点ごと)
-  , spcSigma     :: !Double
-    -- ^ 推定 σ (rule 判定用、 zone A/B/C の境界を計算するのに使う)
-  , spcChartName :: !Text
-    -- ^ "X-bar" / "R" / "I" / "MR" / "p" / "np" / "c" / "u"
-  } deriving (Show)
-
--- ===========================================================================
--- Montgomery 定数 (n = 2..15)
--- ===========================================================================
-
--- | 出典: Montgomery, "Introduction to Statistical Quality Control" 9th ed.
---   Appendix VI。 @(A2, D3, D4, d2)@。
---   subgroup size 範囲外の @n@ では 'Nothing'。
-subgroupConst :: Int -> Maybe (Double, Double, Double, Double)
-subgroupConst n = case n of
-  2  -> Just (1.880, 0.000, 3.267, 1.128)
-  3  -> Just (1.023, 0.000, 2.574, 1.693)
-  4  -> Just (0.729, 0.000, 2.282, 2.059)
-  5  -> Just (0.577, 0.000, 2.115, 2.326)
-  6  -> Just (0.483, 0.000, 2.004, 2.534)
-  7  -> Just (0.419, 0.076, 1.924, 2.704)
-  8  -> Just (0.373, 0.136, 1.864, 2.847)
-  9  -> Just (0.337, 0.184, 1.816, 2.970)
-  10 -> Just (0.308, 0.223, 1.777, 3.078)
-  11 -> Just (0.285, 0.256, 1.744, 3.173)
-  12 -> Just (0.266, 0.283, 1.717, 3.258)
-  13 -> Just (0.249, 0.307, 1.693, 3.336)
-  14 -> Just (0.235, 0.328, 1.672, 3.407)
-  15 -> Just (0.223, 0.347, 1.653, 3.472)
-  _  -> Nothing
-
--- ===========================================================================
--- 内部ヘルパ
--- ===========================================================================
-
-vmean :: Vector Double -> Double
-vmean v
-  | V.null v  = 0
-  | otherwise = V.sum v / fromIntegral (V.length v)
-
-vrange :: Vector Double -> Double
-vrange v
-  | V.null v  = 0
-  | otherwise = V.maximum v - V.minimum v
-
--- | 単一値で埋めた長さ @n@ の Vector。
-vconst :: Int -> Double -> Vector Double
-vconst n x = V.replicate n x
-
-tshow :: Show a => a -> Text
-tshow = T.pack . show
-
-chartTag :: SPCChart -> Text
-chartTag XR  = "XR"
-chartTag IMR = "IMR"
-chartTag P   = "P"
-chartTag NP  = "NP"
-chartTag C   = "C"
-chartTag U   = "U"
-chartTag EWMAChart  = "EWMA"
-chartTag CUSUMChart = "CUSUM"
-
-inputTag :: SPCInput -> Text
-inputTag VarSubgroups{}   = "VarSubgroups"
-inputTag VarIndividual{}  = "VarIndividual"
-inputTag AttrProportion{} = "AttrProportion"
-inputTag AttrCount{}      = "AttrCount"
-inputTag AttrDefects{}    = "AttrDefects"
-inputTag AttrDefectRate{} = "AttrDefectRate"
-inputTag EWMAInput{}      = "EWMAInput"
-inputTag CUSUMInput{}     = "CUSUMInput"
-
--- ===========================================================================
--- 公開関数
--- ===========================================================================
-
--- | 管理図を fit する。 X̄-R / I-MR は 2 chart を返す
--- (順に X̄ chart / R chart、 I chart / MR chart)。
--- chart 種別と入力の組合せが不正な場合 'Left' を返す。
-fitSPC :: SPCChart -> SPCInput -> Either Text [SPCChartResult]
-fitSPC XR  (VarSubgroups subs)    = fitXR subs
-fitSPC IMR (VarIndividual xs)     = fitIMR xs
-fitSPC P   (AttrProportion ds ns) = fitP  ds ns
-fitSPC NP  (AttrCount ds n)       = fitNP ds n
-fitSPC C   (AttrDefects ds)       = fitC  ds
-fitSPC U   (AttrDefectRate ds ns) = fitU  ds ns
-fitSPC EWMAChart  (EWMAInput xs lam ll mu0 s0)        = fitEWMA xs lam ll mu0 s0
-fitSPC CUSUMChart (CUSUMInput xs mu0 s0 k h)          = fitCUSUM xs mu0 s0 k h
-fitSPC chart inp =
-  Left $ "Hanalyze.Stat.SPC.fitSPC: chart kind "
-       <> chartTag chart
-       <> " does not match input "
-       <> inputTag inp
-
--- ---------------------------------------------------------------------------
--- X̄-R chart
--- ---------------------------------------------------------------------------
-
--- | X̄-R chart:
---
---   * X̄ chart: CL = X̿、 UCL = X̿ + A2·R̄、 LCL = X̿ − A2·R̄、 σ̂ = R̄ / d2
---   * R chart: CL = R̄、 UCL = D4·R̄、 LCL = D3·R̄
-fitXR :: Vector (Vector Double) -> Either Text [SPCChartResult]
-fitXR subs
-  | V.null subs = Left "fitSPC XR: empty subgroup list"
-  | otherwise =
-      let !n  = V.length (V.head subs)
-          !k  = V.length subs
-          sizesOk = V.all (\s -> V.length s == n) subs
-      in if not sizesOk
-           then Left "fitSPC XR: subgroup sizes are not uniform"
-           else case subgroupConst n of
-             Nothing -> Left $ "fitSPC XR: subgroup size n=" <> tshow n
-                            <> " is outside supported range (2..15)"
-             Just (a2, d3, d4, d2c) ->
-               let means   = V.map vmean  subs
-                   ranges  = V.map vrange subs
-                   xBarBar = vmean means
-                   rBar    = vmean ranges
-                   sigma   = rBar / d2c
-                   uclX    = xBarBar + a2 * rBar
-                   lclX    = xBarBar - a2 * rBar
-                   uclR    = d4 * rBar
-                   lclR    = d3 * rBar
-                   xChart  = SPCChartResult
-                     { spcPoints    = means
-                     , spcCenter    = xBarBar
-                     , spcUCL       = vconst k uclX
-                     , spcLCL       = vconst k lclX
-                     , spcSigma     = sigma
-                     , spcChartName = "X-bar"
-                     }
-                   rChart  = SPCChartResult
-                     { spcPoints    = ranges
-                     , spcCenter    = rBar
-                     , spcUCL       = vconst k uclR
-                     , spcLCL       = vconst k lclR
-                     , spcSigma     = sigma
-                     , spcChartName = "R"
-                     }
-               in Right [xChart, rChart]
-
--- ---------------------------------------------------------------------------
--- I-MR chart
--- ---------------------------------------------------------------------------
-
--- | I-MR chart:
---
---   * MR_i = |x_i − x_{i−1}|  for i = 1..N−1
---   * I chart:  CL = x̄、 σ̂ = MR̄ / d2(n=2) = MR̄ / 1.128、 UCL/LCL = x̄ ± 3σ̂
---   * MR chart: CL = MR̄、 UCL = D4(2)·MR̄ = 3.267·MR̄、 LCL = D3(2)·MR̄ = 0
-fitIMR :: Vector Double -> Either Text [SPCChartResult]
-fitIMR xs
-  | V.length xs < 2 = Left "fitSPC IMR: need at least 2 individual observations"
-  | otherwise =
-      let !n      = V.length xs
-          xBar    = vmean xs
-          mr      = V.generate (n - 1) (\i -> abs (xs V.! (i + 1) - xs V.! i))
-          mrBar   = vmean mr
-          (_, d3, d4, d2c) = case subgroupConst 2 of
-            Just t  -> t
-            Nothing -> (0, 0, 0, 1.128)  -- 到達不能
-          sigma   = mrBar / d2c
-          uclI    = xBar + 3 * sigma
-          lclI    = xBar - 3 * sigma
-          uclMR   = d4 * mrBar
-          lclMR   = d3 * mrBar
-          iChart  = SPCChartResult
-            { spcPoints    = xs
-            , spcCenter    = xBar
-            , spcUCL       = vconst n uclI
-            , spcLCL       = vconst n lclI
-            , spcSigma     = sigma
-            , spcChartName = "I"
-            }
-          mrChart = SPCChartResult
-            { spcPoints    = mr
-            , spcCenter    = mrBar
-            , spcUCL       = vconst (n - 1) uclMR
-            , spcLCL       = vconst (n - 1) lclMR
-            , spcSigma     = sigma
-            , spcChartName = "MR"
-            }
-      in Right [iChart, mrChart]
-
--- ---------------------------------------------------------------------------
--- p chart (proportion defective, variable subgroup size)
--- ---------------------------------------------------------------------------
-
--- | p chart:
---
---   * p̂_i = d_i / n_i
---   * p̄   = Σ d_i / Σ n_i
---   * CL  = p̄
---   * UCL_i = p̄ + 3·sqrt(p̄(1−p̄)/n_i)、 LCL_i = max(0, …)
---
--- σ̂ は **平均 n** に基づく代表値 (rule 判定用)。
-fitP :: Vector Int -> Vector Int -> Either Text [SPCChartResult]
-fitP ds ns
-  | V.length ds /= V.length ns
-      = Left "fitSPC P: defectives and sample-size series differ in length"
-  | V.null ds = Left "fitSPC P: empty series"
-  | V.any (< 0) ds = Left "fitSPC P: defectives must be non-negative"
-  | V.any (<= 0) ns = Left "fitSPC P: sample sizes must be positive"
-  | V.or (V.zipWith (>) ds ns) = Left "fitSPC P: defectives exceed sample size"
-  | otherwise =
-      let k       = V.length ds
-          totalD  = sum (V.toList ds) :: Int
-          totalN  = sum (V.toList ns) :: Int
-          pBar    = fromIntegral totalD / fromIntegral totalN
-          phat    = V.zipWith (\d n -> fromIntegral d / fromIntegral n) ds ns
-          ucl     = V.map (\ni -> pBar + 3 * sqrt (pBar * (1 - pBar) /
-                                                   fromIntegral ni)) ns
-          lcl     = V.map (\ni -> max 0 $ pBar - 3 * sqrt (pBar * (1 - pBar) /
-                                                           fromIntegral ni)) ns
-          nMean   = fromIntegral totalN / fromIntegral k :: Double
-          sigma   = sqrt (pBar * (1 - pBar) / nMean)
-      in Right [SPCChartResult
-        { spcPoints    = phat
-        , spcCenter    = pBar
-        , spcUCL       = ucl
-        , spcLCL       = lcl
-        , spcSigma     = sigma
-        , spcChartName = "p"
-        }]
-
--- ---------------------------------------------------------------------------
--- np chart (count defective, constant subgroup size n)
--- ---------------------------------------------------------------------------
-
--- | np chart (n は全 subgroup で一定):
---
---   * CL  = n·p̄ = 平均不良数
---   * σ̂  = sqrt(n·p̄·(1−p̄))
---   * UCL = n·p̄ + 3·σ̂、 LCL = max(0, …)
-fitNP :: Vector Int -> Int -> Either Text [SPCChartResult]
-fitNP ds n
-  | V.null ds        = Left "fitSPC NP: empty defectives series"
-  | n <= 0           = Left "fitSPC NP: sample size n must be positive"
-  | V.any (< 0) ds   = Left "fitSPC NP: defectives must be non-negative"
-  | V.any (> n) ds   = Left "fitSPC NP: defectives exceed sample size"
-  | otherwise =
-      let k       = V.length ds
-          totalD  = sum (V.toList ds) :: Int
-          pBar    = fromIntegral totalD / fromIntegral (n * k) :: Double
-          cl      = fromIntegral n * pBar
-          sigma   = sqrt (fromIntegral n * pBar * (1 - pBar))
-          ucl     = cl + 3 * sigma
-          lcl     = max 0 (cl - 3 * sigma)
-          pts     = V.map fromIntegral ds :: Vector Double
-      in Right [SPCChartResult
-        { spcPoints    = pts
-        , spcCenter    = cl
-        , spcUCL       = vconst k ucl
-        , spcLCL       = vconst k lcl
-        , spcSigma     = sigma
-        , spcChartName = "np"
-        }]
-
--- ---------------------------------------------------------------------------
--- c chart (count of defects, constant unit size)
--- ---------------------------------------------------------------------------
-
--- | c chart:
---
---   * CL  = c̄ = 平均欠陥数
---   * σ̂  = sqrt(c̄)
---   * UCL = c̄ + 3·sqrt(c̄)、 LCL = max(0, …)
-fitC :: Vector Int -> Either Text [SPCChartResult]
-fitC ds
-  | V.null ds         = Left "fitSPC C: empty defects series"
-  | V.any (< 0) ds    = Left "fitSPC C: defects must be non-negative"
-  | otherwise =
-      let k       = V.length ds
-          cBar    = fromIntegral (sum (V.toList ds)) / fromIntegral k :: Double
-          sigma   = sqrt cBar
-          ucl     = cBar + 3 * sigma
-          lcl     = max 0 (cBar - 3 * sigma)
-          pts     = V.map fromIntegral ds :: Vector Double
-      in Right [SPCChartResult
-        { spcPoints    = pts
-        , spcCenter    = cBar
-        , spcUCL       = vconst k ucl
-        , spcLCL       = vconst k lcl
-        , spcSigma     = sigma
-        , spcChartName = "c"
-        }]
-
--- ---------------------------------------------------------------------------
--- u chart (defect rate, variable unit size)
--- ---------------------------------------------------------------------------
-
--- | u chart:
---
---   * u_i = d_i / n_i
---   * ū   = Σ d_i / Σ n_i
---   * CL  = ū
---   * UCL_i = ū + 3·sqrt(ū/n_i)、 LCL_i = max(0, …)
-fitU :: Vector Int -> Vector Int -> Either Text [SPCChartResult]
-fitU ds ns
-  | V.length ds /= V.length ns
-      = Left "fitSPC U: defects and unit-size series differ in length"
-  | V.null ds       = Left "fitSPC U: empty series"
-  | V.any (< 0) ds  = Left "fitSPC U: defects must be non-negative"
-  | V.any (<= 0) ns = Left "fitSPC U: unit sizes must be positive"
-  | otherwise =
-      let k       = V.length ds
-          totalD  = fromIntegral (sum (V.toList ds)) :: Double
-          totalN  = fromIntegral (sum (V.toList ns)) :: Double
-          uBar    = totalD / totalN
-          us      = V.zipWith (\d n -> fromIntegral d / fromIntegral n) ds ns
-          ucl     = V.map (\ni -> uBar + 3 * sqrt (uBar / fromIntegral ni)) ns
-          lcl     = V.map (\ni -> max 0 (uBar - 3 * sqrt (uBar / fromIntegral ni))) ns
-          nMean   = totalN / fromIntegral k
-          sigma   = sqrt (uBar / nMean)
-      in Right [SPCChartResult
-        { spcPoints    = us
-        , spcCenter    = uBar
-        , spcUCL       = ucl
-        , spcLCL       = lcl
-        , spcSigma     = sigma
-        , spcChartName = "u"
-        }]
-
--- ===========================================================================
--- 判定ルール (Phase 1.4 / 1.5 で実装)
--- ===========================================================================
-
--- | 判定ルール 1 個。
-data SPCRule = SPCRule
-  { ruleName   :: !Text                       -- ^ "Western Electric 1" / "Nelson 1" 等
-  , ruleNumber :: !Int                        -- ^ ルール番号 (1..8)
-  , ruleCheck  :: SPCChartResult -> [Int]     -- ^ 違反点の 0-origin index list
-  }
-
--- | ルール違反 1 件。
-data SPCViolation = SPCViolation
-  { vRuleName    :: !Text
-  , vRuleNumber  :: !Int
-  , vPointIndex  :: !Int
-  , vChartName   :: !Text   -- ^ どの chart で違反したか (X-bar / R / 等)
-  } deriving (Show, Eq)
-
--- ---------------------------------------------------------------------------
--- 内部パターン検出 (rule 共通)
--- ---------------------------------------------------------------------------
-
--- $patternDetectors
--- ゾーン境界は CL ± k·σ で定義 (σ は 'spcSigma' フィールド)。
--- 可変 limit chart (p / u) では σ は代表値 (平均 n から算出) なので、
--- ゾーン判定はやや近似となる (canvas display 用途では実用上問題なし)。
-
--- | k·σ の絶対値を超えた点の index (0-origin)。 chart 種別非依存。
-beyondSigma :: Double -> SPCChartResult -> [Int]
-beyondSigma k r =
-  let cl    = spcCenter r
-      sigma = spcSigma r
-      pts   = V.toList (spcPoints r)
-  in [ i | (i, x) <- zip [0..] pts
-         , abs (x - cl) > k * sigma ]
-
--- | k·σ を超える点について「+ なら +1、 − なら −1、 ゾーン内なら 0」。
-sideAtSigma :: Double -> SPCChartResult -> [Int]
-sideAtSigma k r =
-  let cl    = spcCenter r
-      sigma = spcSigma r
-      pts   = V.toList (spcPoints r)
-      classify x
-        | x - cl >  k * sigma =  1
-        | x - cl < -k * sigma = -1
-        | otherwise           =  0
-  in map classify pts
-
--- | CL に対する符号 (上 = +1, 下 = -1, 上 = 0)。
-sideOfCenter :: SPCChartResult -> [Int]
-sideOfCenter r =
-  let cl    = spcCenter r
-      pts   = V.toList (spcPoints r)
-      classify x
-        | x >  cl =  1
-        | x <  cl = -1
-        | otherwise = 0
-  in map classify pts
-
--- | N 個連続で同符号 (CL の同じ側) になっている末尾点の index を返す。
---   例: 8 連続 → 連続区間の 8 点目以降を全部 violation として返す。
-runSameSide :: Int -> SPCChartResult -> [Int]
-runSameSide n r = go 0 0 0 (sideOfCenter r) []
-  where
-    go !i !curSide !runLen ss acc = case ss of
-      []     -> reverse acc
-      (s:xs) ->
-        let (curSide', runLen')
-              | s == 0           = (0, 0)
-              | s == curSide     = (curSide, runLen + 1)
-              | otherwise        = (s, 1)
-            acc' | runLen' >= n = i : acc
-                 | otherwise    = acc
-        in go (i + 1) curSide' runLen' xs acc'
-
--- | N 個連続で単調 (全て上昇 or 全て下降) のパターンの末尾 index。
-trendMono :: Int -> SPCChartResult -> [Int]
-trendMono n r = go 0 0 0 (V.toList (spcPoints r)) []
-  where
-    -- direction: +1 = increasing, -1 = decreasing, 0 = none yet
-    go _ _ _ [] acc = reverse acc
-    go _ _ _ [_] acc = reverse acc
-    go !i !dir !runLen (x : ys@(y : _)) acc =
-      let d | y > x =  1
-            | y < x = -1
-            | otherwise = 0
-          (dir', runLen')
-            | d == 0      = (0, 0)
-            | d == dir    = (dir, runLen + 1)
-            | otherwise   = (d, 2)   -- 始まり: 2 点で run=2
-          -- 違反 = runLen が n 以上、 i+1 (現在の y) の index を記録
-          acc' | runLen' >= n = (i + 1) : acc
-               | otherwise    = acc
-      in go (i + 1) dir' runLen' ys acc'
-
--- | N 個連続で交互上下のパターンの末尾 index。
-alternating :: Int -> SPCChartResult -> [Int]
-alternating n r = go 0 0 0 (V.toList (spcPoints r)) []
-  where
-    go _ _ _ [] acc = reverse acc
-    go _ _ _ [_] acc = reverse acc
-    go !i !lastDir !runLen (x : ys@(y : _)) acc =
-      let d | y > x =  1
-            | y < x = -1
-            | otherwise = 0
-          (lastDir', runLen')
-            | d == 0                       = (0, 0)
-            | lastDir == 0                 = (d, 2)
-            | d == negate lastDir          = (d, runLen + 1)
-            | otherwise                    = (d, 2)
-          acc' | runLen' >= n = (i + 1) : acc
-               | otherwise    = acc
-      in go (i + 1) lastDir' runLen' ys acc'
-
--- | k 個連続で σ 倍の絶対値以内 (= ゾーン C 内のみ) の末尾 index。
---   stratification (W-E rule 6 / Nelson 7)。
-withinSigma :: Int -> Double -> SPCChartResult -> [Int]
-withinSigma n k r =
-  let cl    = spcCenter r
-      sigma = spcSigma r
-      pts   = V.toList (spcPoints r)
-      flags = map (\x -> abs (x - cl) <= k * sigma) pts
-  in collectRun n flags
-
--- | k 個連続で σ 倍の絶対値より外 (= ゾーン A or B、 中央線の同/異側問わず) の末尾 index。
---   mixture (W-E rule 7 / Nelson 8)。
-beyondSigmaEither :: Int -> Double -> SPCChartResult -> [Int]
-beyondSigmaEither n k r =
-  let cl    = spcCenter r
-      sigma = spcSigma r
-      pts   = V.toList (spcPoints r)
-      flags = map (\x -> abs (x - cl) > k * sigma) pts
-  in collectRun n flags
-
--- | True が n 個以上連続するパターンの末尾 index 集合。
-collectRun :: Int -> [Bool] -> [Int]
-collectRun n = go 0 0 []
-  where
-    go _ _ acc [] = reverse acc
-    go !i !rn acc (f : fs) =
-      let rn'  = if f then rn + 1 else 0
-          acc' | rn' >= n = i : acc
-               | otherwise = acc
-      in go (i + 1) rn' acc' fs
-
--- | 「直近 m 点のうち k 点以上が k·σ を **同じ側** で超えている」 末尾 index。
---   Western Electric 2 / 3 用 (m, k, σ係数)。
-kOfMBeyondSameSide :: Int -> Int -> Double -> SPCChartResult -> [Int]
-kOfMBeyondSameSide kth m sigK r = go 0 (sideAtSigma sigK r) []
-  where
-    go _ ss acc | length ss < m = reverse acc
-    go !i ss acc =
-      let window = take m ss
-          posCount = length (filter (==  1) window)
-          negCount = length (filter (== -1) window)
-          hit      = posCount >= kth || negCount >= kth
-          -- 違反 index は window の末尾 (= i + m - 1)
-          acc' | hit       = (i + m - 1) : acc
-               | otherwise = acc
-      in case ss of
-           []     -> reverse acc'
-           (_:xs) -> go (i + 1) xs acc'
-
--- ---------------------------------------------------------------------------
--- Western Electric rules (WECO 8 rules)
--- ---------------------------------------------------------------------------
-
--- | Western Electric Company (WECO) rules。 8 rules。
---
--- (Western Electric Statistical Quality Control Handbook 1956 +
--- 一般的な 8-rule 拡張)
---
---   * Rule 1: 1 点が 3σ 超
---   * Rule 2: 3 点中 2 点が同じ側で 2σ 超
---   * Rule 3: 5 点中 4 点が同じ側で 1σ 超
---   * Rule 4: 8 点連続で CL の同じ側
---   * Rule 5: 6 点連続で単調 (上昇 or 下降)
---   * Rule 6: 15 点連続で 1σ 以内 (stratification)
---   * Rule 7: 8 点連続で 1σ 外 (mixture; どちら側でも可)
---   * Rule 8: 14 点連続で交互上下
-westernElectricRules :: [SPCRule]
-westernElectricRules =
-  [ SPCRule "Western Electric 1" 1 (beyondSigma 3)
-  , SPCRule "Western Electric 2" 2 (kOfMBeyondSameSide 2 3 2)
-  , SPCRule "Western Electric 3" 3 (kOfMBeyondSameSide 4 5 1)
-  , SPCRule "Western Electric 4" 4 (runSameSide 8)
-  , SPCRule "Western Electric 5" 5 (trendMono 6)
-  , SPCRule "Western Electric 6" 6 (withinSigma 15 1)
-  , SPCRule "Western Electric 7" 7 (beyondSigmaEither 8 1)
-  , SPCRule "Western Electric 8" 8 (alternating 14)
-  ]
-
--- ---------------------------------------------------------------------------
--- Nelson rules (1984、 8 rules)
--- ---------------------------------------------------------------------------
-
--- | Nelson rules (Nelson, L.S. 1984, J. Qual. Tech.)。 8 rules。
---
--- WE 8 rules と多くが重複するが、 ルール番号と一部の N が異なる:
---
---   * Rule 1: 1 点が 3σ 超                                   (= WE 1)
---   * Rule 2: 9 点連続で CL の同じ側                          (WE 4 は 8 点)
---   * Rule 3: 6 点連続で単調                                  (= WE 5)
---   * Rule 4: 14 点連続で交互上下                              (= WE 8)
---   * Rule 5: 3 点中 2 点が同じ側で 2σ 超                      (= WE 2)
---   * Rule 6: 5 点中 4 点が同じ側で 1σ 超                      (= WE 3)
---   * Rule 7: 15 点連続で 1σ 以内                              (= WE 6)
---   * Rule 8: 8 点連続で 1σ 外 (どちら側でも可)                (= WE 7)
---
--- 検出ロジックは [[westernElectricRules]] と同じヘルパを再利用。
-nelsonRules :: [SPCRule]
-nelsonRules =
-  [ SPCRule "Nelson 1" 1 (beyondSigma 3)
-  , SPCRule "Nelson 2" 2 (runSameSide 9)
-  , SPCRule "Nelson 3" 3 (trendMono 6)
-  , SPCRule "Nelson 4" 4 (alternating 14)
-  , SPCRule "Nelson 5" 5 (kOfMBeyondSameSide 2 3 2)
-  , SPCRule "Nelson 6" 6 (kOfMBeyondSameSide 4 5 1)
-  , SPCRule "Nelson 7" 7 (withinSigma 15 1)
-  , SPCRule "Nelson 8" 8 (beyondSigmaEither 8 1)
-  ]
-
--- | 指定したルール集合で違反点を検出する。
-checkRules :: [SPCRule] -> SPCChartResult -> [SPCViolation]
-checkRules rs r =
-  [ SPCViolation (ruleName ru) (ruleNumber ru) i (spcChartName r)
-  | ru <- rs
-  , i  <- ruleCheck ru r
-  ]
-
--- ---------------------------------------------------------------------------
--- EWMA chart (Phase 11)
--- ---------------------------------------------------------------------------
-
--- | EWMA chart:
---
---   * 再帰: @z_i = λ x_i + (1 − λ) z_{i−1}@, @z_0 = μ₀@
---   * 時変管理限界: @μ₀ ± L σ √(λ/(2−λ) · (1 − (1−λ)^{2i}))@
---   * σ₀ ≤ 0 のとき xs の標本標準偏差で代用。
---
--- 入力検証: 0 < λ ≤ 1, L > 0, |xs| ≥ 1。
-fitEWMA :: Vector Double -> Double -> Double -> Double -> Double
-        -> Either Text [SPCChartResult]
-fitEWMA xs lam ll mu0 s0In
-  | V.null xs                = Left "fitSPC EWMA: empty input"
-  | not (lam > 0 && lam <= 1) = Left "fitSPC EWMA: λ must be in (0, 1]"
-  | ll <= 0                  = Left "fitSPC EWMA: L must be > 0"
-  | otherwise =
-      let !n     = V.length xs
-          !sigma = if s0In > 0 then s0In else sampleSD xs
-          zs     = V.scanl' (\z x -> lam * x + (1 - lam) * z) mu0 xs
-          -- scanl' includes initial → drop the seed
-          zsTail = V.tail zs
-          ucl = V.generate n (\i ->
-            let i1 = fromIntegral (i + 1) :: Double
-                factor = lam / (2 - lam) * (1 - (1 - lam) ** (2 * i1))
-            in mu0 + ll * sigma * sqrt factor)
-          lcl = V.generate n (\i ->
-            let i1 = fromIntegral (i + 1) :: Double
-                factor = lam / (2 - lam) * (1 - (1 - lam) ** (2 * i1))
-            in mu0 - ll * sigma * sqrt factor)
-      in Right [ SPCChartResult
-                   { spcPoints    = zsTail
-                   , spcCenter    = mu0
-                   , spcUCL       = ucl
-                   , spcLCL       = lcl
-                   , spcSigma     = sigma
-                   , spcChartName = "EWMA"
-                   } ]
-
--- ---------------------------------------------------------------------------
--- CUSUM chart (Phase 11)
--- ---------------------------------------------------------------------------
-
--- | CUSUM (両側) chart:
---
---   * @C⁺_i = max(0, x_i − (μ₀ + k σ) + C⁺_{i−1})@,  @C⁺_0 = 0@
---   * @C⁻_i = max(0, (μ₀ − k σ) − x_i + C⁻_{i−1})@,  @C⁻_0 = 0@
---   * 決定限界: @H = h σ@  (上側のみ、 下側は @−H@ として描画用に @-1 × C⁻@ を返す)
---
--- 返り値: [C⁺ chart, C⁻ chart]。 C⁻ chart は points が負方向に出るよう
--- @spcPoints = − C⁻@ として表現し、 LCL = −H、 UCL = 0 とする。
-fitCUSUM :: Vector Double -> Double -> Double -> Double -> Double
-         -> Either Text [SPCChartResult]
-fitCUSUM xs mu0 s0In k h
-  | V.null xs = Left "fitSPC CUSUM: empty input"
-  | k < 0     = Left "fitSPC CUSUM: k must be ≥ 0"
-  | h <= 0    = Left "fitSPC CUSUM: h must be > 0"
-  | otherwise =
-      let !n     = V.length xs
-          !sigma = if s0In > 0 then s0In else sampleSD xs
-          kAbs   = k * sigma
-          hAbs   = h * sigma
-          cPos   = V.scanl' (\c x -> max 0 (c + (x - mu0) - kAbs)) 0 xs
-          cNeg   = V.scanl' (\c x -> max 0 (c + (mu0 - x) - kAbs)) 0 xs
-          cPosT  = V.tail cPos
-          cNegT  = V.tail cNeg
-          chartPos = SPCChartResult
-            { spcPoints    = cPosT
-            , spcCenter    = 0
-            , spcUCL       = vconst n hAbs
-            , spcLCL       = vconst n 0
-            , spcSigma     = sigma
-            , spcChartName = "CUSUM+"
-            }
-          chartNeg = SPCChartResult
-            { spcPoints    = V.map negate cNegT
-            , spcCenter    = 0
-            , spcUCL       = vconst n 0
-            , spcLCL       = vconst n (-hAbs)
-            , spcSigma     = sigma
-            , spcChartName = "CUSUM-"
-            }
-      in Right [chartPos, chartNeg]
-
--- | 標本標準偏差 (n-1 補正)。 EWMA/CUSUM の σ₀ デフォルト用。
-sampleSD :: Vector Double -> Double
-sampleSD xs
-  | V.length xs < 2 = 0
-  | otherwise =
-      let m  = vmean xs
-          ss = V.sum (V.map (\x -> (x - m) ** 2) xs)
-      in sqrt (ss / fromIntegral (V.length xs - 1))
diff --git a/src/Hanalyze/Stat/Standardize.hs b/src/Hanalyze/Stat/Standardize.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/Standardize.hs
+++ /dev/null
@@ -1,114 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Stat.Standardize
--- Description : 入力特徴量の標準化 (z-score) ユーティリティ
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Input-feature standardization (z-score) utilities.
---
--- Use cases:
---
--- * In RFF / kernel models, a single shared length scale @ℓ@ breaks down
---   when features differ in magnitude. Fit @(μ, σ)@ with
---   'fitStandardizer', apply with 'applyStandardizer', and convert
---   model-returned predictions back to original units with
---   'unapplyStandardizer'.
--- * For interactive (JS) predictors where the user enters values in
---   original units (e.g. @energy=80 keV@) via a slider, expose 'stMu' /
---   'stSd' so the browser can apply @(v-μ)/σ@ before sending values into
---   the model. The fields are JSON-friendly.
---
--- Conventions:
---
--- * @y@ is /not/ standardized (the output scale of regression is preserved).
--- * Constant columns (std = 0) are treated as if std = 1, returning
---   @(x - μ)/1 = x - μ@ — effectively centering only.
--- * Single-row columns (n = 1) are likewise treated as std = 1.
-module Hanalyze.Stat.Standardize
-  ( Standardizer (..)
-  , fitStandardizer
-  , applyStandardizer
-  , unapplyStandardizer
-  , applyStandardizerCol
-  , identityStandardizer
-  ) where
-
-import qualified Numeric.LinearAlgebra as LA
-
--- ---------------------------------------------------------------------------
--- 型
--- ---------------------------------------------------------------------------
-
--- | Per-feature mean and standard deviation. The list length is the
--- feature count @p@.
-data Standardizer = Standardizer
-  { stMu :: ![Double]   -- ^ Per-feature mean @μ@.
-  , stSd :: ![Double]   -- ^ Per-feature standard deviation @σ@.
-  } deriving (Eq, Show)
-
--- | The identity standardizer (@μ = 0, σ = 1@) of dimension @p@.
-identityStandardizer :: Int -> Standardizer
-identityStandardizer p = Standardizer (replicate p 0) (replicate p 1)
-
--- ---------------------------------------------------------------------------
--- 学習 (fit)
--- ---------------------------------------------------------------------------
-
--- | Learn the per-column @(mean, std)@ from an @n × p@ matrix.
---
--- * @std@ is the unbiased estimate (@n-1@ denominator).
--- * Columns whose @std@ is below @1e-12@ are coerced to @std = 1@ to
---   avoid divide-by-zero on constant features.
-fitStandardizer :: LA.Matrix Double -> Standardizer
-fitStandardizer x =
-  let cols = LA.toColumns x
-      mus  = map mean cols
-      sds  = zipWith (\c m -> robustSd c m) cols mus
-  in Standardizer mus sds
-  where
-    mean v
-      | LA.size v == 0 = 0
-      | otherwise      = LA.sumElements v / fromIntegral (LA.size v)
-    robustSd v m =
-      let n = LA.size v
-      in if n <= 1
-           then 1.0
-           else
-             let xs   = LA.toList v
-                 ss   = sum [ (x' - m) * (x' - m) | x' <- xs ]
-                 var  = ss / fromIntegral (n - 1)
-                 sd0  = sqrt var
-             in if sd0 < 1e-12 then 1.0 else sd0
-
--- ---------------------------------------------------------------------------
--- 適用 / 復元
--- ---------------------------------------------------------------------------
-
--- | Apply @(x - μ) / σ@ to every row.
-applyStandardizer :: Standardizer -> LA.Matrix Double -> LA.Matrix Double
-applyStandardizer s x =
-  let cols  = LA.toColumns x
-      cols' = zipWith3 transformCol cols (stMu s) (stSd s)
-  in LA.fromColumns cols'
-  where
-    transformCol c m sd = LA.cmap (\v -> (v - m) / sd) c
-
--- | Apply @x · σ + μ@ to every row (standardized space → original units).
-unapplyStandardizer :: Standardizer -> LA.Matrix Double -> LA.Matrix Double
-unapplyStandardizer s x =
-  let cols  = LA.toColumns x
-      cols' = zipWith3 untransformCol cols (stMu s) (stSd s)
-  in LA.fromColumns cols'
-  where
-    untransformCol c m sd = LA.cmap (\v -> v * sd + m) c
-
--- | Single-cell standardization for one column (used by the JS slider
--- predictor). Returns the value unchanged when the index is out of range.
-applyStandardizerCol :: Standardizer -> Int -> Double -> Double
-applyStandardizerCol s k v
-  | k < 0 || k >= length (stMu s) = v
-  | otherwise =
-      let m  = stMu s !! k
-          sd = stSd s !! k
-      in (v - m) / sd
diff --git a/src/Hanalyze/Stat/Summary.hs b/src/Hanalyze/Stat/Summary.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/Summary.hs
+++ /dev/null
@@ -1,59 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- |
--- Module      : Hanalyze.Stat.Summary
--- Description : 事後分布の要約統計 (ArviZ az.summary 相当)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Posterior-distribution summary statistics.
---
--- Provides 'SummaryRow' and 'posteriorSummary', mirroring the columns of
--- ArviZ's @az.summary@ (mean, sd, HDI, ESS, R-hat). Originally lived in
--- @Hanalyze.Viz.MCMC@; moved to the statistics layer to decouple it from the
--- visualization stack.
---
--- HTML rendering and console pretty-printing remain in
--- @Hanalyze.Viz.MCMC.posteriorSummaryHtml@ / @posteriorSummaryFile@ /
--- @printPosteriorSummary@.
-module Hanalyze.Stat.Summary
-  ( SummaryRow (..)
-  , posteriorSummary
-  ) where
-
-import Data.Text (Text)
-import Hanalyze.MCMC.Core (Chain, chainVals)
-import Hanalyze.Stat.MCMC (essBulk, hdi, rhat)
-
--- | One row of posterior summary statistics for a single parameter.
-data SummaryRow = SummaryRow
-  { srName  :: Text     -- ^ Parameter name.
-  , srMean  :: Double   -- ^ Posterior mean.
-  , srSD    :: Double   -- ^ Posterior standard deviation.
-  , srHdiLo :: Double   -- ^ Lower bound of the 94% HDI.
-  , srHdiHi :: Double   -- ^ Upper bound of the 94% HDI.
-  , srEssV  :: Double   -- ^ Effective sample size (rank-normalized bulk ESS, ArviZ @ess_bulk@ 互換).
-  , srRhat  :: Maybe Double  -- ^ Split-R-hat (only for multi-chain runs).
-  } deriving (Show)
-
--- | Compute posterior summaries for the named parameters across one or
--- more chains. With a single chain @R-hat@ is 'Nothing'; with multiple
--- chains, mean / SD / HDI are computed on the pooled samples, while ESS
--- (bulk ESS, ArviZ @ess_bulk@ 互換 = Phase 100 で旧 pooled 'ess' から切替) and
--- split-R-hat are computed across chains.
-posteriorSummary :: [Text] -> [Chain] -> [SummaryRow]
-posteriorSummary params chains =
-  let multi = length chains > 1
-      mkRow p =
-        let perChain = map (chainVals p) chains
-            allVals  = concat perChain
-            n        = length allVals
-            mu       = if n == 0 then 0
-                       else sum allVals / fromIntegral n
-            sd_      = if n < 2 then 0
-                       else sqrt (sum [(x - mu) ^ (2::Int) | x <- allVals]
-                                  / fromIntegral (n - 1))
-            (lo, hi) = hdi 0.94 allVals
-            essV     = essBulk perChain
-            rh       = if multi then rhat perChain else Nothing
-        in SummaryRow p mu sd_ lo hi essV rh
-  in map mkRow params
diff --git a/src/Hanalyze/Stat/Test.hs b/src/Hanalyze/Stat/Test.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/Test.hs
+++ /dev/null
@@ -1,1122 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      : Hanalyze.Stat.Test
--- Description : 統一結果形式を持つ仮説検定群 (パラメトリック/ノンパラ/適合度/正規性/分散)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Hypothesis tests with a unified result format.
---
--- Most tests delegate to the @statistics@ package internals
--- (@Statistics.Test.*@) and add hanalyze-specific niceties: a single
--- 'TestResult' record, effect sizes, confidence intervals, and a
--- consistent two-sided / one-sided @Alternative@ parameter.
---
--- == Test categories
---
---   * __Parametric (location)__: 'tTest1Sample', 'tTestPaired',
---     'tTestWelch', 'tTestStudent', 'anovaOneWay'
---   * __Non-parametric (location / rank)__: 'mannWhitneyU',
---     'wilcoxonSignedRank', 'kruskalWallis'
---   * __Goodness-of-fit / independence__: 'chiSquareGOF',
---     'chiSquareIndep', 'fisherExact2x2'
---   * __Normality__: 'shapiroWilk', 'kolmogorovSmirnovNormal'
---   * __Variance equality__: 'leveneTest', 'bartlettTest', 'fTestVariance'
-module Hanalyze.Stat.Test
-  ( -- * Common types
-    TestResult (..)
-  , Alternative (..)
-    -- * Parametric (location)
-  , tTest1Sample
-  , tTestPaired
-  , tTestWelch
-  , tostWelch
-  , tTestStudent
-  , anovaOneWay
-    -- * Non-parametric (location / rank)
-  , mannWhitneyU
-  , wilcoxonSignedRank
-  , kruskalWallis
-  , friedmanTest
-  , MultiCompareResult (..)
-  , dunnTest
-    -- * Goodness-of-fit / independence
-  , chiSquareGOF
-  , chiSquareIndep
-  , fisherExact2x2
-    -- * Normality
-  , shapiroWilk
-  , kolmogorovSmirnovNormal
-    -- * Variance equality
-  , leveneTest
-  , bartlettTest
-  , fTestVariance
-    -- * Multivariate (Phase 4.3、 request/140)
-  , hotellingsT2
-  , hotellingsT2TwoSample
-  , manova
-  ) where
-
-import qualified Data.List                      as L
-import           Data.Ord                       (comparing)
-import           Data.Text                      (Text)
-import qualified Data.Text                      as T
-import qualified Data.Vector.Storable           as VS
-import qualified Data.Vector.Unboxed            as VU
-import qualified Numeric.LinearAlgebra          as LA
-import qualified Statistics.Distribution        as SD
-import qualified Statistics.Distribution.ChiSquared as ChiSq
-import qualified Statistics.Distribution.FDistribution as FDist
-import qualified Statistics.Distribution.Normal as Normal
-import qualified Statistics.Distribution.StudentT as StuT
-import qualified Statistics.Test.KolmogorovSmirnov as TKS
-import qualified Statistics.Test.KruskalWallis  as TKW
-import qualified Statistics.Test.MannWhitneyU   as TMW
-import qualified Statistics.Test.StudentT       as TST
-import qualified Statistics.Test.Types          as TT
-import qualified Statistics.Types               as STy
-
--- ---------------------------------------------------------------------------
--- Types
--- ---------------------------------------------------------------------------
-
--- | Tail / sidedness of a test.
-data Alternative
-  = TwoSided    -- ^ default; @H1: parameter ≠ value@
-  | Less        -- ^ @H1: parameter < value@
-  | Greater     -- ^ @H1: parameter > value@
-  deriving (Show, Eq)
-
--- | Unified result of a hypothesis test.
-data TestResult = TestResult
-  { trMethod       :: !Text
-    -- ^ Human-readable name of the test.
-  , trStatistic    :: !Double
-    -- ^ Test statistic (t, F, chi², U, W, ...).
-  , trDf           :: !(Maybe (Double, Maybe Double))
-    -- ^ Degrees of freedom: @Just (df1, Just df2)@ for F-tests
-    --   (numerator & denominator), @Just (df, Nothing)@ for one-DF
-    --   tests, @Nothing@ when not applicable.
-  , trPValue       :: !Double
-    -- ^ Two-sided / one-sided p-value depending on 'trAlternative'.
-  , trEffect       :: !(Maybe (Text, Double))
-    -- ^ Optional effect size as @(name, value)@ — Cohen's d, η², φ, …
-  , trCI           :: !(Maybe (Double, Double))
-    -- ^ Optional 95% CI for the test parameter (mean diff, etc.).
-  , trAlternative  :: !Alternative
-  , trNote         :: !(Maybe Text)
-    -- ^ Free-form caveat (e.g. "small-sample asymptotic; consider exact").
-  } deriving (Show)
-
--- | Convert a @statistics@ package @Test@ result into our 'TestResult'.
-fromStatTest
-  :: Text              -- ^ method label
-  -> Alternative       -- ^ alternative used
-  -> Maybe (Double, Maybe Double)  -- ^ degrees of freedom
-  -> Maybe (Text, Double)          -- ^ effect size
-  -> Maybe (Double, Double)        -- ^ confidence interval
-  -> Maybe Text                    -- ^ note
-  -> TT.Test d
-  -> TestResult
-fromStatTest method alt df eff ci note t =
-  TestResult
-    { trMethod      = method
-    , trStatistic   = TT.testStatistics t
-    , trDf          = df
-    , trPValue      = STy.pValue (TT.testSignificance t)
-    , trEffect      = eff
-    , trCI          = ci
-    , trAlternative = alt
-    , trNote        = note
-    }
-
--- | Convert hanalyze @Alternative@ to @statistics@ @PositionTest@ for
--- the location-shift family of tests.
-posTest :: Alternative -> TT.PositionTest
-posTest TwoSided = TT.SamplesDiffer
-posTest Greater  = TT.AGreater
-posTest Less     = TT.BGreater
-
--- | Conversion helpers between Storable vectors and Vector.Unboxed
--- (the @statistics@ package family uses Unboxed).
-toU :: LA.Vector Double -> VU.Vector Double
-toU = VU.fromList . LA.toList
-
--- ---------------------------------------------------------------------------
--- Parametric (location)
--- ---------------------------------------------------------------------------
-
--- | One-sample t-test against a hypothesised population mean @μ₀@.
-tTest1Sample
-  :: LA.Vector Double  -- ^ Sample.
-  -> Double            -- ^ μ₀ (hypothesised mean).
-  -> Alternative
-  -> TestResult
-tTest1Sample xs mu0 alt =
-  let n     = LA.size xs
-      xMean = LA.sumElements xs / fromIntegral n
-      xVar  = LA.sumElements ((xs - LA.scalar xMean) ^ (2 :: Int))
-              / fromIntegral (n - 1)
-      seM   = sqrt (xVar / fromIntegral n)
-      tStat = (xMean - mu0) / seM
-      df    = fromIntegral (n - 1) :: Double
-      tDist = StuT.studentT df
-      tail_ = altTail alt
-      p     = pFromT tail_ tStat tDist
-      cohenD = (xMean - mu0) / sqrt xVar
-      tCrit  = SD.quantile tDist 0.975
-      ci     = (xMean - tCrit * seM, xMean + tCrit * seM)
-  in TestResult
-       { trMethod      = "One-sample t-test"
-       , trStatistic   = tStat
-       , trDf          = Just (df, Nothing)
-       , trPValue      = p
-       , trEffect      = Just ("Cohen's d", cohenD)
-       , trCI          = Just ci
-       , trAlternative = alt
-       , trNote        = Nothing
-       }
-
--- | Paired t-test on @(x, y)@ pairs, testing @H0: mean(x − y) = 0@.
-tTestPaired
-  :: LA.Vector Double
-  -> LA.Vector Double
-  -> Alternative
-  -> TestResult
-tTestPaired xs ys alt =
-  let diffs = xs - ys
-  in (tTest1Sample diffs 0 alt) { trMethod = "Paired t-test" }
-
--- | Welch's two-sample t-test (does not assume equal variance).
-tTestWelch
-  :: LA.Vector Double
-  -> LA.Vector Double
-  -> Alternative
-  -> TestResult
-tTestWelch xs ys alt =
-  let pt = posTest alt
-      tx = TST.welchTTest pt (toU xs) (toU ys)
-      n1 = fromIntegral (LA.size xs) :: Double
-      n2 = fromIntegral (LA.size ys) :: Double
-      m1 = LA.sumElements xs / n1
-      m2 = LA.sumElements ys / n2
-      v1 = LA.sumElements ((xs - LA.scalar m1) ^ (2 :: Int)) / (n1 - 1)
-      v2 = LA.sumElements ((ys - LA.scalar m2) ^ (2 :: Int)) / (n2 - 1)
-      pooledSd = sqrt ((v1 + v2) / 2)
-      cohenD   = if pooledSd > 0 then (m1 - m2) / pooledSd else 0
-      df = (v1/n1 + v2/n2) ^ (2 :: Int)
-           / ((v1/n1)^(2::Int)/(n1-1) + (v2/n2)^(2::Int)/(n2-1))
-  in case tx of
-       Nothing -> noResultTRR "Welch's t-test" alt "insufficient samples"
-       Just t  -> fromStatTest "Welch's t-test" alt
-                    (Just (df, Nothing))
-                    (Just ("Cohen's d", cohenD))
-                    Nothing
-                    Nothing
-                    t
-
--- | TOST (Two One-Sided Tests) for equivalence using Welch's degrees of freedom.
---
--- Tests whether @|μ_A − μ_B| < Δ@ (i.e. the two groups are equivalent within
--- the margin Δ). Implements two one-sided t-tests:
---
---   * Lower: @H₀: μ_A − μ_B ≤ −Δ@ vs @H₁: μ_A − μ_B > −Δ@
---   * Upper: @H₀: μ_A − μ_B ≥ +Δ@ vs @H₁: μ_A − μ_B < +Δ@
---
--- @p_TOST = max(p_lower, p_upper)@. Equivalence is concluded at level α if
--- @p_TOST < α@. The returned 'trCI' is the @(1 − 2α)@ confidence interval
--- (here α = 0.05 → 90% CI), which is the standard TOST CI convention.
-tostWelch
-  :: LA.Vector Double  -- ^ Sample A
-  -> LA.Vector Double  -- ^ Sample B
-  -> Double            -- ^ Equivalence margin Δ (must be > 0)
-  -> TestResult
-tostWelch xs ys delta
-  | delta <= 0 =
-      noResultTRR "TOST (Welch)" TwoSided "delta must be > 0"
-  | LA.size xs < 2 || LA.size ys < 2 =
-      noResultTRR "TOST (Welch)" TwoSided "insufficient samples"
-  | otherwise =
-      let n1 = fromIntegral (LA.size xs) :: Double
-          n2 = fromIntegral (LA.size ys) :: Double
-          m1 = LA.sumElements xs / n1
-          m2 = LA.sumElements ys / n2
-          v1 = LA.sumElements ((xs - LA.scalar m1) ^ (2 :: Int)) / (n1 - 1)
-          v2 = LA.sumElements ((ys - LA.scalar m2) ^ (2 :: Int)) / (n2 - 1)
-          se = sqrt (v1 / n1 + v2 / n2)
-          diff = m1 - m2
-          df = (v1/n1 + v2/n2) ^ (2 :: Int)
-               / ((v1/n1)^(2::Int)/(n1-1) + (v2/n2)^(2::Int)/(n2-1))
-          tDist = StuT.studentT df
-          tLower = (diff - (-delta)) / se   -- want > 0 (upper-tail rejects H0_lower)
-          tUpper = (diff -   delta)  / se   -- want < 0 (lower-tail rejects H0_upper)
-          pLower = pFromT TRight tLower tDist
-          pUpper = pFromT TLeft  tUpper tDist
-          pTost  = max pLower pUpper
-          -- 90% CI (α = 0.05 each side)
-          tCrit = SD.quantile tDist 0.95
-          ci = (diff - tCrit * se, diff + tCrit * se)
-      in TestResult
-           { trMethod      = "TOST (Welch)"
-           , trStatistic   = min (abs tLower) (abs tUpper)
-           , trDf          = Just (df, Nothing)
-           , trPValue      = pTost
-           , trEffect      = Just ("Delta", delta)
-           , trCI          = Just ci
-           , trAlternative = TwoSided
-           , trNote        = Just "Equivalence demonstrated if p < alpha"
-           }
-
--- | Student's two-sample t-test (assumes equal variance).
-tTestStudent
-  :: LA.Vector Double
-  -> LA.Vector Double
-  -> Alternative
-  -> TestResult
-tTestStudent xs ys alt =
-  let pt = posTest alt
-      tx = TST.studentTTest pt (toU xs) (toU ys)
-      n1 = fromIntegral (LA.size xs) :: Double
-      n2 = fromIntegral (LA.size ys) :: Double
-      m1 = LA.sumElements xs / n1
-      m2 = LA.sumElements ys / n2
-      v1 = LA.sumElements ((xs - LA.scalar m1) ^ (2 :: Int)) / (n1 - 1)
-      v2 = LA.sumElements ((ys - LA.scalar m2) ^ (2 :: Int)) / (n2 - 1)
-      pooledV = ((n1-1)*v1 + (n2-1)*v2) / (n1 + n2 - 2)
-      cohenD  = if pooledV > 0 then (m1 - m2) / sqrt pooledV else 0
-      df      = n1 + n2 - 2
-  in case tx of
-       Nothing -> noResultTRR "Student's t-test" alt "insufficient samples"
-       Just t  -> fromStatTest "Student's t-test" alt
-                    (Just (df, Nothing))
-                    (Just ("Cohen's d", cohenD))
-                    Nothing
-                    Nothing
-                    t
-
--- | One-way ANOVA across @k@ groups (F-test on between- vs
--- within-group variance). Returns η² as effect size.
-anovaOneWay :: [LA.Vector Double] -> TestResult
-anovaOneWay groups
-  | length groups < 2 =
-      noResultTRR "One-way ANOVA" TwoSided "need ≥ 2 groups"
-  | otherwise =
-      let k     = length groups
-          ns    = map (fromIntegral . LA.size) groups :: [Double]
-          n     = sum ns
-          means = [ LA.sumElements g / fromIntegral (LA.size g)
-                  | g <- groups ]
-          grand = sum (zipWith (*) ns means) / n
-          ssB   = sum [ ni * (mi - grand)^(2::Int)
-                      | (ni, mi) <- zip ns means ]
-          ssW   = sum [ LA.sumElements ((g - LA.scalar mi)^(2::Int))
-                      | (g, mi) <- zip groups means ]
-          dfB   = fromIntegral (k - 1) :: Double
-          dfW   = n - fromIntegral k
-          msB   = ssB / dfB
-          msW   = ssW / dfW
-          fStat = msB / msW
-          pVal  = SD.complCumulative (FDist.fDistribution (round dfB) (round dfW)) fStat
-          eta2  = ssB / (ssB + ssW)
-      in TestResult
-           { trMethod      = "One-way ANOVA"
-           , trStatistic   = fStat
-           , trDf          = Just (dfB, Just dfW)
-           , trPValue      = pVal
-           , trEffect      = Just ("η²", eta2)
-           , trCI          = Nothing
-           , trAlternative = TwoSided
-           , trNote        = Nothing
-           }
-
--- ---------------------------------------------------------------------------
--- Non-parametric
--- ---------------------------------------------------------------------------
-
--- | Mann–Whitney U test (Wilcoxon rank-sum).
-mannWhitneyU
-  :: LA.Vector Double
-  -> LA.Vector Double
-  -> Alternative
-  -> TestResult
-mannWhitneyU xs ys alt =
-  let pt    = posTest alt
-      pVal  = STy.mkPValue 0.05  -- threshold; actual p inside Test
-      r     = TMW.mannWhitneyUtest pt pVal (toU xs) (toU ys)
-      m     = fromIntegral (LA.size xs) :: Double
-      n     = fromIntegral (LA.size ys) :: Double
-  in case r of
-       Nothing -> noResultTRR "Mann-Whitney U" alt "samples too small"
-       Just _testRes ->
-         -- statistics' API returns TestResult (Significant/NotSignificant)
-         -- without statistic. We compute U manually for richer output.
-         let (u1, u2, p) = mannWhitneyManual (toU xs) (toU ys) alt
-         in TestResult
-              { trMethod      = "Mann-Whitney U"
-              , trStatistic   = min u1 u2
-              , trDf          = Nothing
-              , trPValue      = p
-              , trEffect      = Just ("rank-biserial r", rankBiserial u1 m n)
-              , trCI          = Nothing
-              , trAlternative = alt
-              , trNote        = Just "normal-approximation p-value"
-              }
-
--- | Wilcoxon signed-rank test (paired, non-parametric).
-wilcoxonSignedRank
-  :: LA.Vector Double
-  -> LA.Vector Double
-  -> Alternative
-  -> TestResult
-wilcoxonSignedRank xs ys alt =
-  let (wPlus, wMinus, p) = wilcoxonManual xs ys alt
-  in TestResult
-       { trMethod      = "Wilcoxon signed-rank"
-       , trStatistic   = min wPlus wMinus
-       , trDf          = Nothing
-       , trPValue      = p
-       , trEffect      = Nothing
-       , trCI          = Nothing
-       , trAlternative = alt
-       , trNote        = Just "normal-approximation p-value"
-       }
-
--- | Kruskal-Wallis H test (k-group non-parametric ANOVA).
-kruskalWallis :: [LA.Vector Double] -> TestResult
-kruskalWallis groups
-  | length groups < 2 =
-      noResultTRR "Kruskal-Wallis" TwoSided "need ≥ 2 groups"
-  | otherwise =
-      let groupsU = map toU groups
-          h = TKW.kruskalWallis groupsU :: Double
-          k = length groups
-          dfH = fromIntegral (k - 1) :: Double
-          p = SD.complCumulative (ChiSq.chiSquared (k - 1)) h
-      in TestResult
-           { trMethod      = "Kruskal-Wallis"
-           , trStatistic   = h
-           , trDf          = Just (dfH, Nothing)
-           , trPValue      = p
-           , trEffect      = Nothing
-           , trCI          = Nothing
-           , trAlternative = TwoSided
-           , trNote        = Just "chi-square approximation"
-           }
-
--- | Friedman test — non-parametric two-way ANOVA without replication.
---
--- 入力: n × k 行列。 行 = block (被験者)、 列 = treatment。
--- 各 block 内で treatment を順位付け (1..k) し、 列ごとの平均順位の分散から
--- 検定統計量 Q を構成 (χ²(k-1) 近似)。
-friedmanTest :: LA.Matrix Double -> TestResult
-friedmanTest mat
-  | LA.rows mat < 2 || LA.cols mat < 2 =
-      noResultTRR "Friedman" TwoSided "need ≥ 2 blocks × ≥ 2 treatments"
-  | otherwise =
-      let n = LA.rows mat
-          k = LA.cols mat
-          nD = fromIntegral n :: Double
-          kD = fromIntegral k :: Double
-          -- 各行を順位化 (tie は midrank)
-          rankedRows =
-            [ midrank (LA.toList (LA.flatten (mat LA.? [i])))
-            | i <- [0 .. n - 1] ]
-          colSums = [ sum [ rankedRows !! i !! j | i <- [0 .. n - 1] ]
-                    | j <- [0 .. k - 1] ]
-          q = (12 / (nD * kD * (kD + 1)))
-              * sum [ s * s | s <- colSums ]
-              - 3 * nD * (kD + 1)
-          df = kD - 1
-          p  = SD.complCumulative (ChiSq.chiSquared (k - 1)) q
-      in TestResult
-           { trMethod      = "Friedman"
-           , trStatistic   = q
-           , trDf          = Just (df, Nothing)
-           , trPValue      = p
-           , trEffect      = Nothing
-           , trCI          = Nothing
-           , trAlternative = TwoSided
-           , trNote        = Just "chi-square approximation"
-           }
-
--- | 多重比較の結果。 ペアごとの z 値と raw / adjusted p-value。
-data MultiCompareResult = MultiCompareResult
-  { mcrPairs :: ![(Int, Int)]
-  , mcrZ     :: ![Double]
-  , mcrPRaw  :: ![Double]
-  , mcrPAdj  :: ![Double]   -- Holm correction
-  } deriving (Show)
-
--- | Dunn 多重比較 (Kruskal-Wallis post-hoc)。
---   各グループの平均順位 R̄_i / R̄_j の差を SE で標準化:
---
---     z_{ij} = (R̄_i - R̄_j) / √( (N(N+1)/12) (1/n_i + 1/n_j) )
---
---   p_raw = 2 (1 - Φ(|z|))、 Holm 補正で族別 p_adj。
-dunnTest :: [LA.Vector Double] -> MultiCompareResult
-dunnTest groups =
-  let k      = length groups
-      sizes  = map LA.size groups
-      allRanks = midrank (concatMap LA.toList groups)
-      -- 各グループの平均順位
-      starts = scanl (+) 0 sizes
-      grpRanks = [ take (sizes !! i)
-                     (drop (starts !! i) allRanks)
-                 | i <- [0 .. k - 1] ]
-      meanR i = sum (grpRanks !! i) / fromIntegral (sizes !! i)
-      n      = sum sizes
-      nD     = fromIntegral n :: Double
-      se i j =
-        sqrt (nD * (nD + 1) / 12
-              * (1 / fromIntegral (sizes !! i) + 1 / fromIntegral (sizes !! j)))
-      pairs = [ (i, j) | i <- [0 .. k - 2], j <- [i + 1 .. k - 1] ]
-      zs    = [ (meanR i - meanR j) / se i j | (i, j) <- pairs ]
-      pRaw  = [ 2 * (1 - SD.cumulative Normal.standard (abs z)) | z <- zs ]
-      pAdj  = holmAdjust pRaw
-  in MultiCompareResult
-       { mcrPairs = pairs
-       , mcrZ     = zs
-       , mcrPRaw  = pRaw
-       , mcrPAdj  = pAdj
-       }
-
--- | Holm-Bonferroni p-value adjustment.
-holmAdjust :: [Double] -> [Double]
-holmAdjust ps =
-  let m     = length ps
-      idx   = zip [0 ..] ps
-      sorted = L.sortBy (comparing snd) idx
-      stepwise = zipWith
-        (\rank (origIdx, p) ->
-            (origIdx, min 1 (p * fromIntegral (m - rank))))
-        [0 ..] sorted
-      -- monotone increasing enforcement
-      mono = scanl1 (\(_, prev) (i, p) -> (i, max prev p)) stepwise
-  in map snd (L.sortBy (comparing fst) mono)
-
--- | midrank: 同順位は順位平均。 入力: list of values, 出力: 同 length の rank list。
-midrank :: [Double] -> [Double]
-midrank xs =
-  let indexed = zip [0 :: Int ..] xs
-      sorted  = L.sortBy (comparing snd) indexed
-      n       = length xs
-      -- グループ化: 同値を 1 グループに
-      go _ [] = []
-      go pos (g:gs) =
-        let len = length g
-            avgRank = fromIntegral (sum [pos .. pos + len - 1]) / fromIntegral len + 1
-        in [(i, avgRank) | (i, _) <- g] ++ go (pos + len) gs
-      grouped = groupBy (\(_, a) (_, b) -> a == b) sorted
-      ranked  = go 0 grouped
-  in map snd (L.sortBy (comparing fst) ranked)
-  where
-    groupBy _ [] = []
-    groupBy eq (x:xs') =
-      let (same, rest) = span (eq x) xs'
-      in (x : same) : groupBy eq rest
-
--- ---------------------------------------------------------------------------
--- Goodness-of-fit / independence
--- ---------------------------------------------------------------------------
-
--- | Chi-square goodness-of-fit test.
--- @observed@ and @expected@ must have the same length and @sum expected
--- = sum observed@.
-chiSquareGOF :: LA.Vector Double -> LA.Vector Double -> TestResult
-chiSquareGOF observed expected =
-  let chi2 = LA.sumElements
-              (((observed - expected) ^ (2 :: Int)) / expected)
-      df   = fromIntegral (LA.size observed - 1) :: Double
-      p    = SD.complCumulative (ChiSq.chiSquared (round df)) chi2
-  in TestResult
-       { trMethod      = "Chi-square goodness-of-fit"
-       , trStatistic   = chi2
-       , trDf          = Just (df, Nothing)
-       , trPValue      = p
-       , trEffect      = Nothing
-       , trCI          = Nothing
-       , trAlternative = TwoSided
-       , trNote        = Nothing
-       }
-
--- | Chi-square independence test on a contingency table (rows × cols).
--- Returns Cramér's V as effect size.
-chiSquareIndep :: LA.Matrix Double -> TestResult
-chiSquareIndep tbl =
-  let r        = LA.rows tbl
-      c        = LA.cols tbl
-      rowSums  = tbl LA.#> LA.konst 1 c
-      colSums  = LA.konst 1 r LA.<# tbl
-      total    = LA.sumElements tbl
-      expected = LA.outer rowSums colSums / LA.scalar total
-      diff2    = (tbl - expected) ^ (2 :: Int)
-      contrib  = LA.sumElements (diff2 / expected)
-      df       = fromIntegral ((r - 1) * (c - 1)) :: Double
-      p        = SD.complCumulative (ChiSq.chiSquared (round df)) contrib
-      cramerV  = sqrt (contrib / (total * fromIntegral (min r c - 1)))
-  in TestResult
-       { trMethod      = "Chi-square independence"
-       , trStatistic   = contrib
-       , trDf          = Just (df, Nothing)
-       , trPValue      = p
-       , trEffect      = Just ("Cramér's V", cramerV)
-       , trCI          = Nothing
-       , trAlternative = TwoSided
-       , trNote        = Nothing
-       }
-
--- | Fisher's exact test on a 2×2 contingency table.
--- @[[a, b], [c, d]]@. Returns the (one-sided or two-sided) exact
--- p-value from the hypergeometric distribution.
-fisherExact2x2 :: ((Int, Int), (Int, Int)) -> Alternative -> TestResult
-fisherExact2x2 ((a, b), (c, d)) alt =
-  let n       = a + b + c + d
-      r1      = a + b   -- row 1 marginal
-      c1      = a + c   -- col 1 marginal
-      -- Hypergeometric: drawing r1 items from n where c1 are "success".
-      pmf k   = fromIntegral (choose c1 k * choose (n - c1) (r1 - k))
-              / fromIntegral (choose n r1)
-      kMin    = max 0 (r1 - (n - c1))
-      kMax    = min r1 c1
-      pAt     = pmf a
-      p       = case alt of
-        Less     -> sum [pmf k | k <- [kMin .. a]]
-        Greater  -> sum [pmf k | k <- [a .. kMax]]
-        TwoSided ->
-          -- Sum of pmf at all k with pmf k <= pmf a (standard def).
-          sum [pmf k | k <- [kMin .. kMax], pmf k <= pAt + 1e-15]
-      oddsRatio | b * c == 0 = 1 / 0
-                | otherwise  = fromIntegral (a * d) / fromIntegral (b * c)
-  in TestResult
-       { trMethod      = "Fisher's exact (2×2)"
-       , trStatistic   = oddsRatio
-       , trDf          = Nothing
-       , trPValue      = p
-       , trEffect      = Just ("odds ratio", oddsRatio)
-       , trCI          = Nothing
-       , trAlternative = alt
-       , trNote        = Nothing
-       }
-
--- ---------------------------------------------------------------------------
--- Normality
--- ---------------------------------------------------------------------------
-
--- | Shapiro-Wilk test (@n@ ≤ 5000). Implements Royston's 1992
--- approximation. Returns the W statistic and asymptotic p-value.
-shapiroWilk :: LA.Vector Double -> TestResult
-shapiroWilk xs0 =
-  let n      = LA.size xs0
-      xs     = LA.toList (sortVec xs0)  :: [Double]
-      mean   = sum xs / fromIntegral n
-      ss     = sum [ (x - mean) ^ (2 :: Int) | x <- xs ]
-      -- Royston coefficients via Bloom's expected normal order stats.
-      -- Approximate m_i = Φ⁻¹((i − 3/8) / (n + 1/4)).
-      mIs    = [ SD.quantile Normal.standard
-                   ((fromIntegral i - 3 / 8) / (fromIntegral n + 1 / 4))
-               | i <- [1 .. n] ]
-      mTm    = sum [m^(2::Int) | m <- mIs]
-      aIs    = [ m / sqrt mTm | m <- mIs ]
-      wNum   = sum (zipWith (*) aIs xs) ^ (2 :: Int)
-      w      = wNum / ss
-      -- Royston 1992 approximation for n ∈ [4, 11]
-      -- For larger n use the lognormal-of-(1-W) approximation.
-      pApprox
-        | n < 4     = 1
-        | n <= 11   =
-            let g  = -2.273 + 0.459 * fromIntegral n
-                mu = 0.5440 - 0.39978 * fromIntegral n
-                     + 0.025054 * fromIntegral n^(2::Int)
-                     - 0.0006714 * fromIntegral n^(3::Int)
-                sigma = exp (1.30405 - 0.04213 * fromIntegral n
-                            - 0.0005006 * fromIntegral n^(2::Int))
-                z = (g + log (1 - w) - mu) / sigma
-            in 1 - SD.cumulative Normal.standard z
-        | otherwise =
-            let mu    = -1.5861 - 0.31082 * log (fromIntegral n)
-                        - 0.083751 * (log (fromIntegral n))^(2::Int)
-                        + 0.0038915 * (log (fromIntegral n))^(3::Int)
-                sigma = exp (-0.4803 - 0.082676 * log (fromIntegral n)
-                            + 0.0030302 * (log (fromIntegral n))^(2::Int))
-                z = (log (1 - w) - mu) / sigma
-            in 1 - SD.cumulative Normal.standard z
-  in TestResult
-       { trMethod      = "Shapiro-Wilk"
-       , trStatistic   = w
-       , trDf          = Nothing
-       , trPValue      = pApprox
-       , trEffect      = Nothing
-       , trCI          = Nothing
-       , trAlternative = TwoSided
-       , trNote        = Just "Royston 1992 approximation; n ≤ 5000"
-       }
-
--- | Kolmogorov-Smirnov goodness-of-fit test against the standard
--- Normal distribution (one-sample).
-kolmogorovSmirnovNormal :: LA.Vector Double -> TestResult
-kolmogorovSmirnovNormal xs =
-  let xsU = toU xs
-      d   = TKS.kolmogorovSmirnovD Normal.standard xsU
-      n   = LA.size xs
-      p   = TKS.kolmogorovSmirnovProbability n d
-  in TestResult
-       { trMethod      = "Kolmogorov-Smirnov (vs Normal(0,1))"
-       , trStatistic   = d
-       , trDf          = Nothing
-       , trPValue      = p
-       , trEffect      = Nothing
-       , trCI          = Nothing
-       , trAlternative = TwoSided
-       , trNote        = Nothing
-       }
-
--- ---------------------------------------------------------------------------
--- Variance equality
--- ---------------------------------------------------------------------------
-
--- | Levene's test for equality of variances across k groups.
--- Uses median-based formulation (Brown-Forsythe variant) which is
--- more robust than mean-based to non-normal data.
-leveneTest :: [LA.Vector Double] -> TestResult
-leveneTest groups
-  | length groups < 2 =
-      noResultTRR "Levene's test" TwoSided "need ≥ 2 groups"
-  | otherwise =
-      let k       = length groups
-          ns      = map LA.size groups
-          n       = sum ns
-          medians = map sampleMedian groups
-          -- Z_ij = |x_ij - median_i|
-          zs      = [ LA.cmap (\x -> abs (x - med)) g
-                    | (g, med) <- zip groups medians ]
-          zMeans  = [ LA.sumElements z / fromIntegral (LA.size z) | z <- zs ]
-          zGrand  = sum [ LA.sumElements z | z <- zs ] / fromIntegral n
-          ssB     = sum [ fromIntegral ni * (zi - zGrand) ^ (2 :: Int)
-                        | (ni, zi) <- zip ns zMeans ]
-          ssW     = sum [ LA.sumElements ((z - LA.scalar zi)^(2::Int))
-                        | (z, zi) <- zip zs zMeans ]
-          dfB     = fromIntegral (k - 1) :: Double
-          dfW     = fromIntegral (n - k) :: Double
-          fStat   = (ssB / dfB) / (ssW / dfW)
-          p       = SD.complCumulative
-                      (FDist.fDistribution (k - 1) (n - k)) fStat
-      in TestResult
-           { trMethod      = "Levene's test (Brown-Forsythe)"
-           , trStatistic   = fStat
-           , trDf          = Just (dfB, Just dfW)
-           , trPValue      = p
-           , trEffect      = Nothing
-           , trCI          = Nothing
-           , trAlternative = TwoSided
-           , trNote        = Nothing
-           }
-
--- | Bartlett's test for equality of variances (assumes normality,
--- more powerful than Levene when normality holds).
-bartlettTest :: [LA.Vector Double] -> TestResult
-bartlettTest groups
-  | length groups < 2 =
-      noResultTRR "Bartlett's test" TwoSided "need ≥ 2 groups"
-  | otherwise =
-      let k    = length groups
-          ns   = map (fromIntegral . LA.size) groups :: [Double]
-          n    = sum ns
-          vars = map sampleVariance groups
-          spv  = sum [ (ni - 1) * vi | (ni, vi) <- zip ns vars ]
-                 / (n - fromIntegral k)
-          numer = (n - fromIntegral k) * log spv
-                  - sum [ (ni - 1) * log vi | (ni, vi) <- zip ns vars ]
-          c    = 1 + (1 / (3 * fromIntegral (k - 1)))
-                   * (sum [1 / (ni - 1) | ni <- ns] - 1 / (n - fromIntegral k))
-          chi2 = numer / c
-          dfB  = fromIntegral (k - 1) :: Double
-          p    = SD.complCumulative (ChiSq.chiSquared (k - 1)) chi2
-      in TestResult
-           { trMethod      = "Bartlett's test"
-           , trStatistic   = chi2
-           , trDf          = Just (dfB, Nothing)
-           , trPValue      = p
-           , trEffect      = Nothing
-           , trCI          = Nothing
-           , trAlternative = TwoSided
-           , trNote        = Just "assumes normality"
-           }
-
--- | F-test for variance ratio between two samples (parametric).
-fTestVariance :: LA.Vector Double -> LA.Vector Double -> Alternative
-              -> TestResult
-fTestVariance xs ys alt =
-  let n1 = fromIntegral (LA.size xs) :: Double
-      n2 = fromIntegral (LA.size ys) :: Double
-      m1 = LA.sumElements xs / n1
-      m2 = LA.sumElements ys / n2
-      v1 = LA.sumElements ((xs - LA.scalar m1)^(2::Int)) / (n1 - 1)
-      v2 = LA.sumElements ((ys - LA.scalar m2)^(2::Int)) / (n2 - 1)
-      f  = v1 / v2
-      df1 = n1 - 1
-      df2 = n2 - 1
-      fd  = FDist.fDistribution (round df1) (round df2)
-      p  = case alt of
-        TwoSided -> 2 * min (SD.cumulative fd f) (SD.complCumulative fd f)
-        Greater  -> SD.complCumulative fd f
-        Less     -> SD.cumulative fd f
-  in TestResult
-       { trMethod      = "F-test for equal variances"
-       , trStatistic   = f
-       , trDf          = Just (df1, Just df2)
-       , trPValue      = p
-       , trEffect      = Just ("variance ratio", f)
-       , trCI          = Nothing
-       , trAlternative = alt
-       , trNote        = Just "assumes normality"
-       }
-
--- ---------------------------------------------------------------------------
--- Internal helpers
--- ---------------------------------------------------------------------------
-
--- | Sentinel result when test inputs are insufficient.
-noResultTRR :: Text -> Alternative -> Text -> TestResult
-noResultTRR method alt msg = TestResult
-  { trMethod      = method
-  , trStatistic   = 0
-  , trDf          = Nothing
-  , trPValue      = 1 / 0
-  , trEffect      = Nothing
-  , trCI          = Nothing
-  , trAlternative = alt
-  , trNote        = Just msg
-  }
-
--- | Side / tail used for p-value computation.
-data Tail = TLeft | TRight | TBoth
-
-altTail :: Alternative -> Tail
-altTail Less     = TLeft
-altTail Greater  = TRight
-altTail TwoSided = TBoth
-
-pFromT :: Tail -> Double -> StuT.StudentT -> Double
-pFromT TLeft  t d = SD.cumulative d t
-pFromT TRight t d = SD.complCumulative d t
-pFromT TBoth  t d = 2 * min (SD.cumulative d t) (SD.complCumulative d t)
-
--- | Sample median.
-sampleMedian :: LA.Vector Double -> Double
-sampleMedian v =
-  let xs = sortDoubles (LA.toList v)
-      n  = length xs
-  in if even n
-       then (xs !! (n `div` 2 - 1) + xs !! (n `div` 2)) / 2
-       else xs !! (n `div` 2)
-  where
-    sortDoubles :: [Double] -> [Double]
-    sortDoubles []     = []
-    sortDoubles (x:xs) = sortDoubles [y | y <- xs, y < x]
-                      ++ [x]
-                      ++ sortDoubles [y | y <- xs, y >= x]
-
--- | Unbiased sample variance.
-sampleVariance :: LA.Vector Double -> Double
-sampleVariance v =
-  let n = fromIntegral (LA.size v) :: Double
-      m = LA.sumElements v / n
-  in LA.sumElements ((v - LA.scalar m) ^ (2 :: Int)) / (n - 1)
-
--- | n choose k (Int).
-choose :: Int -> Int -> Integer
-choose n k
-  | k < 0 || k > n = 0
-  | k == 0 || k == n = 1
-  | otherwise = product [fromIntegral (n - i + 1) | i <- [1 .. k]]
-                `div` product [fromIntegral i | i <- [1 .. k]]
-
--- | Sort an LA vector (ascending) via 'Data.List.sort' (mergesort,
--- O(n log n) / O(n) space). Phase 11b (2026-05-14): replaced naive list
--- quicksort to avoid pivot-bias O(n²) blowup on large inputs.
-sortVec :: LA.Vector Double -> LA.Vector Double
-sortVec v = LA.fromList (L.sort (LA.toList v))
-
--- | Manual Mann-Whitney U with normal approximation (handles ties).
-mannWhitneyManual
-  :: VU.Vector Double
-  -> VU.Vector Double
-  -> Alternative
-  -> (Double, Double, Double)
-mannWhitneyManual xs ys alt =
-  let n1 = fromIntegral (VU.length xs) :: Double
-      n2 = fromIntegral (VU.length ys) :: Double
-      tagged = [(x, 1::Int) | x <- VU.toList xs]
-            ++ [(y, 2::Int) | y <- VU.toList ys]
-      sorted = L.sortBy (comparing fst) tagged
-      ranks  = assignRanks (map fst sorted)
-      r1     = sum [ rk | (rk, (_, g)) <- zip ranks sorted, g == 1 ]
-      u1     = r1 - n1 * (n1 + 1) / 2
-      u2     = n1 * n2 - u1
-      u      = min u1 u2
-      meanU  = n1 * n2 / 2
-      varU   = n1 * n2 * (n1 + n2 + 1) / 12
-      z      = (u - meanU) / sqrt varU
-      p      = case alt of
-        TwoSided -> 2 * SD.cumulative Normal.standard z
-        Less     -> SD.cumulative Normal.standard z
-        Greater  -> SD.complCumulative Normal.standard z
-  in (u1, u2, p)
-
--- | Average ranks (handles ties via mid-rank).
-assignRanks :: [Double] -> [Double]
-assignRanks vs =
-  let n = length vs
-      pairs = zip [1 :: Int ..] vs
-      go [] = []
-      go ((i, v):rest) =
-        let same = takeWhile ((== v) . snd) ((i, v):rest)
-            others = drop (length same) ((i, v):rest)
-            ranks = map fromIntegral (map fst same)
-            avg = sum ranks / fromIntegral (length ranks)
-        in replicate (length same) avg ++ go others
-  in go pairs ++ [] ++ replicate 0 (fromIntegral n)
-
--- | Rank-biserial correlation effect size for Mann-Whitney.
-rankBiserial :: Double -> Double -> Double -> Double
-rankBiserial u1 m n = 1 - 2 * u1 / (m * n)
-
--- | Manual Wilcoxon signed-rank with normal approximation.
-wilcoxonManual
-  :: LA.Vector Double
-  -> LA.Vector Double
-  -> Alternative
-  -> (Double, Double, Double)
-wilcoxonManual xs ys alt =
-  let diffs   = LA.toList (xs - ys)
-      nonZero = filter (/= 0) diffs
-      absD    = map abs nonZero
-      ranks   = assignRanks absD
-      paired  = zip nonZero ranks
-      wPlus   = sum [ rk | (d, rk) <- paired, d > 0 ]
-      wMinus  = sum [ rk | (d, rk) <- paired, d < 0 ]
-      n       = fromIntegral (length nonZero) :: Double
-      meanW   = n * (n + 1) / 4
-      varW    = n * (n + 1) * (2 * n + 1) / 24
-      w       = min wPlus wMinus
-      z       = (w - meanW) / sqrt varW
-      p       = case alt of
-        TwoSided -> 2 * SD.cumulative Normal.standard z
-        Less     -> SD.cumulative Normal.standard z
-        Greater  -> SD.complCumulative Normal.standard z
-  in (wPlus, wMinus, p)
-
--- ===========================================================================
--- 多変量検定 (Phase 4.3、 request/140)
--- ===========================================================================
-
--- | 1 サンプル Hotelling T² 検定 (H_0: μ = μ_0)。
---
--- 入力:
---
---   * X (n × p): 各行が 1 観測の多変量ベクトル
---   * μ_0 (長さ p): 仮説の平均
---
--- 統計量と分布:
---
--- > T² = n · (μ̂ − μ_0)ᵀ S⁻¹ (μ̂ − μ_0)
--- > F  = ((n − p) / ((n − 1) · p)) · T²,    df = (p, n − p)
---
--- 戻り値の 'trStatistic' は F 値、 'trEffect' に @("T²", T²)@ を格納。
-hotellingsT2 :: LA.Matrix Double -> LA.Vector Double -> TestResult
-hotellingsT2 x mu0
-  | n < 2 = noResultTRR "Hotelling T² (1-sample)" TwoSided "need ≥ 2 observations"
-  | p < 1 = noResultTRR "Hotelling T² (1-sample)" TwoSided "need ≥ 1 variable"
-  | LA.size mu0 /= p =
-      noResultTRR "Hotelling T² (1-sample)" TwoSided "μ_0 length mismatch"
-  | n <= p =
-      noResultTRR "Hotelling T² (1-sample)" TwoSided "need n > p (covariance singular)"
-  | otherwise =
-      let nD     = fromIntegral n :: Double
-          pD     = fromIntegral p :: Double
-          xMean  = columnMeans x
-          diff   = xMean - mu0
-          sCov   = sampleCovariance x
-          maybeT2 = do
-            sInv <- LA.linearSolve sCov (LA.asColumn diff)
-            return $! nD * LA.sumElements (diff * LA.flatten sInv)
-      in case maybeT2 of
-           Nothing -> noResultTRR "Hotelling T² (1-sample)" TwoSided
-                                  "covariance matrix singular"
-           Just t2 ->
-             let df1   = pD
-                 df2   = nD - pD
-                 fStat = (df2 / ((nD - 1) * pD)) * t2
-                 pVal  = SD.complCumulative
-                           (FDist.fDistribution (round df1) (round df2))
-                           fStat
-             in TestResult
-                  { trMethod      = "Hotelling T² (1-sample)"
-                  , trStatistic   = fStat
-                  , trDf          = Just (df1, Just df2)
-                  , trPValue      = pVal
-                  , trEffect      = Just ("T²", t2)
-                  , trCI          = Nothing
-                  , trAlternative = TwoSided
-                  , trNote        = Nothing
-                  }
-  where
-    n = LA.rows x
-    p = LA.cols x
-
--- | 2 サンプル Hotelling T² 検定 (等分散仮定、 H_0: μ_X = μ_Y)。
---
--- 入力: X (n_1 × p)、 Y (n_2 × p)。 両標本の次元 p は一致が必要。
---
--- 統計量:
---
--- > T² = (n_1·n_2 / (n_1+n_2)) · (μ̂_1 − μ̂_2)ᵀ S_p⁻¹ (μ̂_1 − μ̂_2)
--- > F  = ((n_1+n_2−p−1) / ((n_1+n_2−2)·p)) · T²,  df = (p, n_1+n_2−p−1)
-hotellingsT2TwoSample :: LA.Matrix Double -> LA.Matrix Double -> TestResult
-hotellingsT2TwoSample x y
-  | n1 < 2 || n2 < 2 =
-      noResultTRR "Hotelling T² (2-sample)" TwoSided "each group needs ≥ 2 observations"
-  | LA.cols x /= LA.cols y =
-      noResultTRR "Hotelling T² (2-sample)" TwoSided "dimension mismatch (p_X ≠ p_Y)"
-  | n1 + n2 - p - 1 <= 0 =
-      noResultTRR "Hotelling T² (2-sample)" TwoSided "need n_1 + n_2 > p + 1"
-  | otherwise =
-      let n1D   = fromIntegral n1 :: Double
-          n2D   = fromIntegral n2 :: Double
-          pD    = fromIntegral p :: Double
-          m1    = columnMeans x
-          m2    = columnMeans y
-          s1    = sampleCovariance x
-          s2    = sampleCovariance y
-          sP    = LA.scale ((n1D - 1) / (n1D + n2D - 2)) s1
-                + LA.scale ((n2D - 1) / (n1D + n2D - 2)) s2
-          diff  = m1 - m2
-          maybeT2 = do
-            sInv <- LA.linearSolve sP (LA.asColumn diff)
-            return $! (n1D * n2D / (n1D + n2D))
-                    * LA.sumElements (diff * LA.flatten sInv)
-      in case maybeT2 of
-           Nothing -> noResultTRR "Hotelling T² (2-sample)" TwoSided
-                                  "pooled covariance singular"
-           Just t2 ->
-             let df1   = pD
-                 df2   = n1D + n2D - pD - 1
-                 fStat = (df2 / ((n1D + n2D - 2) * pD)) * t2
-                 pVal  = SD.complCumulative
-                           (FDist.fDistribution (round df1) (round df2))
-                           fStat
-             in TestResult
-                  { trMethod      = "Hotelling T² (2-sample)"
-                  , trStatistic   = fStat
-                  , trDf          = Just (df1, Just df2)
-                  , trPValue      = pVal
-                  , trEffect      = Just ("T²", t2)
-                  , trCI          = Nothing
-                  , trAlternative = TwoSided
-                  , trNote        = Nothing
-                  }
-  where
-    n1 = LA.rows x
-    n2 = LA.rows y
-    p  = LA.cols x
-
--- | 1 元配置 MANOVA (H_0: 全群の μ が等しい)。
---
--- 入力: 各群の観測行列リスト @[X_1, X_2, ..., X_k]@、 各 X_i は @n_i × p@。
---
--- 統計量: Wilks' Λ = det(W) / det(W + B)。
---   B = between-group SSCP、 W = within-group SSCP。
--- p-value は Rao の F 近似:
---
--- > s = sqrt((p²·q² − 4) / (p² + q² − 5))     (q = k − 1)
--- > m = N − 1 − (p + q + 1) / 2
--- > df1 = p · q,   df2 = m·s − (p·q − 2) / 2
--- > F   = ((1 − Λ^(1/s)) / Λ^(1/s)) · (df2 / df1)
---
--- 'trStatistic' に F 値、 'trEffect' に @("Wilks Λ", Λ)@。
-manova :: [LA.Matrix Double] -> TestResult
-manova groups
-  | k < 2 = noResultTRR "MANOVA (one-way)" TwoSided "need ≥ 2 groups"
-  | any (\g -> LA.rows g < 2) groups =
-      noResultTRR "MANOVA (one-way)" TwoSided "each group needs ≥ 2 observations"
-  | not (all ((== p) . LA.cols) groups) =
-      noResultTRR "MANOVA (one-way)" TwoSided "dimension mismatch across groups"
-  | otherwise =
-      let nis      = map (fromIntegral . LA.rows) groups :: [Double]
-          totalN   = sum nis
-          pD       = fromIntegral p :: Double
-          q        = fromIntegral (k - 1) :: Double
-          groupMs  = map columnMeans groups
-          allMean  = LA.scale (1 / totalN)
-                     (foldr1 (+) (zipWith LA.scale nis groupMs))
-          mkOuter v = LA.outer v v
-          bMat     = foldr1 (+)
-                       [ LA.scale ni (mkOuter (m - allMean))
-                       | (ni, m) <- zip nis groupMs ]
-          wMat     = foldr1 (+) [ withinSSCP g (groupMs !! i)
-                                | (i, g) <- zip [0 ..] groups ]
-          detW     = LA.det wMat
-          detTot   = LA.det (wMat + bMat)
-      in if detTot == 0
-           then noResultTRR "MANOVA (one-way)" TwoSided
-                            "W+B is singular"
-           else
-             let wilks = detW / detTot
-                 -- Rao F approximation
-                 numS  = pD*pD * q*q - 4
-                 denS  = pD*pD + q*q - 5
-                 s | denS > 0 && numS > 0 = sqrt (numS / denS)
-                   | otherwise            = 1
-                 mAdj  = totalN - 1 - (pD + q + 1) / 2
-                 df1   = pD * q
-                 df2   = mAdj * s - (pD * q - 2) / 2
-                 lam1s = wilks ** (1 / s)
-                 fStat = ((1 - lam1s) / lam1s) * (df2 / df1)
-                 df1i  = max 1 (round df1)
-                 df2i  = max 1 (round df2)
-                 pVal  = if df2 > 0 && fStat > 0
-                           then SD.complCumulative
-                                  (FDist.fDistribution df1i df2i) fStat
-                           else 1.0
-             in TestResult
-                  { trMethod      = "MANOVA (one-way, Wilks' Λ)"
-                  , trStatistic   = fStat
-                  , trDf          = Just (df1, Just df2)
-                  , trPValue      = pVal
-                  , trEffect      = Just ("Wilks Λ", wilks)
-                  , trCI          = Nothing
-                  , trAlternative = TwoSided
-                  , trNote        = Nothing
-                  }
-  where
-    k = length groups
-    p = if null groups then 0 else LA.cols (head groups)
-
--- ---------------------------------------------------------------------------
--- 多変量 helper
--- ---------------------------------------------------------------------------
-
--- | 列ごとの平均 (= サンプル平均ベクトル)。
-columnMeans :: LA.Matrix Double -> LA.Vector Double
-columnMeans m =
-  let n = fromIntegral (LA.rows m) :: Double
-  in LA.scale (1 / n) (LA.fromList [ LA.sumElements (m LA.¿ [j])
-                                    | j <- [0 .. LA.cols m - 1] ])
-
--- | 標本共分散行列 (n - 1 分母)。
-sampleCovariance :: LA.Matrix Double -> LA.Matrix Double
-sampleCovariance m =
-  let n      = fromIntegral (LA.rows m) :: Double
-      means  = columnMeans m
-      meanRow = LA.asRow means
-      centered = m - LA.fromRows (replicate (LA.rows m) means)
-      _ = meanRow  -- silence unused warning
-  in LA.scale (1 / (n - 1)) (LA.tr centered LA.<> centered)
-
--- | 群内 SSCP: Σ (x_{ij} − x̄_i)(x_{ij} − x̄_i)ᵀ
-withinSSCP :: LA.Matrix Double -> LA.Vector Double -> LA.Matrix Double
-withinSSCP g groupMean =
-  let centered = g - LA.fromRows (replicate (LA.rows g) groupMean)
-  in LA.tr centered LA.<> centered
-
diff --git a/src/Hanalyze/Stat/VI.hs b/src/Hanalyze/Stat/VI.hs
deleted file mode 100644
--- a/src/Hanalyze/Stat/VI.hs
+++ /dev/null
@@ -1,430 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
--- |
--- Module      : Hanalyze.Stat.VI
--- Description : 変分推論 (ADVI: Automatic Differentiation Variational Inference)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
--- Variational inference (ADVI — Automatic Differentiation Variational
--- Inference).
---
--- Implements the mean-field normal VI of Kucukelbir et al. (2017). Uses
--- the same unconstrained transform as HMC/NUTS and maximizes the ELBO
--- with Adam.
---
--- Approximating family: @q(u; φ) = Π_i Normal(u_i; μ_i, σ_i)@
---
--- @
--- ELBO = E_q[log p(θ,y) + log|J|] + Σ_i H[Normal(μ_i, σ_i)]
---      = E_q[logJointU(u)] + Σ_i ω_i + N/2 × (1 + log 2π)
--- @
---
--- Gradient (reparameterization trick):
---
--- @
--- u^s = μ + σ ⊙ ε^s,  ε^s ~ N(0, I)
--- ∂ELBO/∂μ_i ≈ (1/S) Σ_s ∂logJointU/∂u_i |_{u^s}
--- ∂ELBO/∂ω_i ≈ (1/S) Σ_s ε_i^s × σ_i × ∂logJointU/∂u_i |_{u^s} + 1
--- @
---
--- @
--- let cfg = defaultVIConfig { viIterations = 1000 }
--- result <- advi model cfg initParams gen
--- print (viPostMeans result)
--- @
-module Hanalyze.Stat.VI
-  ( VIConfig (..)
-  , defaultVIConfig
-  , VIResult (..)
-  , VIMethod (..)
-  , advi
-  , fullRankAdvi
-  ) where
-
-import Control.DeepSeq (force)
-import Control.Monad (forM, forM_, replicateM, when)
-import Data.IORef
-import qualified Data.Map.Strict as Map
-import System.Random.MWC (GenIO)
-import System.Random.MWC.Distributions (standard)
-
-import Hanalyze.Model.HBM (ModelP, Params, sampleNames, getTransforms)
-import Hanalyze.Optim.Adam (adamStep)
-import Hanalyze.MCMC.HMC  ( logJointU, paramsToVec, vecToParams
-                 , toUnconstrainedParams, fromUnconstrainedParams )
-
--- ---------------------------------------------------------------------------
--- 設定
--- ---------------------------------------------------------------------------
-
--- | ADVI configuration.
-data VIConfig = VIConfig
-  { viIterations   :: Int     -- ^ Number of Adam iterations.
-  , viSamples      :: Int     -- ^ Monte Carlo samples per ELBO gradient (5–10 typical).
-  , viLearningRate :: Double  -- ^ Adam learning rate @α@.
-  , viBeta1        :: Double  -- ^ Adam @β₁@ (default 0.9).
-  , viBeta2        :: Double  -- ^ Adam @β₂@ (default 0.999).
-  , viEpsilon      :: Double  -- ^ Adam @ε@ (default 1e-8).
-  , viNumDraws     :: Int     -- ^ Number of post-fit draws from @q@.
-  , viGradStep     :: Double  -- ^ Finite-difference step for numeric gradients.
-  } deriving (Show)
-
--- | Sensible defaults for ADVI: 1000 iterations, 5 MC samples, Adam at
--- @α = 0.1@.
-defaultVIConfig :: VIConfig
-defaultVIConfig = VIConfig
-  { viIterations   = 1000
-  , viSamples      = 5
-  , viLearningRate = 0.1
-  , viBeta1        = 0.9
-  , viBeta2        = 0.999
-  , viEpsilon      = 1e-8
-  , viNumDraws     = 2000
-  , viGradStep     = 1e-5
-  }
-
--- ---------------------------------------------------------------------------
--- 結果
--- ---------------------------------------------------------------------------
-
--- | VI 近似法。 mean-field (`advi`) と full-rank (`fullRankAdvi`) を区別する。
-data VIMethod = MeanField | FullRank
-  deriving (Show, Eq)
-
--- | ADVI result. mean-field と full-rank の両方が返す。 full-rank では
--- @viCovU@ に @n×n@ 下三角 Cholesky 因子 @L@ (unconstrained 空間) が入る。
-data VIResult = VIResult
-  { viPostMeans   :: Params           -- ^ Posterior means (constrained space, sample mean).
-  , viPostSDs     :: Params           -- ^ Posterior SDs   (constrained space).
-  , viMuU         :: [Double]         -- ^ Variational mean @μ@ (unconstrained).
-  , viSigmaU      :: [Double]         -- ^ Variational SD   @σ@ (unconstrained、 mean-field の場合は対角要素、 full-rank なら L_ii)。
-  , viCovU        :: Maybe [[Double]] -- ^ Full-rank ADVI: 下三角 Cholesky 因子 @L@ ([row][col])、 @LLᵀ = Σ@。 mean-field では @Nothing@。
-  , viMethod      :: VIMethod         -- ^ どちらの近似法か。
-  , viElboHistory :: [Double]         -- ^ ELBO trajectory (for convergence inspection).
-  , viDraws       :: [Params]         -- ^ Posterior draws in the constrained space (length 'viNumDraws').
-  } deriving (Show)
-
--- ---------------------------------------------------------------------------
--- ADVI
--- ---------------------------------------------------------------------------
-
--- | Run mean-field normal ADVI.
---
--- Optimization happens in unconstrained space; samples are mapped back
--- to the constrained space on the way out. Constrained parameters
--- (e.g. @Exponential → PositiveT@) are transformed automatically.
-advi :: ModelP r -> VIConfig -> Params -> GenIO -> IO VIResult
-advi model cfg initP gen = do
-  let names      = sampleNames model
-      transforms = getTransforms model
-      n          = length names
-      initU      = paramsToVec names (toUnconstrainedParams transforms initP)
-
-      -- unconstrained 空間での log p(θ,y) + log|J| (Jacobian 補正済み)
-      logJ :: [Double] -> Double
-      logJ uVec = logJointU model transforms (vecToParams names uVec)
-
-      -- 有限差分勾配 ∂logJ/∂u
-      h = viGradStep cfg
-      numGrad :: [Double] -> [Double]
-      numGrad uVec =
-        [ let ui  = uVec !! i
-              lp  = logJ (replaceAt i (ui + h) uVec)
-              lm  = logJ (replaceAt i (ui - h) uVec)
-              raw = (lp - lm) / (2 * h)
-          in if isNaN raw || isInfinite raw then 0 else raw
-        | i <- [0 .. n-1]
-        ]
-
-  -- 変分パラメータ: μ (unconstrained 平均), ω = log(σ) (log 標準偏差)
-  muRef    <- newIORef initU
-  omegaRef <- newIORef (replicate n 0.0)  -- σ = exp(0) = 1 で初期化
-
-  -- Adam の 1次/2次モーメント
-  m1MuRef <- newIORef (replicate n 0.0)
-  m2MuRef <- newIORef (replicate n 0.0)
-  m1OmRef <- newIORef (replicate n 0.0)
-  m2OmRef <- newIORef (replicate n 0.0)
-
-  elboRef <- newIORef []
-
-  let b1    = viBeta1        cfg
-      b2    = viBeta2        cfg
-      eps_  = viEpsilon      cfg
-      alpha = viLearningRate cfg
-      sNum  = viSamples      cfg
-
-  -- Adam ループ
-  forM_ [1 .. viIterations cfg] $ \t -> do
-    mu    <- readIORef muRef
-    omega <- readIORef omegaRef
-    let sigma = map exp omega
-
-    -- MC 勾配推定
-    mcResults <- forM [1 .. sNum] $ \_ -> do
-      epsilons <- replicateM n (standard gen)
-      let -- u^s = μ + σ ⊙ ε  (reparameterization)
-          uVec = zipWith3 (\m s e -> m + s * e) mu sigma epsilons
-          lj   = logJ uVec
-          g    = numGrad uVec
-          -- ∂ELBO/∂μ_i = ∂logJ/∂u_i
-          dMu  = g
-          -- ∂ELBO/∂ω_i = ε_i × σ_i × ∂logJ/∂u_i + 1  (+1 はエントロピー項)
-          dOm  = zipWith3 (\e s gi -> e * s * gi + 1) epsilons sigma g
-      return (lj, dMu, dOm)
-
-    let sD    = fromIntegral sNum :: Double
-        !ljMC = sum (map (\(l,_,_) -> l) mcResults) / sD
-        -- ELBO = E[logJointU] + Σω + N/2×(1+log2π)
-        !elboV = ljMC + sum omega + fromIntegral n * 0.5 * (1 + log (2*pi))
-        !gMu   = force (map (/ sD) $ foldr1 (zipWith (+)) (map (\(_,g,_) -> g) mcResults))
-        !gOm   = force (map (/ sD) $ foldr1 (zipWith (+)) (map (\(_,_,g) -> g) mcResults))
-
-    modifyIORef' elboRef (elboV :)
-
-    -- Adam で μ を更新
-    m1Mu <- readIORef m1MuRef
-    m2Mu <- readIORef m2MuRef
-    let (m1Mu', m2Mu', dxMu) = adamStep b1 b2 eps_ alpha t m1Mu m2Mu gMu
-    -- Phase Q3 (2026-05-14): 'zipWith (+)' / Adam の各リストは lazy で、
-    -- IORef に書き戻すとそのまま thunk のまま積まれ、次イテレーションで
-    -- 読み出されると `zipWith (+) thunk_{t-1} ...` が再帰的に重なる。
-    -- iter=10000 K=20 で max residency 85 MB / 総 alloc 222 GB を観測。
-    -- 'force' で spine + 各要素を NF にし、t 階層の thunk チェーンを断つ。
-    writeIORef m1MuRef (force m1Mu')
-    writeIORef m2MuRef (force m2Mu')
-    writeIORef muRef   (force (zipWith (+) mu dxMu))
-
-    -- Adam で ω を更新
-    m1Om <- readIORef m1OmRef
-    m2Om <- readIORef m2OmRef
-    let (m1Om', m2Om', dxOm) = adamStep b1 b2 eps_ alpha t m1Om m2Om gOm
-    writeIORef m1OmRef (force m1Om')
-    writeIORef m2OmRef (force m2Om')
-    writeIORef omegaRef (force (zipWith (+) omega dxOm))
-
-  -- 収束後: q(u; φ*) からサンプリングして constrained 空間に変換
-  muFinal    <- readIORef muRef
-  omegaFinal <- readIORef omegaRef
-  let sigmaFinal = map exp omegaFinal
-
-  draws <- forM [1 .. viNumDraws cfg] $ \_ -> do
-    epsilons <- replicateM n (standard gen)
-    let uVec = zipWith3 (\m s e -> m + s * e) muFinal sigmaFinal epsilons
-    return (fromUnconstrainedParams transforms (vecToParams names uVec))
-
-  -- サンプルから事後平均・SD を計算
-  let nD        = fromIntegral (viNumDraws cfg) :: Double
-      getVals p = map (Map.findWithDefault 0 p) draws
-      muP     p = let vs = getVals p in sum vs / nD
-      sdP     p = let vs = getVals p
-                      mu = muP p
-                  in sqrt (sum (map (\v -> (v - mu) ^ (2::Int)) vs) / nD)
-      postMeans = Map.fromList [(nm, muP nm) | nm <- names]
-      postSDs   = Map.fromList [(nm, sdP nm) | nm <- names]
-
-  elboHistory <- fmap reverse (readIORef elboRef)
-
-  return VIResult
-    { viPostMeans   = postMeans
-    , viPostSDs     = postSDs
-    , viMuU         = muFinal
-    , viSigmaU      = sigmaFinal
-    , viCovU        = Nothing
-    , viMethod      = MeanField
-    , viElboHistory = elboHistory
-    , viDraws       = draws
-    }
-
--- ---------------------------------------------------------------------------
--- Full-rank ADVI (Phase 37-A5)
--- ---------------------------------------------------------------------------
-
--- | Full-rank ADVI: 共分散を含めた変分近似 @q(u) = N(μ, LLᵀ)@ を最適化する。
---
--- 平均場 'advi' との違い:
---
--- * 変分パラメータは @μ@ (n-vector) と @L@ (下三角 n×n、 対角は log で
---   parameterize して正値保証)
--- * @u = μ + L·ε@ の reparameterization で勾配を取り、 ELBO の補正項は
---   @log|L| = Σ log L_ii = Σ ω_i@
--- * 推定共分散 @Σ = LLᵀ@ は @viCovU@ に入る (下三角 @L@ そのもの)
---
--- 平均場と比べて posterior の相関を捉えられるが、 パラメタ数 @O(n²)@、
--- 計算量も @O(n² S)@ per iteration なので n が大きいモデルでは重い。
--- 平均場が「SD を過小評価」 する hierarchical model で特に有用。
-fullRankAdvi :: ModelP r -> VIConfig -> Params -> GenIO -> IO VIResult
-fullRankAdvi model cfg initP gen = do
-  let names      = sampleNames model
-      transforms = getTransforms model
-      n          = length names
-      initU      = paramsToVec names (toUnconstrainedParams transforms initP)
-
-      logJ :: [Double] -> Double
-      logJ uVec = logJointU model transforms (vecToParams names uVec)
-
-      h = viGradStep cfg
-      numGrad :: [Double] -> [Double]
-      numGrad uVec =
-        [ let ui  = uVec !! i
-              lp  = logJ (replaceAt i (ui + h) uVec)
-              lm  = logJ (replaceAt i (ui - h) uVec)
-              raw = (lp - lm) / (2 * h)
-          in if isNaN raw || isInfinite raw then 0 else raw
-        | i <- [0 .. n-1]
-        ]
-
-  -- 変分パラメータ: μ (n-vector)、 ω (n-vector、 ω_i = log L_ii)、
-  -- offdiag (下三角の i > j 要素を行優先で並べた長さ n(n-1)/2 のリスト)
-  muRef    <- newIORef initU
-  omegaRef <- newIORef (replicate n 0.0)             -- L_ii = exp(0) = 1
-  let nOff = n * (n - 1) `div` 2
-  offRef   <- newIORef (replicate nOff 0.0)          -- off-diag は 0 で初期化
-
-  -- Adam モーメント (μ / ω / offdiag それぞれ)
-  m1MuRef <- newIORef (replicate n 0.0)
-  m2MuRef <- newIORef (replicate n 0.0)
-  m1OmRef <- newIORef (replicate n 0.0)
-  m2OmRef <- newIORef (replicate n 0.0)
-  m1OffRef <- newIORef (replicate nOff 0.0)
-  m2OffRef <- newIORef (replicate nOff 0.0)
-
-  elboRef <- newIORef []
-
-  let b1    = viBeta1        cfg
-      b2    = viBeta2        cfg
-      eps_  = viEpsilon      cfg
-      alpha = viLearningRate cfg
-      sNum  = viSamples      cfg
-
-  forM_ [1 .. viIterations cfg] $ \t -> do
-    mu     <- readIORef muRef
-    omega  <- readIORef omegaRef
-    offdg  <- readIORef offRef
-    let lMat  = buildL n omega offdg                  -- 下三角 L
-
-    -- MC 勾配
-    mcResults <- forM [1 .. sNum] $ \_ -> do
-      epsilons <- replicateM n (standard gen)
-      let uVec = vecAdd mu (matVec lMat epsilons)
-          lj   = logJ uVec
-          gU   = numGrad uVec                          -- ∂lp/∂u_i, length n
-          dMu  = gU                                    -- ∂ELBO/∂μ_i = gU_i
-          -- ∂ELBO/∂ω_i = ε_i × L_ii × gU_i + 1  (entropy +1)
-          dOm  = [ epsilons !! i
-                 * (lMat !! i !! i)
-                 * (gU !! i) + 1
-                 | i <- [0 .. n-1] ]
-          -- ∂ELBO/∂L_ij (i > j) = ε_j × gU_i  (no entropy contribution)
-          dOff = [ (epsilons !! j) * (gU !! i)
-                 | i <- [1 .. n-1], j <- [0 .. i-1] ]
-      return (lj, dMu, dOm, dOff)
-
-    let sD    = fromIntegral sNum :: Double
-        !ljMC = sum (map (\(l,_,_,_) -> l) mcResults) / sD
-        -- ELBO = E[logJointU] + log|L| + n/2 (1 + log 2π)
-        !elboV = ljMC + sum omega + fromIntegral n * 0.5 * (1 + log (2*pi))
-        !gMu   = force (map (/ sD) $ foldr1 (zipWith (+))
-                                     (map (\(_,g,_,_) -> g) mcResults))
-        !gOm   = force (map (/ sD) $ foldr1 (zipWith (+))
-                                     (map (\(_,_,g,_) -> g) mcResults))
-        !gOff  = if nOff == 0
-                   then []
-                   else force (map (/ sD) $ foldr1 (zipWith (+))
-                                            (map (\(_,_,_,g) -> g) mcResults))
-
-    modifyIORef' elboRef (elboV :)
-
-    -- Adam で μ
-    m1Mu <- readIORef m1MuRef
-    m2Mu <- readIORef m2MuRef
-    let (m1Mu', m2Mu', dxMu) = adamStep b1 b2 eps_ alpha t m1Mu m2Mu gMu
-    writeIORef m1MuRef (force m1Mu')
-    writeIORef m2MuRef (force m2Mu')
-    writeIORef muRef   (force (zipWith (+) mu dxMu))
-
-    -- Adam で ω
-    m1Om <- readIORef m1OmRef
-    m2Om <- readIORef m2OmRef
-    let (m1Om', m2Om', dxOm) = adamStep b1 b2 eps_ alpha t m1Om m2Om gOm
-    writeIORef m1OmRef (force m1Om')
-    writeIORef m2OmRef (force m2Om')
-    writeIORef omegaRef (force (zipWith (+) omega dxOm))
-
-    -- Adam で off-diagonal (n=1 のときは空)
-    when (nOff > 0) $ do
-      m1Off <- readIORef m1OffRef
-      m2Off <- readIORef m2OffRef
-      let (m1Off', m2Off', dxOff) = adamStep b1 b2 eps_ alpha t m1Off m2Off gOff
-      writeIORef m1OffRef (force m1Off')
-      writeIORef m2OffRef (force m2Off')
-      writeIORef offRef   (force (zipWith (+) offdg dxOff))
-
-  -- 収束後
-  muFinal    <- readIORef muRef
-  omegaFinal <- readIORef omegaRef
-  offFinal   <- readIORef offRef
-  let lFinal   = buildL n omegaFinal offFinal
-      lDiag    = [ lFinal !! i !! i | i <- [0 .. n-1] ]
-
-  draws <- forM [1 .. viNumDraws cfg] $ \_ -> do
-    epsilons <- replicateM n (standard gen)
-    let uVec = vecAdd muFinal (matVec lFinal epsilons)
-    return (fromUnconstrainedParams transforms (vecToParams names uVec))
-
-  let nD        = fromIntegral (viNumDraws cfg) :: Double
-      getVals p = map (Map.findWithDefault 0 p) draws
-      muP     p = let vs = getVals p in sum vs / nD
-      sdP     p = let vs = getVals p
-                      mu = muP p
-                  in sqrt (sum (map (\v -> (v - mu) ^ (2::Int)) vs) / nD)
-      postMeans = Map.fromList [(nm, muP nm) | nm <- names]
-      postSDs   = Map.fromList [(nm, sdP nm) | nm <- names]
-
-  elboHistory <- fmap reverse (readIORef elboRef)
-
-  return VIResult
-    { viPostMeans   = postMeans
-    , viPostSDs     = postSDs
-    , viMuU         = muFinal
-    , viSigmaU      = lDiag
-    , viCovU        = Just lFinal
-    , viMethod      = FullRank
-    , viElboHistory = elboHistory
-    , viDraws       = draws
-    }
-
--- | 下三角 L を構築。 @omega@ は対角 (L_ii = exp ω_i)、
--- @offdg@ は (i, j) for i > j を行優先 (i 昇順、 同 i 内で j 昇順) で
--- 並べたリスト。 結果は @n × n@ 行列、 上三角は 0。
-buildL :: Int -> [Double] -> [Double] -> [[Double]]
-buildL n omega offdg =
-  let -- offdg をインデックス map に変換
-      offMap = Map.fromList (zip pairs offdg)
-      pairs  = [ (i, j) | i <- [1 .. n-1], j <- [0 .. i-1] ]
-      diag i = exp (omega !! i)
-      row i  = [ if j < i  then Map.findWithDefault 0 (i, j) offMap
-                 else if j == i then diag i
-                 else 0
-               | j <- [0 .. n-1] ]
-  in [ row i | i <- [0 .. n-1] ]
-
--- | 行列・ベクトル積 @y = M·x@。
-matVec :: [[Double]] -> [Double] -> [Double]
-matVec mat x = [ sum (zipWith (*) row x) | row <- mat ]
-
--- | ベクトル足し算。
-vecAdd :: [Double] -> [Double] -> [Double]
-vecAdd = zipWith (+)
-
--- ---------------------------------------------------------------------------
--- 補助関数
--- ---------------------------------------------------------------------------
-
--- adamStep は Hanalyze.Optim.Adam に集約 (Phase R0)。
--- 再 export することで既存の利用箇所はそのまま動く。
-
--- | リストの i 番目要素を x で置換する。
-replaceAt :: Int -> Double -> [Double] -> [Double]
-replaceAt i x xs = take i xs ++ [x] ++ drop (i + 1) xs
diff --git a/src/Hanalyze/Viz/AnalysisReport.hs b/src/Hanalyze/Viz/AnalysisReport.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/AnalysisReport.hs
+++ /dev/null
@@ -1,2109 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.AnalysisReport
--- Description : 【非推奨】 LM/GLM/GLMM/GP/HBM 専用の sum-type ベース HTML レポート (ReportBuilder に移行済)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- | __DEPRECATED__ — sum-type-based HTML report dedicated to
--- LM / GLM / GLMM / GP / HBM (~2000 lines). Superseded by
--- 'Hanalyze.Viz.ReportBuilder' (compositional @ReportSection@ + @Reportable@
--- typeclass). New models / visualizations should use the ReportBuilder
--- side. This module is kept for backwards compatibility with the
--- existing CLI (@hanalyze regress --report@) and will be removed in a
--- future release.
---
--- Legacy section layout:
---
---   1. Data characteristics (N, column statistics, histograms).
---   2. Model overview (kind, formula, family / link).
---   3. Regression results (coefficient table, R², scatter, residual plots).
---   4. Interactive prediction (live scatter with CI / PI).
---   5. Appendix (theoretical background).
-module Hanalyze.Viz.AnalysisReport {-# DEPRECATED "Hanalyze.Viz.AnalysisReport is deprecated; use Hanalyze.Viz.ReportBuilder for new code." #-}
-  ( -- * 設定
-    AnalysisReportConfig (..)
-  , defaultAnalysisConfig
-    -- * Smooth-fit data
-  , SmoothData (..)
-    -- * Fit summary
-  , FitSummary (..)
-  , mkFitSummary
-  , GLMMSummary (..)
-  , mkGLMMSummary
-    -- * GP fit summary
-  , GPKernelFit (..)
-  , GPFitSummary (..)
-    -- * HBM (Bayesian) fit summary
-  , HBMRegSummary (..)
-    -- * Model fit (unified type)
-  , ModelFit (..)
-    -- * Named plot
-  , NamedPlot (..)
-    -- * Report generation
-  , writeAnalysisReport
-  , writeAnalysisReportPlots
-    -- * Multi-model comparison report
-  , CompareEntry (..)
-  , writeComparisonReport
-  ) where
-
-import Data.Aeson (encode)
-import Data.ByteString.Lazy (toStrict)
-import Data.List (sort)
-import Data.Text (Text)
-import qualified Data.Text    as T
-import qualified Data.Text.IO as TIO
-import Data.Text.Encoding (decodeUtf8)
-import Graphics.Vega.VegaLite (VegaLite, fromVL)
-import Numeric (showFFloat)
-import qualified Data.Vector as V
-import qualified Numeric.LinearAlgebra as LA
-
-import qualified DataFrame.Operations.Core     as DX
-import qualified DataFrame.Internal.DataFrame as DXD
-import Hanalyze.DataIO.Convert (getDoubleVec, getTextVec)
-import Hanalyze.MCMC.Core    (Chain, chainSamples, chainAccepted, chainTotal)
-import Hanalyze.Model.Core   (FitResult (..), coeffList, fittedList,
-                     residualsV, rSquared1)
-import Hanalyze.Model.GLM    (Family (..), LinkFn (..))
-import Hanalyze.Stat.ModelSelect (WAICResult (..), LOOResult (..))
-import Hanalyze.Model.GLMM   (GLMMResult (..))
-import Hanalyze.Model.GP     (Kernel (..), GPParams (..), GPResult (..), GPPredData (..))
-import Hanalyze.Model.HBM    (ModelGraph)
-import Hanalyze.Viz.Assets   (vegaJS, vegaLiteJS, vegaEmbedJS)
-import Hanalyze.Viz.Core     (PlotConfig (..), OutputFormat (..), writeSpec)
-import Hanalyze.Viz.GP       (gpPlot)
-import Hanalyze.Viz.ModelGraph (buildMermaid)
-
--- ---------------------------------------------------------------------------
--- Public types
--- ---------------------------------------------------------------------------
-
-data AnalysisReportConfig = AnalysisReportConfig
-  { arcTitle :: Text
-  } deriving (Show)
-
-defaultAnalysisConfig :: Text -> AnalysisReportConfig
-defaultAnalysisConfig = AnalysisReportConfig
-
--- | スムーズフィット曲線データ (対話的予測チャート用)。
-data SmoothData = SmoothData
-  { sdXs      :: [Double]  -- ^ グリッド x 値
-  , sdYs      :: [Double]  -- ^ 予測 y 値
-  , sdLower   :: [Double]  -- ^ CI/PI 下限
-  , sdUpper   :: [Double]  -- ^ CI/PI 上限
-  , sdHasBand :: Bool      -- ^ バンドを持つか
-  } deriving (Show)
-
--- | LM / GLM の回帰サマリー。
-data FitSummary = FitSummary
-  { fsModelType    :: Text                       -- ^ "LM", "GLM (Poisson/Log)" etc.
-  , fsFormula      :: Text                       -- ^ "y ~ x + x²"
-  , fsCoeffs       :: [(Text, Double)]           -- ^ (ラベル, 値)
-  , fsR2           :: Double                     -- ^ R² or McFadden R²
-  , fsR2Label      :: Text                       -- ^ "R²" or "McFadden R²"
-  , fsFitted       :: [Double]                   -- ^ fitted values
-  , fsResiduals    :: [Double]                   -- ^ residuals
-  , fsLinkName     :: Text                       -- ^ "identity"|"log"|"logit"|"sqrt"
-  , fsXColDegs     :: [(Text, Int)]              -- ^ x列と次数 (JS予測用)
-  , fsSmoothData   :: Maybe (Text, SmoothData)   -- ^ (x列名, スムーズデータ) 単回帰のみ
-  , fsModelSelect  :: Maybe (WAICResult, LOOResult) -- ^ WAIC/LOO-CV (--waic 時のみ)
-  } deriving (Show)
-
-mkFitSummary
-  :: Family
-  -> LinkFn
-  -> [(Text, Int)]
-  -> Maybe (Text, SmoothData)
-  -> FitResult
-  -> FitSummary
-mkFitSummary fam lnk colDegs mSmooth res = FitSummary
-  { fsModelType    = modelTypeLabel fam lnk
-  , fsFormula      = formulaText colDegs
-  , fsCoeffs       = zip (coeffLabels colDegs) (coeffList res)
-  , fsR2           = rSquared1 res
-  , fsR2Label      = r2Label fam
-  , fsFitted       = fittedList res
-  , fsResiduals    = LA.toList (residualsV res)
-  , fsLinkName     = linkName lnk
-  , fsXColDegs     = colDegs
-  , fsSmoothData   = mSmooth
-  , fsModelSelect  = Nothing
-  }
-
--- | GLMM / LME のサマリー。
-data GLMMSummary = GLMMSummary
-  { gsModelType    :: Text
-  , gsFormula      :: Text
-  , gsFixed        :: [(Text, Double)]
-  , gsR2           :: Double
-  , gsR2Label      :: Text
-  , gsGroupCol     :: Text
-  , gsRandVar      :: Double
-  , gsResidVar     :: Double
-  , gsICC          :: Double
-  , gsBLUPs        :: [(Text, Double)]
-  , gsFitted       :: [Double]
-  , gsResiduals    :: [Double]
-  , gsLinkName     :: Text
-  , gsXColDegs     :: [(Text, Int)]
-  , gsSmoothData   :: Maybe (Text, SmoothData)
-  , gsModelSelect  :: Maybe (WAICResult, LOOResult)  -- ^ 条件付き WAIC/LOO (--waic 時)
-  } deriving (Show)
-
-mkGLMMSummary
-  :: Family
-  -> LinkFn
-  -> [(Text, Int)]
-  -> Text
-  -> Maybe (Text, SmoothData)
-  -> GLMMResult
-  -> GLMMSummary
-mkGLMMSummary fam lnk colDegs grpCol mSmooth gr = GLMMSummary
-  { gsModelType    = glmmTypeLabel fam lnk
-  , gsFormula      = formulaText colDegs <> " | " <> grpCol
-  , gsFixed        = zip (coeffLabels colDegs) (coeffList (glmmFixed gr))
-  , gsR2           = rSquared1 (glmmFixed gr)
-  , gsR2Label      = r2Label fam
-  , gsGroupCol     = grpCol
-  , gsRandVar      = glmmRandVar gr
-  , gsResidVar     = glmmResidVar gr
-  , gsICC          = glmmICC gr
-  , gsBLUPs        = zip (V.toList (glmmGroups gr)) (V.toList (glmmBLUPs gr))
-  , gsFitted       = fittedList (glmmFixed gr)
-  , gsResiduals    = LA.toList (residualsV (glmmFixed gr))
-  , gsLinkName     = linkName lnk
-  , gsXColDegs     = colDegs
-  , gsSmoothData   = mSmooth
-  , gsModelSelect  = Nothing
-  }
-
--- | GP の1カーネルのフィット結果。
-data GPKernelFit = GPKernelFit
-  { gkLabel    :: Text
-  , gkKernel   :: Kernel
-  , gkParams   :: GPParams
-  , gkResult   :: GPResult
-  , gkLML      :: Double
-  , gkPredData :: GPPredData
-  } deriving (Show)
-
--- | GP 回帰サマリー (複数カーネル比較)。
-data GPFitSummary = GPFitSummary
-  { gfKernelFits :: [GPKernelFit]   -- ^ LML 降順でソート済み
-  , gfXCol       :: Text
-  , gfYCol       :: Text
-  , gfTrainXs    :: [Double]
-  , gfTrainYs    :: [Double]
-  } deriving (Show)
-
--- | HBM (ベイズ回帰) のサマリー。
--- 内部に LM 互換の 'FitSummary' を持ち、加えて DAG と MCMC チェーンを保持する。
-data HBMRegSummary = HBMRegSummary
-  { hbmsFit           :: FitSummary    -- ^ 回帰スタイルの基本サマリー
-                                       -- (係数 = 事後平均、smoothData = 信用区間付き予測曲線)
-  , hbmsModelGraph    :: ModelGraph    -- ^ Mermaid DAG (モデル概要に表示)
-  , hbmsChain         :: Chain         -- ^ MCMC チェーン (回帰結果に診断プロット表示)
-  , hbmsParams        :: [Text]        -- ^ 全潜在変数名 (alpha/beta/sigma 等)
-  , hbmsPosteriorRows :: [(Text, Double, Double, Double, Double)]
-                                       -- ^ (name, mean, sd, q025, q975)
-  } deriving (Show)
-
--- | モデルフィットの統一型。
-data ModelFit
-  = RegFit   FitSummary
-  | MixFit   GLMMSummary
-  | GPFit    GPFitSummary
-  | HBMFit   HBMRegSummary
-  | NoRegFit
-
--- | 名前付き Vega-Lite プロット。
-data NamedPlot = NamedPlot
-  { npName :: Text
-  , npTitle :: Text
-  , npSpec  :: VegaLite
-  }
-
--- ---------------------------------------------------------------------------
--- Entry point
--- ---------------------------------------------------------------------------
-
-writeAnalysisReport
-  :: FilePath
-  -> AnalysisReportConfig
-  -> DXD.DataFrame
-  -> [Text]
-  -> Text
-  -> ModelFit
-  -> [NamedPlot]
-  -> IO ()
-writeAnalysisReport path cfg df xCols yCol fit plots =
-  TIO.writeFile path (buildHtml cfg df xCols yCol fit plots)
-
--- | レポートに含まれる Vega-Lite プロットを個別ファイルとして書き出す。
---
--- 各 'NamedPlot' を @<prefix>-<idx>-<name>.<ext>@ に出力する。
--- HTML 専用要素 (DAG, 事後分布表, 対話的予測 UI, ヒストグラム JS) は
--- vl-convert で変換できないためスキップする。
---
--- 戻り値: 書き出したファイルパスのリスト。
-writeAnalysisReportPlots
-  :: FilePath        -- ^ ファイル名プレフィックス (拡張子なし)
-  -> OutputFormat    -- ^ PNG / SVG (HTML は 'writeAnalysisReport' を使うこと)
-  -> [NamedPlot]
-  -> IO [FilePath]
-writeAnalysisReportPlots prefix fmt plots = do
-  let ext = case fmt of
-        PNG  -> ".png"
-        SVG  -> ".svg"
-        HTML -> ".html"
-      paths = [ prefix <> "-" <> show (i :: Int) <> "-"
-                <> sanitize (T.unpack (npName p)) <> ext
-              | (i, p) <- zip [1..] plots ]
-  mapM_ (\(path, p) -> writeSpec fmt path (npSpec p))
-        (zip paths plots)
-  return paths
-  where
-    sanitize = map (\c -> if c `elem` ("/\\: " :: String) then '_' else c)
-
--- ---------------------------------------------------------------------------
--- HTML builder
--- ---------------------------------------------------------------------------
-
-buildHtml :: AnalysisReportConfig -> DXD.DataFrame -> [Text] -> Text -> ModelFit -> [NamedPlot] -> Text
-buildHtml cfg df xCols yCol fit plots = T.unlines $
-  [ "<!DOCTYPE html>"
-  , "<html lang=\"ja\">"
-  , "<head>"
-  , "  <meta charset=\"utf-8\">"
-  , "  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
-  , "  <title>" <> arcTitle cfg <> "</title>"
-  , "  <script>" <> vegaJS      <> "</script>"
-  , "  <script>" <> vegaLiteJS  <> "</script>"
-  , "  <script>" <> vegaEmbedJS <> "</script>"
-  , if isHBMFit fit
-      then "  <script src=\"https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js\"></script>"
-      else ""
-  , "  <style>" , reportCss , "  </style>"
-  , "</head>"
-  , "<body>"
-  , navBar cfg fit
-  , "<main>"
-  , dataSummarySection df xCols yCol
-  , modelSection fit
-  , resultsSection fit plots
-  ] ++
-  predictionSection df xCols yCol fit ++
-  [ appendixSection fit
-  , "</main>"
-  , "<script>"
-  , if isHBMFit fit
-      then "mermaid.initialize({ startOnLoad: true, theme: 'default' });"
-      else ""
-  , embedScript plots
-  , gpVegaEmbedJS fit
-  , columnDataJS df xCols yCol
-  , predChartSpecJS fit xCols yCol df
-  , gpModelsDataJS fit
-  , predJS fit
-  , histogramInitJS (xCols ++ [yCol])
-  , gpTabSwitchJS fit
-  , smoothScrollScript
-  , "</script>"
-  , "</body>"
-  , "</html>"
-  ]
-
--- ---------------------------------------------------------------------------
--- Nav bar
--- ---------------------------------------------------------------------------
-
-navBar :: AnalysisReportConfig -> ModelFit -> Text
-navBar cfg fit = T.unlines
-  [ "<nav>"
-  , "  <h1>&#128202; " <> arcTitle cfg <> "</h1>"
-  , "  <a class=\"nav-link\" href=\"#sec-data\">データ</a>"
-  , "  <a class=\"nav-link\" href=\"#sec-model\">" <> modelNavLabel fit <> "</a>"
-  , "  <a class=\"nav-link\" href=\"#sec-results\">結果</a>"
-  , if hasPrediction fit
-      then "  <a class=\"nav-link\" href=\"#sec-predict\">予測</a>"
-      else ""
-  , "  <a class=\"nav-link\" href=\"#sec-appendix\">付録</a>"
-  , "</nav>"
-  ]
-
-modelNavLabel :: ModelFit -> Text
-modelNavLabel (GPFit _)  = "モデル比較"
-modelNavLabel (HBMFit _) = "モデル"
-modelNavLabel _          = "モデル"
-
-hasPrediction :: ModelFit -> Bool
-hasPrediction NoRegFit = False
-hasPrediction _        = True
-
-isGPFit :: ModelFit -> Bool
-isGPFit (GPFit _) = True
-isGPFit _         = False
-
-isHBMFit :: ModelFit -> Bool
-isHBMFit (HBMFit _) = True
-isHBMFit _          = False
-
--- ---------------------------------------------------------------------------
--- Section 1: Data summary with histograms
--- ---------------------------------------------------------------------------
-
-dataSummarySection :: DXD.DataFrame -> [Text] -> Text -> Text
-dataSummarySection df xCols yCol = T.unlines $
-  [ "<section id=\"sec-data\">"
-  , "  <h2><span class=\"sec-icon\">&#128202;</span> 1. データの特性</h2>"
-  , "  <div class=\"stat-grid\" style=\"margin-bottom:20px\">"
-  , statBox "N (サンプル数)" (T.pack (show ((fst (DX.dimensions df))))) False
-  , "  </div>"
-  , "  <div class=\"col-cards\">"
-  ] ++
-  concatMap (colCard df "説明変数") xCols ++
-  colCard df "目的変数" yCol ++
-  [ "  </div>"
-  , "</section>"
-  ]
-
-colCard :: DXD.DataFrame -> Text -> Text -> [Text]
-colCard df role col =
-  case getDoubleVec col df of
-    Nothing -> []
-    Just v  ->
-      let sorted  = sort (V.toList v)
-          n       = length sorted
-          nD      = fromIntegral n :: Double
-          mn      = head sorted
-          mx      = last sorted
-          mu      = sum sorted / nD
-          sd      = sqrt (sum (map (\x -> (x - mu)^(2::Int)) sorted) / nD)
-          med     = if odd n
-                      then sorted !! (n `div` 2)
-                      else (sorted !! (n `div` 2 - 1) + sorted !! (n `div` 2)) / 2
-          skew    = if sd < 1e-12 then 0
-                    else sum (map (\x -> ((x - mu)/sd)^(3::Int)) sorted) / nD
-          histId  = "hist-" <> col
-      in [ "    <div class=\"col-card\">"
-         , "      <div class=\"col-card-title\">"
-         , "        <span class=\"col-role\">" <> role <> "</span>"
-         , "        <span class=\"col-name\">" <> col <> "</span>"
-         , "      </div>"
-         , "      <div class=\"col-card-body\">"
-         , "        <div class=\"col-hist\"><div id=\"" <> histId <> "\"></div></div>"
-         , "        <div class=\"col-stats-mini\">"
-         , colStatRow "N" (T.pack (show n))
-         , colStatRow "最小値" (fmt4 mn)
-         , colStatRow "最大値" (fmt4 mx)
-         , colStatRow "平均" (fmt4 mu)
-         , colStatRow "中央値" (fmt4 med)
-         , colStatRow "標準偏差" (fmt4 sd)
-         , colStatRow "歪度" (fmt4 skew)
-         , "        </div>"
-         , "      </div>"
-         , "    </div>"
-         ]
-
-colStatRow :: Text -> Text -> Text
-colStatRow k v =
-  "          <div class=\"col-stat-row\"><span class=\"sk\">" <> k
-  <> "</span><span class=\"sv\">" <> v <> "</span></div>"
-
--- ---------------------------------------------------------------------------
--- Section 2: Model overview
--- ---------------------------------------------------------------------------
-
-modelSection :: ModelFit -> Text
-modelSection NoRegFit = T.unlines
-  [ "<section id=\"sec-model\">"
-  , "  <h2><span class=\"sec-icon\">&#9878;</span> 2. モデル概要</h2>"
-  , "  <p>回帰モデルなし (散布図のみ)</p>"
-  , "</section>"
-  ]
-modelSection (RegFit fs) = T.unlines $
-  [ "<section id=\"sec-model\">"
-  , "  <h2><span class=\"sec-icon\">&#9878;</span> 2. モデル概要</h2>"
-  , "  <div class=\"info-grid\">"
-  , infoBox "モデル種別" (fsModelType fs)
-  , infoBox "回帰式" (fsFormula fs)
-  , infoBox "リンク関数" (fsLinkName fs)
-  , "  </div>"
-  ] ++ waicLooSection (fsModelSelect fs) ++
-  [ "</section>"
-  ]
-modelSection (HBMFit hs) =
-  let fs = hbmsFit hs
-  in T.unlines $
-    [ "<section id=\"sec-model\">"
-    , "  <h2><span class=\"sec-icon\">&#9878;</span> 2. モデル概要</h2>"
-    , "  <div class=\"info-grid\">"
-    , infoBox "モデル種別" (fsModelType fs)
-    , infoBox "回帰式" (fsFormula fs)
-    , infoBox "尤度" (fsLinkName fs)
-    , "  </div>"
-    , "  <h3 style=\"margin-top:20px\">モデル DAG</h3>"
-    , "  <p class=\"sec-desc\" style=\"font-size:.85em;color:#555\">"
-    , "    依存グラフは <code>extractDeps</code> (Track 型による多相 DSL の解釈) で自動抽出。"
-    , "  </p>"
-    , "  <div class=\"mermaid-wrap\">"
-    , "    <pre class=\"mermaid\">"
-    , buildMermaid (hbmsModelGraph hs)
-    , "    </pre>"
-    , "  </div>"
-    , "  <div class=\"legend\" style=\"margin-top:8px;font-size:.82em;color:#666\">"
-    , "    <span style=\"display:inline-block;width:11px;height:11px;background:#4C72B0;border-radius:2px;margin-right:4px;vertical-align:middle\"></span>latent &nbsp;&nbsp;"
-    , "    <span style=\"display:inline-block;width:11px;height:11px;background:#DD8844;border-radius:2px;margin-right:4px;vertical-align:middle\"></span>observed"
-    , "  </div>"
-    , "</section>"
-    ]
-modelSection (MixFit gs) = T.unlines $
-  [ "<section id=\"sec-model\">"
-  , "  <h2><span class=\"sec-icon\">&#9878;</span> 2. モデル概要</h2>"
-  , "  <div class=\"info-grid\">"
-  , infoBox "モデル種別" (gsModelType gs)
-  , infoBox "固定効果式" (gsFormula gs)
-  , infoBox "グループ変数" (gsGroupCol gs)
-  , infoBox "リンク関数" (gsLinkName gs)
-  , "  </div>"
-  , "  <h3>分散成分</h3>"
-  , "  <div class=\"stat-grid\">"
-  , statBox ("σ²_u (" <> gsGroupCol gs <> ")") (fmt4 (gsRandVar gs)) False
-  , statBox "σ² (残差)" (fmt4 (gsResidVar gs)) False
-  , statBox "ICC" (fmt4 (gsICC gs)) False
-  , "  </div>"
-  ] ++ waicLooSection (gsModelSelect gs) ++
-  [ "  <h3>BLUP (グループ別ランダム切片)</h3>"
-  , blupTable (gsBLUPs gs)
-  , "</section>"
-  ]
-modelSection (GPFit gf) = T.unlines $
-  [ "<section id=\"sec-model\">"
-  , "  <h2><span class=\"sec-icon\">&#9878;</span> 2. モデル比較</h2>"
-  , "  <div class=\"info-grid\">"
-  , infoBox "モデル種別" "GP Regression"
-  , infoBox "説明変数" (gfXCol gf)
-  , infoBox "目的変数" (gfYCol gf)
-  , infoBox "比較カーネル数" (T.pack (show (length (gfKernelFits gf))))
-  , "  </div>"
-  , "  <p style=\"font-size:.88em;color:#666;margin-bottom:14px\">"
-  , "    対数周辺尤度 (LML) が高いほどデータへの適合が良い。ハイパーパラメータは自動最適化済み。"
-  , "  </p>"
-  , "  <table>"
-  , "    <thead><tr>"
-  , "      <th>カーネル</th><th style=\"text-align:right\">ℓ</th>"
-  , "      <th style=\"text-align:right\">σ_f</th><th style=\"text-align:right\">σ_n</th>"
-  , "      <th style=\"text-align:right\">p</th><th style=\"text-align:right\">LML ↑</th>"
-  , "      <th style=\"text-align:right\">順位</th>"
-  , "    </tr></thead>"
-  , "    <tbody>"
-  ] ++
-  zipWith (gpModelRow (maximum (map gkLML (gfKernelFits gf)))) [1..] (gfKernelFits gf) ++
-  [ "    </tbody>"
-  , "  </table>"
-  , "  <p style=\"margin-top:12px;font-size:.82em;color:#888\">"
-  , "    LML = log p(y | X, θ)。データ適合とモデル複雑度ペナルティのバランス。"
-  , "  </p>"
-  , "</section>"
-  ]
-
-gpModelRow :: Double -> Int -> GPKernelFit -> Text
-gpModelRow bestLML rank fit =
-  let isBest = gkLML fit == bestLML
-      style  = if isBest then " style=\"background:#f0faf0;font-weight:600\"" else ""
-      hasPer = gkKernel fit == Periodic
-      badge  = if isBest then " <span style=\"background:#e8f4e8;color:#2e7d32;padding:1px 7px;border-radius:10px;font-size:.78em\">Best</span>" else ""
-  in T.unlines
-       [ "      <tr" <> style <> ">"
-       , "        <td>" <> gkLabel fit <> badge <> "</td>"
-       , "        <td style=\"text-align:right\">" <> fmt4 (gpLengthScale (gkParams fit)) <> "</td>"
-       , "        <td style=\"text-align:right\">" <> fmt4 (sqrt (gpSignalVar (gkParams fit))) <> "</td>"
-       , "        <td style=\"text-align:right\">" <> fmt4 (sqrt (gpNoiseVar (gkParams fit))) <> "</td>"
-       , "        <td style=\"text-align:right\">" <> (if hasPer then fmt4 (gpPeriod (gkParams fit)) else "—") <> "</td>"
-       , "        <td style=\"text-align:right\">" <> fmt4 (gkLML fit) <> "</td>"
-       , "        <td style=\"text-align:right\">#" <> T.pack (show (rank :: Int)) <> "</td>"
-       , "      </tr>"
-       ]
-
--- | WAIC/LOO-CV の結果をスタットボックスで表示する HTML フラグメント。
-waicLooSection :: Maybe (WAICResult, LOOResult) -> [Text]
-waicLooSection Nothing = []
-waicLooSection (Just (w, l)) =
-  let kBad = looKHatBad l
-      kAlert = kBad > 0
-  in [ "  <h3 style=\"margin-top:20px\">モデル比較指標 (WAIC / LOO-CV)</h3>"
-     , "  <div class=\"stat-grid\">"
-     , statBox "WAIC ↓" (fmt4 (waicValue w)) False
-     , statBox "LOO ↓"  (fmt4 (looValue l))  False
-     , statBox "p_WAIC" (fmt4 (waicPwaic w)) False
-     , statBox "LOO SE" (fmt4 (looSE l))     False
-     , statBox ("k̂>0.7") (T.pack (show kBad) <> "件") kAlert
-     , "  </div>"
-     , "  <p style=\"font-size:.84em;color:#666;margin-top:8px\">"
-     , "    WAIC/LOO は小さいほど良い。p_WAIC = 実効パラメータ数。"
-     , if kAlert
-         then "k̂&gt;0.7 の観測値が多い場合は LOO 推定の信頼性が低下する。"
-         else "k̂&gt;0.7 の観測値はなく LOO は安定。"
-     , "  </p>"
-     ]
-
-blupTable :: [(Text, Double)] -> Text
-blupTable blups = T.unlines $
-  [ "  <table style=\"max-width:400px\">"
-  , "    <thead><tr><th>グループ</th><th>BLUP (û_j)</th></tr></thead>"
-  , "    <tbody>"
-  ] ++ map row blups ++
-  [ "    </tbody>"
-  , "  </table>"
-  ]
-  where
-    row (g, v) = "      <tr><td>" <> g <> "</td><td>" <> fmtSigned v <> "</td></tr>"
-
--- ---------------------------------------------------------------------------
--- Section 3: Regression results
--- ---------------------------------------------------------------------------
-
-resultsSection :: ModelFit -> [NamedPlot] -> Text
-resultsSection (GPFit gf) _ = T.unlines $
-  [ "<section id=\"sec-results\">"
-  , "  <h2><span class=\"sec-icon\">&#128200;</span> 3. 回帰結果</h2>"
-  , "  <p style=\"font-size:.88em;color:#666;margin-bottom:14px\">青い帯 = 平均 ± 2σ (≈95% 信用区間)。黒点 = 訓練データ。</p>"
-  , "  <div class=\"tab-bar\">"
-  ] ++
-  zipWith (gpTabBtn (gfKernelFits gf)) [0..] (gfKernelFits gf) ++
-  [ "  </div>" ] ++
-  concatMap (gpTabContent gf) (zip [0..] (gfKernelFits gf)) ++
-  [ "</section>" ]
-resultsSection fit plots = T.unlines $
-  [ "<section id=\"sec-results\">"
-  , "  <h2><span class=\"sec-icon\">&#128200;</span> 3. 回帰結果</h2>"
-  ] ++
-  fitTable fit ++
-  [ residualSummary fit ] ++
-  concatMap plotDiv (zip [0::Int ..] plots) ++
-  [ "</section>" ]
-
-gpTabBtn :: [GPKernelFit] -> Int -> GPKernelFit -> Text
-gpTabBtn fits i fit =
-  let bestLML = maximum (map gkLML fits)
-      star    = if gkLML fit == bestLML then " &#11088;" else ""
-      active  = if i == 0 then " active" else ""
-  in "  <button class=\"tab-btn" <> active <> "\" onclick=\"showGPTab(" <> T.pack (show i) <> ")\">"
-     <> gkLabel fit <> star <> "</button>"
-
-gpTabContent :: GPFitSummary -> (Int, GPKernelFit) -> [Text]
-gpTabContent gf (i, fit) =
-  let active = if i == 0 then " active" else ""
-      pCfg   = PlotConfig
-                 { plotTitle  = gkLabel fit <> " — GP Regression"
-                 , plotWidth  = 700
-                 , plotHeight = 320
-                 }
-      spec   = gpPlot pCfg (gfXCol gf) (gfYCol gf)
-                 (zip (gfTrainXs gf) (gfTrainYs gf)) (gkResult fit)
-      json   = specJson spec
-      hasPer = gkKernel fit == Periodic
-  in [ "  <div id=\"gp-tab-" <> T.pack (show i) <> "\" class=\"tab-content" <> active <> "\">"
-     , "    <div class=\"vl-wrap\"><div id=\"vl-gp-" <> T.pack (show i) <> "\"></div></div>"
-     , "    <script>window.__vlGP" <> T.pack (show i) <> " = " <> json <> ";</script>"
-     , "    <div style=\"margin-top:12px;background:#f7f9fc;border-radius:8px;padding:10px 16px;"
-     , "         display:flex;gap:20px;flex-wrap:wrap;font-size:.85em;\">"
-     , "      <span><b>カーネル:</b> " <> gkLabel fit <> "</span>"
-     , "      <span><b>ℓ =</b> " <> fmt4 (gpLengthScale (gkParams fit)) <> "</span>"
-     , "      <span><b>σ_f =</b> " <> fmt4 (sqrt (gpSignalVar (gkParams fit))) <> "</span>"
-     , "      <span><b>σ_n =</b> " <> fmt4 (sqrt (gpNoiseVar (gkParams fit))) <> "</span>"
-     , if hasPer then "      <span><b>p =</b> " <> fmt4 (gpPeriod (gkParams fit)) <> "</span>"
-                 else ""
-     , "      <span style=\"margin-left:auto;color:#888\"><b>LML =</b> " <> fmt4 (gkLML fit) <> "</span>"
-     , "    </div>"
-     , "  </div>"
-     ]
-
-fitTable :: ModelFit -> [Text]
-fitTable NoRegFit = []
-fitTable (RegFit fs) =
-  [ "  <h3>係数</h3>"
-  , "  <table style=\"max-width:600px\">"
-  , "    <thead><tr><th>パラメータ</th><th>推定値</th></tr></thead>"
-  , "    <tbody>"
-  ] ++
-  map (\(l,v) -> "      <tr><td>" <> l <> "</td><td>" <> fmtSigned v <> "</td></tr>")
-      (fsCoeffs fs) ++
-  [ "    </tbody>"
-  , "  </table>"
-  , "  <div class=\"stat-grid\" style=\"margin-top:14px\">"
-  , statBox (fsR2Label fs) (fmt4 (fsR2 fs)) True
-  , "  </div>"
-  ]
-fitTable (HBMFit hs) =
-  let fs = hbmsFit hs
-      ch = hbmsChain hs
-      total    = chainTotalOf ch
-      accepted = chainAcceptedOf ch
-      acceptR  = if total == 0 then 0
-                 else fromIntegral accepted / fromIntegral total :: Double
-      nSamp    = chainNSamples ch
-  in [ "  <h3>事後分布サマリー</h3>"
-     , "  <p class=\"sec-desc\" style=\"font-size:.85em;color:#555\">"
-     , "    各潜在変数の事後平均・標準偏差・95% 信用区間 (2.5% / 97.5% 分位点)。"
-     , "  </p>"
-     , "  <table style=\"max-width:760px\">"
-     , "    <thead><tr><th>パラメータ</th><th>事後平均</th>"
-       <> "<th>事後 SD</th><th>2.5%</th><th>97.5%</th></tr></thead>"
-     , "    <tbody>"
-     ] ++
-     map posteriorRowHtml (hbmsPosteriorRows hs) ++
-     [ "    </tbody>"
-     , "  </table>"
-     , "  <div class=\"stat-grid\" style=\"margin-top:14px\">"
-     , statBox (fsR2Label fs) (fmt4 (fsR2 fs)) True
-     , statBox "サンプル数" (T.pack (show nSamp)) False
-     , statBox "受容率" (fmt1 (acceptR * 100) <> "%") False
-     , "  </div>"
-     ]
-  where
-    posteriorRowHtml (n, m, sd_, lo, hi) =
-      "      <tr><td>" <> n <> "</td>"
-      <> "<td>" <> fmtSigned m <> "</td>"
-      <> "<td>" <> fmt4 sd_ <> "</td>"
-      <> "<td>" <> fmtSigned lo <> "</td>"
-      <> "<td>" <> fmtSigned hi <> "</td></tr>"
-fitTable (MixFit gs) =
-  [ "  <h3>固定効果係数</h3>"
-  , "  <table style=\"max-width:600px\">"
-  , "    <thead><tr><th>パラメータ</th><th>推定値</th></tr></thead>"
-  , "    <tbody>"
-  ] ++
-  map (\(l,v) -> "      <tr><td>" <> l <> "</td><td>" <> fmtSigned v <> "</td></tr>")
-      (gsFixed gs) ++
-  [ "    </tbody>"
-  , "  </table>"
-  , "  <div class=\"stat-grid\" style=\"margin-top:14px\">"
-  , statBox (gsR2Label gs) (fmt4 (gsR2 gs)) True
-  , statBox "ICC" (fmt4 (gsICC gs)) False
-  , "  </div>"
-  ]
-
-chainNSamples :: Chain -> Int
-chainNSamples = length . chainSamples
-
-chainTotalOf :: Chain -> Int
-chainTotalOf = chainTotal
-
-chainAcceptedOf :: Chain -> Int
-chainAcceptedOf = chainAccepted
-
-residualSummary :: ModelFit -> Text
-residualSummary fit =
-  let resids = case fit of
-                 RegFit  fs -> fsResiduals fs
-                 MixFit  gs -> gsResiduals gs
-                 HBMFit  hs -> fsResiduals (hbmsFit hs)
-                 NoRegFit   -> []
-      n      = fromIntegral (length resids) :: Double
-      rmse   = if n == 0 then 0 else sqrt (sum (map (^(2::Int)) resids) / n)
-      mx     = if null resids then 0 else maximum (map abs resids)
-  in if null resids then ""
-     else T.unlines
-       [ "  <h3>残差サマリー</h3>"
-       , "  <div class=\"stat-grid\">"
-       , statBox "RMSE"       (fmt4 rmse) False
-       , statBox "最大絶対残差" (fmt4 mx)   False
-       , "  </div>"
-       ]
-
-plotDiv :: (Int, NamedPlot) -> [Text]
-plotDiv (i, np) =
-  let divId = npName np <> "-" <> T.pack (show i)
-  in [ "  <h3>" <> npTitle np <> "</h3>"
-     , "  <div class=\"vl-wrap\"><div id=\"" <> divId <> "\"></div></div>"
-     , "  <script>window.__vl_" <> T.pack (show i) <> " = " <> specJson (npSpec np) <> ";</script>"
-     ]
-
--- ---------------------------------------------------------------------------
--- Section 4: Interactive prediction
--- ---------------------------------------------------------------------------
-
-predictionSection :: DXD.DataFrame -> [Text] -> Text -> ModelFit -> [Text]
-predictionSection _ _ _ NoRegFit = []
-predictionSection _ _ _ (GPFit gf) = gpPredictionSection gf
-predictionSection df xCols yCol fit =
-  let -- データ範囲を ±50% 拡張してスライダーに使う
-      xRanges = [ (col, mn, mx, smin, smax)
-                | col <- xCols
-                , Just v <- [getDoubleVec col df]
-                , let mn   = V.minimum v
-                , let mx   = V.maximum v
-                , let ext  = max 1e-8 (mx - mn) * 0.5
-                , let smin = mn - ext
-                , let smax = mx + ext
-                ]
-      groups = case fit of
-                 MixFit gs -> map fst (gsBLUPs gs)
-                 _         -> []
-      hasSingle = length xCols == 1 && case smoothDataFor fit of { Just _ -> True; Nothing -> False }
-  in [ "<section id=\"sec-predict\">"
-     , "  <h2><span class=\"sec-icon\">&#127919;</span> 4. 対話的予測</h2>"
-     , "  <p class=\"sec-desc\">"
-     , "    スライダーまたは入力欄で説明変数の値を変えると、"
-     , "    回帰曲線上の予測点がリアルタイムで移動します。"
-     , "    スライダーはデータ範囲の ±50% まで外挿できます。"
-     , "  </p>"
-     , "  <div class=\"predict-layout\">"
-     , "    <div class=\"predict-left\">"
-     , "      <div class=\"predict-controls\">"
-     ] ++
-     concatMap xSlider xRanges ++
-     (if null groups then []
-      else [ "        <div class=\"slider-row\">"
-           , "          <label>グループ (" <> grpCol fit <> "):</label>"
-           , "          <select id=\"pred-group\" onchange=\"updatePrediction()\">"
-           , T.concat [ "            <option value=\"" <> g <> "\">" <> g <> "</option>\n"
-                      | g <- groups ]
-           , "          </select>"
-           , "        </div>"
-           ]) ++
-     [ "      </div>"
-     , "      <div class=\"predict-output\">"
-     , "        <div class=\"pred-box mean-box\">"
-     , "          <div class=\"plbl\">予測値 (" <> yCol <> ")"
-     , "            <span id=\"extrap-warn\" class=\"extrap-badge\" style=\"display:none\">外挿</span>"
-     , "          </div>"
-     , "          <div class=\"pval\" id=\"pred-y\">—</div>"
-     , "          <div class=\"psub\">g⁻¹(η)</div>"
-     , "        </div>"
-     , "        <div class=\"pred-box\">"
-     , "          <div class=\"plbl\">線形予測子 (η)</div>"
-     , "          <div class=\"pval\" id=\"pred-eta\">—</div>"
-     , "          <div class=\"psub\">Xβ</div>"
-     , "        </div>"
-     , if hasSingle
-         then "        <div class=\"pred-box ci-box\">"
-              <> "<div class=\"plbl\" id=\"ci-lbl\">95% CI</div>"
-              <> "<div class=\"pval\" id=\"pred-ci-lo\">—</div>"
-              <> "<div class=\"psub\" id=\"pred-ci-hi\">—</div>"
-              <> "</div>"
-         else ""
-     , "      </div>"
-     , "    </div>"
-     , if hasSingle
-         then "    <div class=\"predict-chart\"><div id=\"pred-chart\"></div></div>"
-         else ""
-     , "  </div>"
-     , "</section>"
-     ]
-  where
-    grpCol (MixFit gs) = gsGroupCol gs
-    grpCol _           = ""
-
-gpPredictionSection :: GPFitSummary -> [Text]
-gpPredictionSection gf =
-  let xs    = gfTrainXs gf
-      xMin  = minimum xs
-      xMax  = maximum xs
-      ext   = max 1e-8 (xMax - xMin) * 0.5
-      smin  = xMin - ext
-      smax  = xMax + ext
-      step  = (smax - smin) / 500
-      mid   = (smin + smax) / 2
-      xCol  = gfXCol gf
-      yCol  = gfYCol gf
-  in [ "<section id=\"sec-predict\">"
-     , "  <h2><span class=\"sec-icon\">&#127919;</span> 4. 対話的予測</h2>"
-     , "  <p class=\"sec-desc\">スライダーまたは入力欄で x 値を変えると、選択したカーネルの GP 事後平均と信用区間をリアルタイムで計算します。曲線はベストカーネルを表示。</p>"
-     , "  <div class=\"predict-layout\">"
-     , "    <div class=\"predict-left\">"
-     , "      <div class=\"predict-controls\">"
-     , "        <div class=\"slider-row\">"
-     , "          <label>カーネル:</label>"
-     , "          <select id=\"pred-kernel\" onchange=\"updateGPPrediction()\">"
-     , T.concat [ "            <option value=\"" <> T.pack (show i) <> "\">"
-                  <> gkLabel fit
-                  <> " (LML=" <> fmt4 (gkLML fit) <> ")"
-                  <> "</option>\n"
-                | (i, fit) <- zip [0 :: Int ..] (gfKernelFits gf) ]
-     , "          </select>"
-     , "        </div>"
-     , "        <div class=\"slider-row\">"
-     , "          <label>" <> xCol <> ":</label>"
-     , "          <input type=\"range\" id=\"x-gp\""
-     , "                 min=\"" <> fmtJS smin <> "\" max=\"" <> fmtJS smax <> "\""
-     , "                 step=\"" <> fmtJS step <> "\" value=\"" <> fmtJS mid <> "\""
-     , "                 oninput=\"syncGPSlider()\">"
-     , "          <input type=\"number\" id=\"x-gp-num\""
-     , "                 step=\"" <> fmtJS step <> "\" value=\"" <> fmtJS mid <> "\""
-     , "                 onchange=\"syncGPNum()\">"
-     , "        </div>"
-     , "      </div>"
-     , "      <div class=\"predict-output\">"
-     , "        <div class=\"pred-box mean-box\">"
-     , "          <div class=\"plbl\">事後平均 (" <> yCol <> ")"
-     , "            <span id=\"gp-extrap-warn\" class=\"extrap-badge\" style=\"display:none\">外挿</span>"
-     , "          </div>"
-     , "          <div class=\"pval\" id=\"gp-pred-mean\">—</div>"
-     , "          <div class=\"psub\">μ(x*)</div>"
-     , "        </div>"
-     , "        <div class=\"pred-box\">"
-     , "          <div class=\"plbl\">標準偏差</div>"
-     , "          <div class=\"pval\" id=\"gp-pred-std\">—</div>"
-     , "          <div class=\"psub\">σ(x*)</div>"
-     , "        </div>"
-     , "        <div class=\"pred-box ci-box\">"
-     , "          <div class=\"plbl\">95% 信用区間</div>"
-     , "          <div class=\"pval\" id=\"gp-pred-lo\">—</div>"
-     , "          <div class=\"psub\" id=\"gp-pred-hi\">—</div>"
-     , "        </div>"
-     , "      </div>"
-     , "    </div>"
-     , "    <div class=\"predict-chart\"><div id=\"pred-chart\"></div></div>"
-     , "  </div>"
-     , "</section>"
-     ]
-
-smoothDataFor :: ModelFit -> Maybe (Text, SmoothData)
-smoothDataFor (RegFit fs) = fsSmoothData fs
-smoothDataFor (MixFit gs) = gsSmoothData gs
-smoothDataFor (HBMFit hs) = fsSmoothData (hbmsFit hs)
-smoothDataFor NoRegFit    = Nothing
-
--- (col, data_min, data_max, slider_min, slider_max)
-xSlider :: (Text, Double, Double, Double, Double) -> [Text]
-xSlider (col, _mn, _mx, smin, smax) =
-  let step = (smax - smin) / 500
-      mid  = (smin + smax) / 2
-      sid  = "x-" <> col
-  in [ "        <div class=\"slider-row\">"
-     , "          <label>" <> col <> ":</label>"
-     , "          <input type=\"range\" id=\"" <> sid <> "\""
-     , "                 min=\"" <> fmtJS smin <> "\" max=\"" <> fmtJS smax <> "\""
-     , "                 step=\"" <> fmtJS step <> "\" value=\"" <> fmtJS mid <> "\""
-     , "                 oninput=\"syncSlider('" <> col <> "')\">"
-     , "          <input type=\"number\" id=\"x-num-" <> col <> "\""
-     , "                 step=\"" <> fmtJS step <> "\" value=\"" <> fmtJS mid <> "\""
-     , "                 onchange=\"syncNum('" <> col <> "')\">"
-     , "        </div>"
-     ]
-
--- ---------------------------------------------------------------------------
--- Section 5: Appendix
--- ---------------------------------------------------------------------------
-
-appendixSection :: ModelFit -> Text
-appendixSection fit = T.unlines
-  [ "<section id=\"sec-appendix\">"
-  , "  <h2><span class=\"sec-icon\">&#128218;</span> 5. 付録: モデルの原理</h2>"
-  , appendixContent fit
-  , "</section>"
-  ]
-
-appendixContent :: ModelFit -> Text
-appendixContent NoRegFit = "  <p>回帰モデルなし。</p>"
-appendixContent (RegFit fs) = T.unlines $
-  [ "  <div class=\"appendix-block\">"
-  , "    <h4>" <> fsModelType fs <> " モデル</h4>"
-  , "    <p>一般化線形モデル (GLM) は線形予測子 η = Xβ をリンク関数 g で連結します:</p>"
-  , "    <div class=\"formula\">g(E[y]) = β₀ + β₁x₁ + β₂x₁² + ...</div>"
-  , "    <p>リンク関数 <b>" <> fsLinkName fs <> "</b> を使用しています。</p>"
-  , "  </div>"
-  , lmAppendix (fsLinkName fs)
-  ] ++ waicLooAppendix (fsModelSelect fs)
-appendixContent (MixFit gs) = T.unlines
-  [ "  <div class=\"appendix-block\">"
-  , "    <h4>" <> gsModelType gs <> " モデル</h4>"
-  , "    <p>混合効果モデルはグループ固有のランダム切片 û_j を固定効果に加えます:</p>"
-  , "    <div class=\"formula\">g(E[y_ij]) = β₀ + β₁x + ... + û_j,  û_j ~ N(0, σ²_u)</div>"
-  , "    <p><b>ICC</b> = σ²_u / (σ²_u + σ²) = " <> fmt4 (gsICC gs) <> "</p>"
-  , "  </div>"
-  , lmAppendix (gsLinkName gs)
-  ]
-appendixContent (HBMFit hs) = T.unlines
-  [ "  <div class=\"appendix-block\">"
-  , "    <h4>" <> fsModelType (hbmsFit hs) <> "</h4>"
-  , "    <p>ベイズ線形回帰では係数を点推定ではなく <b>事後分布</b> として推定します:</p>"
-  , "    <div class=\"formula\">"
-  , "      α ~ Normal(0, σ_α),&nbsp; β ~ Normal(0, σ_β),&nbsp; σ ~ Exponential(1)<br>"
-  , "      y_i ~ Normal(α + β·x_i, σ)"
-  , "    </div>"
-  , "    <p>推論は NUTS (No-U-Turn Sampler, AD 勾配) で実行。"
-  , "    各パラメータの 95% 信用区間 = 事後分布の 2.5%/97.5% 分位点。</p>"
-  , "    <p>予測曲線の <b>信用区間バンド</b> は、グリッド点 x* に対して"
-  , "    全事後サンプル (α^(s), β^(s)) で μ^(s) = α^(s) + β^(s)·x* を計算し、"
-  , "    その分布の 2.5%/97.5% 分位点を取ったものです。</p>"
-  , "  </div>"
-  ]
-appendixContent (GPFit gf) = T.unlines
-  [ "  <div class=\"appendix-block\">"
-  , "    <h4>ガウス過程 (Gaussian Process) とは</h4>"
-  , "    <p>ガウス過程は関数に対する確率分布です。平均関数 m(x) とカーネル k(x,x') によって定義されます:</p>"
-  , "    <div class=\"formula\">f(x) ~ GP( m(x), k(x, x') )</div>"
-  , "    <p>訓練データ (X, y) を条件付けた事後分布:</p>"
-  , "    <div class=\"formula\">"
-  , "    μ(x*) = K(x*, X) · [K(X,X) + σ²_n I]⁻¹ · y<br>"
-  , "    σ²(x*) = k(x*, x*) − K(x*, X) · [K(X,X) + σ²_n I]⁻¹ · K(X, x*)"
-  , "    </div>"
-  , "  </div>"
-  , gpKernelAppendix gf
-  , "  <div class=\"appendix-block\">"
-  , "    <h4>対数周辺尤度 (LML) によるモデル選択</h4>"
-  , "    <div class=\"formula\">log p(y|X,θ) = −½ yᵀ K⁻¹ y − ½ log|K| − n/2 · log(2π)</div>"
-  , "    <p>LML はデータ適合度とモデル複雑度ペナルティのバランスを取ります。</p>"
-  , "  </div>"
-  ]
-
-gpKernelAppendix :: GPFitSummary -> Text
-gpKernelAppendix gf = T.unlines $
-  [ "  <div class=\"appendix-block\">"
-  , "    <h4>使用したカーネル関数</h4>"
-  ] ++
-  concatMap kernelDesc (map gkKernel (gfKernelFits gf)) ++
-  [ "  </div>" ]
-  where
-    kernelDesc RBF =
-      [ "    <p><b>RBF (二乗指数カーネル)</b></p>"
-      , "    <div class=\"formula\">k(x, x') = σ²_f · exp( −(x−x')² / (2ℓ²) )</div>"
-      ]
-    kernelDesc Matern52 =
-      [ "    <p><b>Matérn 5/2 カーネル</b></p>"
-      , "    <div class=\"formula\">k(x, x') = σ²_f · (1 + √5·r/ℓ + 5r²/(3ℓ²)) · exp(−√5·r/ℓ)</div>"
-      ]
-    kernelDesc Periodic =
-      [ "    <p><b>Periodic カーネル</b></p>"
-      , "    <div class=\"formula\">k(x, x') = σ²_f · exp( −2 sin²(π|x−x'|/p) / ℓ² )</div>"
-      ]
-    kernelDesc Linear =
-      [ "    <p><b>Linear (内積) カーネル</b></p>"
-      , "    <div class=\"formula\">k(x, x') = σ²_f · (x·x')</div>"
-      ]
-    kernelDesc (Poly d) =
-      [ "    <p><b>Polynomial カーネル (次数 " <> T.pack (show d) <> ")</b></p>"
-      , "    <div class=\"formula\">k(x, x') = (γ·(x·x') + 1)^" <> T.pack (show d) <> " &nbsp; (γ = 1/(2ℓ²))</div>"
-      ]
-
-lmAppendix :: Text -> Text
-lmAppendix link = T.unlines
-  [ "  <div class=\"appendix-block\">"
-  , "    <h4>リンク関数とその逆関数</h4>"
-  , "    <table style=\"max-width:500px\">"
-  , "      <thead><tr><th>リンク</th><th>g(μ)</th><th>g⁻¹(η) (予測値変換)</th></tr></thead>"
-  , "      <tbody>"
-  , "        <tr" <> markActive "identity" link <> "><td>identity</td><td>μ</td><td>η</td></tr>"
-  , "        <tr" <> markActive "log"      link <> "><td>log</td><td>log(μ)</td><td>exp(η)</td></tr>"
-  , "        <tr" <> markActive "logit"    link <> "><td>logit</td><td>log(μ/(1-μ))</td><td>1/(1+exp(-η))</td></tr>"
-  , "        <tr" <> markActive "sqrt"     link <> "><td>sqrt</td><td>√μ</td><td>η²</td></tr>"
-  , "      </tbody>"
-  , "    </table>"
-  , "  </div>"
-  ]
-  where
-    markActive l cur = if l == cur then " style=\"background:#f0faf0;font-weight:600\"" else ""
-
-waicLooAppendix :: Maybe (WAICResult, LOOResult) -> [Text]
-waicLooAppendix Nothing = []
-waicLooAppendix (Just _) =
-  [ "  <div class=\"appendix-block\">"
-  , "    <h4>WAIC と LOO-CV</h4>"
-  , "    <p><b>WAIC</b> (Widely Applicable Information Criterion) は"
-  , "    事後予測分布に基づくモデル比較指標です:</p>"
-  , "    <div class=\"formula\">"
-  , "    WAIC = −2 × (lppd − p_WAIC)<br>"
-  , "    lppd = Σᵢ log E_θ[p(yᵢ|θ)]  (対数点予測密度)<br>"
-  , "    p_WAIC = Σᵢ Var_θ[log p(yᵢ|θ)]  (実効パラメータ数)"
-  , "    </div>"
-  , "    <p><b>LOO-CV</b> (PSIS-LOO) は各観測を1つ除いた予測精度の推定値です。"
-  , "    Pareto k̂ 診断: k̂ &lt; 0.5 = 良好、0.5–0.7 = 許容、&gt; 0.7 = 要注意。</p>"
-  , "    <p>いずれも <b>値が小さいほど良い</b>。WAIC ≈ LOO であれば両者は一致。</p>"
-  , "    <p>LM では flat prior の解析的事後分布からサンプリング。"
-  , "    GLM では Laplace 近似 β ~ MVN(β̂, Fisher⁻¹) を使用。</p>"
-  , "  </div>"
-  ]
-
--- ---------------------------------------------------------------------------
--- JavaScript: embed main plots
--- ---------------------------------------------------------------------------
-
-embedScript :: [NamedPlot] -> Text
-embedScript plots = T.unlines
-  [ "vegaEmbed('#" <> npName np <> "-" <> T.pack (show i)
-    <> "', window.__vl_" <> T.pack (show i)
-    <> ", {renderer:'canvas',actions:false}).catch(console.error);"
-  | (i, np) <- zip [0::Int ..] plots
-  ]
-
--- ---------------------------------------------------------------------------
--- JavaScript: column raw data (for histogram rendering)
--- ---------------------------------------------------------------------------
-
-columnDataJS :: DXD.DataFrame -> [Text] -> Text -> Text
-columnDataJS df xCols yCol = T.unlines
-  [ "const columnData = {" <> entries <> "};"
-  , "const xColNames  = " <> jsStrArray xCols <> ";"
-  , "const yColName   = " <> jsStr yCol <> ";"
-  ]
-  where
-    allCols = xCols ++ [yCol]
-    entry c = case getDoubleVec c df of
-      Nothing -> ""
-      Just v  -> jsStr c <> ": " <> jsDoubleArray (V.toList v)
-    entries = T.intercalate "," (filter (not . T.null) (map entry allCols))
-
--- ヒストグラムを動的に描画する JS。
---
--- ビン数は **Freedman-Diaconis 公式** で自動選択する:
---   bin width = 2 · IQR / n^(1/3)
---   k         = ceil((max − min) / bin width)
--- ロバスト (外れ値に強く) かつ N に応じて適切な粒度になる。
--- IQR=0 の場合は Sturges 公式 (k = ceil(log₂ n + 1)) にフォールバック。
--- いずれにせよ最終的に [5, 25] にクランプして極端を避ける。
-histogramInitJS :: [Text] -> Text
-histogramInitJS cols = T.unlines $
-  [ "(function() {"
-  , "  function chooseBins(vals) {"
-  , "    const n = vals.length;"
-  , "    if (n < 4) return 5;"
-  , "    const sorted = [...vals].sort((a,b) => a-b);"
-  , "    const q1   = sorted[Math.floor(n * 0.25)];"
-  , "    const q3   = sorted[Math.floor(n * 0.75)];"
-  , "    const iqr  = q3 - q1;"
-  , "    const range = sorted[n-1] - sorted[0];"
-  , "    let k;"
-  , "    if (iqr > 0 && range > 0) {"
-  , "      // Freedman-Diaconis"
-  , "      const w = 2 * iqr / Math.pow(n, 1/3);"
-  , "      k = Math.ceil(range / w);"
-  , "    } else {"
-  , "      // Sturges (フォールバック)"
-  , "      k = Math.ceil(Math.log2(Math.max(2, n)) + 1);"
-  , "    }"
-  , "    return Math.max(5, Math.min(25, k));"
-  , "  }"
-  , "  function makeHistSpec(col, vals) {"
-  , "    const k = chooseBins(vals);"
-  , "    return {"
-  , "      '$schema': 'https://vega.github.io/schema/vega-lite/v5.json',"
-  , "      width: 240, height: 130,"
-  , "      background: 'transparent',"
-  , "      data: {values: vals.map(v => ({v}))},"
-  , "      mark: {type:'bar',color:'#4472c4',cornerRadiusEnd:2,tooltip:true},"
-  , "      encoding: {"
-  , "        x: {field:'v', bin:{maxbins:k, nice:true}, type:'quantitative', axis:{title:col, labelFontSize:10}},"
-  , "        y: {aggregate:'count', type:'quantitative', axis:{title:'度数', labelFontSize:10}}"
-  , "      }"
-  , "    };"
-  , "  }"
-  ] ++
-  [ "  vegaEmbed('#hist-" <> col <> "', makeHistSpec(" <> jsStr col <> ", columnData[" <> jsStr col <> "] || []), {actions:false}).catch(console.error);"
-  | col <- cols
-  ] ++
-  [ "})();" ]
-
--- ---------------------------------------------------------------------------
--- JavaScript: interactive prediction chart spec
--- ---------------------------------------------------------------------------
-
-predChartSpecJS :: ModelFit -> [Text] -> Text -> DXD.DataFrame -> Text
-predChartSpecJS (GPFit gf) _ _ _ =
-  -- ベストカーネルの曲線でチャートを構築
-  case gfKernelFits gf of
-    [] -> "window.__pred_chart = null;"
-    (best:_) ->
-      let res        = gkResult best
-          scatterData = jsonArr
-            [ "{\"x\":" <> fmtJS x <> ",\"y\":" <> fmtJS y <> "}"
-            | (x, y) <- zip (gfTrainXs gf) (gfTrainYs gf) ]
-          curveData = jsonArr
-            [ "{\"x\":" <> fmtJS x <> ",\"y\":" <> fmtJS y <> "}"
-            | (x, y) <- zip (gpTestX res) (gpMean res) ]
-          bandData = jsonArr
-            [ "{\"x\":" <> fmtJS x <> ",\"lo\":" <> fmtJS lo <> ",\"hi\":" <> fmtJS hi <> "}"
-            | (x, lo, hi) <- zip3 (gpTestX res) (gpLower res) (gpUpper res) ]
-          xMin = minimum (gfTrainXs gf)
-          xMax = maximum (gfTrainXs gf)
-          boundsData = "[{\"x\":" <> fmtJS xMin <> "},{\"x\":" <> fmtJS xMax <> "}]"
-          spec = buildPredChartJson (gfXCol gf) (gfYCol gf) scatterData curveData bandData boundsData True
-      in T.unlines
-           [ "window.__pred_chart = " <> spec <> ";"
-           , "const gpDataXMin = " <> fmtJS xMin <> ";"
-           , "const gpDataXMax = " <> fmtJS xMax <> ";"
-           ]
-  where jsonArr xs = "[" <> T.intercalate "," xs <> "]"
-predChartSpecJS fit xCols yCol df =
-  case (smoothDataFor fit, xCols) of
-    (Just (xCol, sd), [_]) ->
-      case (getDoubleVec xCol df, getDoubleVec yCol df) of
-        (Just xVec, Just yVec) ->
-          let scatterData = jsonArr
-                [ "{\"x\":" <> fmtJS x <> ",\"y\":" <> fmtJS y <> "}"
-                | (x, y) <- zip (V.toList xVec) (V.toList yVec) ]
-              curveData = jsonArr
-                [ "{\"x\":" <> fmtJS x <> ",\"y\":" <> fmtJS y <> "}"
-                | (x, y) <- zip (sdXs sd) (sdYs sd) ]
-              bandData = if sdHasBand sd
-                then jsonArr
-                  [ "{\"x\":" <> fmtJS x <> ",\"lo\":" <> fmtJS lo <> ",\"hi\":" <> fmtJS hi <> "}"
-                  | (x, lo, hi) <- zip3 (sdXs sd) (sdLower sd) (sdUpper sd) ]
-                else "[]"
-              hasBandJS  = if sdHasBand sd then "true" else "false"
-              smoothLoJS = jsDoubleArray (sdLower sd)
-              smoothHiJS = jsDoubleArray (sdUpper sd)
-              smoothXsJS = jsDoubleArray (sdXs sd)
-              dataXMin   = V.minimum xVec
-              dataXMax   = V.maximum xVec
-              boundsData = "[{\"x\":" <> fmtJS dataXMin <> "},{\"x\":" <> fmtJS dataXMax <> "}]"
-              spec       = buildPredChartJson xCol yCol scatterData curveData bandData boundsData (sdHasBand sd)
-          in T.unlines
-               [ "window.__pred_chart = " <> spec <> ";"
-               , "const predXCol      = " <> jsStr xCol <> ";"
-               , "const smoothHasBand = " <> hasBandJS <> ";"
-               , "const smoothXs = " <> smoothXsJS <> ";"
-               , "const smoothLo = " <> smoothLoJS <> ";"
-               , "const smoothHi = " <> smoothHiJS <> ";"
-               , "const dataXMin = " <> fmtJS dataXMin <> ";"
-               , "const dataXMax = " <> fmtJS dataXMax <> ";"
-               ]
-        _ -> "window.__pred_chart = null;"
-    _ -> "window.__pred_chart = null;"
-  where
-    jsonArr xs = "[" <> T.intercalate "," xs <> "]"
-
-buildPredChartJson :: Text -> Text -> Text -> Text -> Text -> Text -> Bool -> Text
-buildPredChartJson xCol yCol scatterData curveData bandData boundsData hasBand = T.unlines
-  [ "{"
-  , "  \"$schema\": \"https://vega.github.io/schema/vega-lite/v5.json\","
-  , "  \"width\": 480, \"height\": 300,"
-  , "  \"datasets\": {"
-  , "    \"scatter\": " <> scatterData <> ","
-  , "    \"curve\":   " <> curveData <> ","
-  , "    \"band\":    " <> bandData <> ","
-  , "    \"data_bounds\": " <> boundsData <> ","
-  , "    \"pred_point\": [],"
-  , "    \"pred_ci\":    []"
-  , "  },"
-  , "  \"layer\": ["
-  , if hasBand then bandLayer else ""
-  , "    {"
-  , "      \"data\": {\"name\": \"curve\"},"
-  , "      \"mark\": {\"type\": \"line\", \"color\": \"#2a7dbc\", \"strokeWidth\": 2.5},"
-  , "      \"encoding\": {"
-  , "        \"x\": {\"field\": \"x\", \"type\": \"quantitative\", \"axis\": {\"title\": " <> jsStr xCol <> "}},"
-  , "        \"y\": {\"field\": \"y\", \"type\": \"quantitative\", \"axis\": {\"title\": " <> jsStr yCol <> "}}"
-  , "      }"
-  , "    },"
-  , "    {"
-  , "      \"data\": {\"name\": \"scatter\"},"
-  , "      \"mark\": {\"type\": \"point\", \"opacity\": 0.55, \"color\": \"#555\", \"size\": 50},"
-  , "      \"encoding\": {"
-  , "        \"x\": {\"field\": \"x\", \"type\": \"quantitative\"},"
-  , "        \"y\": {\"field\": \"y\", \"type\": \"quantitative\"}"
-  , "      }"
-  , "    },"
-  -- データ範囲境界の縦線 (外挿域の視覚的インジケーター)
-  , "    {"
-  , "      \"data\": {\"name\": \"data_bounds\"},"
-  , "      \"mark\": {\"type\": \"rule\", \"color\": \"#bbb\", \"strokeDash\": [5,4], \"strokeWidth\": 1},"
-  , "      \"encoding\": {\"x\": {\"field\": \"x\", \"type\": \"quantitative\"}}"
-  , "    },"
-  , if hasBand then predCILayer else ""
-  , "    {"
-  , "      \"data\": {\"name\": \"pred_point\"},"
-  , "      \"mark\": {\"type\": \"point\", \"color\": \"#e74c3c\", \"size\": 180,"
-  , "                \"filled\": true, \"stroke\": \"white\", \"strokeWidth\": 1.5},"
-  , "      \"encoding\": {"
-  , "        \"x\": {\"field\": \"x\", \"type\": \"quantitative\"},"
-  , "        \"y\": {\"field\": \"y\", \"type\": \"quantitative\"},"
-  , "        \"tooltip\": [{\"field\": \"x\", \"type\": \"quantitative\", \"title\": " <> jsStr xCol <> "},"
-  , "                      {\"field\": \"y\", \"type\": \"quantitative\", \"title\": " <> jsStr yCol <> "}]"
-  , "      }"
-  , "    }"
-  , "  ]"
-  , "}"
-  ]
-  where
-    bandLayer = T.unlines
-      [ "    {"
-      , "      \"data\": {\"name\": \"band\"},"
-      , "      \"mark\": {\"type\": \"area\", \"opacity\": 0.18, \"color\": \"#2a7dbc\"},"
-      , "      \"encoding\": {"
-      , "        \"x\": {\"field\": \"x\", \"type\": \"quantitative\"},"
-      , "        \"y\": {\"field\": \"lo\", \"type\": \"quantitative\"},"
-      , "        \"y2\": {\"field\": \"hi\"}"
-      , "      }"
-      , "    },"
-      ]
-    predCILayer = T.unlines
-      [ "    {"
-      , "      \"data\": {\"name\": \"pred_ci\"},"
-      , "      \"mark\": {\"type\": \"rule\", \"color\": \"#e74c3c\", \"strokeWidth\": 2, \"strokeDash\": [4,3]},"
-      , "      \"encoding\": {"
-      , "        \"x\": {\"field\": \"x\", \"type\": \"quantitative\"},"
-      , "        \"y\": {\"field\": \"lo\", \"type\": \"quantitative\"},"
-      , "        \"y2\": {\"field\": \"hi\"}"
-      , "      }"
-      , "    },"
-      ]
-
--- ---------------------------------------------------------------------------
--- JavaScript: prediction logic
--- ---------------------------------------------------------------------------
-
--- ---------------------------------------------------------------------------
--- JavaScript: GP-specific helpers
--- ---------------------------------------------------------------------------
-
-gpVegaEmbedJS :: ModelFit -> Text
-gpVegaEmbedJS (GPFit gf) = T.unlines $
-  [ "vegaEmbed('#vl-gp-" <> T.pack (show i)
-    <> "', window.__vlGP" <> T.pack (show i)
-    <> ", {renderer:'canvas',actions:false}).catch(console.error);"
-  | i <- [0 .. length (gfKernelFits gf) - 1]
-  ]
-gpVegaEmbedJS _ = ""
-
-gpModelsDataJS :: ModelFit -> Text
-gpModelsDataJS (GPFit gf) = T.unlines
-  [ "const gpModels = " <> jsGPModels (gfKernelFits gf) <> ";"
-  ]
-gpModelsDataJS _ = ""
-
-jsGPModels :: [GPKernelFit] -> Text
-jsGPModels fits = "[" <> T.intercalate "," (map jsGPModel fits) <> "]"
-
-jsGPModel :: GPKernelFit -> Text
-jsGPModel fit = T.unlines
-  [ "{"
-  , "  kernel: '" <> jsKernelId (gkKernel fit) <> "',"
-  , "  params: " <> jsGPParams (gkKernel fit) (gkParams fit) <> ","
-  , "  trainX: " <> jsDoubleArray (pdTrainX (gkPredData fit)) <> ","
-  , "  alpha:  " <> jsDoubleArray (pdAlpha  (gkPredData fit)) <> ","
-  , "  kyInv:  " <> jsMatrix      (pdKyInv  (gkPredData fit))
-  , "}"
-  ]
-
-jsKernelId :: Kernel -> Text
-jsKernelId RBF      = "rbf"
-jsKernelId Matern52 = "matern52"
-jsKernelId Periodic = "periodic"
-jsKernelId Linear   = "linear"
-jsKernelId (Poly _) = "poly"
-
-jsGPParams :: Kernel -> GPParams -> Text
-jsGPParams ker p =
-  "{ell:" <> fmtJS (gpLengthScale p)
-  <> ",sf2:" <> fmtJS (gpSignalVar p)
-  <> ",sn2:" <> fmtJS (gpNoiseVar p)
-  <> (if ker == Periodic then ",period:" <> fmtJS (gpPeriod p) else "")
-  <> "}"
-
-jsMatrix :: [[Double]] -> Text
-jsMatrix rows = "[" <> T.intercalate "," (map jsDoubleArray rows) <> "]"
-
-gpTabSwitchJS :: ModelFit -> Text
-gpTabSwitchJS (GPFit _) = T.unlines
-  [ "function showGPTab(idx) {"
-  , "  document.querySelectorAll('.tab-content').forEach((el,i) => {"
-  , "    el.classList.toggle('active', i === idx);"
-  , "  });"
-  , "  document.querySelectorAll('.tab-btn').forEach((el,i) => {"
-  , "    el.classList.toggle('active', i === idx);"
-  , "  });"
-  , "}"
-  ]
-gpTabSwitchJS _ = ""
-
--- ---------------------------------------------------------------------------
--- CSS addition for tabs (appended in reportCss)
--- ---------------------------------------------------------------------------
-
-predJS :: ModelFit -> Text
-predJS NoRegFit = ""
-predJS (GPFit _) = T.unlines
-  [ "// ----- GP 予測 JS -----"
-  , "function kernelEval(ker, p, x1, x2) {"
-  , "  if (ker === 'rbf') {"
-  , "    const d = x1 - x2, l = p.ell;"
-  , "    return p.sf2 * Math.exp(-(d*d) / (2*l*l));"
-  , "  } else if (ker === 'matern52') {"
-  , "    const d = Math.abs(x1 - x2), l = p.ell;"
-  , "    const s = Math.sqrt(5) * d / l;"
-  , "    return p.sf2 * (1 + s + s*s/3) * Math.exp(-s);"
-  , "  } else {"
-  , "    const d = Math.abs(x1 - x2);"
-  , "    const s = Math.sin(Math.PI * d / p.period);"
-  , "    return p.sf2 * Math.exp(-2 * s*s / (p.ell * p.ell));"
-  , "  }"
-  , "}"
-  , ""
-  , "function gpPredict(midx, xStar) {"
-  , "  const m = gpModels[midx];"
-  , "  const kStar = m.trainX.map(xi => kernelEval(m.kernel, m.params, xi, xStar));"
-  , "  const mean  = kStar.reduce((s, k, i) => s + k * m.alpha[i], 0);"
-  , "  const v     = m.kyInv.map(row => row.reduce((s, v, j) => s + v * kStar[j], 0));"
-  , "  const kss   = kernelEval(m.kernel, m.params, xStar, xStar);"
-  , "  const variance = Math.max(0, kss - kStar.reduce((s, k, i) => s + k * v[i], 0));"
-  , "  return { mean, std: Math.sqrt(variance) };"
-  , "}"
-  , ""
-  , "window.__predView = null;"
-  , ""
-  , "function updateGPPrediction() {"
-  , "  const xStar = parseFloat(document.getElementById('x-gp').value);"
-  , "  const midx  = parseInt(document.getElementById('pred-kernel').value);"
-  , "  const { mean, std } = gpPredict(midx, xStar);"
-  , "  const lo = mean - 2 * std, hi = mean + 2 * std;"
-  , "  const el = id => document.getElementById(id);"
-  , "  if (el('gp-pred-mean')) el('gp-pred-mean').textContent = mean.toFixed(5);"
-  , "  if (el('gp-pred-std'))  el('gp-pred-std').textContent  = std.toFixed(5);"
-  , "  if (el('gp-pred-lo'))   el('gp-pred-lo').textContent   = lo.toFixed(5);"
-  , "  if (el('gp-pred-hi'))   el('gp-pred-hi').textContent   = hi.toFixed(5);"
-  , "  if (typeof gpDataXMin !== 'undefined') {"
-  , "    const extrap = xStar < gpDataXMin || xStar > gpDataXMax;"
-  , "    const warn = el('gp-extrap-warn');"
-  , "    if (warn) warn.style.display = extrap ? 'inline-block' : 'none';"
-  , "  }"
-  , "  if (window.__predView) {"
-  , "    const { mean: m0 } = gpPredict(0, xStar);"
-  , "    window.__predView.change('pred_point',"
-  , "      vega.changeset().remove(() => true).insert([{x: xStar, y: m0}])).run();"
-  , "    window.__predView.change('pred_ci',"
-  , "      vega.changeset().remove(() => true)"
-  , "        .insert([{x: xStar, lo: m0 - 2*gpPredict(0,xStar).std,"
-  , "                  hi: m0 + 2*gpPredict(0,xStar).std}])).run();"
-  , "  }"
-  , "}"
-  , ""
-  , "function syncGPSlider() {"
-  , "  const v = document.getElementById('x-gp').value;"
-  , "  const n = document.getElementById('x-gp-num');"
-  , "  if (n) n.value = parseFloat(v).toFixed(5);"
-  , "  updateGPPrediction();"
-  , "}"
-  , ""
-  , "function syncGPNum() {"
-  , "  const v = parseFloat(document.getElementById('x-gp-num').value);"
-  , "  const s = document.getElementById('x-gp');"
-  , "  if (s) s.value = v;"
-  , "  updateGPPrediction();"
-  , "}"
-  , ""
-  , "if (window.__pred_chart) {"
-  , "  vegaEmbed('#pred-chart', window.__pred_chart, {renderer:'canvas',actions:false})"
-  , "    .then(({view}) => { window.__predView = view; updateGPPrediction(); })"
-  , "    .catch(console.error);"
-  , "} else {"
-  , "  updateGPPrediction();"
-  , "}"
-  ]
-predJS fit = T.unlines $
-  [ "// ----- 予測 JS -----"
-  , "const linkName   = '" <> lnk <> "';"
-  , "const xColDegs   = " <> jsXColDegs colDegs <> ";"
-  , "const coeffs     = " <> jsDoubleArray (map snd cs) <> ";"
-  ] ++
-  (case fit of
-     MixFit gs -> ["const blups = " <> jsBLUPs (gsBLUPs gs) <> ";"]
-     _         -> []) ++
-  [ ""
-  , "// Vega view (初期化後にセット)"
-  , "window.__predView = null;"
-  , ""
-  , "function invLink(link, eta) {"
-  , "  switch(link) {"
-  , "    case 'log':   return Math.exp(eta);"
-  , "    case 'logit': return 1 / (1 + Math.exp(-eta));"
-  , "    case 'sqrt':  return eta * eta;"
-  , "    default:      return eta;"
-  , "  }"
-  , "}"
-  , ""
-  , "function computeEta(xVals, groupName) {"
-  , "  let eta = coeffs[0];"
-  , "  let i = 1;"
-  , "  for (const [col, deg] of xColDegs) {"
-  , "    const x = parseFloat(xVals[col] || 0);"
-  , "    for (let k = 1; k <= deg; k++) {"
-  , "      eta += coeffs[i++] * Math.pow(x, k);"
-  , "    }"
-  , "  }"
-  ] ++
-  (case fit of
-     MixFit _ ->
-       [ "  if (groupName) {"
-       , "    const b = blups.find(([g]) => g === groupName);"
-       , "    if (b) eta += b[1];"
-       , "  }"
-       ]
-     _ -> []) ++
-  [ "  return eta;"
-  , "}"
-  , ""
-  , "function getXVals() {"
-  , "  const vals = {};"
-  , "  for (const [col] of xColDegs) {"
-  , "    vals[col] = document.getElementById('x-' + col)?.value || '0';"
-  , "  }"
-  , "  return vals;"
-  , "}"
-  , ""
-  -- CI補間 (smoothXs は predChartSpecJS でセット)
-  , "function interpAt(x, arr) {"
-  , "  const n = smoothXs.length;"
-  , "  if (!n) return 0;"
-  , "  if (x <= smoothXs[0])   return arr[0];"
-  , "  if (x >= smoothXs[n-1]) return arr[n-1];"
-  , "  let lo = 0, hi = n - 1;"
-  , "  while (lo < hi - 1) {"
-  , "    const mid = (lo + hi) >> 1;"
-  , "    if (smoothXs[mid] <= x) lo = mid; else hi = mid;"
-  , "  }"
-  , "  const t = (x - smoothXs[lo]) / (smoothXs[hi] - smoothXs[lo]);"
-  , "  return arr[lo] + t * (arr[hi] - arr[lo]);"
-  , "}"
-  , ""
-  , "function updatePrediction() {"
-  , "  const xVals = getXVals();"
-  , groupSelectJS fit
-  , "  const eta = computeEta(xVals, grp);"
-  , "  const y   = invLink(linkName, eta);"
-  , "  const etaEl = document.getElementById('pred-eta');"
-  , "  const yEl   = document.getElementById('pred-y');"
-  , "  if (etaEl) etaEl.textContent = eta.toFixed(4);"
-  , "  if (yEl)   yEl.textContent   = y.toFixed(4);"
-  , ""
-  , "  // 外挿域チェック"
-  , "  if (typeof predXCol !== 'undefined' && typeof dataXMin !== 'undefined') {"
-  , "    const xv = parseFloat(xVals[predXCol] || 0);"
-  , "    const isExtrap = xv < dataXMin || xv > dataXMax;"
-  , "    const warnEl = document.getElementById('extrap-warn');"
-  , "    if (warnEl) warnEl.style.display = isExtrap ? 'inline-block' : 'none';"
-  , "  }"
-  , ""
-  , "  // Vega チャート更新"
-  , "  if (window.__predView && typeof predXCol !== 'undefined') {"
-  , "    const xv = parseFloat(xVals[predXCol] || 0);"
-  , "    window.__predView.change('pred_point',"
-  , "      vega.changeset().remove(() => true).insert([{x: xv, y}])).run();"
-  , "    if (smoothHasBand) {"
-  , "      const lo = interpAt(xv, smoothLo);"
-  , "      const hi = interpAt(xv, smoothHi);"
-  , "      window.__predView.change('pred_ci',"
-  , "        vega.changeset().remove(() => true).insert([{x: xv, lo, hi}])).run();"
-  , "      const loEl = document.getElementById('pred-ci-lo');"
-  , "      const hiEl = document.getElementById('pred-ci-hi');"
-  , "      if (loEl) loEl.textContent = lo.toFixed(4);"
-  , "      if (hiEl) hiEl.textContent = hi.toFixed(4);"
-  , "    }"
-  , "  }"
-  , "}"
-  , ""
-  , "function syncSlider(col) {"
-  , "  const v = document.getElementById('x-' + col).value;"
-  , "  const num = document.getElementById('x-num-' + col);"
-  , "  if (num) num.value = parseFloat(v).toFixed(5);"
-  , "  updatePrediction();"
-  , "}"
-  , ""
-  , "function syncNum(col) {"
-  , "  const v = parseFloat(document.getElementById('x-num-' + col).value);"
-  , "  const sld = document.getElementById('x-' + col);"
-  , "  if (sld) sld.value = v;"
-  , "  updatePrediction();"
-  , "}"
-  , ""
-  , "// 予測チャートの初期化"
-  , "if (window.__pred_chart) {"
-  , "  vegaEmbed('#pred-chart', window.__pred_chart, {renderer:'canvas',actions:false})"
-  , "    .then(({view}) => {"
-  , "      window.__predView = view;"
-  , "      updatePrediction();"
-  , "    }).catch(console.error);"
-  , "} else {"
-  , "  updatePrediction();"
-  , "}"
-  ]
-  where
-    (cs, colDegs, lnk) = fitDataFor fit
-
-fitDataFor :: ModelFit -> ([(Text, Double)], [(Text, Int)], Text)
-fitDataFor (RegFit fs) = (fsCoeffs fs, fsXColDegs fs, fsLinkName fs)
-fitDataFor (MixFit gs) = (gsFixed gs,  gsXColDegs gs,  gsLinkName gs)
-fitDataFor (HBMFit hs) = let fs = hbmsFit hs
-                         in (fsCoeffs fs, fsXColDegs fs, fsLinkName fs)
-fitDataFor (GPFit _)   = ([], [], "identity")
-fitDataFor NoRegFit    = ([], [], "identity")
-
-groupSelectJS :: ModelFit -> Text
-groupSelectJS (MixFit _) =
-  "  const sel = document.getElementById('pred-group');\n" <>
-  "  const grp = sel ? sel.value : null;"
-groupSelectJS _ = "  const grp = null;"
-
-smoothScrollScript :: Text
-smoothScrollScript = T.unlines
-  [ "document.querySelectorAll('.nav-link').forEach(a => {"
-  , "  a.addEventListener('click', e => {"
-  , "    e.preventDefault();"
-  , "    const t = document.querySelector(a.getAttribute('href'));"
-  , "    if (t) t.scrollIntoView({ behavior: 'smooth' });"
-  , "  });"
-  , "});"
-  ]
-
--- ---------------------------------------------------------------------------
--- JS helpers
--- ---------------------------------------------------------------------------
-
-jsStr :: Text -> Text
-jsStr t = "\"" <> t <> "\""
-
-jsStrArray :: [Text] -> Text
-jsStrArray xs = "[" <> T.intercalate "," (map jsStr xs) <> "]"
-
-jsXColDegs :: [(Text, Int)] -> Text
-jsXColDegs xs = "[" <> T.intercalate "," (map kv xs) <> "]"
-  where kv (c, d) = "[\"" <> c <> "\"," <> T.pack (show d) <> "]"
-
-jsDoubleArray :: [Double] -> Text
-jsDoubleArray xs = "[" <> T.intercalate "," (map fmtJS xs) <> "]"
-
-jsBLUPs :: [(Text, Double)] -> Text
-jsBLUPs bs = "[" <> T.intercalate "," (map kv bs) <> "]"
-  where kv (g, v) = "[\"" <> g <> "\"," <> fmtJS v <> "]"
-
-specJson :: VegaLite -> Text
-specJson = decodeUtf8 . toStrict . encode . fromVL
-
--- ---------------------------------------------------------------------------
--- Formatting helpers
--- ---------------------------------------------------------------------------
-
-fmtJS :: Double -> Text
-fmtJS v
-  | isNaN v      = "0"
-  | isInfinite v = if v > 0 then "1e308" else "-1e308"
-  | otherwise    = T.pack (showFFloat (Just 10) v "")
-
-fmt4 :: Double -> Text
-fmt4 v = T.pack (showFFloat (Just 4) v "")
-
-fmt1 :: Double -> Text
-fmt1 v = T.pack (showFFloat (Just 1) v "")
-
-fmtSigned :: Double -> Text
-fmtSigned v
-  | v >= 0    = " " <> fmt4 v
-  | otherwise = fmt4 v
-
--- ---------------------------------------------------------------------------
--- Model text helpers
--- ---------------------------------------------------------------------------
-
-linkName :: LinkFn -> Text
-linkName Identity = "identity"
-linkName Log      = "log"
-linkName Logit    = "logit"
-linkName Sqrt     = "sqrt"
-
-modelTypeLabel :: Family -> LinkFn -> Text
-modelTypeLabel Gaussian Identity = "LM (Gaussian / Identity)"
-modelTypeLabel fam lnk =
-  "GLM (" <> T.pack (show fam) <> " / " <> linkName lnk <> ")"
-
-glmmTypeLabel :: Family -> LinkFn -> Text
-glmmTypeLabel Gaussian Identity = "LME (Gaussian, exact EM)"
-glmmTypeLabel fam lnk =
-  "GLMM (" <> T.pack (show fam) <> " / " <> linkName lnk <> ", Laplace)"
-
-r2Label :: Family -> Text
-r2Label Gaussian = "R²"
-r2Label _        = "McFadden R²"
-
-formulaText :: [(Text, Int)] -> Text
-formulaText colDegs =
-  "y ~ " <> T.intercalate " + "
-  [ col <> if k == 1 then "" else "^" <> T.pack (show k)
-  | (col, deg) <- colDegs
-  , k <- [1..deg]
-  ]
-
-coeffLabels :: [(Text, Int)] -> [Text]
-coeffLabels colDegs =
-  "β₀ (intercept)" : zipWith lbl [1..] terms
-  where
-    terms = [(col, k) | (col, deg) <- colDegs, k <- [1..deg]]
-    lbl i (col, k) =
-      "β" <> T.pack (show (i::Int)) <> " ("
-      <> col
-      <> (if k == 1 then "" else "^" <> T.pack (show k))
-      <> ")"
-
--- ---------------------------------------------------------------------------
--- HTML component builders
--- ---------------------------------------------------------------------------
-
-statBox :: Text -> Text -> Bool -> Text
-statBox lbl val hi = T.unlines
-  [ "    <div class=\"stat-box" <> (if hi then " highlight" else "") <> "\">"
-  , "      <div class=\"lbl\">" <> lbl <> "</div>"
-  , "      <div class=\"val\">" <> val <> "</div>"
-  , "    </div>"
-  ]
-
-infoBox :: Text -> Text -> Text
-infoBox lbl val = T.unlines
-  [ "    <div class=\"info-box\">"
-  , "      <div class=\"lbl\">" <> lbl <> "</div>"
-  , "      <div class=\"ival\">" <> val <> "</div>"
-  , "    </div>"
-  ]
-
--- ---------------------------------------------------------------------------
--- CSS
--- ---------------------------------------------------------------------------
-
-reportCss :: Text
-reportCss = T.unlines
-  [ "* { box-sizing: border-box; margin: 0; padding: 0; }"
-  , "body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; color: #333; line-height: 1.6; }"
-  , "nav { position: sticky; top: 0; z-index: 100; background: #1e3a5c;"
-  , "      padding: 10px 28px; display: flex; gap: 20px; align-items: center;"
-  , "      box-shadow: 0 2px 6px rgba(0,0,0,.25); }"
-  , "nav h1 { color: #ecf0f1; font-size: 1em; font-weight: 600; flex: 1; }"
-  , ".nav-link { color: #9ab; text-decoration: none; font-size: .82em; white-space: nowrap; }"
-  , ".nav-link:hover { color: #fff; }"
-  , "main { max-width: 1160px; margin: 0 auto; padding: 32px 20px; }"
-  , "section { background: white; border-radius: 12px; padding: 26px 28px;"
-  , "          margin-bottom: 28px; box-shadow: 0 2px 10px rgba(0,0,0,.07); }"
-  , "h2 { font-size: 1.05em; font-weight: 700; color: #1e3a5c; margin-bottom: 18px;"
-  , "     border-bottom: 2px solid #e4e9f0; padding-bottom: 8px; display: flex; align-items: center; gap: 8px; }"
-  , "h3 { font-size: .92em; font-weight: 600; color: #2a5298; margin: 18px 0 10px; }"
-  , ".sec-icon { font-size: 1.1em; }"
-  , ".sec-desc { font-size: .88em; color: #666; margin-bottom: 16px; }"
-  -- stat boxes
-  , ".stat-grid { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }"
-  , ".stat-box { background: #f7f9fc; border: 1px solid #e4e9f0; border-radius: 10px;"
-  , "            padding: 12px 16px; min-width: 120px; text-align: center; }"
-  , ".stat-box .lbl { font-size: .7em; color: #888; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 4px; }"
-  , ".stat-box .val { font-size: 1.25em; font-weight: 700; color: #1e3a5c; }"
-  , ".stat-box.highlight { background: #e8f4e8; border-color: #4caf50; }"
-  , ".stat-box.highlight .val { color: #2e7d32; }"
-  -- info boxes
-  , ".mermaid-wrap { background:#f7fafc; border-radius:8px; padding:24px; margin:12px 0; text-align:center; overflow-x:auto; }"
-  , ".mermaid-wrap .mermaid { display:inline-block; min-width:320px; min-height:200px;"
-  , "   font-family:'Segoe UI',sans-serif; line-height:1.4; }"
-  , ".mermaid-wrap .mermaid svg { max-width:100%; height:auto; min-height:240px; }"
-  , ".info-grid { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }"
-  , ".info-box { background: #f7f9fc; border: 1px solid #e4e9f0; border-radius: 10px;"
-  , "            padding: 12px 18px; min-width: 180px; }"
-  , ".info-box .lbl { font-size: .72em; color: #888; text-transform: uppercase; letter-spacing: .04em; margin-bottom: 4px; }"
-  , ".info-box .ival { font-size: .95em; font-weight: 600; color: #1e3a5c; }"
-  -- column cards (data section)
-  , ".col-cards { display: flex; flex-wrap: wrap; gap: 16px; }"
-  , ".col-card { background: #f7f9fc; border: 1px solid #e4e9f0; border-radius: 10px;"
-  , "            padding: 16px 18px; flex: 1; min-width: 320px; }"
-  , ".col-card-title { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }"
-  , ".col-role { font-size: .7em; background: #1e3a5c; color: white; border-radius: 4px;"
-  , "            padding: 2px 7px; text-transform: uppercase; letter-spacing: .04em; }"
-  , ".col-name { font-size: .95em; font-weight: 700; color: #1e3a5c; }"
-  , ".col-card-body { display: flex; gap: 14px; align-items: flex-start; }"
-  , ".col-hist { flex: 1; min-width: 0; }"
-  , ".col-stats-mini { min-width: 140px; font-size: .82em; }"
-  , ".col-stat-row { display: flex; justify-content: space-between; padding: 3px 0;"
-  , "                border-bottom: 1px solid #eef; gap: 8px; }"
-  , ".col-stat-row .sk { color: #777; }"
-  , ".col-stat-row .sv { font-family: monospace; font-weight: 600; color: #1e3a5c; text-align: right; }"
-  -- tables
-  , "table { width: 100%; border-collapse: collapse; font-size: .88em; margin-bottom: 8px; }"
-  , "thead tr { background: #f0f4f8; }"
-  , "th { padding: 8px 14px; text-align: left; font-weight: 600; color: #444; }"
-  , "td { padding: 7px 14px; border-bottom: 1px solid #f0f2f5; font-family: monospace; }"
-  , "td:first-child { font-family: inherit; font-weight: 500; }"
-  , "tr:last-child td { border-bottom: none; }"
-  , ".vl-wrap { overflow-x: auto; margin-bottom: 8px; }"
-  -- prediction section layout
-  , ".predict-layout { display: flex; gap: 20px; flex-wrap: wrap; }"
-  , ".predict-left { flex: 0 0 340px; min-width: 280px; }"
-  , ".predict-chart { flex: 1; min-width: 320px; }"
-  , ".predict-controls { background: #f7f9fc; border-radius: 10px; padding: 16px 18px; margin-bottom: 14px; }"
-  , ".slider-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }"
-  , ".slider-row label { font-size: .88em; color: #555; min-width: 80px; font-weight: 500; }"
-  , "input[type=range] { flex: 1; min-width: 120px; accent-color: #1e3a5c; }"
-  , "input[type=number] { width: 105px; padding: 5px 8px; border: 1.5px solid #c0ccd8; border-radius: 6px; font-size: .88em; }"
-  , "select { padding: 6px 10px; border: 1.5px solid #c0ccd8; border-radius: 6px; font-size: .86em; background: white; }"
-  , ".predict-output { display: flex; gap: 10px; flex-wrap: wrap; }"
-  , ".pred-box { flex: 1; min-width: 100px; background: white; border: 1.5px solid #e4e9f0;"
-  , "            border-radius: 10px; padding: 12px 14px; text-align: center; }"
-  , ".pred-box .plbl { font-size: .72em; color: #888; text-transform: uppercase; letter-spacing: .04em; }"
-  , ".pred-box .pval { font-size: 1.3em; font-weight: 700; color: #1e3a5c; margin: 4px 0; }"
-  , ".pred-box .psub { font-size: .76em; color: #999; }"
-  , ".pred-box.mean-box { border-color: #1e3a5c; }"
-  , ".pred-box.ci-box   { border-color: #e74c3c; }"
-  , ".pred-box.ci-box .pval { font-size: 1.0em; color: #c0392b; }"
-  , ".tab-bar { display: flex; gap: 6px; margin-bottom: 18px; flex-wrap: wrap; }"
-  , ".tab-btn { padding: 7px 18px; border: 1.5px solid #c0ccd8; border-radius: 20px;"
-  , "           background: white; color: #555; cursor: pointer; font-size: .88em;"
-  , "           transition: all .15s; }"
-  , ".tab-btn:hover { border-color: #1e3a5c; color: #1e3a5c; }"
-  , ".tab-btn.active { background: #1e3a5c; color: white; border-color: #1e3a5c; }"
-  , ".tab-content { display: none; }"
-  , ".tab-content.active { display: block; }"
-  , ".extrap-badge { background: #ff9800; color: white; border-radius: 4px;"
-  , "                padding: 1px 7px; font-size: .7em; font-weight: 700;"
-  , "                margin-left: 6px; vertical-align: middle; letter-spacing: .03em; }"
-  -- appendix
-  , ".appendix-block { background: #f7f9fc; border-left: 4px solid #1e3a5c;"
-  , "                  padding: 14px 18px; margin: 12px 0; border-radius: 0 8px 8px 0; }"
-  , ".appendix-block h4 { font-size: .9em; font-weight: 700; color: #1e3a5c; margin-bottom: 6px; }"
-  , ".appendix-block p, .appendix-block li { font-size: .88em; color: #444; margin-bottom: 4px; }"
-  , ".formula { background: #f7f9fc; border: 1px solid #e4e9f0; border-radius: 8px;"
-  , "           padding: 10px 14px; margin: 8px 0; font-family: monospace; font-size: .88em; color: #333; }"
-  , ".cmp-table { width: 100%; border-collapse: collapse; font-size: .88em; margin: 12px 0; }"
-  , ".cmp-table th { background: #f0f4f9; padding: 9px 12px; text-align: left;"
-  , "                font-weight: 600; color: #2c3e50; border-bottom: 2px solid #d0d7e3; }"
-  , ".cmp-table td { padding: 7px 12px; border-bottom: 1px solid #eef2f6; }"
-  , ".cmp-table td.num { text-align: right; font-variant-numeric: tabular-nums; }"
-  , ".cmp-color { display: inline-block; width: 14px; height: 14px; border-radius: 3px;"
-  , "             margin-right: 6px; vertical-align: middle; border: 1px solid rgba(0,0,0,.15); }"
-  , ".cmp-ci    { color: #555; font-size: .82em; }"
-  ]
-
--- ===========================================================================
--- 複数モデル比較レポート
--- ===========================================================================
-
--- | 比較レポートに含めるモデルエントリ。
-data CompareEntry = CompareEntry
-  { ceLabel :: Text       -- ^ モデル表示名 (例: "LM (Pooled)")
-  , ceColor :: Text       -- ^ プロットの色 (CSS カラーコード, 例: "#e41a1c")
-  , ceFit   :: ModelFit
-  }
-
--- | 複数モデルを 1 つの HTML レポートに並べた比較レポートを生成する。
---
--- セクション構成:
---   1. データの特性 (1 度だけ)
---   2. モデル概要 (各モデルの種別・式・係数を 1 行ずつ並べた表)
---   3. 予測曲線オーバーレイ (全モデルの曲線 + 信用区間を 1 つの散布図に)
---   4. 係数比較 (forest plot 形式の表)
---   5. WAIC/LOO 比較 (利用可能なモデルのみ)
-writeComparisonReport
-  :: FilePath
-  -> AnalysisReportConfig
-  -> DXD.DataFrame
-  -> [Text]              -- ^ x 列名 (典型的には 1 つ)
-  -> Text                -- ^ y 列名
-  -> [CompareEntry]
-  -> IO ()
-writeComparisonReport path cfg df xCols yCol entries =
-  TIO.writeFile path (buildCompareHtml cfg df xCols yCol entries)
-
-buildCompareHtml
-  :: AnalysisReportConfig -> DXD.DataFrame -> [Text] -> Text
-  -> [CompareEntry] -> Text
-buildCompareHtml cfg df xCols yCol entries = T.unlines $
-  [ "<!DOCTYPE html>"
-  , "<html lang=\"ja\">"
-  , "<head>"
-  , "  <meta charset=\"utf-8\">"
-  , "  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
-  , "  <title>" <> arcTitle cfg <> "</title>"
-  , "  <script>" <> vegaJS      <> "</script>"
-  , "  <script>" <> vegaLiteJS  <> "</script>"
-  , "  <script>" <> vegaEmbedJS <> "</script>"
-  , "  <style>" , reportCss , "  </style>"
-  , "</head>"
-  , "<body>"
-  , compareNavBar cfg
-  , "<main>"
-  , dataSummarySection df xCols yCol
-  , compareModelsSection entries
-  , compareOverlaySection xCols yCol
-  , compareCoefSection entries
-  , compareWaicSection entries
-  , "</main>"
-  , "<script>"
-  , compareOverlayJS df xCols yCol entries
-  , columnDataJS df xCols yCol
-  , histogramInitJS (xCols ++ [yCol])
-  , smoothScrollScript
-  , "</script>"
-  , "</body>"
-  , "</html>"
-  ]
-
-compareNavBar :: AnalysisReportConfig -> Text
-compareNavBar cfg = T.unlines
-  [ "<nav>"
-  , "  <h1>&#128202; " <> arcTitle cfg <> "</h1>"
-  , "  <a class=\"nav-link\" href=\"#sec-data\">データ</a>"
-  , "  <a class=\"nav-link\" href=\"#sec-cmp-models\">モデル一覧</a>"
-  , "  <a class=\"nav-link\" href=\"#sec-cmp-overlay\">予測比較</a>"
-  , "  <a class=\"nav-link\" href=\"#sec-cmp-coef\">係数比較</a>"
-  , "  <a class=\"nav-link\" href=\"#sec-cmp-waic\">WAIC/LOO</a>"
-  , "</nav>"
-  ]
-
--- | モデル一覧表
-compareModelsSection :: [CompareEntry] -> Text
-compareModelsSection entries = T.unlines $
-  [ "<section id=\"sec-cmp-models\">"
-  , "  <h2><span class=\"sec-icon\">&#9878;</span> 2. モデル一覧</h2>"
-  , "  <p class=\"sec-desc\">本レポートで比較する " <> T.pack (show (length entries))
-    <> " モデルの概要。色はオーバーレイ図と凡例で共通。</p>"
-  , "  <table class=\"cmp-table\">"
-  , "    <thead><tr>"
-  , "      <th></th><th>モデル</th><th>種別</th><th>回帰式</th>"
-  , "      <th class=\"num\">R²</th>"
-  , "    </tr></thead>"
-  , "    <tbody>"
-  ] ++
-  map modelRowHtml entries ++
-  [ "    </tbody>"
-  , "  </table>"
-  , "</section>"
-  ]
-  where
-    modelRowHtml e = T.unlines
-      [ "      <tr>"
-      , "        <td><span class=\"cmp-color\" style=\"background:" <> ceColor e <> "\"></span></td>"
-      , "        <td><b>" <> ceLabel e <> "</b></td>"
-      , "        <td>" <> modelTypeOf (ceFit e) <> "</td>"
-      , "        <td>" <> modelFormulaOf (ceFit e) <> "</td>"
-      , "        <td class=\"num\">" <> fmt4 (modelR2Of (ceFit e)) <> "</td>"
-      , "      </tr>"
-      ]
-
-modelTypeOf :: ModelFit -> Text
-modelTypeOf (RegFit fs)  = fsModelType fs
-modelTypeOf (MixFit gs)  = gsModelType gs
-modelTypeOf (HBMFit hs)  = fsModelType (hbmsFit hs)
-modelTypeOf (GPFit _)    = "Gaussian Process"
-modelTypeOf NoRegFit     = "—"
-
-modelFormulaOf :: ModelFit -> Text
-modelFormulaOf (RegFit fs)  = fsFormula fs
-modelFormulaOf (MixFit gs)  = gsFormula gs
-modelFormulaOf (HBMFit hs)  = fsFormula (hbmsFit hs)
-modelFormulaOf (GPFit _)    = "y ~ GP(m, k)"
-modelFormulaOf NoRegFit     = "—"
-
-modelR2Of :: ModelFit -> Double
-modelR2Of (RegFit fs)  = fsR2 fs
-modelR2Of (MixFit gs)  = gsR2 gs
-modelR2Of (HBMFit hs)  = fsR2 (hbmsFit hs)
-modelR2Of _            = 0
-
--- | 予測曲線オーバーレイ (描画は JS で実装)
-compareOverlaySection :: [Text] -> Text -> Text
-compareOverlaySection xCols yCol = T.unlines
-  [ "<section id=\"sec-cmp-overlay\">"
-  , "  <h2><span class=\"sec-icon\">&#128200;</span> 3. 予測曲線比較</h2>"
-  , "  <p class=\"sec-desc\">同一データに対する各モデルの予測曲線を重ね描き。"
-  , "    HBM など信用区間を持つモデルはバンドも表示。</p>"
-  , "  <div class=\"vl-wrap\"><div id=\"cmp-overlay\"></div></div>"
-  , if length xCols /= 1
-      then "  <p style=\"font-size:.85em;color:#888\">x 列が単一でないため曲線比較は省略しました。</p>"
-      else "  <p style=\"font-size:.82em;color:#666;margin-top:12px\">x = "
-           <> head xCols <> ", y = " <> yCol <> "</p>"
-  , "</section>"
-  ]
-
--- | 係数比較表 (HBM は CI 付き)
-compareCoefSection :: [CompareEntry] -> Text
-compareCoefSection entries = T.unlines $
-  [ "<section id=\"sec-cmp-coef\">"
-  , "  <h2><span class=\"sec-icon\">&#128300;</span> 4. 係数比較</h2>"
-  , "  <p class=\"sec-desc\">各モデルが推定したパラメータの一覧。"
-  , "    HBM は事後平均と 95% 信用区間 [2.5%, 97.5%] を表示。</p>"
-  , "  <table class=\"cmp-table\">"
-  , "    <thead><tr>"
-  , "      <th></th><th>モデル</th><th>パラメータ</th>"
-  , "      <th class=\"num\">推定値</th><th class=\"num\">95% CI</th>"
-  , "    </tr></thead>"
-  , "    <tbody>"
-  ] ++
-  concatMap coefRowsForEntry entries ++
-  [ "    </tbody>"
-  , "  </table>"
-  , "</section>"
-  ]
-  where
-    coefRowsForEntry e =
-      let coefs = extractCoefRows (ceFit e)
-          n = length coefs
-      in zipWith (mkCoefRow e n) [0 :: Int ..] coefs
-
-    mkCoefRow e n i (cname, val, mci) =
-      let firstCol = if i == 0
-                      then "<td rowspan=\"" <> T.pack (show n) <> "\">"
-                           <> "<span class=\"cmp-color\" style=\"background:" <> ceColor e <> "\"></span>"
-                           <> "</td><td rowspan=\"" <> T.pack (show n) <> "\"><b>"
-                           <> ceLabel e <> "</b></td>"
-                      else ""
-          ciCell = case mci of
-            Just (lo, hi) -> "<span class=\"cmp-ci\">[" <> fmtSigned lo
-                          <> ", " <> fmtSigned hi <> "]</span>"
-            Nothing       -> "<span class=\"cmp-ci\">—</span>"
-      in T.unlines
-           [ "      <tr>"
-           , "        " <> firstCol
-           , "        <td>" <> cname <> "</td>"
-           , "        <td class=\"num\">" <> fmtSigned val <> "</td>"
-           , "        <td class=\"num\">" <> ciCell <> "</td>"
-           , "      </tr>"
-           ]
-
-extractCoefRows :: ModelFit -> [(Text, Double, Maybe (Double, Double))]
-extractCoefRows (RegFit fs)  = [(n, v, Nothing) | (n, v) <- fsCoeffs fs]
-extractCoefRows (MixFit gs)  = [(n, v, Nothing) | (n, v) <- gsFixed gs]
-extractCoefRows (HBMFit hs)  =
-  [ (n, m, Just (lo, hi))
-  | (n, m, _, lo, hi) <- hbmsPosteriorRows hs ]
-extractCoefRows _            = []
-
--- | WAIC / LOO 比較 (どれか 1 つでも持っていれば表示)
-compareWaicSection :: [CompareEntry] -> Text
-compareWaicSection entries =
-  let rows = [ (e, w, l) | e <- entries
-             , let mws = waicLooOf (ceFit e)
-             , Just (w, l) <- [mws] ]
-  in if null rows
-     then ""
-     else
-       let bestWaic = minimum [waicValue w | (_, w, _) <- rows]
-           bestLoo  = minimum [looValue  l | (_, _, l) <- rows]
-       in T.unlines $
-       [ "<section id=\"sec-cmp-waic\">"
-       , "  <h2><span class=\"sec-icon\">&#128202;</span> 5. WAIC / LOO 比較</h2>"
-       , "  <p class=\"sec-desc\">情報量規準が小さいほど良い。"
-         <> "ΔWAIC ≈ 0 のモデル群は実質的に同等。最良モデルを <b>★</b> で示す。</p>"
-       , "  <table class=\"cmp-table\">"
-       , "    <thead><tr>"
-       , "      <th></th><th>モデル</th><th class=\"num\">WAIC</th>"
-       , "      <th class=\"num\">ΔWAIC</th><th class=\"num\">LOO</th>"
-       , "      <th class=\"num\">ΔLOO</th>"
-       , "    </tr></thead>"
-       , "    <tbody>"
-       ] ++
-       map (waicRowHtml bestWaic bestLoo) rows ++
-       [ "    </tbody>"
-       , "  </table>"
-       , "</section>"
-       ]
-  where
-    waicRowHtml bw bl (e, w, l) =
-      let dw = waicValue w - bw
-          dl = looValue  l - bl
-          star x = if x == 0 then " ★" else ""
-      in T.unlines
-           [ "      <tr>"
-           , "        <td><span class=\"cmp-color\" style=\"background:"
-             <> ceColor e <> "\"></span></td>"
-           , "        <td><b>" <> ceLabel e <> "</b></td>"
-           , "        <td class=\"num\">" <> fmt4 (waicValue w) <> "</td>"
-           , "        <td class=\"num\">" <> fmt4 dw <> star dw <> "</td>"
-           , "        <td class=\"num\">" <> fmt4 (looValue l) <> "</td>"
-           , "        <td class=\"num\">" <> fmt4 dl <> star dl <> "</td>"
-           , "      </tr>"
-           ]
-
-waicLooOf :: ModelFit -> Maybe (WAICResult, LOOResult)
-waicLooOf (RegFit fs) = fsModelSelect fs
-waicLooOf (HBMFit hs) = fsModelSelect (hbmsFit hs)
-waicLooOf (MixFit gs) = gsModelSelect gs
-waicLooOf _           = Nothing
-
--- | オーバーレイ用の Vega-Lite spec を組み立てる JS (data URL 経由)
-compareOverlayJS :: DXD.DataFrame -> [Text] -> Text -> [CompareEntry] -> Text
-compareOverlayJS df xCols yCol entries
-  | length xCols /= 1 = ""
-  | otherwise =
-      let xCol = head xCols
-          (xs, ys) = case (getDoubleVec xCol df, getDoubleVec yCol df) of
-            (Just xv, Just yv) -> (V.toList xv, V.toList yv)
-            _                  -> ([], [])
-          gs = case getTextVec "group" df of
-                 Just gv -> map Just (V.toList gv)
-                 Nothing -> map (const Nothing) xs
-          dataPoints = T.intercalate "," $
-            zipWith3 (\x y mg ->
-              let g = maybe "" (\t -> ",\"g\":\"" <> t <> "\"") mg
-              in "{\"x\":" <> fmtJS x <> ",\"y\":" <> fmtJS y <> g <> "}")
-              xs ys gs
-          hasGroups = any ( /= Nothing) gs
-          -- 各 modelLayer は ",{band},{line}" 形式で返す (リーディングカンマあり)
-          modelLayers = T.concat (map (modelLayer xCol yCol) entries)
-          legendItems = T.intercalate "," (map legendItem entries)
-      in T.unlines
-           [ "(function() {"
-           , "  const spec = {"
-           , "    '$schema':'https://vega.github.io/schema/vega-lite/v5.json',"
-           , "    width: 720, height: 400, background:'transparent',"
-           , "    layer: ["
-           , "      {"
-           , "        data:{values:[" <> dataPoints <> "]},"
-           , "        mark:{type:'circle',size:60,opacity:0.7},"
-           , "        encoding:{"
-           , "          x:{field:'x',type:'quantitative',axis:{title:'" <> xCol <> "'}},"
-           , "          y:{field:'y',type:'quantitative',axis:{title:'" <> yCol <> "'}}"
-           , if hasGroups
-               then "          ,color:{field:'g',type:'nominal',scale:{scheme:'tableau10'},legend:{title:'group'}}"
-               else "          ,color:{value:'#888'}"
-           , "        }"
-           , "      }"
-           , modelLayers   -- 各要素はすでに先頭カンマ付き
-           , "    ]"
-           , "  };"
-           , "  vegaEmbed('#cmp-overlay', spec, {actions:false}).catch(console.error);"
-           , "  // モデル凡例 (色対応をテキストで表示)"
-           , "  const lg = [" <> legendItems <> "];"
-           , "  console.log('Model legend:', lg);"
-           , "})();"
-           ]
-  where
-    legendItem e = "{\"label\":\"" <> ceLabel e <> "\",\"color\":\"" <> ceColor e <> "\"}"
-
--- | 1 モデルの予測曲線レイヤー (smoothData があれば線 + バンド)
-modelLayer :: Text -> Text -> CompareEntry -> Text
-modelLayer xCol yCol e =
-  case smoothDataFor (ceFit e) of
-    Nothing -> ""
-    Just (_, sd) ->
-      let pts = T.intercalate "," $
-            zipWith4 (\x y lo hi ->
-              "{\"x\":" <> fmtJS x <> ",\"y\":" <> fmtJS y
-              <> ",\"lo\":" <> fmtJS lo <> ",\"hi\":" <> fmtJS hi <> "}")
-            (sdXs sd) (sdYs sd) (sdLower sd) (sdUpper sd)
-          color = ceColor e
-          band  = if sdHasBand sd
-                  then T.unlines
-                    [ "      ,{"
-                    , "        data:{values:[" <> pts <> "]},"
-                    , "        mark:{type:'area',color:'" <> color <> "',opacity:0.18},"
-                    , "        encoding:{"
-                    , "          x:{field:'x',type:'quantitative'},"
-                    , "          y:{field:'lo',type:'quantitative'},"
-                    , "          y2:{field:'hi'}"
-                    , "        }"
-                    , "      }"
-                    ]
-                  else ""
-          line = T.unlines
-            [ "      ,{"
-            , "        data:{values:[" <> pts <> "]},"
-            , "        mark:{type:'line',color:'" <> color
-              <> "',strokeWidth:2.5,tooltip:{content:'data'}},"
-            , "        encoding:{"
-            , "          x:{field:'x',type:'quantitative'},"
-            , "          y:{field:'y',type:'quantitative'}"
-            , "        }"
-            , "      }"
-            ]
-      in band <> line
-  where
-    _ = (xCol, yCol)  -- 軸タイトルは点レイヤーで設定済み
-
--- 4 引数 zipWith
-zipWith4 :: (a -> b -> c -> d -> e) -> [a] -> [b] -> [c] -> [d] -> [e]
-zipWith4 f (a:as) (b:bs) (c:cs) (d:ds) = f a b c d : zipWith4 f as bs cs ds
-zipWith4 _ _ _ _ _ = []
diff --git a/src/Hanalyze/Viz/Assets.hs b/src/Hanalyze/Viz/Assets.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/Assets.hs
+++ /dev/null
@@ -1,233 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.Assets
--- Description : オフライン HTML レポート用に Vega/Vega-Lite/Vega-Embed の JS 本体を同梱する自動生成モジュール
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- Auto-generated from assets/ — do not edit by hand.
--- Bundles Vega v5, Vega-Lite v5, Vega-Embed v6 for offline HTML reports.
-module Hanalyze.Viz.Assets
-  ( vegaJS
-  , vegaLiteJS
-  , vegaEmbedJS
-  ) where
-
-import Data.Text (Text)
-import qualified Data.Text as T
-
-vegaJS :: Text
-vegaJS = T.concat
-  [ "!function(t,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?e(exports):\"function\"==typeof define&&define.amd?define([\"exports\"],e):e((t=\"undefined\"!=typeof globalThis?globalThis:t||self).vega={})}(this,(function(t){\"use strict\";function e(t,e,n){return t.fields=e||[],t.fname=n,t}function n(t){return null==t?null:t.fname}function r(t){return null==t?null:t.fields}function i(t){return 1===t.length?o(t[0]):a(t)}const o=t=>function(e){return e[t]},a=t=>{const e=t.length;return function(n){for(let r=0;r<e;++r)n=n[t[r]];return n}};function s(t){throw Error(t)}function u(t){const e=[],n=t.length;let r,i,o,a=null,u=0,l=\"\";function c(){e.push(l+t.substring(r,i)),l=\"\",r=i+1}for(t+=\"\",r=i=0;i<n;++i)if(o=t[i],\"\\\\\"===o)l+=t.substring(r,i++),r=i;else if(o===a)c(),a=null,u=-1;else{if(a)continue;r===u&&'\"'===o||r===u&&\"'\"===o?(r=i+1,a=o):\".\"!==o||u?\"[\"===o?(i>r&&c(),u=r=i+1):\"]\"===o&&(u||s(\"Access path missing open bracket: \"+t),u>0&&c(),u=0,r=i+1):i>r?c():r=i+1}return u&&s(\"Access path missing closing bracket: \"+t),a&&s(\"Access path missing closing quote: \"+t),i>r&&(i++,c()),e}function l(t,n,r){const o=u(t);return t=1===o.length?o[0]:t,e((r&&r.get||i)(o),[t],n||t)}const c=l(\"id\"),f=e((t=>t),[],\"identity\"),h=e((()=>0),[],\"zero\"),d=e((()=>1),[],\"one\"),p=e((()=>!0),[],\"true\"),g=e((()=>!1),[],\"false\"),m=new Set(Object.getOwnPropertyNames(Object.prototype));function y(t,e,n){const r=[e].concat([].slice.call(n));console[t].apply(console,r)}const v=0,_=1,x=2,b=3,w=4;function k(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:y,r=t||v;return{level(t){return arguments.length?(r=+t,this):r},error(){return r>=_&&n(e||\"error\",\"ERROR\",arguments),this},warn(){return r>=x&&n(e||\"warn\",\"WARN\",arguments),this},info(){return r>=b&&n(e||\"log\",\"INFO\",arguments),this},debug(){return r>=w&&n(e||\"log\",\"DEBUG\",arguments),this}}}var A=Array.isArray;function M(t){return t===Object(t)}const E=t=>\"__proto__\"!==t;function D(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];return e.reduce(((t,e)=>{for(const n in e)if(\"signals\"===n)t.signals=F(t.signals,e.signals);else{const r=\"legend\"===n?{layout:1}:\"style\"===n||null;C(t,n,e[n],r)}return t}),{})}function C(t,e,n,r){if(!E(e))return;let i,o;if(M(n)&&!A(n))for(i in o=M(t[e])?t[e]:t[e]={},n)r&&(!0===r||r[i])?C(o,i,n[i]):E(i)&&(o[i]=n[i]);else t[e]=n}function F(t,e){if(null==t)return e;const n={},r=[];function i(t){n[t.name]||(n[t.name]=1,r.push(t))}return e.forEach(i),t.forEach(i),r}function S(t){return t[t.length-1]}function $(t){return null==t||\"\"===t?null:+t}const T=t=>e=>t*Math.exp(e),B=t=>e=>Math.log(t*e),N=t=>e=>Math.sign(e)*Math.log1p(Math.abs(e/t)),z=t=>e=>Math.sign(e)*Math.expm1(Math.abs(e))*t,O=t=>e=>e<0?-Math.pow(-e,t):Math.pow(e,t);function R(t,e,n,r){const i=n(t[0]),o=n(S(t)),a=(o-i)*e;return[r(i-a),r(o-a)]}function L(t,e){return R(t,e,$,f)}function U(t,e){var n=Math.sign(t[0]);return R(t,e,B(n),T(n))}function q(t,e,n){return R(t,e,O(n),O(1/n))}function P(t,e,n){return R(t,e,N(n),z(n))}function j(t,e,n,r,i){const o=r(t[0]),a=r(S(t)),s=null!=e?r(e):(o+a)/2;return[i(s+(o-s)*n),i(s+(a-s)*n)]}function I(t,e,n){return j(t,e,n,$,f)}function W(t,e,n){const r=Math.sign(t[0]);return j(t,e,n,B(r),T(r))}function H(t,e,n,r){return j(t,e,n,O(r),O(1/r))}function Y(t,e,n,r){return j(t,e,n,N(r),z(r))}function G(t){return 1+~~(new Date(t).getMonth()/3)}function V(t){return 1+~~(new Date(t).getUTCMonth()/3)}function X(t){return null!=t?A(t)?t:[t]:[]}function J(t,e,n){let r,i=t[0],o=t[1];return o<i&&(r=o,o=i,i=r),r=o-i,r>=n-e?[e,n]:[i=Math.min(Math.max(i,e),n-r),i+r]}function Z(t){return\"function\"==typeof t}const Q=\"descending\";function K(t,n,i){i=i||{},n=X(n)||[];const o=[],a=[],s={},u=i.comparator||et;return X(t).forEach(((t,e)=>{null!=t&&(o.push(n[e]===Q?-1:1),a.push(t=Z(t)?t:l(t,null,i)),(r(t)||[]).forEach((t=>s[t]=1)))})),0===a.length?null:e(u(a,o),Object.keys(s))}const tt=(t,e)=>(t<e||null==t)&&null!=e?-1:(t>e||null==e)&&null!=t?1:(e=e instanceof Date?+e:e,(t=t instanceof Date?+t:t)!==t&&e==e?-1:e!=e&&t==t?1:0),et=(t,e)=>1===t.length?nt(t[0],e[0]):rt(t,e,t.length),nt=(t"
-  , ",e)=>function(n,r){return tt(t(n),t(r))*e},rt=(t,e,n)=>(e.push(0),function(r,i){let o,a=0,s=-1;for(;0===a&&++s<n;)o=t[s],a=tt(o(r),o(i));return a*e[s]});function it(t){return Z(t)?t:()=>t}function ot(t,e){let n;return r=>{n&&clearTimeout(n),n=setTimeout((()=>(e(r),n=null)),t)}}function at(t){for(let e,n,r=1,i=arguments.length;r<i;++r)for(n in e=arguments[r],e)t[n]=e[n];return t}function st(t,e){let n,r,i,o,a=0;if(t&&(n=t.length))if(null==e){for(r=t[a];a<n&&(null==r||r!=r);r=t[++a]);for(i=o=r;a<n;++a)r=t[a],null!=r&&(r<i&&(i=r),r>o&&(o=r))}else{for(r=e(t[a]);a<n&&(null==r||r!=r);r=e(t[++a]));for(i=o=r;a<n;++a)r=e(t[a]),null!=r&&(r<i&&(i=r),r>o&&(o=r))}return[i,o]}function ut(t,e){const n=t.length;let r,i,o,a,s,u=-1;if(null==e){for(;++u<n;)if(i=t[u],null!=i&&i>=i){r=o=i;break}if(u===n)return[-1,-1];for(a=s=u;++u<n;)i=t[u],null!=i&&(r>i&&(r=i,a=u),o<i&&(o=i,s=u))}else{for(;++u<n;)if(i=e(t[u],u,t),null!=i&&i>=i){r=o=i;break}if(u===n)return[-1,-1];for(a=s=u;++u<n;)i=e(t[u],u,t),null!=i&&(r>i&&(r=i,a=u),o<i&&(o=i,s=u))}return[a,s]}function lt(t,e){return Object.hasOwn(t,e)}const ct={};function ft(t){let e,n={};function r(t){return lt(n,t)&&n[t]!==ct}const i={size:0,empty:0,object:n,has:r,get:t=>r(t)?n[t]:void 0,set(t,e){return r(t)||(++i.size,n[t]===ct&&--i.empty),n[t]=e,this},delete(t){return r(t)&&(--i.size,++i.empty,n[t]=ct),this},clear(){i.size=i.empty=0,i.object=n={}},test(t){return arguments.length?(e=t,i):e},clean(){const t={};let r=0;for(const i in n){const o=n[i];o===ct||e&&e(o)||(t[i]=o,++r)}i.size=r,i.empty=0,i.object=n=t}};return t&&Object.keys(t).forEach((e=>{i.set(e,t[e])})),i}function ht(t,e,n,r,i,o){if(!n&&0!==n)return o;const a=+n;let s,u=t[0],l=S(t);l<u&&(s=u,u=l,l=s),s=Math.abs(e-u);const c=Math.abs(l-e);return s<c&&s<=a?r:c<=a?i:o}function dt(t,e,n){const r=t.prototype=Object.create(e.prototype);return Object.defineProperty(r,\"constructor\",{value:t,writable:!0,enumerable:!0,configurable:!0}),at(r,n)}function pt(t,e,n,r){let i,o=e[0],a=e[e.length-1];return o>a&&(i=o,o=a,a=i),r=void 0===r||r,((n=void 0===n||n)?o<=t:o<t)&&(r?t<=a:t<a)}function gt(t){return\"boolean\"==typeof t}function mt(t){return\"[object Date]\"===Object.prototype.toString.call(t)}function yt(t){return t&&Z(t[Symbol.iterator])}function vt(t){return\"number\"==typeof t}function _t(t){return\"[object RegExp]\"===Object.prototype.toString.call(t)}function xt(t){return\"string\"==typeof t}function bt(t,n,r){t&&(t=n?X(t).map((t=>t.replace(/\\\\(.)/g,\"$1\"))):X(t));const o=t&&t.length,a=r&&r.get||i,s=t=>a(n?[t]:u(t));let l;if(o)if(1===o){const e=s(t[0]);l=function(t){return\"\"+e(t)}}else{const e=t.map(s);l=function(t){let n=\"\"+e[0](t),r=0;for(;++r<o;)n+=\"|\"+e[r](t);return n}}else l=function(){return\"\"};return e(l,t,\"key\")}function wt(t,e){const n=t[0],r=S(t),i=+e;return i?1===i?r:n+i*(r-n):n}function kt(t){let e,n,r;t=+t||1e4;const i=()=>{e={},n={},r=0},o=(i,o)=>(++r>t&&(n=e,e={},r=1),e[i]=o);return i(),{clear:i,has:t=>lt(e,t)||lt(n,t),get:t=>lt(e,t)?e[t]:lt(n,t)?o(t,n[t]):void 0,set:(t,n)=>lt(e,t)?e[t]=n:o(t,n)}}function At(t,e,n,r){const i=e.length,o=n.length;if(!o)return e;if(!i)return n;const a=r||new e.constructor(i+o);let s=0,u=0,l=0;for(;s<i&&u<o;++l)a[l]=t(e[s],n[u])>0?n[u++]:e[s++];for(;s<i;++s,++l)a[l]=e[s];for(;u<o;++u,++l)a[l]=n[u];return a}function Mt(t,e){let n=\"\";for(;--e>=0;)n+=t;return n}function Et(t,e,n,r){const i=n||\" \",o=t+\"\",a=e-o.length;return a<=0?o:\"left\"===r?Mt(i,a)+o:\"center\"===r?Mt(i,~~(a/2))+o+Mt(i,Math.ceil(a/2)):o+Mt(i,a)}function Dt(t){return t&&S(t)-t[0]||0}function Ct(t){return A(t)?\"[\"+t.map(Ct)+\"]\":M(t)||xt(t)?JSON.stringify(t).replace(\"\\u2028\",\"\\\\u2028\").replace(\"\\u2029\",\"\\\\u2029\"):t}function Ft(t){return null==t||\"\"===t?null:!(!t||\"false\"===t||\"0\"===t)&&!!t}const St=t=>vt(t)||mt(t)?t:Date.parse(t);function $t(t,e){return e=e||St,null==t||\"\"===t?null:e(t)}function Tt(t){return null==t||\"\"===t?null:t+\"\"}function Bt(t){const e={},n=t.length;for(let r=0;r<n;++r)e[t[r]]=!0;return e}function Nt(t,e,n,r){const i=null!=r?r:\"…\",o=t+\"\",a=o.length,s=Math.max(0,e-i.length);return a<=e?o:\"left\"===n?i+o.slice(a-s):\"center\"===n?o.slice(0"
-  , ",Math.ceil(s/2))+i+o.slice(a-~~(s/2)):o.slice(0,s)+i}function zt(t,e,n){if(t)if(e){const r=t.length;for(let i=0;i<r;++i){const r=e(t[i]);r&&n(r,i,t)}}else t.forEach(n)}var Ot={},Rt={},Lt=34,Ut=10,qt=13;function Pt(t){return new Function(\"d\",\"return {\"+t.map((function(t,e){return JSON.stringify(t)+\": d[\"+e+'] || \"\"'})).join(\",\")+\"}\")}function jt(t){var e=Object.create(null),n=[];return t.forEach((function(t){for(var r in t)r in e||n.push(e[r]=r)})),n}function It(t,e){var n=t+\"\",r=n.length;return r<e?new Array(e-r+1).join(0)+n:n}function Wt(t){var e,n=t.getUTCHours(),r=t.getUTCMinutes(),i=t.getUTCSeconds(),o=t.getUTCMilliseconds();return isNaN(t)?\"Invalid Date\":((e=t.getUTCFullYear())<0?\"-\"+It(-e,6):e>9999?\"+\"+It(e,6):It(e,4))+\"-\"+It(t.getUTCMonth()+1,2)+\"-\"+It(t.getUTCDate(),2)+(o?\"T\"+It(n,2)+\":\"+It(r,2)+\":\"+It(i,2)+\".\"+It(o,3)+\"Z\":i?\"T\"+It(n,2)+\":\"+It(r,2)+\":\"+It(i,2)+\"Z\":r||n?\"T\"+It(n,2)+\":\"+It(r,2)+\"Z\":\"\")}function Ht(t){var e=new RegExp('[\"'+t+\"\\n\\r]\"),n=t.charCodeAt(0);function r(t,e){var r,i=[],o=t.length,a=0,s=0,u=o<=0,l=!1;function c(){if(u)return Rt;if(l)return l=!1,Ot;var e,r,i=a;if(t.charCodeAt(i)===Lt){for(;a++<o&&t.charCodeAt(a)!==Lt||t.charCodeAt(++a)===Lt;);return(e=a)>=o?u=!0:(r=t.charCodeAt(a++))===Ut?l=!0:r===qt&&(l=!0,t.charCodeAt(a)===Ut&&++a),t.slice(i+1,e-1).replace(/\"\"/g,'\"')}for(;a<o;){if((r=t.charCodeAt(e=a++))===Ut)l=!0;else if(r===qt)l=!0,t.charCodeAt(a)===Ut&&++a;else if(r!==n)continue;return t.slice(i,e)}return u=!0,t.slice(i,o)}for(t.charCodeAt(o-1)===Ut&&--o,t.charCodeAt(o-1)===qt&&--o;(r=c())!==Rt;){for(var f=[];r!==Ot&&r!==Rt;)f.push(r),r=c();e&&null==(f=e(f,s++))||i.push(f)}return i}function i(e,n){return e.map((function(e){return n.map((function(t){return a(e[t])})).join(t)}))}function o(e){return e.map(a).join(t)}function a(t){return null==t?\"\":t instanceof Date?Wt(t):e.test(t+=\"\")?'\"'+t.replace(/\"/g,'\"\"')+'\"':t}return{parse:function(t,e){var n,i,o=r(t,(function(t,r){if(n)return n(t,r-1);i=t,n=e?function(t,e){var n=Pt(t);return function(r,i){return e(n(r),i,t)}}(t,e):Pt(t)}));return o.columns=i||[],o},parseRows:r,format:function(e,n){return null==n&&(n=jt(e)),[n.map(a).join(t)].concat(i(e,n)).join(\"\\n\")},formatBody:function(t,e){return null==e&&(e=jt(t)),i(t,e).join(\"\\n\")},formatRows:function(t){return t.map(o).join(\"\\n\")},formatRow:o,formatValue:a}}function Yt(t){return t}function Gt(t,e){return\"string\"==typeof e&&(e=t.objects[e]),\"GeometryCollection\"===e.type?{type:\"FeatureCollection\",features:e.geometries.map((function(e){return Vt(t,e)}))}:Vt(t,e)}function Vt(t,e){var n=e.id,r=e.bbox,i=null==e.properties?{}:e.properties,o=Xt(t,e);return null==n&&null==r?{type:\"Feature\",properties:i,geometry:o}:null==r?{type:\"Feature\",id:n,properties:i,geometry:o}:{type:\"Feature\",id:n,bbox:r,properties:i,geometry:o}}function Xt(t,e){var n=function(t){if(null==t)return Yt;var e,n,r=t.scale[0],i=t.scale[1],o=t.translate[0],a=t.translate[1];return function(t,s){s||(e=n=0);var u=2,l=t.length,c=new Array(l);for(c[0]=(e+=t[0])*r+o,c[1]=(n+=t[1])*i+a;u<l;)c[u]=t[u],++u;return c}}(t.transform),r=t.arcs;function i(t,e){e.length&&e.pop();for(var i=r[t<0?~t:t],o=0,a=i.length;o<a;++o)e.push(n(i[o],o));t<0&&function(t,e){for(var n,r=t.length,i=r-e;i<--r;)n=t[i],t[i++]=t[r],t[r]=n}(e,a)}function o(t){return n(t)}function a(t){for(var e=[],n=0,r=t.length;n<r;++n)i(t[n],e);return e.length<2&&e.push(e[0]),e}function s(t){for(var e=a(t);e.length<4;)e.push(e[0]);return e}function u(t){return t.map(s)}return function t(e){var n,r=e.type;switch(r){case\"GeometryCollection\":return{type:r,geometries:e.geometries.map(t)};case\"Point\":n=o(e.coordinates);break;case\"MultiPoint\":n=e.coordinates.map(o);break;case\"LineString\":n=a(e.arcs);break;case\"MultiLineString\":n=e.arcs.map(a);break;case\"Polygon\":n=u(e.arcs);break;case\"MultiPolygon\":n=e.arcs.map(u);break;default:return null}return{type:r,coordinates:n}}(e)}function Jt(t,e){var n={},r={},i={},o=[],a=-1;function s(t,e){for(var r in t){var i=t[r];delete e[i.start],delete i.start,delete i.end,i.forEach((function(t){n[t<0?~t:t]=1})),o.push(i)}}return e.forEach((function(n,r){var i,o"
-  , "=t.arcs[n<0?~n:n];o.length<3&&!o[1][0]&&!o[1][1]&&(i=e[++a],e[a]=n,e[r]=i)})),e.forEach((function(e){var n,o,a=function(e){var n,r=t.arcs[e<0?~e:e],i=r[0];t.transform?(n=[0,0],r.forEach((function(t){n[0]+=t[0],n[1]+=t[1]}))):n=r[r.length-1];return e<0?[n,i]:[i,n]}(e),s=a[0],u=a[1];if(n=i[s])if(delete i[n.end],n.push(e),n.end=u,o=r[u]){delete r[o.start];var l=o===n?n:n.concat(o);r[l.start=n.start]=i[l.end=o.end]=l}else r[n.start]=i[n.end]=n;else if(n=r[u])if(delete r[n.start],n.unshift(e),n.start=s,o=i[s]){delete i[o.end];var c=o===n?n:o.concat(n);r[c.start=o.start]=i[c.end=n.end]=c}else r[n.start]=i[n.end]=n;else r[(n=[e]).start=s]=i[n.end=u]=n})),s(i,r),s(r,i),e.forEach((function(t){n[t<0?~t:t]||o.push([t])})),o}function Zt(t){return Xt(t,Qt.apply(this,arguments))}function Qt(t,e,n){var r,i,o;if(arguments.length>1)r=function(t,e,n){var r,i=[],o=[];function a(t){var e=t<0?~t:t;(o[e]||(o[e]=[])).push({i:t,g:r})}function s(t){t.forEach(a)}function u(t){t.forEach(s)}function l(t){t.forEach(u)}function c(t){switch(r=t,t.type){case\"GeometryCollection\":t.geometries.forEach(c);break;case\"LineString\":s(t.arcs);break;case\"MultiLineString\":case\"Polygon\":u(t.arcs);break;case\"MultiPolygon\":l(t.arcs)}}return c(e),o.forEach(null==n?function(t){i.push(t[0].i)}:function(t){n(t[0].g,t[t.length-1].g)&&i.push(t[0].i)}),i}(0,e,n);else for(i=0,r=new Array(o=t.arcs.length);i<o;++i)r[i]=i;return{type:\"MultiLineString\",arcs:Jt(t,r)}}function Kt(t,e){return null==t||null==e?NaN:t<e?-1:t>e?1:t>=e?0:NaN}function te(t,e){return null==t||null==e?NaN:e<t?-1:e>t?1:e>=t?0:NaN}function ee(t){let e,n,r;function i(t,r){let i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:t.length;if(i<o){if(0!==e(r,r))return o;do{const e=i+o>>>1;n(t[e],r)<0?i=e+1:o=e}while(i<o)}return i}return 2!==t.length?(e=Kt,n=(e,n)=>Kt(t(e),n),r=(e,n)=>t(e)-n):(e=t===Kt||t===te?t:ne,n=t,r=t),{left:i,center:function(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;const o=i(t,e,n,(arguments.length>3&&void 0!==arguments[3]?arguments[3]:t.length)-1);return o>n&&r(t[o-1],e)>-r(t[o],e)?o-1:o},right:function(t,r){let i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:t.length;if(i<o){if(0!==e(r,r))return o;do{const e=i+o>>>1;n(t[e],r)<=0?i=e+1:o=e}while(i<o)}return i}}}function ne(){return 0}function re(t){return null===t?NaN:+t}const ie=ee(Kt),oe=ie.right,ae=ie.left;ee(re).center;class se{constructor(){this._partials=new Float64Array(32),this._n=0}add(t){const e=this._partials;let n=0;for(let r=0;r<this._n&&r<32;r++){const i=e[r],o=t+i,a=Math.abs(t)<Math.abs(i)?t-(o-i):i-(o-t);a&&(e[n++]=a),t=o}return e[n]=t,this._n=n+1,this}valueOf(){const t=this._partials;let e,n,r,i=this._n,o=0;if(i>0){for(o=t[--i];i>0&&(e=o,n=t[--i],o=e+n,r=n-(o-e),!r););i>0&&(r<0&&t[i-1]<0||r>0&&t[i-1]>0)&&(n=2*r,e=o+n,n==e-o&&(o=e))}return o}}class ue extends Map{constructor(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:de;if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:e}}),null!=t)for(const[e,n]of t)this.set(e,n)}get(t){return super.get(ce(this,t))}has(t){return super.has(ce(this,t))}set(t,e){return super.set(fe(this,t),e)}delete(t){return super.delete(he(this,t))}}class le extends Set{constructor(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:de;if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:e}}),null!=t)for(const e of t)this.add(e)}has(t){return super.has(ce(this,t))}add(t){return super.add(fe(this,t))}delete(t){return super.delete(he(this,t))}}function ce(t,e){let{_intern:n,_key:r}=t;const i=r(e);return n.has(i)?n.get(i):e}function fe(t,e){let{_intern:n,_key:r}=t;const i=r(e);return n.has(i)?n.get(i):(n.set(i,e),e)}function he(t,e){let{_intern:n,_key:r}=t;const i=r(e);return n.has(i)&&(e=n.get(i),n.delete(i)),e}function de(t){return null!==t&&\"object\"==typeof t?t.valueOf():t}function pe(t,e){return(null==t||!(t>=t))-(null==e||!(e>=e))||(t<e?-1:t>e?1:0)}const ge=Math.sqrt"
-  , "(50),me=Math.sqrt(10),ye=Math.sqrt(2);function ve(t,e,n){const r=(e-t)/Math.max(0,n),i=Math.floor(Math.log10(r)),o=r/Math.pow(10,i),a=o>=ge?10:o>=me?5:o>=ye?2:1;let s,u,l;return i<0?(l=Math.pow(10,-i)/a,s=Math.round(t*l),u=Math.round(e*l),s/l<t&&++s,u/l>e&&--u,l=-l):(l=Math.pow(10,i)*a,s=Math.round(t/l),u=Math.round(e/l),s*l<t&&++s,u*l>e&&--u),u<s&&.5<=n&&n<2?ve(t,e,2*n):[s,u,l]}function _e(t,e,n){if(!((n=+n)>0))return[];if((t=+t)===(e=+e))return[t];const r=e<t,[i,o,a]=r?ve(e,t,n):ve(t,e,n);if(!(o>=i))return[];const s=o-i+1,u=new Array(s);if(r)if(a<0)for(let t=0;t<s;++t)u[t]=(o-t)/-a;else for(let t=0;t<s;++t)u[t]=(o-t)*a;else if(a<0)for(let t=0;t<s;++t)u[t]=(i+t)/-a;else for(let t=0;t<s;++t)u[t]=(i+t)*a;return u}function xe(t,e,n){return ve(t=+t,e=+e,n=+n)[2]}function be(t,e,n){n=+n;const r=(e=+e)<(t=+t),i=r?xe(e,t,n):xe(t,e,n);return(r?-1:1)*(i<0?1/-i:i)}function we(t,e){let n;if(void 0===e)for(const e of t)null!=e&&(n<e||void 0===n&&e>=e)&&(n=e);else{let r=-1;for(let i of t)null!=(i=e(i,++r,t))&&(n<i||void 0===n&&i>=i)&&(n=i)}return n}function ke(t,e){let n;if(void 0===e)for(const e of t)null!=e&&(n>e||void 0===n&&e>=e)&&(n=e);else{let r=-1;for(let i of t)null!=(i=e(i,++r,t))&&(n>i||void 0===n&&i>=i)&&(n=i)}return n}function Ae(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:1/0,i=arguments.length>4?arguments[4]:void 0;if(e=Math.floor(e),n=Math.floor(Math.max(0,n)),r=Math.floor(Math.min(t.length-1,r)),!(n<=e&&e<=r))return t;for(i=void 0===i?pe:function(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:Kt;if(t===Kt)return pe;if(\"function\"!=typeof t)throw new TypeError(\"compare is not a function\");return(e,n)=>{const r=t(e,n);return r||0===r?r:(0===t(n,n))-(0===t(e,e))}}(i);r>n;){if(r-n>600){const o=r-n+1,a=e-n+1,s=Math.log(o),u=.5*Math.exp(2*s/3),l=.5*Math.sqrt(s*u*(o-u)/o)*(a-o/2<0?-1:1);Ae(t,e,Math.max(n,Math.floor(e-a*u/o+l)),Math.min(r,Math.floor(e+(o-a)*u/o+l)),i)}const o=t[e];let a=n,s=r;for(Me(t,n,e),i(t[r],o)>0&&Me(t,n,r);a<s;){for(Me(t,a,s),++a,--s;i(t[a],o)<0;)++a;for(;i(t[s],o)>0;)--s}0===i(t[n],o)?Me(t,n,s):(++s,Me(t,s,r)),s<=e&&(n=s+1),e<=s&&(r=s-1)}return t}function Me(t,e,n){const r=t[e];t[e]=t[n],t[n]=r}function Ee(t,e,n){if(t=Float64Array.from(function*(t,e){if(void 0===e)for(let e of t)null!=e&&(e=+e)>=e&&(yield e);else{let n=-1;for(let r of t)null!=(r=e(r,++n,t))&&(r=+r)>=r&&(yield r)}}(t,n)),(r=t.length)&&!isNaN(e=+e)){if(e<=0||r<2)return ke(t);if(e>=1)return we(t);var r,i=(r-1)*e,o=Math.floor(i),a=we(Ae(t,o).subarray(0,o+1));return a+(ke(t.subarray(o+1))-a)*(i-o)}}function De(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:re;if((r=t.length)&&!isNaN(e=+e)){if(e<=0||r<2)return+n(t[0],0,t);if(e>=1)return+n(t[r-1],r-1,t);var r,i=(r-1)*e,o=Math.floor(i),a=+n(t[o],o,t);return a+(+n(t[o+1],o+1,t)-a)*(i-o)}}function Ce(t,e){return Ee(t,.5,e)}function Fe(t){return Array.from(function*(t){for(const e of t)yield*e}(t))}function Se(t,e,n){t=+t,e=+e,n=(i=arguments.length)<2?(e=t,t=0,1):i<3?1:+n;for(var r=-1,i=0|Math.max(0,Math.ceil((e-t)/n)),o=new Array(i);++r<i;)o[r]=t+r*n;return o}function $e(t,e){let n=0;for(let e of t)(e=+e)&&(n+=e);return n}function Te(t){return t instanceof le?t:new le(t)}function Be(t,e){if((n=(t=e?t.toExponential(e-1):t.toExponential()).indexOf(\"e\"))<0)return null;var n,r=t.slice(0,n);return[r.length>1?r[0]+r.slice(2):r,+t.slice(n+1)]}function Ne(t){return(t=Be(Math.abs(t)))?t[1]:NaN}var ze,Oe=/^(?:(.)?([<>=^]))?([+\\-( ])?([$#])?(0)?(\\d+)?(,)?(\\.\\d+)?(~)?([a-z%])?$/i;function Re(t){if(!(e=Oe.exec(t)))throw new Error(\"invalid format: \"+t);var e;return new Le({fill:e[1],align:e[2],sign:e[3],symbol:e[4],zero:e[5],width:e[6],comma:e[7],precision:e[8]&&e[8].slice(1),trim:e[9],type:e[10]})}function Le(t){this.fill=void 0===t.fill?\" \":t.fill+\"\",this.align=void 0===t.align?\">\":t.align+\"\",this.sign=void 0===t.sign?\"-\":t.sign+\"\",this.symbol=void 0===t.symbol?\"\":t.symbol+\"\",this.zero=!!t.zero,this.width=void 0===t.width?void 0:+t.width,this.comma=!!t.comma,this.precision=void 0===t.precision"
-  , "?void 0:+t.precision,this.trim=!!t.trim,this.type=void 0===t.type?\"\":t.type+\"\"}function Ue(t,e){var n=Be(t,e);if(!n)return t+\"\";var r=n[0],i=n[1];return i<0?\"0.\"+new Array(-i).join(\"0\")+r:r.length>i+1?r.slice(0,i+1)+\".\"+r.slice(i+1):r+new Array(i-r.length+2).join(\"0\")}Re.prototype=Le.prototype,Le.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?\"0\":\"\")+(void 0===this.width?\"\":Math.max(1,0|this.width))+(this.comma?\",\":\"\")+(void 0===this.precision?\"\":\".\"+Math.max(0,0|this.precision))+(this.trim?\"~\":\"\")+this.type};var qe={\"%\":(t,e)=>(100*t).toFixed(e),b:t=>Math.round(t).toString(2),c:t=>t+\"\",d:function(t){return Math.abs(t=Math.round(t))>=1e21?t.toLocaleString(\"en\").replace(/,/g,\"\"):t.toString(10)},e:(t,e)=>t.toExponential(e),f:(t,e)=>t.toFixed(e),g:(t,e)=>t.toPrecision(e),o:t=>Math.round(t).toString(8),p:(t,e)=>Ue(100*t,e),r:Ue,s:function(t,e){var n=Be(t,e);if(!n)return t+\"\";var r=n[0],i=n[1],o=i-(ze=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,a=r.length;return o===a?r:o>a?r+new Array(o-a+1).join(\"0\"):o>0?r.slice(0,o)+\".\"+r.slice(o):\"0.\"+new Array(1-o).join(\"0\")+Be(t,Math.max(0,e+o-1))[0]},X:t=>Math.round(t).toString(16).toUpperCase(),x:t=>Math.round(t).toString(16)};function Pe(t){return t}var je,Ie,We,He=Array.prototype.map,Ye=[\"y\",\"z\",\"a\",\"f\",\"p\",\"n\",\"µ\",\"m\",\"\",\"k\",\"M\",\"G\",\"T\",\"P\",\"E\",\"Z\",\"Y\"];function Ge(t){var e,n,r=void 0===t.grouping||void 0===t.thousands?Pe:(e=He.call(t.grouping,Number),n=t.thousands+\"\",function(t,r){for(var i=t.length,o=[],a=0,s=e[0],u=0;i>0&&s>0&&(u+s+1>r&&(s=Math.max(1,r-u)),o.push(t.substring(i-=s,i+s)),!((u+=s+1)>r));)s=e[a=(a+1)%e.length];return o.reverse().join(n)}),i=void 0===t.currency?\"\":t.currency[0]+\"\",o=void 0===t.currency?\"\":t.currency[1]+\"\",a=void 0===t.decimal?\".\":t.decimal+\"\",s=void 0===t.numerals?Pe:function(t){return function(e){return e.replace(/[0-9]/g,(function(e){return t[+e]}))}}(He.call(t.numerals,String)),u=void 0===t.percent?\"%\":t.percent+\"\",l=void 0===t.minus?\"−\":t.minus+\"\",c=void 0===t.nan?\"NaN\":t.nan+\"\";function f(t){var e=(t=Re(t)).fill,n=t.align,f=t.sign,h=t.symbol,d=t.zero,p=t.width,g=t.comma,m=t.precision,y=t.trim,v=t.type;\"n\"===v?(g=!0,v=\"g\"):qe[v]||(void 0===m&&(m=12),y=!0,v=\"g\"),(d||\"0\"===e&&\"=\"===n)&&(d=!0,e=\"0\",n=\"=\");var _=\"$\"===h?i:\"#\"===h&&/[boxX]/.test(v)?\"0\"+v.toLowerCase():\"\",x=\"$\"===h?o:/[%p]/.test(v)?u:\"\",b=qe[v],w=/[defgprs%]/.test(v);function k(t){var i,o,u,h=_,k=x;if(\"c\"===v)k=b(t)+k,t=\"\";else{var A=(t=+t)<0||1/t<0;if(t=isNaN(t)?c:b(Math.abs(t),m),y&&(t=function(t){t:for(var e,n=t.length,r=1,i=-1;r<n;++r)switch(t[r]){case\".\":i=e=r;break;case\"0\":0===i&&(i=r),e=r;break;default:if(!+t[r])break t;i>0&&(i=0)}return i>0?t.slice(0,i)+t.slice(e+1):t}(t)),A&&0==+t&&\"+\"!==f&&(A=!1),h=(A?\"(\"===f?f:l:\"-\"===f||\"(\"===f?\"\":f)+h,k=(\"s\"===v?Ye[8+ze/3]:\"\")+k+(A&&\"(\"===f?\")\":\"\"),w)for(i=-1,o=t.length;++i<o;)if(48>(u=t.charCodeAt(i))||u>57){k=(46===u?a+t.slice(i+1):t.slice(i))+k,t=t.slice(0,i);break}}g&&!d&&(t=r(t,1/0));var M=h.length+t.length+k.length,E=M<p?new Array(p-M+1).join(e):\"\";switch(g&&d&&(t=r(E+t,E.length?p-k.length:1/0),E=\"\"),n){case\"<\":t=h+t+k+E;break;case\"=\":t=h+E+t+k;break;case\"^\":t=E.slice(0,M=E.length>>1)+h+t+k+E.slice(M);break;default:t=E+h+t+k}return s(t)}return m=void 0===m?6:/[gprs]/.test(v)?Math.max(1,Math.min(21,m)):Math.max(0,Math.min(20,m)),k.toString=function(){return t+\"\"},k}return{format:f,formatPrefix:function(t,e){var n=f(((t=Re(t)).type=\"f\",t)),r=3*Math.max(-8,Math.min(8,Math.floor(Ne(e)/3))),i=Math.pow(10,-r),o=Ye[8+r/3];return function(t){return n(i*t)+o}}}}function Ve(t){return Math.max(0,-Ne(Math.abs(t)))}function Xe(t,e){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(Ne(e)/3)))-Ne(Math.abs(t)))}function Je(t,e){return t=Math.abs(t),e=Math.abs(e)-t,Math.max(0,Ne(e)-Ne(t))+1}!function(t){je=Ge(t),Ie=je.format,We=je.formatPrefix}({thousands:\",\",grouping:[3],currency:[\"$\",\"\"]});const Ze=new Date,Qe=new Date;function Ke(t,e,n,r){function i(e){return t(e=0===arguments.length?new Date:new Date(+e)),e}return i.floor=e=>(t(e=new Date(+e)),e),i.ceil=n=>(t(n=new Date(n-1)),e(n,1),t(n),n),i.roun"
-  , "d=t=>{const e=i(t),n=i.ceil(t);return t-e<n-t?e:n},i.offset=(t,n)=>(e(t=new Date(+t),null==n?1:Math.floor(n)),t),i.range=(n,r,o)=>{const a=[];if(n=i.ceil(n),o=null==o?1:Math.floor(o),!(n<r&&o>0))return a;let s;do{a.push(s=new Date(+n)),e(n,o),t(n)}while(s<n&&n<r);return a},i.filter=n=>Ke((e=>{if(e>=e)for(;t(e),!n(e);)e.setTime(e-1)}),((t,r)=>{if(t>=t)if(r<0)for(;++r<=0;)for(;e(t,-1),!n(t););else for(;--r>=0;)for(;e(t,1),!n(t););})),n&&(i.count=(e,r)=>(Ze.setTime(+e),Qe.setTime(+r),t(Ze),t(Qe),Math.floor(n(Ze,Qe))),i.every=t=>(t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?e=>r(e)%t==0:e=>i.count(0,e)%t==0):i:null)),i}const tn=Ke((()=>{}),((t,e)=>{t.setTime(+t+e)}),((t,e)=>e-t));tn.every=t=>(t=Math.floor(t),isFinite(t)&&t>0?t>1?Ke((e=>{e.setTime(Math.floor(e/t)*t)}),((e,n)=>{e.setTime(+e+n*t)}),((e,n)=>(n-e)/t)):tn:null),tn.range;const en=1e3,nn=6e4,rn=36e5,on=864e5,an=6048e5,sn=2592e6,un=31536e6,ln=Ke((t=>{t.setTime(t-t.getMilliseconds())}),((t,e)=>{t.setTime(+t+e*en)}),((t,e)=>(e-t)/en),(t=>t.getUTCSeconds()));ln.range;const cn=Ke((t=>{t.setTime(t-t.getMilliseconds()-t.getSeconds()*en)}),((t,e)=>{t.setTime(+t+e*nn)}),((t,e)=>(e-t)/nn),(t=>t.getMinutes()));cn.range;const fn=Ke((t=>{t.setUTCSeconds(0,0)}),((t,e)=>{t.setTime(+t+e*nn)}),((t,e)=>(e-t)/nn),(t=>t.getUTCMinutes()));fn.range;const hn=Ke((t=>{t.setTime(t-t.getMilliseconds()-t.getSeconds()*en-t.getMinutes()*nn)}),((t,e)=>{t.setTime(+t+e*rn)}),((t,e)=>(e-t)/rn),(t=>t.getHours()));hn.range;const dn=Ke((t=>{t.setUTCMinutes(0,0,0)}),((t,e)=>{t.setTime(+t+e*rn)}),((t,e)=>(e-t)/rn),(t=>t.getUTCHours()));dn.range;const pn=Ke((t=>t.setHours(0,0,0,0)),((t,e)=>t.setDate(t.getDate()+e)),((t,e)=>(e-t-(e.getTimezoneOffset()-t.getTimezoneOffset())*nn)/on),(t=>t.getDate()-1));pn.range;const gn=Ke((t=>{t.setUTCHours(0,0,0,0)}),((t,e)=>{t.setUTCDate(t.getUTCDate()+e)}),((t,e)=>(e-t)/on),(t=>t.getUTCDate()-1));gn.range;const mn=Ke((t=>{t.setUTCHours(0,0,0,0)}),((t,e)=>{t.setUTCDate(t.getUTCDate()+e)}),((t,e)=>(e-t)/on),(t=>Math.floor(t/on)));function yn(t){return Ke((e=>{e.setDate(e.getDate()-(e.getDay()+7-t)%7),e.setHours(0,0,0,0)}),((t,e)=>{t.setDate(t.getDate()+7*e)}),((t,e)=>(e-t-(e.getTimezoneOffset()-t.getTimezoneOffset())*nn)/an))}mn.range;const vn=yn(0),_n=yn(1),xn=yn(2),bn=yn(3),wn=yn(4),kn=yn(5),An=yn(6);function Mn(t){return Ke((e=>{e.setUTCDate(e.getUTCDate()-(e.getUTCDay()+7-t)%7),e.setUTCHours(0,0,0,0)}),((t,e)=>{t.setUTCDate(t.getUTCDate()+7*e)}),((t,e)=>(e-t)/an))}vn.range,_n.range,xn.range,bn.range,wn.range,kn.range,An.range;const En=Mn(0),Dn=Mn(1),Cn=Mn(2),Fn=Mn(3),Sn=Mn(4),$n=Mn(5),Tn=Mn(6);En.range,Dn.range,Cn.range,Fn.range,Sn.range,$n.range,Tn.range;const Bn=Ke((t=>{t.setDate(1),t.setHours(0,0,0,0)}),((t,e)=>{t.setMonth(t.getMonth()+e)}),((t,e)=>e.getMonth()-t.getMonth()+12*(e.getFullYear()-t.getFullYear())),(t=>t.getMonth()));Bn.range;const Nn=Ke((t=>{t.setUTCDate(1),t.setUTCHours(0,0,0,0)}),((t,e)=>{t.setUTCMonth(t.getUTCMonth()+e)}),((t,e)=>e.getUTCMonth()-t.getUTCMonth()+12*(e.getUTCFullYear()-t.getUTCFullYear())),(t=>t.getUTCMonth()));Nn.range;const zn=Ke((t=>{t.setMonth(0,1),t.setHours(0,0,0,0)}),((t,e)=>{t.setFullYear(t.getFullYear()+e)}),((t,e)=>e.getFullYear()-t.getFullYear()),(t=>t.getFullYear()));zn.every=t=>isFinite(t=Math.floor(t))&&t>0?Ke((e=>{e.setFullYear(Math.floor(e.getFullYear()/t)*t),e.setMonth(0,1),e.setHours(0,0,0,0)}),((e,n)=>{e.setFullYear(e.getFullYear()+n*t)})):null,zn.range;const On=Ke((t=>{t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)}),((t,e)=>{t.setUTCFullYear(t.getUTCFullYear()+e)}),((t,e)=>e.getUTCFullYear()-t.getUTCFullYear()),(t=>t.getUTCFullYear()));function Rn(t,e,n,r,i,o){const a=[[ln,1,en],[ln,5,5e3],[ln,15,15e3],[ln,30,3e4],[o,1,nn],[o,5,3e5],[o,15,9e5],[o,30,18e5],[i,1,rn],[i,3,108e5],[i,6,216e5],[i,12,432e5],[r,1,on],[r,2,1728e5],[n,1,an],[e,1,sn],[e,3,7776e6],[t,1,un]];function s(e,n,r){const i=Math.abs(n-e)/r,o=ee((t=>{let[,,e]=t;return e})).right(a,i);if(o===a.length)return t.every(be(e/un,n/un,r));if(0===o)return tn.every(Math.max(be(e,n,r),1));const[s,u]=a[i/a[o-1][2]<a[o][2]/i?o-1:o];return s.every(u)}return[function"
-  , "(t,e,n){const r=e<t;r&&([t,e]=[e,t]);const i=n&&\"function\"==typeof n.range?n:s(t,e,n),o=i?i.range(t,+e+1):[];return r?o.reverse():o},s]}On.every=t=>isFinite(t=Math.floor(t))&&t>0?Ke((e=>{e.setUTCFullYear(Math.floor(e.getUTCFullYear()/t)*t),e.setUTCMonth(0,1),e.setUTCHours(0,0,0,0)}),((e,n)=>{e.setUTCFullYear(e.getUTCFullYear()+n*t)})):null,On.range;const[Ln,Un]=Rn(On,Nn,En,mn,dn,fn),[qn,Pn]=Rn(zn,Bn,vn,pn,hn,cn),jn=\"year\",In=\"quarter\",Wn=\"month\",Hn=\"week\",Yn=\"date\",Gn=\"day\",Vn=\"dayofyear\",Xn=\"hours\",Jn=\"minutes\",Zn=\"seconds\",Qn=\"milliseconds\",Kn=[jn,In,Wn,Hn,Yn,Gn,Vn,Xn,Jn,Zn,Qn],tr=Kn.reduce(((t,e,n)=>(t[e]=1+n,t)),{});function er(t){const e=X(t).slice(),n={};e.length||s(\"Missing time unit.\"),e.forEach((t=>{lt(tr,t)?n[t]=1:s(`Invalid time unit: ${t}.`)}));return(n[Hn]||n[Gn]?1:0)+(n[In]||n[Wn]||n[Yn]?1:0)+(n[Vn]?1:0)>1&&s(`Incompatible time units: ${t}`),e.sort(((t,e)=>tr[t]-tr[e])),e}const nr={[jn]:\"%Y \",[In]:\"Q%q \",[Wn]:\"%b \",[Yn]:\"%d \",[Hn]:\"W%U \",[Gn]:\"%a \",[Vn]:\"%j \",[Xn]:\"%H:00\",[Jn]:\"00:%M\",[Zn]:\":%S\",[Qn]:\".%L\",[`${jn}-${Wn}`]:\"%Y-%m \",[`${jn}-${Wn}-${Yn}`]:\"%Y-%m-%d \",[`${Xn}-${Jn}`]:\"%H:%M\"};function rr(t,e){const n=at({},nr,e),r=er(t),i=r.length;let o,a,s=\"\",u=0;for(u=0;u<i;)for(o=r.length;o>u;--o)if(a=r.slice(u,o).join(\"-\"),null!=n[a]){s+=n[a],u=o;break}return s.trim()}const ir=new Date;function or(t){return ir.setFullYear(t),ir.setMonth(0),ir.setDate(1),ir.setHours(0,0,0,0),ir}function ar(t){return ur(new Date(t))}function sr(t){return lr(new Date(t))}function ur(t){return pn.count(or(t.getFullYear())-1,t)}function lr(t){return vn.count(or(t.getFullYear())-1,t)}function cr(t){return or(t).getDay()}function fr(t,e,n,r,i,o,a){if(0<=t&&t<100){const s=new Date(-1,e,n,r,i,o,a);return s.setFullYear(t),s}return new Date(t,e,n,r,i,o,a)}function hr(t){return pr(new Date(t))}function dr(t){return gr(new Date(t))}function pr(t){const e=Date.UTC(t.getUTCFullYear(),0,1);return gn.count(e-1,t)}function gr(t){const e=Date.UTC(t.getUTCFullYear(),0,1);return En.count(e-1,t)}function mr(t){return ir.setTime(Date.UTC(t,0,1)),ir.getUTCDay()}function yr(t,e,n,r,i,o,a){if(0<=t&&t<100){const t=new Date(Date.UTC(-1,e,n,r,i,o,a));return t.setUTCFullYear(n.y),t}return new Date(Date.UTC(t,e,n,r,i,o,a))}function vr(t,e,n,r,i){const o=e||1,a=S(t),s=(t,e,i)=>function(t,e,n,r){const i=n<=1?t:r?(e,i)=>r+n*Math.floor((t(e,i)-r)/n):(e,r)=>n*Math.floor(t(e,r)/n);return e?(t,n)=>e(i(t,n),n):i}(n[i=i||t],r[i],t===a&&o,e),u=new Date,l=Bt(t),c=l[jn]?s(jn):it(2012),f=l[Wn]?s(Wn):l[In]?s(In):h,p=l[Hn]&&l[Gn]?s(Gn,1,Hn+Gn):l[Hn]?s(Hn,1):l[Gn]?s(Gn,1):l[Yn]?s(Yn,1):l[Vn]?s(Vn,1):d,g=l[Xn]?s(Xn):h,m=l[Jn]?s(Jn):h,y=l[Zn]?s(Zn):h,v=l[Qn]?s(Qn):h;return function(t){u.setTime(+t);const e=c(u);return i(e,f(u),p(u,e),g(u),m(u),y(u),v(u))}}function _r(t,e,n){return e+7*t-(n+6)%7}const xr={[jn]:t=>t.getFullYear(),[In]:t=>Math.floor(t.getMonth()/3),[Wn]:t=>t.getMonth(),[Yn]:t=>t.getDate(),[Xn]:t=>t.getHours(),[Jn]:t=>t.getMinutes(),[Zn]:t=>t.getSeconds(),[Qn]:t=>t.getMilliseconds(),[Vn]:t=>ur(t),[Hn]:t=>lr(t),[Hn+Gn]:(t,e)=>_r(lr(t),t.getDay(),cr(e)),[Gn]:(t,e)=>_r(1,t.getDay(),cr(e))},br={[In]:t=>3*t,[Hn]:(t,e)=>_r(t,0,cr(e))};function wr(t,e){return vr(t,e||1,xr,br,fr)}const kr={[jn]:t=>t.getUTCFullYear(),[In]:t=>Math.floor(t.getUTCMonth()/3),[Wn]:t=>t.getUTCMonth(),[Yn]:t=>t.getUTCDate(),[Xn]:t=>t.getUTCHours(),[Jn]:t=>t.getUTCMinutes(),[Zn]:t=>t.getUTCSeconds(),[Qn]:t=>t.getUTCMilliseconds(),[Vn]:t=>pr(t),[Hn]:t=>gr(t),[Gn]:(t,e)=>_r(1,t.getUTCDay(),mr(e)),[Hn+Gn]:(t,e)=>_r(gr(t),t.getUTCDay(),mr(e))},Ar={[In]:t=>3*t,[Hn]:(t,e)=>_r(t,0,mr(e))};function Mr(t,e){return vr(t,e||1,kr,Ar,yr)}const Er={[jn]:zn,[In]:Bn.every(3),[Wn]:Bn,[Hn]:vn,[Yn]:pn,[Gn]:pn,[Vn]:pn,[Xn]:hn,[Jn]:cn,[Zn]:ln,[Qn]:tn},Dr={[jn]:On,[In]:Nn.every(3),[Wn]:Nn,[Hn]:En,[Yn]:gn,[Gn]:gn,[Vn]:gn,[Xn]:dn,[Jn]:fn,[Zn]:ln,[Qn]:tn};function Cr(t){return Er[t]}function Fr(t){return Dr[t]}function Sr(t,e,n){return t?t.offset(e,n):void 0}function $r(t,e,n){return Sr(Cr(t),e,n)}function Tr(t,e,n){return Sr(Fr(t),e,n)}function Br(t,e,n,r){return t?t.range(e,n,r):void 0}function Nr(t,e,n,r){return B"
-  , "r(Cr(t),e,n,r)}function zr(t,e,n,r){return Br(Fr(t),e,n,r)}const Or=1e3,Rr=6e4,Lr=36e5,Ur=864e5,qr=2592e6,Pr=31536e6,jr=[jn,Wn,Yn,Xn,Jn,Zn,Qn],Ir=jr.slice(0,-1),Wr=Ir.slice(0,-1),Hr=Wr.slice(0,-1),Yr=Hr.slice(0,-1),Gr=[jn,Wn],Vr=[jn],Xr=[[Ir,1,Or],[Ir,5,5e3],[Ir,15,15e3],[Ir,30,3e4],[Wr,1,Rr],[Wr,5,3e5],[Wr,15,9e5],[Wr,30,18e5],[Hr,1,Lr],[Hr,3,108e5],[Hr,6,216e5],[Hr,12,432e5],[Yr,1,Ur],[[jn,Hn],1,6048e5],[Gr,1,qr],[Gr,3,7776e6],[Vr,1,Pr]];function Jr(t){const e=t.extent,n=t.maxbins||40,r=Math.abs(Dt(e))/n;let i,o,a=ee((t=>t[2])).right(Xr,r);return a===Xr.length?(i=Vr,o=be(e[0]/Pr,e[1]/Pr,n)):a?(a=Xr[r/Xr[a-1][2]<Xr[a][2]/r?a-1:a],i=a[0],o=a[1]):(i=jr,o=Math.max(be(e[0],e[1],n),1)),{units:i,step:o}}function Zr(t){if(0<=t.y&&t.y<100){var e=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return e.setFullYear(t.y),e}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function Qr(t){if(0<=t.y&&t.y<100){var e=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return e.setUTCFullYear(t.y),e}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function Kr(t,e,n){return{y:t,m:e,d:n,H:0,M:0,S:0,L:0}}function ti(t){var e=t.dateTime,n=t.date,r=t.time,i=t.periods,o=t.days,a=t.shortDays,s=t.months,u=t.shortMonths,l=hi(i),c=di(i),f=hi(o),h=di(o),d=hi(a),p=di(a),g=hi(s),m=di(s),y=hi(u),v=di(u),_={a:function(t){return a[t.getDay()]},A:function(t){return o[t.getDay()]},b:function(t){return u[t.getMonth()]},B:function(t){return s[t.getMonth()]},c:null,d:Ni,e:Ni,f:Ui,g:Ji,G:Qi,H:zi,I:Oi,j:Ri,L:Li,m:qi,M:Pi,p:function(t){return i[+(t.getHours()>=12)]},q:function(t){return 1+~~(t.getMonth()/3)},Q:wo,s:ko,S:ji,u:Ii,U:Wi,V:Yi,w:Gi,W:Vi,x:null,X:null,y:Xi,Y:Zi,Z:Ki,\"%\":bo},x={a:function(t){return a[t.getUTCDay()]},A:function(t){return o[t.getUTCDay()]},b:function(t){return u[t.getUTCMonth()]},B:function(t){return s[t.getUTCMonth()]},c:null,d:to,e:to,f:oo,g:yo,G:_o,H:eo,I:no,j:ro,L:io,m:ao,M:so,p:function(t){return i[+(t.getUTCHours()>=12)]},q:function(t){return 1+~~(t.getUTCMonth()/3)},Q:wo,s:ko,S:uo,u:lo,U:co,V:ho,w:po,W:go,x:null,X:null,y:mo,Y:vo,Z:xo,\"%\":bo},b={a:function(t,e,n){var r=d.exec(e.slice(n));return r?(t.w=p.get(r[0].toLowerCase()),n+r[0].length):-1},A:function(t,e,n){var r=f.exec(e.slice(n));return r?(t.w=h.get(r[0].toLowerCase()),n+r[0].length):-1},b:function(t,e,n){var r=y.exec(e.slice(n));return r?(t.m=v.get(r[0].toLowerCase()),n+r[0].length):-1},B:function(t,e,n){var r=g.exec(e.slice(n));return r?(t.m=m.get(r[0].toLowerCase()),n+r[0].length):-1},c:function(t,n,r){return A(t,e,n,r)},d:Ai,e:Ai,f:Si,g:xi,G:_i,H:Ei,I:Ei,j:Mi,L:Fi,m:ki,M:Di,p:function(t,e,n){var r=l.exec(e.slice(n));return r?(t.p=c.get(r[0].toLowerCase()),n+r[0].length):-1},q:wi,Q:Ti,s:Bi,S:Ci,u:gi,U:mi,V:yi,w:pi,W:vi,x:function(t,e,r){return A(t,n,e,r)},X:function(t,e,n){return A(t,r,e,n)},y:xi,Y:_i,Z:bi,\"%\":$i};function w(t,e){return function(n){var r,i,o,a=[],s=-1,u=0,l=t.length;for(n instanceof Date||(n=new Date(+n));++s<l;)37===t.charCodeAt(s)&&(a.push(t.slice(u,s)),null!=(i=ai[r=t.charAt(++s)])?r=t.charAt(++s):i=\"e\"===r?\" \":\"0\",(o=e[r])&&(r=o(n,i)),a.push(r),u=s+1);return a.push(t.slice(u,s)),a.join(\"\")}}function k(t,e){return function(n){var r,i,o=Kr(1900,void 0,1);if(A(o,t,n+=\"\",0)!=n.length)return null;if(\"Q\"in o)return new Date(o.Q);if(\"s\"in o)return new Date(1e3*o.s+(\"L\"in o?o.L:0));if(e&&!(\"Z\"in o)&&(o.Z=0),\"p\"in o&&(o.H=o.H%12+12*o.p),void 0===o.m&&(o.m=\"q\"in o?o.q:0),\"V\"in o){if(o.V<1||o.V>53)return null;\"w\"in o||(o.w=1),\"Z\"in o?(i=(r=Qr(Kr(o.y,0,1))).getUTCDay(),r=i>4||0===i?Dn.ceil(r):Dn(r),r=gn.offset(r,7*(o.V-1)),o.y=r.getUTCFullYear(),o.m=r.getUTCMonth(),o.d=r.getUTCDate()+(o.w+6)%7):(i=(r=Zr(Kr(o.y,0,1))).getDay(),r=i>4||0===i?_n.ceil(r):_n(r),r=pn.offset(r,7*(o.V-1)),o.y=r.getFullYear(),o.m=r.getMonth(),o.d=r.getDate()+(o.w+6)%7)}else(\"W\"in o||\"U\"in o)&&(\"w\"in o||(o.w=\"u\"in o?o.u%7:\"W\"in o?1:0),i=\"Z\"in o?Qr(Kr(o.y,0,1)).getUTCDay():Zr(Kr(o.y,0,1)).getDay(),o.m=0,o.d=\"W\"in o?(o.w+6)%7+7*o.W-(i+5)%7:o.w+7*o.U-(i+6)%7);return\"Z\"in o?(o.H+=o.Z/100|0,o.M+=o.Z%100,Qr(o)):Zr(o)}}function A(t,e,n,r){for(var i,o,a=0,s=e.length,u=n.length;a<s;){if(r>=u)return-1;if(37===(i="
-  , "e.charCodeAt(a++))){if(i=e.charAt(a++),!(o=b[i in ai?e.charAt(a++):i])||(r=o(t,n,r))<0)return-1}else if(i!=n.charCodeAt(r++))return-1}return r}return _.x=w(n,_),_.X=w(r,_),_.c=w(e,_),x.x=w(n,x),x.X=w(r,x),x.c=w(e,x),{format:function(t){var e=w(t+=\"\",_);return e.toString=function(){return t},e},parse:function(t){var e=k(t+=\"\",!1);return e.toString=function(){return t},e},utcFormat:function(t){var e=w(t+=\"\",x);return e.toString=function(){return t},e},utcParse:function(t){var e=k(t+=\"\",!0);return e.toString=function(){return t},e}}}var ei,ni,ri,ii,oi,ai={\"-\":\"\",_:\" \",0:\"0\"},si=/^\\s*\\d+/,ui=/^%/,li=/[\\\\^$*+?|[\\]().{}]/g;function ci(t,e,n){var r=t<0?\"-\":\"\",i=(r?-t:t)+\"\",o=i.length;return r+(o<n?new Array(n-o+1).join(e)+i:i)}function fi(t){return t.replace(li,\"\\\\$&\")}function hi(t){return new RegExp(\"^(?:\"+t.map(fi).join(\"|\")+\")\",\"i\")}function di(t){return new Map(t.map(((t,e)=>[t.toLowerCase(),e])))}function pi(t,e,n){var r=si.exec(e.slice(n,n+1));return r?(t.w=+r[0],n+r[0].length):-1}function gi(t,e,n){var r=si.exec(e.slice(n,n+1));return r?(t.u=+r[0],n+r[0].length):-1}function mi(t,e,n){var r=si.exec(e.slice(n,n+2));return r?(t.U=+r[0],n+r[0].length):-1}function yi(t,e,n){var r=si.exec(e.slice(n,n+2));return r?(t.V=+r[0],n+r[0].length):-1}function vi(t,e,n){var r=si.exec(e.slice(n,n+2));return r?(t.W=+r[0],n+r[0].length):-1}function _i(t,e,n){var r=si.exec(e.slice(n,n+4));return r?(t.y=+r[0],n+r[0].length):-1}function xi(t,e,n){var r=si.exec(e.slice(n,n+2));return r?(t.y=+r[0]+(+r[0]>68?1900:2e3),n+r[0].length):-1}function bi(t,e,n){var r=/^(Z)|([+-]\\d\\d)(?::?(\\d\\d))?/.exec(e.slice(n,n+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||\"00\")),n+r[0].length):-1}function wi(t,e,n){var r=si.exec(e.slice(n,n+1));return r?(t.q=3*r[0]-3,n+r[0].length):-1}function ki(t,e,n){var r=si.exec(e.slice(n,n+2));return r?(t.m=r[0]-1,n+r[0].length):-1}function Ai(t,e,n){var r=si.exec(e.slice(n,n+2));return r?(t.d=+r[0],n+r[0].length):-1}function Mi(t,e,n){var r=si.exec(e.slice(n,n+3));return r?(t.m=0,t.d=+r[0],n+r[0].length):-1}function Ei(t,e,n){var r=si.exec(e.slice(n,n+2));return r?(t.H=+r[0],n+r[0].length):-1}function Di(t,e,n){var r=si.exec(e.slice(n,n+2));return r?(t.M=+r[0],n+r[0].length):-1}function Ci(t,e,n){var r=si.exec(e.slice(n,n+2));return r?(t.S=+r[0],n+r[0].length):-1}function Fi(t,e,n){var r=si.exec(e.slice(n,n+3));return r?(t.L=+r[0],n+r[0].length):-1}function Si(t,e,n){var r=si.exec(e.slice(n,n+6));return r?(t.L=Math.floor(r[0]/1e3),n+r[0].length):-1}function $i(t,e,n){var r=ui.exec(e.slice(n,n+1));return r?n+r[0].length:-1}function Ti(t,e,n){var r=si.exec(e.slice(n));return r?(t.Q=+r[0],n+r[0].length):-1}function Bi(t,e,n){var r=si.exec(e.slice(n));return r?(t.s=+r[0],n+r[0].length):-1}function Ni(t,e){return ci(t.getDate(),e,2)}function zi(t,e){return ci(t.getHours(),e,2)}function Oi(t,e){return ci(t.getHours()%12||12,e,2)}function Ri(t,e){return ci(1+pn.count(zn(t),t),e,3)}function Li(t,e){return ci(t.getMilliseconds(),e,3)}function Ui(t,e){return Li(t,e)+\"000\"}function qi(t,e){return ci(t.getMonth()+1,e,2)}function Pi(t,e){return ci(t.getMinutes(),e,2)}function ji(t,e){return ci(t.getSeconds(),e,2)}function Ii(t){var e=t.getDay();return 0===e?7:e}function Wi(t,e){return ci(vn.count(zn(t)-1,t),e,2)}function Hi(t){var e=t.getDay();return e>=4||0===e?wn(t):wn.ceil(t)}function Yi(t,e){return t=Hi(t),ci(wn.count(zn(t),t)+(4===zn(t).getDay()),e,2)}function Gi(t){return t.getDay()}function Vi(t,e){return ci(_n.count(zn(t)-1,t),e,2)}function Xi(t,e){return ci(t.getFullYear()%100,e,2)}function Ji(t,e){return ci((t=Hi(t)).getFullYear()%100,e,2)}function Zi(t,e){return ci(t.getFullYear()%1e4,e,4)}function Qi(t,e){var n=t.getDay();return ci((t=n>=4||0===n?wn(t):wn.ceil(t)).getFullYear()%1e4,e,4)}function Ki(t){var e=t.getTimezoneOffset();return(e>0?\"-\":(e*=-1,\"+\"))+ci(e/60|0,\"0\",2)+ci(e%60,\"0\",2)}function to(t,e){return ci(t.getUTCDate(),e,2)}function eo(t,e){return ci(t.getUTCHours(),e,2)}function no(t,e){return ci(t.getUTCHours()%12||12,e,2)}function ro(t,e){return ci(1+gn.count(On(t),t),e,3)}function io(t,e){return ci(t.getUTCMillisecond"
-  , "s(),e,3)}function oo(t,e){return io(t,e)+\"000\"}function ao(t,e){return ci(t.getUTCMonth()+1,e,2)}function so(t,e){return ci(t.getUTCMinutes(),e,2)}function uo(t,e){return ci(t.getUTCSeconds(),e,2)}function lo(t){var e=t.getUTCDay();return 0===e?7:e}function co(t,e){return ci(En.count(On(t)-1,t),e,2)}function fo(t){var e=t.getUTCDay();return e>=4||0===e?Sn(t):Sn.ceil(t)}function ho(t,e){return t=fo(t),ci(Sn.count(On(t),t)+(4===On(t).getUTCDay()),e,2)}function po(t){return t.getUTCDay()}function go(t,e){return ci(Dn.count(On(t)-1,t),e,2)}function mo(t,e){return ci(t.getUTCFullYear()%100,e,2)}function yo(t,e){return ci((t=fo(t)).getUTCFullYear()%100,e,2)}function vo(t,e){return ci(t.getUTCFullYear()%1e4,e,4)}function _o(t,e){var n=t.getUTCDay();return ci((t=n>=4||0===n?Sn(t):Sn.ceil(t)).getUTCFullYear()%1e4,e,4)}function xo(){return\"+0000\"}function bo(){return\"%\"}function wo(t){return+t}function ko(t){return Math.floor(+t/1e3)}function Ao(t){const e={};return n=>e[n]||(e[n]=t(n))}function Mo(t){const e=Ao(t.format),n=t.formatPrefix;return{format:e,formatPrefix:n,formatFloat(t){const n=Re(t||\",\");if(null==n.precision){switch(n.precision=12,n.type){case\"%\":n.precision-=2;break;case\"e\":n.precision-=1}return r=e(n),i=e(\".1f\")(1)[1],t=>{const e=r(t),n=e.indexOf(i);if(n<0)return e;let o=function(t,e){let n,r=t.lastIndexOf(\"e\");if(r>0)return r;for(r=t.length;--r>e;)if(n=t.charCodeAt(r),n>=48&&n<=57)return r+1}(e,n);const a=o<e.length?e.slice(o):\"\";for(;--o>n;)if(\"0\"!==e[o]){++o;break}return e.slice(0,o)+a}}return e(n);var r,i},formatSpan(t,r,i,o){o=Re(null==o?\",f\":o);const a=be(t,r,i),s=Math.max(Math.abs(t),Math.abs(r));let u;if(null==o.precision)switch(o.type){case\"s\":return isNaN(u=Xe(a,s))||(o.precision=u),n(o,s);case\"\":case\"e\":case\"g\":case\"p\":case\"r\":isNaN(u=Je(a,s))||(o.precision=u-(\"e\"===o.type));break;case\"f\":case\"%\":isNaN(u=Ve(a))||(o.precision=u-2*(\"%\"===o.type))}return e(o)}}}let Eo,Do;function Co(){return Eo=Mo({format:Ie,formatPrefix:We})}function Fo(t){return Mo(Ge(t))}function So(t){return arguments.length?Eo=Fo(t):Eo}function $o(t,e,n){M(n=n||{})||s(`Invalid time multi-format specifier: ${n}`);const r=e(Zn),i=e(Jn),o=e(Xn),a=e(Yn),u=e(Hn),l=e(Wn),c=e(In),f=e(jn),h=t(n[Qn]||\".%L\"),d=t(n[Zn]||\":%S\"),p=t(n[Jn]||\"%I:%M\"),g=t(n[Xn]||\"%I %p\"),m=t(n[Yn]||n[Gn]||\"%a %d\"),y=t(n[Hn]||\"%b %d\"),v=t(n[Wn]||\"%B\"),_=t(n[In]||\"%B\"),x=t(n[jn]||\"%Y\");return t=>(r(t)<t?h:i(t)<t?d:o(t)<t?p:a(t)<t?g:l(t)<t?u(t)<t?m:y:f(t)<t?c(t)<t?v:_:x)(t)}function To(t){const e=Ao(t.format),n=Ao(t.utcFormat);return{timeFormat:t=>xt(t)?e(t):$o(e,Cr,t),utcFormat:t=>xt(t)?n(t):$o(n,Fr,t),timeParse:Ao(t.parse),utcParse:Ao(t.utcParse)}}function Bo(){return Do=To({format:ni,parse:ri,utcFormat:ii,utcParse:oi})}function No(t){return To(ti(t))}function zo(t){return arguments.length?Do=No(t):Do}!function(t){ei=ti(t),ni=ei.format,ri=ei.parse,ii=ei.utcFormat,oi=ei.utcParse}({dateTime:\"%x, %X\",date:\"%-m/%-d/%Y\",time:\"%-I:%M:%S %p\",periods:[\"AM\",\"PM\"],days:[\"Sunday\",\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\",\"Saturday\"],shortDays:[\"Sun\",\"Mon\",\"Tue\",\"Wed\",\"Thu\",\"Fri\",\"Sat\"],months:[\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"],shortMonths:[\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"]}),Co(),Bo();const Oo=(t,e)=>at({},t,e);function Ro(t,e){const n=t?Fo(t):So(),r=e?No(e):zo();return Oo(n,r)}function Lo(t,e){const n=arguments.length;return n&&2!==n&&s(\"defaultLocale expects either zero or two arguments.\"),n?Oo(So(t),zo(e)):Oo(So(),zo())}const Uo=/^(data:|([A-Za-z]+:)?\\/\\/)/,qo=/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|file|data):|[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))/i,Po=/[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205f\\u3000]/g,jo=\"file://\";async function Io(t,e){const n=await this.sanitize(t,e),r=n.href;return n.localFile?this.file(r):this.http(r,e)}async function Wo(t,e){e=at({},this.options,e);const n=this.fileAccess,r={href:null};let i,o,a;const u=qo.test(t.replace(Po,\"\"));null!=t&&\"string\"==typeof t&&u||s(\"Sanitize failure, invalid URI: \"+Ct(t));const l=Uo"
-  , ".test(t);return(a=e.baseURL)&&!l&&(t.startsWith(\"/\")||a.endsWith(\"/\")||(t=\"/\"+t),t=a+t),o=(i=t.startsWith(jo))||\"file\"===e.mode||\"http\"!==e.mode&&!l&&n,i?t=t.slice(jo.length):t.startsWith(\"//\")&&(\"file\"===e.defaultProtocol?(t=t.slice(2),o=!0):t=(e.defaultProtocol||\"http\")+\":\"+t),Object.defineProperty(r,\"localFile\",{value:!!o}),r.href=t,e.target&&(r.target=e.target+\"\"),e.rel&&(r.rel=e.rel+\"\"),\"image\"===e.context&&e.crossOrigin&&(r.crossOrigin=e.crossOrigin+\"\"),r}function Ho(t){return Yo}async function Yo(){s(\"No file system access.\")}function Go(t){return t?async function(e,n){const r=at({},this.options.http,n),i=n&&n.response,o=await t(e,r);return o.ok?Z(o[i])?o[i]():o.text():s(o.status+\"\"+o.statusText)}:Vo}async function Vo(){s(\"No HTTP fetch method available.\")}const Xo=t=>null!=t&&t==t,Jo=t=>!(Number.isNaN(+t)||t instanceof Date),Zo={boolean:Ft,integer:$,number:$,date:$t,string:Tt,unknown:f},Qo=[t=>\"true\"===t||\"false\"===t||!0===t||!1===t,t=>Jo(t)&&Number.isInteger(+t),Jo,t=>!Number.isNaN(Date.parse(t))],Ko=[\"boolean\",\"integer\",\"number\",\"date\"];function ta(t,e){if(!t||!t.length)return\"unknown\";const n=t.length,r=Qo.length,i=Qo.map(((t,e)=>e+1));for(let o,a,s=0,u=0;s<n;++s)for(a=e?t[s][e]:t[s],o=0;o<r;++o)if(i[o]&&Xo(a)&&!Qo[o](a)&&(i[o]=0,++u,u===Qo.length))return\"string\";return Ko[i.reduce(((t,e)=>0===t?e:t),0)-1]}function ea(t,e){return e.reduce(((e,n)=>(e[n]=ta(t,n),e)),{})}function na(t){const e=function(e,n){const r={delimiter:t};return ra(e,n?at(n,r):r)};return e.responseType=\"text\",e}function ra(t,e){return e.header&&(t=e.header.map(Ct).join(e.delimiter)+\"\\n\"+t),Ht(e.delimiter).parse(t+\"\")}function ia(t,e){const n=e&&e.property?l(e.property):f;return!M(t)||(r=t,\"function\"==typeof Buffer&&Z(Buffer.isBuffer)&&Buffer.isBuffer(r))?n(JSON.parse(t)):function(t,e){!A(t)&&yt(t)&&(t=[...t]);return e&&e.copy?JSON.parse(JSON.stringify(t)):t}(n(t),e);var r}ra.responseType=\"text\",ia.responseType=\"json\";const oa={interior:(t,e)=>t!==e,exterior:(t,e)=>t===e};function aa(t,e){let n,r,i,o;return t=ia(t,e),e&&e.feature?(n=Gt,i=e.feature):e&&e.mesh?(n=Zt,i=e.mesh,o=oa[e.filter]):s(\"Missing TopoJSON feature or mesh parameter.\"),r=(r=t.objects[i])?n(t,r,o):s(\"Invalid TopoJSON object: \"+i),r&&r.features||[r]}aa.responseType=\"json\";const sa={dsv:ra,csv:na(\",\"),tsv:na(\"\\t\"),json:ia,topojson:aa};function ua(t,e){return arguments.length>1?(sa[t]=e,this):lt(sa,t)?sa[t]:null}function la(t){const e=ua(t);return e&&e.responseType||\"text\"}function ca(t,e,n,r){const i=ua((e=e||{}).type||\"json\");return i||s(\"Unknown data format type: \"+e.type),t=i(t,e),e.parse&&function(t,e,n,r){if(!t.length)return;const i=zo();n=n||i.timeParse,r=r||i.utcParse;let o,a,s,u,l,c,f=t.columns||Object.keys(t[0]);\"auto\"===e&&(e=ea(t,f));f=Object.keys(e);const h=f.map((t=>{const i=e[t];let o,a;if(i&&(i.startsWith(\"date:\")||i.startsWith(\"utc:\"))){o=i.split(/:(.+)?/,2),a=o[1],(\"'\"===a[0]&&\"'\"===a[a.length-1]||'\"'===a[0]&&'\"'===a[a.length-1])&&(a=a.slice(1,-1));return(\"utc\"===o[0]?r:n)(a)}if(!Zo[i])throw Error(\"Illegal format pattern: \"+t+\":\"+i);return Zo[i]}));for(s=0,l=t.length,c=f.length;s<l;++s)for(o=t[s],u=0;u<c;++u)a=f[u],o[a]=h[u](o[a])}(t,e.parse,n,r),lt(t,\"columns\")&&delete t.columns,t}const fa=function(t,e){return e=>({options:e||{},sanitize:Wo,load:Io,fileAccess:!1,file:Ho(),http:Go(t)})}(\"undefined\"!=typeof fetch&&fetch);function ha(t){const e=t||f,n=[],r={};return n.add=t=>{const i=e(t);return r[i]||(r[i]=1,n.push(t)),n},n.remove=t=>{const i=e(t);if(r[i]){r[i]=0;const e=n.indexOf(t);e>=0&&n.splice(e,1)}return n},n}async function da(t,e){try{await e(t)}catch(e){t.error(e)}}const pa=Symbol(\"vega_id\");let ga=1;function ma(t){return!(!t||!ya(t))}function ya(t){return t[pa]}function va(t,e){return t[pa]=e,t}function _a(t){const e=t===Object(t)?t:{data:t};return ya(e)?e:va(e,ga++)}function xa(t){return ba(t,_a({}))}function ba(t,e){for(const n in t)e[n]=t[n];return e}function wa(t,e){return va(e,ya(t))}function ka(t,e){return t?e?(n,r)=>t(n,r)||ya(e(n))-ya(e(r)):(e,n)=>t(e,n)||ya(e)-ya(n):null}function Aa(t){return t&&t.constructor===Ma}function Ma(){const t=[],e=["
-  , "],n=[],r=[],i=[];let o=null,a=!1;return{constructor:Ma,insert(e){const n=X(e),r=n.length;for(let e=0;e<r;++e)t.push(n[e]);return this},remove(t){const n=Z(t)?r:e,i=X(t),o=i.length;for(let t=0;t<o;++t)n.push(i[t]);return this},modify(t,e,r){const o={field:e,value:it(r)};return Z(t)?(o.filter=t,i.push(o)):(o.tuple=t,n.push(o)),this},encode(t,e){return Z(t)?i.push({filter:t,field:e}):n.push({tuple:t,field:e}),this},clean(t){return o=t,this},reflow(){return a=!0,this},pulse(s,u){const l={},c={};let f,h,d,p,g,m;for(f=0,h=u.length;f<h;++f)l[ya(u[f])]=1;for(f=0,h=e.length;f<h;++f)g=e[f],l[ya(g)]=-1;for(f=0,h=r.length;f<h;++f)p=r[f],u.forEach((t=>{p(t)&&(l[ya(t)]=-1)}));for(f=0,h=t.length;f<h;++f)g=t[f],m=ya(g),l[m]?l[m]=1:s.add.push(_a(t[f]));for(f=0,h=u.length;f<h;++f)g=u[f],l[ya(g)]<0&&s.rem.push(g);function y(t,e,n){n?t[e]=n(t):s.encode=e,a||(c[ya(t)]=t)}for(f=0,h=n.length;f<h;++f)d=n[f],g=d.tuple,p=d.field,m=l[ya(g)],m>0&&(y(g,p,d.value),s.modifies(p));for(f=0,h=i.length;f<h;++f)d=i[f],p=d.filter,u.forEach((t=>{p(t)&&l[ya(t)]>0&&y(t,d.field,d.value)})),s.modifies(d.field);if(a)s.mod=e.length||r.length?u.filter((t=>l[ya(t)]>0)):u.slice();else for(m in c)s.mod.push(c[m]);return(o||null==o&&(e.length||r.length))&&s.clean(!0),s}}}const Ea=\"_:mod:_\";function Da(){Object.defineProperty(this,Ea,{writable:!0,value:{}})}Da.prototype={set(t,e,n,r){const i=this,o=i[t],a=i[Ea];return null!=e&&e>=0?(o[e]!==n||r)&&(o[e]=n,a[e+\":\"+t]=-1,a[t]=-1):(o!==n||r)&&(i[t]=n,a[t]=A(n)?1+n.length:-1),i},modified(t,e){const n=this[Ea];if(!arguments.length){for(const t in n)if(n[t])return!0;return!1}if(A(t)){for(let e=0;e<t.length;++e)if(n[t[e]])return!0;return!1}return null!=e&&e>=0?e+1<n[t]||!!n[e+\":\"+t]:!!n[t]},clear(){return this[Ea]={},this}};let Ca=0;const Fa=new Da;function Sa(t,e,n,r){this.id=++Ca,this.value=t,this.stamp=-1,this.rank=-1,this.qrank=-1,this.flags=0,e&&(this._update=e),n&&this.parameters(n,r)}function $a(t){return function(e){const n=this.flags;return 0===arguments.length?!!(n&t):(this.flags=e?n|t:n&~t,this)}}Sa.prototype={targets(){return this._targets||(this._targets=ha(c))},set(t){return this.value!==t?(this.value=t,1):0},skip:$a(1),modified:$a(2),parameters(t,e,n){e=!1!==e;const r=this._argval=this._argval||new Da,i=this._argops=this._argops||[],o=[];let a,u,l,c;const f=(t,n,a)=>{a instanceof Sa?(a!==this&&(e&&a.targets().add(this),o.push(a)),i.push({op:a,name:t,index:n})):r.set(t,n,a)};for(a in t)if(u=t[a],\"pulse\"===a)X(u).forEach((t=>{t instanceof Sa?t!==this&&(t.targets().add(this),o.push(t)):s(\"Pulse parameters must be operator instances.\")})),this.source=u;else if(A(u))for(r.set(a,-1,Array(l=u.length)),c=0;c<l;++c)f(a,c,u[c]);else f(a,-1,u);return this.marshall().clear(),n&&(i.initonly=!0),o},marshall(t){const e=this._argval||Fa,n=this._argops;let r,i,o,a;if(n){const s=n.length;for(i=0;i<s;++i)r=n[i],o=r.op,a=o.modified()&&o.stamp===t,e.set(r.name,r.index,o.value,a);if(n.initonly){for(i=0;i<s;++i)r=n[i],r.op.targets().remove(this);this._argops=null,this._update=null}}return e},detach(){const t=this._argops;let e,n,r,i;if(t)for(e=0,n=t.length;e<n;++e)r=t[e],i=r.op,i._targets&&i._targets.remove(this);this.pulse=null,this.source=null},evaluate(t){const e=this._update;if(e){const n=this.marshall(t.stamp),r=e.call(this,n,t);if(n.clear(),r!==this.value)this.value=r;else if(!this.modified())return t.StopPropagation}},run(t){if(t.stamp<this.stamp)return t.StopPropagation;let e;return this.skip()?(this.skip(!1),e=0):e=this.evaluate(t),this.pulse=e||t}};let Ta=0;function Ba(t,e,n){this.id=++Ta,this.value=null,n&&(this.receive=n),t&&(this._filter=t),e&&(this._apply=e)}function Na(t,e,n){return new Ba(t,e,n)}Ba.prototype={_filter:p,_apply:f,targets(){return this._targets||(this._targets=ha(c))},consume(t){return arguments.length?(this._consume=!!t,this):!!this._consume},receive(t){if(this._filter(t)){const e=this.value=this._apply(t),n=this._targets,r=n?n.length:0;for(let t=0;t<r;++t)n[t].receive(e);this._consume&&(t.preventDefault(),t.stopPropagation())}},filter(t){const e=Na(t);return this.targets().add(e),e},apply(t){const e=Na(n"
-  , "ull,t);return this.targets().add(e),e},merge(){const t=Na();this.targets().add(t);for(let e=0,n=arguments.length;e<n;++e)arguments[e].targets().add(t);return t},throttle(t){let e=-1;return this.filter((()=>{const n=Date.now();return n-e>t?(e=n,1):0}))},debounce(t){const e=Na();return this.targets().add(Na(null,null,ot(t,(t=>{const n=t.dataflow;e.receive(t),n&&n.run&&n.run()})))),e},between(t,e){let n=!1;return t.targets().add(Na(null,null,(()=>n=!0))),e.targets().add(Na(null,null,(()=>n=!1))),this.filter((()=>n))},detach(){this._filter=p,this._targets=null}};const za={skip:!0};function Oa(t,e,n,r,i,o){const a=at({},o,za);let s,u;Z(n)||(n=it(n)),void 0===r?s=e=>t.touch(n(e)):Z(r)?(u=new Sa(null,r,i,!1),s=e=>{u.evaluate(e);const r=n(e),i=u.value;Aa(i)?t.pulse(r,i,o):t.update(r,i,a)}):s=e=>t.update(n(e),r,a),e.apply(s)}function Ra(t,e,n,r,i,o){if(void 0===r)e.targets().add(n);else{const a=o||{},s=new Sa(null,function(t,e){return e=Z(e)?e:it(e),t?function(n,r){const i=e(n,r);return t.skip()||(t.skip(i!==this.value).value=i),i}:e}(n,r),i,!1);s.modified(a.force),s.rank=e.rank,e.targets().add(s),n&&(s.skip(!0),s.value=n.value,s.targets().add(n),t.connect(n,[s]))}}const La={};function Ua(t,e,n){this.dataflow=t,this.stamp=null==e?-1:e,this.add=[],this.rem=[],this.mod=[],this.fields=null,this.encode=n||null}function qa(t,e){const n=[];return zt(t,e,(t=>n.push(t))),n}function Pa(t,e){const n={};return t.visit(e,(t=>{n[ya(t)]=1})),t=>n[ya(t)]?null:t}function ja(t,e){return t?(n,r)=>t(n,r)&&e(n,r):e}function Ia(t,e,n,r){const i=this;let o=0;this.dataflow=t,this.stamp=e,this.fields=null,this.encode=r||null,this.pulses=n;for(const t of n)if(t.stamp===e){if(t.fields){const e=i.fields||(i.fields={});for(const n in t.fields)e[n]=1}t.changed(i.ADD)&&(o|=i.ADD),t.changed(i.REM)&&(o|=i.REM),t.changed(i.MOD)&&(o|=i.MOD)}this.changes=o}function Wa(t){return t.error(\"Dataflow already running. Use runAsync() to chain invocations.\"),t}Ua.prototype={StopPropagation:La,ADD:1,REM:2,MOD:4,ADD_REM:3,ADD_MOD:5,ALL:7,REFLOW:8,SOURCE:16,NO_SOURCE:32,NO_FIELDS:64,fork(t){return new Ua(this.dataflow).init(this,t)},clone(){const t=this.fork(7);return t.add=t.add.slice(),t.rem=t.rem.slice(),t.mod=t.mod.slice(),t.source&&(t.source=t.source.slice()),t.materialize(23)},addAll(){let t=this;return!t.source||t.add===t.rem||!t.rem.length&&t.source.length===t.add.length||(t=new Ua(this.dataflow).init(this),t.add=t.source,t.rem=[]),t},init(t,e){const n=this;return n.stamp=t.stamp,n.encode=t.encode,!t.fields||64&e||(n.fields=t.fields),1&e?(n.addF=t.addF,n.add=t.add):(n.addF=null,n.add=[]),2&e?(n.remF=t.remF,n.rem=t.rem):(n.remF=null,n.rem=[]),4&e?(n.modF=t.modF,n.mod=t.mod):(n.modF=null,n.mod=[]),32&e?(n.srcF=null,n.source=null):(n.srcF=t.srcF,n.source=t.source,t.cleans&&(n.cleans=t.cleans)),n},runAfter(t){this.dataflow.runAfter(t)},changed(t){const e=t||7;return 1&e&&this.add.length||2&e&&this.rem.length||4&e&&this.mod.length},reflow(t){if(t)return this.fork(7).reflow();const e=this.add.length,n=this.source&&this.source.length;return n&&n!==e&&(this.mod=this.source,e&&this.filter(4,Pa(this,1))),this},clean(t){return arguments.length?(this.cleans=!!t,this):this.cleans},modifies(t){const e=this.fields||(this.fields={});return A(t)?t.forEach((t=>e[t]=!0)):e[t]=!0,this},modified(t,e){const n=this.fields;return!(!e&&!this.mod.length||!n)&&(arguments.length?A(t)?t.some((t=>n[t])):n[t]:!!n)},filter(t,e){const n=this;return 1&t&&(n.addF=ja(n.addF,e)),2&t&&(n.remF=ja(n.remF,e)),4&t&&(n.modF=ja(n.modF,e)),16&t&&(n.srcF=ja(n.srcF,e)),n},materialize(t){const e=this;return 1&(t=t||7)&&e.addF&&(e.add=qa(e.add,e.addF),e.addF=null),2&t&&e.remF&&(e.rem=qa(e.rem,e.remF),e.remF=null),4&t&&e.modF&&(e.mod=qa(e.mod,e.modF),e.modF=null),16&t&&e.srcF&&(e.source=e.source.filter(e.srcF),e.srcF=null),e},visit(t,e){const n=this,r=e;if(16&t)return zt(n.source,n.srcF,r),n;1&t&&zt(n.add,n.addF,r),2&t&&zt(n.rem,n.remF,r),4&t&&zt(n.mod,n.modF,r);const i=n.source;if(8&t&&i){const t=n.add.length+n.mod.length;t===i.length||zt(i,t?Pa(n,5):n.srcF,r)}return n}},dt(Ia,Ua,{fork(t){const e=new Ua(this.datafl"
-  , "ow).init(this,t&this.NO_FIELDS);return void 0!==t&&(t&e.ADD&&this.visit(e.ADD,(t=>e.add.push(t))),t&e.REM&&this.visit(e.REM,(t=>e.rem.push(t))),t&e.MOD&&this.visit(e.MOD,(t=>e.mod.push(t)))),e},changed(t){return this.changes&t},modified(t){const e=this,n=e.fields;return n&&e.changes&e.MOD?A(t)?t.some((t=>n[t])):n[t]:0},filter(){s(\"MultiPulse does not support filtering.\")},materialize(){s(\"MultiPulse does not support materialization.\")},visit(t,e){const n=this,r=n.pulses,i=r.length;let o=0;if(t&n.SOURCE)for(;o<i;++o)r[o].visit(t,e);else for(;o<i;++o)r[o].stamp===n.stamp&&r[o].visit(t,e);return n}});const Ha={skip:!1,force:!1};function Ya(t){let e=[];return{clear:()=>e=[],size:()=>e.length,peek:()=>e[0],push:n=>(e.push(n),Ga(e,0,e.length-1,t)),pop:()=>{const n=e.pop();let r;return e.length?(r=e[0],e[0]=n,function(t,e,n){const r=e,i=t.length,o=t[e];let a,s=1+(e<<1);for(;s<i;)a=s+1,a<i&&n(t[s],t[a])>=0&&(s=a),t[e]=t[s],s=1+((e=s)<<1);t[e]=o,Ga(t,r,e,n)}(e,0,t)):r=n,r}}}function Ga(t,e,n,r){let i,o;const a=t[n];for(;n>e&&(o=n-1>>1,i=t[o],r(a,i)<0);)t[n]=i,n=o;return t[n]=a}function Va(){this.logger(k()),this.logLevel(_),this._clock=0,this._rank=0,this._locale=Lo();try{this._loader=fa()}catch(t){}this._touched=ha(c),this._input={},this._pulse=null,this._heap=Ya(((t,e)=>t.qrank-e.qrank)),this._postrun=[]}function Xa(t){return function(){return this._log[t].apply(this,arguments)}}function Ja(t,e){Sa.call(this,t,null,e)}Va.prototype={stamp(){return this._clock},loader(t){return arguments.length?(this._loader=t,this):this._loader},locale(t){return arguments.length?(this._locale=t,this):this._locale},logger(t){return arguments.length?(this._log=t,this):this._log},error:Xa(\"error\"),warn:Xa(\"warn\"),info:Xa(\"info\"),debug:Xa(\"debug\"),logLevel:Xa(\"level\"),cleanThreshold:1e4,add:function(t,e,n,r){let i,o=1;return t instanceof Sa?i=t:t&&t.prototype instanceof Sa?i=new t:Z(t)?i=new Sa(null,t):(o=0,i=new Sa(t,e)),this.rank(i),o&&(r=n,n=e),n&&this.connect(i,i.parameters(n,r)),this.touch(i),i},connect:function(t,e){const n=t.rank,r=e.length;for(let i=0;i<r;++i)if(n<e[i].rank)return void this.rerank(t)},rank:function(t){t.rank=++this._rank},rerank:function(t){const e=[t];let n,r,i;for(;e.length;)if(this.rank(n=e.pop()),r=n._targets)for(i=r.length;--i>=0;)e.push(n=r[i]),n===t&&s(\"Cycle detected in dataflow graph.\")},pulse:function(t,e,n){this.touch(t,n||Ha);const r=new Ua(this,this._clock+(this._pulse?0:1)),i=t.pulse&&t.pulse.source||[];return r.target=t,this._input[t.id]=e.pulse(r,i),this},touch:function(t,e){const n=e||Ha;return this._pulse?this._enqueue(t):this._touched.add(t),n.skip&&t.skip(!0),this},update:function(t,e,n){const r=n||Ha;return(t.set(e)||r.force)&&this.touch(t,r),this},changeset:Ma,ingest:function(t,e,n){return e=this.parse(e,n),this.pulse(t,this.changeset().insert(e))},parse:function(t,e){const n=this.locale();return ca(t,e,n.timeParse,n.utcParse)},preload:async function(t,e,n){const r=this,i=r._pending||function(t){let e;const n=new Promise((t=>e=t));return n.requests=0,n.done=()=>{0==--n.requests&&(t._pending=null,e(t))},t._pending=n}(r);i.requests+=1;const o=await r.request(e,n);return r.pulse(t,r.changeset().remove(p).insert(o.data||[])),i.done(),o},request:async function(t,e){const n=this;let r,i=0;try{r=await n.loader().load(t,{context:\"dataflow\",response:la(e&&e.type)});try{r=n.parse(r,e)}catch(e){i=-2,n.warn(\"Data ingestion failed\",t,e)}}catch(e){i=-1,n.warn(\"Loading failed\",t,e)}return{data:r,status:i}},events:function(t,e,n,r){const i=this,o=Na(n,r),a=function(t){t.dataflow=i;try{o.receive(t)}catch(t){i.error(t)}finally{i.run()}};let s;s=\"string\"==typeof t&&\"undefined\"!=typeof document?document.querySelectorAll(t):X(t);const u=s.length;for(let t=0;t<u;++t)s[t].addEventListener(e,a);return o},on:function(t,e,n,r,i){return(t instanceof Sa?Ra:Oa)(this,t,e,n,r,i),this},evaluate:async function(t,e,n){const r=this,i=[];if(r._pulse)return Wa(r);if(r._pending&&await r._pending,e&&await da(r,e),!r._touched.length)return r.debug(\"Dataflow invoked, but nothing to do.\"),r;const o=++r._clock;r._pulse=new Ua(r,o,t),r._touched.f"
-  , "orEach((t=>r._enqueue(t,!0))),r._touched=ha(c);let a,s,u,l=0;try{for(;r._heap.size()>0;)a=r._heap.pop(),a.rank===a.qrank?(s=a.run(r._getPulse(a,t)),s.then?s=await s:s.async&&(i.push(s.async),s=La),s!==La&&a._targets&&a._targets.forEach((t=>r._enqueue(t))),++l):r._enqueue(a,!0)}catch(t){r._heap.clear(),u=t}if(r._input={},r._pulse=null,r.debug(`Pulse ${o}: ${l} operators`),u&&(r._postrun=[],r.error(u)),r._postrun.length){const t=r._postrun.sort(((t,e)=>e.priority-t.priority));r._postrun=[];for(let e=0;e<t.length;++e)await da(r,t[e].callback)}return n&&await da(r,n),i.length&&Promise.all(i).then((t=>r.runAsync(null,(()=>{t.forEach((t=>{try{t(r)}catch(t){r.error(t)}}))})))),r},run:function(t,e,n){return this._pulse?Wa(this):(this.evaluate(t,e,n),this)},runAsync:async function(t,e,n){for(;this._running;)await this._running;const r=()=>this._running=null;return(this._running=this.evaluate(t,e,n)).then(r,r),this._running},runAfter:function(t,e,n){if(this._pulse||e)this._postrun.push({priority:n||0,callback:t});else try{t(this)}catch(t){this.error(t)}},_enqueue:function(t,e){const n=t.stamp<this._clock;n&&(t.stamp=this._clock),(n||e)&&(t.qrank=t.rank,this._heap.push(t))},_getPulse:function(t,e){const n=t.source,r=this._clock;return n&&A(n)?new Ia(this,r,n.map((t=>t.pulse)),e):this._input[t.id]||function(t,e){if(e&&e.stamp===t.stamp)return e;t=t.fork(),e&&e!==La&&(t.source=e.source);return t}(this._pulse,n&&n.pulse)}},dt(Ja,Sa,{run(t){if(t.stamp<this.stamp)return t.StopPropagation;let e;return this.skip()?this.skip(!1):e=this.evaluate(t),e=e||t,e.then?e=e.then((t=>this.pulse=t)):e!==t.StopPropagation&&(this.pulse=e),e},evaluate(t){const e=this.marshall(t.stamp),n=this.transform(e,t);return e.clear(),n},transform(){}});const Za={};function Qa(t){const e=Ka(t);return e&&e.Definition||null}function Ka(t){return t=t&&t.toLowerCase(),lt(Za,t)?Za[t]:null}function*ts(t,e){if(null==e)for(let e of t)null!=e&&\"\"!==e&&(e=+e)>=e&&(yield e);else{let n=-1;for(let r of t)r=e(r,++n,t),null!=r&&\"\"!==r&&(r=+r)>=r&&(yield r)}}function es(t,e,n){const r=Float64Array.from(ts(t,n));return r.sort(Kt),e.map((t=>De(r,t)))}function ns(t,e){return es(t,[.25,.5,.75],e)}function rs(t,e){const n=t.length,r=function(t,e){const n=function(t,e){let n,r=0,i=0,o=0;if(void 0===e)for(let e of t)null!=e&&(e=+e)>=e&&(n=e-i,i+=n/++r,o+=n*(e-i));else{let a=-1;for(let s of t)null!=(s=e(s,++a,t))&&(s=+s)>=s&&(n=s-i,i+=n/++r,o+=n*(s-i))}if(r>1)return o/(r-1)}(t,e);return n?Math.sqrt(n):n}(t,e),i=ns(t,e),o=(i[2]-i[0])/1.34;return 1.06*(Math.min(r,o)||r||Math.abs(i[0])||1)*Math.pow(n,-.2)}function is(t){const e=t.maxbins||20,n=t.base||10,r=Math.log(n),i=t.divide||[5,2];let o,a,s,u,l,c,f=t.extent[0],h=t.extent[1];const d=t.span||h-f||Math.abs(f)||1;if(t.step)o=t.step;else if(t.steps){for(u=d/e,l=0,c=t.steps.length;l<c&&t.steps[l]<u;++l);o=t.steps[Math.max(0,l-1)]}else{for(a=Math.ceil(Math.log(e)/r),s=t.minstep||0,o=Math.max(s,Math.pow(n,Math.round(Math.log(d)/r)-a));Math.ceil(d/o)>e;)o*=n;for(l=0,c=i.length;l<c;++l)u=o/i[l],u>=s&&d/u<=e&&(o=u)}u=Math.log(o);const p=u>=0?0:1+~~(-u/r),g=Math.pow(n,-p-1);return(t.nice||void 0===t.nice)&&(u=Math.floor(f/o+g)*o,f=f<u?u-o:u,h=Math.ceil(h/o)*o),{start:f,stop:h===f?f+o:h,step:o}}function os(e,n,r,i){if(!e.length)return[void 0,void 0];const o=Float64Array.from(ts(e,i)),a=o.length,s=n;let u,l,c,f;for(c=0,f=Array(s);c<s;++c){for(u=0,l=0;l<a;++l)u+=o[~~(t.random()*a)];f[c]=u/a}return f.sort(Kt),[Ee(f,r/2),Ee(f,1-r/2)]}function as(t,e,n,r){r=r||(t=>t);const i=t.length,o=new Float64Array(i);let a,s=0,u=1,l=r(t[0]),c=l,f=l+e;for(;u<i;++u){if(a=r(t[u]),a>=f){for(c=(l+c)/2;s<u;++s)o[s]=c;f=a+e,l=a}c=a}for(c=(l+c)/2;s<u;++s)o[s]=c;return n?function(t,e){const n=t.length;let r,i,o=0,a=1;for(;t[o]===t[a];)++a;for(;a<n;){for(r=a+1;t[a]===t[r];)++r;if(t[a]-t[a-1]<e){for(i=a+(o+r-a-a>>1);i<a;)t[i++]=t[a];for(;i>a;)t[i--]=t[o]}o=a,a=r}return t}(o,e+e/4):o}t.random=Math.random;const ss=Math.sqrt(2*Math.PI),us=Math.SQRT2;let ls=NaN;function cs(e,n){e=e||0,n=null==n?1:n;let r,i,o=0,a=0;if(ls==ls)o=ls,ls=NaN;else{do{o=2*t.random()-1,a=2*t.random()-1,r=o"
-  , "*o+a*a}while(0===r||r>1);i=Math.sqrt(-2*Math.log(r)/r),o*=i,ls=a*i}return e+o*n}function fs(t,e,n){const r=(t-(e||0))/(n=null==n?1:n);return Math.exp(-.5*r*r)/(n*ss)}function hs(t,e,n){const r=(t-(e=e||0))/(n=null==n?1:n),i=Math.abs(r);let o;if(i>37)o=0;else{const t=Math.exp(-i*i/2);let e;i<7.07106781186547?(e=.0352624965998911*i+.700383064443688,e=e*i+6.37396220353165,e=e*i+33.912866078383,e=e*i+112.079291497871,e=e*i+221.213596169931,e=e*i+220.206867912376,o=t*e,e=.0883883476483184*i+1.75566716318264,e=e*i+16.064177579207,e=e*i+86.7807322029461,e=e*i+296.564248779674,e=e*i+637.333633378831,e=e*i+793.826512519948,e=e*i+440.413735824752,o/=e):(e=i+.65,e=i+4/e,e=i+3/e,e=i+2/e,e=i+1/e,o=t/e/2.506628274631)}return r>0?1-o:o}function ds(t,e,n){return t<0||t>1?NaN:(e||0)+(null==n?1:n)*us*function(t){let e,n=-Math.log((1-t)*(1+t));n<6.25?(n-=3.125,e=-364441206401782e-35,e=e*n-16850591381820166e-35,e=128584807152564e-32+e*n,e=11157877678025181e-33+e*n,e=e*n-1333171662854621e-31,e=20972767875968562e-33+e*n,e=6637638134358324e-30+e*n,e=e*n-4054566272975207e-29,e=e*n-8151934197605472e-29,e=26335093153082323e-28+e*n,e=e*n-12975133253453532e-27,e=e*n-5415412054294628e-26,e=1.0512122733215323e-9+e*n,e=e*n-4.112633980346984e-9,e=e*n-2.9070369957882005e-8,e=4.2347877827932404e-7+e*n,e=e*n-13654692000834679e-22,e=e*n-13882523362786469e-21,e=.00018673420803405714+e*n,e=e*n-.000740702534166267,e=e*n-.006033670871430149,e=.24015818242558962+e*n,e=1.6536545626831027+e*n):n<16?(n=Math.sqrt(n)-3.25,e=2.2137376921775787e-9,e=9.075656193888539e-8+e*n,e=e*n-2.7517406297064545e-7,e=1.8239629214389228e-8+e*n,e=15027403968909828e-22+e*n,e=e*n-4013867526981546e-21,e=29234449089955446e-22+e*n,e=12475304481671779e-21+e*n,e=e*n-47318229009055734e-21,e=6828485145957318e-20+e*n,e=24031110387097894e-21+e*n,e=e*n-.0003550375203628475,e=.0009532893797373805+e*n,e=e*n-.0016882755560235047,e=.002491442096107851+e*n,e=e*n-.003751208507569241,e=.005370914553590064+e*n,e=1.0052589676941592+e*n,e=3.0838856104922208+e*n):Number.isFinite(n)?(n=Math.sqrt(n)-5,e=-27109920616438573e-27,e=e*n-2.555641816996525e-10,e=1.5076572693500548e-9+e*n,e=e*n-3.789465440126737e-9,e=7.61570120807834e-9+e*n,e=e*n-1.496002662714924e-8,e=2.914795345090108e-8+e*n,e=e*n-6.771199775845234e-8,e=2.2900482228026655e-7+e*n,e=e*n-9.9298272942317e-7,e=4526062597223154e-21+e*n,e=e*n-1968177810553167e-20,e=7599527703001776e-20+e*n,e=e*n-.00021503011930044477,e=e*n-.00013871931833623122,e=1.0103004648645344+e*n,e=4.849906401408584+e*n):e=1/0;return e*t}(2*t-1)}function ps(t,e){let n,r;const i={mean(t){return arguments.length?(n=t||0,i):n},stdev(t){return arguments.length?(r=null==t?1:t,i):r},sample:()=>cs(n,r),pdf:t=>fs(t,n,r),cdf:t=>hs(t,n,r),icdf:t=>ds(t,n,r)};return i.mean(t).stdev(e)}function gs(e,n){const r=ps();let i=0;const o={data(t){return arguments.length?(e=t,i=t?t.length:0,o.bandwidth(n)):e},bandwidth(t){return arguments.length?(!(n=t)&&e&&(n=rs(e)),o):n},sample:()=>e[~~(t.random()*i)]+n*r.sample(),pdf(t){let o=0,a=0;for(;a<i;++a)o+=r.pdf((t-e[a])/n);return o/n/i},cdf(t){let o=0,a=0;for(;a<i;++a)o+=r.cdf((t-e[a])/n);return o/i},icdf(){throw Error(\"KDE icdf not supported.\")}};return o.data(e)}function ms(t,e){return t=t||0,e=null==e?1:e,Math.exp(t+cs()*e)}function ys(t,e,n){if(t<=0)return 0;e=e||0,n=null==n?1:n;const r=(Math.log(t)-e)/n;return Math.exp(-.5*r*r)/(n*ss*t)}function vs(t,e,n){return hs(Math.log(t),e,n)}function _s(t,e,n){return Math.exp(ds(t,e,n))}function xs(t,e){let n,r;const i={mean(t){return arguments.length?(n=t||0,i):n},stdev(t){return arguments.length?(r=null==t?1:t,i):r},sample:()=>ms(n,r),pdf:t=>ys(t,n,r),cdf:t=>vs(t,n,r),icdf:t=>_s(t,n,r)};return i.mean(t).stdev(e)}function bs(e,n){let r,i=0;const o={weights(t){return arguments.length?(r=function(t){const e=[];let n,r=0;for(n=0;n<i;++n)r+=e[n]=null==t[n]?1:+t[n];for(n=0;n<i;++n)e[n]/=r;return e}(n=t||[]),o):n},distributions(t){return arguments.length?(t?(i=t.length,e=t):(i=0,e=[]),o.weights(n)):e},sample(){const n=t.random();let o=e[i-1],a=r[0],s=0;for(;s<i-1;a+=r[++s])if(n<a){o=e[s];break}return o.sample()},pdf("
-  , "t){let n=0,o=0;for(;o<i;++o)n+=r[o]*e[o].pdf(t);return n},cdf(t){let n=0,o=0;for(;o<i;++o)n+=r[o]*e[o].cdf(t);return n},icdf(){throw Error(\"Mixture icdf not supported.\")}};return o.distributions(e).weights(n)}function ws(e,n){return null==n&&(n=null==e?1:e,e=0),e+(n-e)*t.random()}function ks(t,e,n){return null==n&&(n=null==e?1:e,e=0),t>=e&&t<=n?1/(n-e):0}function As(t,e,n){return null==n&&(n=null==e?1:e,e=0),t<e?0:t>n?1:(t-e)/(n-e)}function Ms(t,e,n){return null==n&&(n=null==e?1:e,e=0),t>=0&&t<=1?e+t*(n-e):NaN}function Es(t,e){let n,r;const i={min(t){return arguments.length?(n=t||0,i):n},max(t){return arguments.length?(r=null==t?1:t,i):r},sample:()=>ws(n,r),pdf:t=>ks(t,n,r),cdf:t=>As(t,n,r),icdf:t=>Ms(t,n,r)};return null==e&&(e=null==t?1:t,t=0),i.min(t).max(e)}function Ds(t,e,n){let r=0,i=0;for(const o of t){const t=n(o);null==e(o)||null==t||isNaN(t)||(r+=(t-r)/++i)}return{coef:[r],predict:()=>r,rSquared:0}}function Cs(t,e,n,r){const i=r-t*t,o=Math.abs(i)<1e-24?0:(n-t*e)/i;return[e-o*t,o]}function Fs(t,e,n,r){t=t.filter((t=>{let r=e(t),i=n(t);return null!=r&&(r=+r)>=r&&null!=i&&(i=+i)>=i})),r&&t.sort(((t,n)=>e(t)-e(n)));const i=t.length,o=new Float64Array(i),a=new Float64Array(i);let s,u,l,c=0,f=0,h=0;for(l of t)o[c]=s=+e(l),a[c]=u=+n(l),++c,f+=(s-f)/c,h+=(u-h)/c;for(c=0;c<i;++c)o[c]-=f,a[c]-=h;return[o,a,f,h]}function Ss(t,e,n,r){let i,o,a=-1;for(const s of t)i=e(s),o=n(s),null!=i&&(i=+i)>=i&&null!=o&&(o=+o)>=o&&r(i,o,++a)}function $s(t,e,n,r,i){let o=0,a=0;return Ss(t,e,n,((t,e)=>{const n=e-i(t),s=e-r;o+=n*n,a+=s*s})),1-o/a}function Ts(t,e,n){let r=0,i=0,o=0,a=0,s=0;Ss(t,e,n,((t,e)=>{++s,r+=(t-r)/s,i+=(e-i)/s,o+=(t*e-o)/s,a+=(t*t-a)/s}));const u=Cs(r,i,o,a),l=t=>u[0]+u[1]*t;return{coef:u,predict:l,rSquared:$s(t,e,n,i,l)}}function Bs(t,e,n){let r=0,i=0,o=0,a=0,s=0;Ss(t,e,n,((t,e)=>{++s,t=Math.log(t),r+=(t-r)/s,i+=(e-i)/s,o+=(t*e-o)/s,a+=(t*t-a)/s}));const u=Cs(r,i,o,a),l=t=>u[0]+u[1]*Math.log(t);return{coef:u,predict:l,rSquared:$s(t,e,n,i,l)}}function Ns(t,e,n){const[r,i,o,a]=Fs(t,e,n);let s,u,l,c=0,f=0,h=0,d=0,p=0;Ss(t,e,n,((t,e)=>{s=r[p++],u=Math.log(e),l=s*e,c+=(e*u-c)/p,f+=(l-f)/p,h+=(l*u-h)/p,d+=(s*l-d)/p}));const[g,m]=Cs(f/a,c/a,h/a,d/a),y=t=>Math.exp(g+m*(t-o));return{coef:[Math.exp(g-m*o),m],predict:y,rSquared:$s(t,e,n,a,y)}}function zs(t,e,n){let r=0,i=0,o=0,a=0,s=0,u=0;Ss(t,e,n,((t,e)=>{const n=Math.log(t),l=Math.log(e);++u,r+=(n-r)/u,i+=(l-i)/u,o+=(n*l-o)/u,a+=(n*n-a)/u,s+=(e-s)/u}));const l=Cs(r,i,o,a),c=t=>l[0]*Math.pow(t,l[1]);return l[0]=Math.exp(l[0]),{coef:l,predict:c,rSquared:$s(t,e,n,s,c)}}function Os(t,e,n){const[r,i,o,a]=Fs(t,e,n),s=r.length;let u,l,c,f,h=0,d=0,p=0,g=0,m=0;for(u=0;u<s;)l=r[u],c=i[u++],f=l*l,h+=(f-h)/u,d+=(f*l-d)/u,p+=(f*f-p)/u,g+=(l*c-g)/u,m+=(f*c-m)/u;const y=p-h*h,v=h*y-d*d,_=(m*h-g*d)/v,x=(g*y-m*d)/v,b=-_*h,w=t=>_*(t-=o)*t+x*t+b+a;return{coef:[b-x*o+_*o*o+a,x-2*_*o,_],predict:w,rSquared:$s(t,e,n,a,w)}}function Rs(t,e,n,r){if(0===r)return Ds(t,e,n);if(1===r)return Ts(t,e,n);if(2===r)return Os(t,e,n);const[i,o,a,s]=Fs(t,e,n),u=i.length,l=[],c=[],f=r+1;let h,d,p,g,m;for(h=0;h<f;++h){for(p=0,g=0;p<u;++p)g+=Math.pow(i[p],h)*o[p];for(l.push(g),m=new Float64Array(f),d=0;d<f;++d){for(p=0,g=0;p<u;++p)g+=Math.pow(i[p],h+d);m[d]=g}c.push(m)}c.push(l);const y=function(t){const e=t.length-1,n=[];let r,i,o,a,s;for(r=0;r<e;++r){for(a=r,i=r+1;i<e;++i)Math.abs(t[r][i])>Math.abs(t[r][a])&&(a=i);for(o=r;o<e+1;++o)s=t[o][r],t[o][r]=t[o][a],t[o][a]=s;for(i=r+1;i<e;++i)for(o=e;o>=r;o--)t[o][i]-=t[o][r]*t[r][i]/t[r][r]}for(i=e-1;i>=0;--i){for(s=0,o=i+1;o<e;++o)s+=t[o][i]*n[o];n[i]=(t[e][i]-s)/t[i][i]}return n}(c),v=t=>{t-=a;let e=s+y[0]+y[1]*t+y[2]*t*t;for(h=3;h<f;++h)e+=y[h]*Math.pow(t,h);return e};return{coef:Ls(f,y,-a,s),predict:v,rSquared:$s(t,e,n,s,v)}}function Ls(t,e,n,r){const i=Array(t);let o,a,s,u;for(o=0;o<t;++o)i[o]=0;for(o=t-1;o>=0;--o)for(s=e[o],u=1,i[o]+=s,a=1;a<=o;++a)u*=(o+1-a)/a,i[o-a]+=s*Math.pow(n,a)*u;return i[0]+=r,i}function Us(t,e,n,r){const[i,o,a,s]=Fs(t,e,n,!0),u=i.length,l=Math.max(2,~~(r*u)),c=new Float64Array(u),f=new Float64Array(u),h=new Float64Array(u).fill(1);for(let t=-"
-  , "1;++t<=2;){const e=[0,l-1];for(let t=0;t<u;++t){const n=i[t],r=e[0],a=e[1],s=n-i[r]>i[a]-n?r:a;let u=0,l=0,d=0,p=0,g=0;const m=1/Math.abs(i[s]-n||1);for(let t=r;t<=a;++t){const e=i[t],r=o[t],a=qs(Math.abs(n-e)*m)*h[t],s=e*a;u+=a,l+=s,d+=r*a,p+=r*s,g+=e*s}const[y,v]=Cs(l/u,d/u,p/u,g/u);c[t]=y+v*n,f[t]=Math.abs(o[t]-c[t]),Ps(i,t+1,e)}if(2===t)break;const n=Ce(f);if(Math.abs(n)<1e-12)break;for(let t,e,r=0;r<u;++r)t=f[r]/(6*n),h[r]=t>=1?1e-12:(e=1-t*t)*e}return function(t,e,n,r){const i=t.length,o=[];let a,s=0,u=0,l=[];for(;s<i;++s)a=t[s]+n,l[0]===a?l[1]+=(e[s]-l[1])/++u:(u=0,l[1]+=r,l=[a,e[s]],o.push(l));return l[1]+=r,o}(i,c,a,s)}function qs(t){return(t=1-t*t*t)*t*t}function Ps(t,e,n){const r=t[e];let i=n[0],o=n[1]+1;if(!(o>=t.length))for(;e>i&&t[o]-r<=r-t[i];)n[0]=++i,n[1]=o,++o}const js=.5*Math.PI/180;function Is(t,e,n,r){n=n||25,r=Math.max(n,r||200);const i=e=>[e,t(e)],o=e[0],a=e[1],s=a-o,u=s/r,l=[i(o)],c=[];if(n===r){for(let t=1;t<r;++t)l.push(i(o+t/n*s));return l.push(i(a)),l}c.push(i(a));for(let t=n;--t>0;)c.push(i(o+t/n*s));let f=l[0],h=c[c.length-1];const d=1/s,p=function(t,e){let n=t,r=t;const i=e.length;for(let t=0;t<i;++t){const i=e[t][1];i<n&&(n=i),i>r&&(r=i)}return 1/(r-n)}(f[1],c);for(;h;){const t=i((f[0]+h[0])/2);t[0]-f[0]>=u&&Ws(f,t,h,d,p)>js?c.push(t):(f=h,l.push(h),c.pop()),h=c[c.length-1]}return l}function Ws(t,e,n,r,i){const o=Math.atan2(i*(n[1]-t[1]),r*(n[0]-t[0])),a=Math.atan2(i*(e[1]-t[1]),r*(e[0]-t[0]));return Math.abs(o-a)}function Hs(t){return t&&t.length?1===t.length?t[0]:(e=t,t=>{const n=e.length;let r=1,i=String(e[0](t));for(;r<n;++r)i+=\"|\"+e[r](t);return i}):function(){return\"\"};var e}function Ys(t,e,n){return n||t+(e?\"_\"+e:\"\")}const Gs=()=>{},Vs={init:Gs,add:Gs,rem:Gs,idx:0},Xs={values:{init:t=>t.cell.store=!0,value:t=>t.cell.data.values(),idx:-1},count:{value:t=>t.cell.num},__count__:{value:t=>t.missing+t.valid},missing:{value:t=>t.missing},valid:{value:t=>t.valid},sum:{init:t=>t.sum=0,value:t=>t.valid?t.sum:void 0,add:(t,e)=>t.sum+=+e,rem:(t,e)=>t.sum-=e},product:{init:t=>t.product=1,value:t=>t.valid?t.product:void 0,add:(t,e)=>t.product*=e,rem:(t,e)=>t.product/=e},mean:{init:t=>t.mean=0,value:t=>t.valid?t.mean:void 0,add:(t,e)=>(t.mean_d=e-t.mean,t.mean+=t.mean_d/t.valid),rem:(t,e)=>(t.mean_d=e-t.mean,t.mean-=t.valid?t.mean_d/t.valid:t.mean)},average:{value:t=>t.valid?t.mean:void 0,req:[\"mean\"],idx:1},variance:{init:t=>t.dev=0,value:t=>t.valid>1?t.dev/(t.valid-1):void 0,add:(t,e)=>t.dev+=t.mean_d*(e-t.mean),rem:(t,e)=>t.dev-=t.mean_d*(e-t.mean),req:[\"mean\"],idx:1},variancep:{value:t=>t.valid>1?t.dev/t.valid:void 0,req:[\"variance\"],idx:2},stdev:{value:t=>t.valid>1?Math.sqrt(t.dev/(t.valid-1)):void 0,req:[\"variance\"],idx:2},stdevp:{value:t=>t.valid>1?Math.sqrt(t.dev/t.valid):void 0,req:[\"variance\"],idx:2},stderr:{value:t=>t.valid>1?Math.sqrt(t.dev/(t.valid*(t.valid-1))):void 0,req:[\"variance\"],idx:2},distinct:{value:t=>t.cell.data.distinct(t.get),req:[\"values\"],idx:3},ci0:{value:t=>t.cell.data.ci0(t.get),req:[\"values\"],idx:3},ci1:{value:t=>t.cell.data.ci1(t.get),req:[\"values\"],idx:3},median:{value:t=>t.cell.data.q2(t.get),req:[\"values\"],idx:3},q1:{value:t=>t.cell.data.q1(t.get),req:[\"values\"],idx:3},q3:{value:t=>t.cell.data.q3(t.get),req:[\"values\"],idx:3},min:{init:t=>t.min=void 0,value:t=>t.min=Number.isNaN(t.min)?t.cell.data.min(t.get):t.min,add:(t,e)=>{(e<t.min||void 0===t.min)&&(t.min=e)},rem:(t,e)=>{e<=t.min&&(t.min=NaN)},req:[\"values\"],idx:4},max:{init:t=>t.max=void 0,value:t=>t.max=Number.isNaN(t.max)?t.cell.data.max(t.get):t.max,add:(t,e)=>{(e>t.max||void 0===t.max)&&(t.max=e)},rem:(t,e)=>{e>=t.max&&(t.max=NaN)},req:[\"values\"],idx:4},argmin:{init:t=>t.argmin=void 0,value:t=>t.argmin||t.cell.data.argmin(t.get),add:(t,e,n)=>{e<t.min&&(t.argmin=n)},rem:(t,e)=>{e<=t.min&&(t.argmin=void 0)},req:[\"min\",\"values\"],idx:3},argmax:{init:t=>t.argmax=void 0,value:t=>t.argmax||t.cell.data.argmax(t.get),add:(t,e,n)=>{e>t.max&&(t.argmax=n)},rem:(t,e)=>{e>=t.max&&(t.argmax=void 0)},req:[\"max\",\"values\"],idx:3},exponential:{init:(t,e)=>{t.exp=0,t.exp_r=e},value:t=>t.valid?t.exp*(1-t.exp_r)/(1-t.exp_r**"
-  , "t.valid):void 0,add:(t,e)=>t.exp=t.exp_r*t.exp+e,rem:(t,e)=>t.exp=(t.exp-e/t.exp_r**(t.valid-1))/t.exp_r},exponentialb:{value:t=>t.valid?t.exp*(1-t.exp_r):void 0,req:[\"exponential\"],idx:1}},Js=Object.keys(Xs).filter((t=>\"__count__\"!==t));function Zs(t,e,n){return Xs[t](n,e)}function Qs(t,e){return t.idx-e.idx}function Ks(){this.valid=0,this.missing=0,this._ops.forEach((t=>null==t.aggregate_param?t.init(this):t.init(this,t.aggregate_param)))}function tu(t,e){null!=t&&\"\"!==t?t==t&&(++this.valid,this._ops.forEach((n=>n.add(this,t,e)))):++this.missing}function eu(t,e){null!=t&&\"\"!==t?t==t&&(--this.valid,this._ops.forEach((n=>n.rem(this,t,e)))):--this.missing}function nu(t){return this._out.forEach((e=>t[e.out]=e.value(this))),t}function ru(t,e){const n=e||f,r=function(t){const e={};t.forEach((t=>e[t.name]=t));const n=t=>{t.req&&t.req.forEach((t=>{e[t]||n(e[t]=Xs[t]())}))};return t.forEach(n),Object.values(e).sort(Qs)}(t),i=t.slice().sort(Qs);function o(t){this._ops=r,this._out=i,this.cell=t,this.init()}return o.prototype.init=Ks,o.prototype.add=tu,o.prototype.rem=eu,o.prototype.set=nu,o.prototype.get=n,o.fields=t.map((t=>t.out)),o}function iu(t){this._key=t?l(t):ya,this.reset()}[...Js,\"__count__\"].forEach((t=>{Xs[t]=function(t,e){return(n,r)=>at({name:t,aggregate_param:r,out:n||t},Vs,e)}(t,Xs[t])}));const ou=iu.prototype;function au(t){Ja.call(this,null,t),this._adds=[],this._mods=[],this._alen=0,this._mlen=0,this._drop=!0,this._cross=!1,this._dims=[],this._dnames=[],this._measures=[],this._countOnly=!1,this._counts=null,this._prev=null,this._inputs=null,this._outputs=null}ou.reset=function(){this._add=[],this._rem=[],this._ext=null,this._get=null,this._q=null},ou.add=function(t){this._add.push(t)},ou.rem=function(t){this._rem.push(t)},ou.values=function(){if(this._get=null,0===this._rem.length)return this._add;const t=this._add,e=this._rem,n=this._key,r=t.length,i=e.length,o=Array(r-i),a={};let s,u,l;for(s=0;s<i;++s)a[n(e[s])]=1;for(s=0,u=0;s<r;++s)a[n(l=t[s])]?a[n(l)]=0:o[u++]=l;return this._rem=[],this._add=o},ou.distinct=function(t){const e=this.values(),n={};let r,i=e.length,o=0;for(;--i>=0;)r=t(e[i])+\"\",lt(n,r)||(n[r]=1,++o);return o},ou.extent=function(t){if(this._get!==t||!this._ext){const e=this.values(),n=ut(e,t);this._ext=[e[n[0]],e[n[1]]],this._get=t}return this._ext},ou.argmin=function(t){return this.extent(t)[0]||{}},ou.argmax=function(t){return this.extent(t)[1]||{}},ou.min=function(t){const e=this.extent(t)[0];return null!=e?t(e):void 0},ou.max=function(t){const e=this.extent(t)[1];return null!=e?t(e):void 0},ou.quartile=function(t){return this._get===t&&this._q||(this._q=ns(this.values(),t),this._get=t),this._q},ou.q1=function(t){return this.quartile(t)[0]},ou.q2=function(t){return this.quartile(t)[1]},ou.q3=function(t){return this.quartile(t)[2]},ou.ci=function(t){return this._get===t&&this._ci||(this._ci=os(this.values(),1e3,.05,t),this._get=t),this._ci},ou.ci0=function(t){return this.ci(t)[0]},ou.ci1=function(t){return this.ci(t)[1]},au.Definition={type:\"Aggregate\",metadata:{generates:!0,changes:!0},params:[{name:\"groupby\",type:\"field\",array:!0},{name:\"ops\",type:\"enum\",array:!0,values:Js},{name:\"aggregate_params\",type:\"number\",null:!0,array:!0},{name:\"fields\",type:\"field\",null:!0,array:!0},{name:\"as\",type:\"string\",null:!0,array:!0},{name:\"drop\",type:\"boolean\",default:!0},{name:\"cross\",type:\"boolean\",default:!1},{name:\"key\",type:\"field\"}]},dt(au,Ja,{transform(t,e){const n=this,r=e.fork(e.NO_SOURCE|e.NO_FIELDS),i=t.modified();return n.stamp=r.stamp,n.value&&(i||e.modified(n._inputs,!0))?(n._prev=n.value,n.value=i?n.init(t):Object.create(null),e.visit(e.SOURCE,(t=>n.add(t)))):(n.value=n.value||n.init(t),e.visit(e.REM,(t=>n.rem(t))),e.visit(e.ADD,(t=>n.add(t)))),r.modifies(n._outputs),n._drop=!1!==t.drop,t.cross&&n._dims.length>1&&(n._drop=!1,n.cross()),e.clean()&&n._drop&&r.clean(!0).runAfter((()=>this.clean())),n.changes(r)},cross(){const t=this,e=t.value,n=t._dnames,r=n.map((()=>({}))),i=n.length;function o(t){let e,o,a,s;for(e in t)for(a=t[e].tuple,o=0;o<i;++o)r[o][s=a[n[o]]]=s}o(t._prev),o(e),function o("
-  , "a,s,u){const l=n[u],c=r[u++];for(const n in c){const r=a?a+\"|\"+n:n;s[l]=c[n],u<i?o(r,s,u):e[r]||t.cell(r,s)}}(\"\",{},0)},init(t){const e=this._inputs=[],i=this._outputs=[],o={};function a(t){const n=X(r(t)),i=n.length;let a,s=0;for(;s<i;++s)o[a=n[s]]||(o[a]=1,e.push(a))}this._dims=X(t.groupby),this._dnames=this._dims.map((t=>{const e=n(t);return a(t),i.push(e),e})),this.cellkey=t.key?t.key:Hs(this._dims),this._countOnly=!0,this._counts=[],this._measures=[];const u=t.fields||[null],l=t.ops||[\"count\"],c=t.aggregate_params||[null],f=t.as||[],h=u.length,d={};let p,g,m,y,v,_,x;for(h!==l.length&&s(\"Unmatched number of fields and aggregate ops.\"),x=0;x<h;++x)p=u[x],g=l[x],m=c[x]||null,null==p&&\"count\"!==g&&s(\"Null aggregate field specified.\"),v=n(p),_=Ys(g,v,f[x]),i.push(_),\"count\"!==g?(y=d[v],y||(a(p),y=d[v]=[],y.field=p,this._measures.push(y)),\"count\"!==g&&(this._countOnly=!1),y.push(Zs(g,m,_))):this._counts.push(_);return this._measures=this._measures.map((t=>ru(t,t.field))),Object.create(null)},cellkey:Hs(),cell(t,e){let n=this.value[t];return n?0===n.num&&this._drop&&n.stamp<this.stamp?(n.stamp=this.stamp,this._adds[this._alen++]=n):n.stamp<this.stamp&&(n.stamp=this.stamp,this._mods[this._mlen++]=n):(n=this.value[t]=this.newcell(t,e),this._adds[this._alen++]=n),n},newcell(t,e){const n={key:t,num:0,agg:null,tuple:this.newtuple(e,this._prev&&this._prev[t]),stamp:this.stamp,store:!1};if(!this._countOnly){const t=this._measures,e=t.length;n.agg=Array(e);for(let r=0;r<e;++r)n.agg[r]=new t[r](n)}return n.store&&(n.data=new iu),n},newtuple(t,e){const n=this._dnames,r=this._dims,i=r.length,o={};for(let e=0;e<i;++e)o[n[e]]=r[e](t);return e?wa(e.tuple,o):_a(o)},clean(){const t=this.value;for(const e in t)0===t[e].num&&delete t[e]},add(t){const e=this.cellkey(t),n=this.cell(e,t);if(n.num+=1,this._countOnly)return;n.store&&n.data.add(t);const r=n.agg;for(let e=0,n=r.length;e<n;++e)r[e].add(r[e].get(t),t)},rem(t){const e=this.cellkey(t),n=this.cell(e,t);if(n.num-=1,this._countOnly)return;n.store&&n.data.rem(t);const r=n.agg;for(let e=0,n=r.length;e<n;++e)r[e].rem(r[e].get(t),t)},celltuple(t){const e=t.tuple,n=this._counts;t.store&&t.data.values();for(let r=0,i=n.length;r<i;++r)e[n[r]]=t.num;if(!this._countOnly){const n=t.agg;for(let t=0,r=n.length;t<r;++t)n[t].set(e)}return e},changes(t){const e=this._adds,n=this._mods,r=this._prev,i=this._drop,o=t.add,a=t.rem,s=t.mod;let u,l,c,f;if(r)for(l in r)u=r[l],i&&!u.num||a.push(u.tuple);for(c=0,f=this._alen;c<f;++c)o.push(this.celltuple(e[c])),e[c]=null;for(c=0,f=this._mlen;c<f;++c)u=n[c],(0===u.num&&i?a:s).push(this.celltuple(u)),n[c]=null;return this._alen=this._mlen=0,this._prev=null,t}});function su(t){Ja.call(this,null,t)}function uu(t,e,n){const r=t;let i=e||[],o=n||[],a={},s=0;return{add:t=>o.push(t),remove:t=>a[r(t)]=++s,size:()=>i.length,data:(t,e)=>(s&&(i=i.filter((t=>!a[r(t)])),a={},s=0),e&&t&&i.sort(t),o.length&&(i=t?At(t,i,o.sort(t)):i.concat(o),o=[]),i)}}function lu(t){Ja.call(this,[],t)}function cu(t){Sa.call(this,null,fu,t)}function fu(t){return this.value&&!t.modified()?this.value:K(t.fields,t.orders)}function hu(t){Ja.call(this,null,t)}function du(t){Ja.call(this,null,t)}su.Definition={type:\"Bin\",metadata:{modifies:!0},params:[{name:\"field\",type:\"field\",required:!0},{name:\"interval\",type:\"boolean\",default:!0},{name:\"anchor\",type:\"number\"},{name:\"maxbins\",type:\"number\",default:20},{name:\"base\",type:\"number\",default:10},{name:\"divide\",type:\"number\",array:!0,default:[5,2]},{name:\"extent\",type:\"number\",array:!0,length:2,required:!0},{name:\"span\",type:\"number\"},{name:\"step\",type:\"number\"},{name:\"steps\",type:\"number\",array:!0},{name:\"minstep\",type:\"number\",default:0},{name:\"nice\",type:\"boolean\",default:!0},{name:\"name\",type:\"string\"},{name:\"as\",type:\"string\",array:!0,length:2,default:[\"bin0\",\"bin1\"]}]},dt(su,Ja,{transform(t,e){const n=!1!==t.interval,i=this._bins(t),o=i.start,a=i.step,s=t.as||[\"bin0\",\"bin1\"],u=s[0],l=s[1];let c;return c=t.modified()?(e=e.reflow(!0)).SOURCE:e.modified(r(t.field))?e.ADD_MOD:e.ADD,e.visit(c,n?t=>{const e=i(t);t[u]=e,t[l]=null==e?null:o+a*(1+(e-o)/a)}:t"
-  , "=>t[u]=i(t)),e.modifies(n?s:u)},_bins(t){if(this.value&&!t.modified())return this.value;const i=t.field,o=is(t),a=o.step;let s,u,l=o.start,c=l+Math.ceil((o.stop-l)/a)*a;null!=(s=t.anchor)&&(u=s-(l+a*Math.floor((s-l)/a)),l+=u,c+=u);const f=function(t){let e=$(i(t));return null==e?null:e<l?-1/0:e>c?1/0:(e=Math.max(l,Math.min(e,c-a)),l+a*Math.floor(1e-14+(e-l)/a))};return f.start=l,f.stop=o.stop,f.step=a,this.value=e(f,r(i),t.name||\"bin_\"+n(i))}}),lu.Definition={type:\"Collect\",metadata:{source:!0},params:[{name:\"sort\",type:\"compare\"}]},dt(lu,Ja,{transform(t,e){const n=e.fork(e.ALL),r=uu(ya,this.value,n.materialize(n.ADD).add),i=t.sort,o=e.changed()||i&&(t.modified(\"sort\")||e.modified(i.fields));return n.visit(n.REM,r.remove),this.modified(o),this.value=n.source=r.data(ka(i),o),e.source&&e.source.root&&(this.value.root=e.source.root),n}}),dt(cu,Sa),hu.Definition={type:\"CountPattern\",metadata:{generates:!0,changes:!0},params:[{name:\"field\",type:\"field\",required:!0},{name:\"case\",type:\"enum\",values:[\"upper\",\"lower\",\"mixed\"],default:\"mixed\"},{name:\"pattern\",type:\"string\",default:'[\\\\w\"]+'},{name:\"stopwords\",type:\"string\",default:\"\"},{name:\"as\",type:\"string\",array:!0,length:2,default:[\"text\",\"count\"]}]},dt(hu,Ja,{transform(t,e){const n=e=>n=>{for(var r,i=function(t,e,n){switch(e){case\"upper\":t=t.toUpperCase();break;case\"lower\":t=t.toLowerCase()}return t.match(n)}(s(n),t.case,o)||[],u=0,l=i.length;u<l;++u)a.test(r=i[u])||e(r)},r=this._parameterCheck(t,e),i=this._counts,o=this._match,a=this._stop,s=t.field,u=t.as||[\"text\",\"count\"],l=n((t=>i[t]=1+(i[t]||0))),c=n((t=>i[t]-=1));return r?e.visit(e.SOURCE,l):(e.visit(e.ADD,l),e.visit(e.REM,c)),this._finish(e,u)},_parameterCheck(t,e){let n=!1;return!t.modified(\"stopwords\")&&this._stop||(this._stop=new RegExp(\"^\"+(t.stopwords||\"\")+\"$\",\"i\"),n=!0),!t.modified(\"pattern\")&&this._match||(this._match=new RegExp(t.pattern||\"[\\\\w']+\",\"g\"),n=!0),(t.modified(\"field\")||e.modified(t.field.fields))&&(n=!0),n&&(this._counts={}),n},_finish(t,e){const n=this._counts,r=this._tuples||(this._tuples={}),i=e[0],o=e[1],a=t.fork(t.NO_SOURCE|t.NO_FIELDS);let s,u,l;for(s in n)u=r[s],l=n[s]||0,!u&&l?(r[s]=u=_a({}),u[i]=s,u[o]=l,a.add.push(u)):0===l?(u&&a.rem.push(u),n[s]=null,r[s]=null):u[o]!==l&&(u[o]=l,a.mod.push(u));return a.modifies(e)}}),du.Definition={type:\"Cross\",metadata:{generates:!0},params:[{name:\"filter\",type:\"expr\"},{name:\"as\",type:\"string\",array:!0,length:2,default:[\"a\",\"b\"]}]},dt(du,Ja,{transform(t,e){const n=e.fork(e.NO_SOURCE),r=t.as||[\"a\",\"b\"],i=r[0],o=r[1],a=!this.value||e.changed(e.ADD_REM)||t.modified(\"as\")||t.modified(\"filter\");let s=this.value;return a?(s&&(n.rem=s),s=e.materialize(e.SOURCE).source,n.add=this.value=function(t,e,n,r){for(var i,o,a=[],s={},u=t.length,l=0;l<u;++l)for(s[e]=o=t[l],i=0;i<u;++i)s[n]=t[i],r(s)&&(a.push(_a(s)),(s={})[e]=o);return a}(s,i,o,t.filter||p)):n.mod=s,n.source=this.value,n.modifies(r)}});const pu={kde:gs,mixture:bs,normal:ps,lognormal:xs,uniform:Es},gu=\"function\";function mu(t,e){const n=t[gu];lt(pu,n)||s(\"Unknown distribution function: \"+n);const r=pu[n]();for(const n in t)\"field\"===n?r.data((t.from||e()).map(t[n])):\"distributions\"===n?r[n](t[n].map((t=>mu(t,e)))):typeof r[n]===gu&&r[n](t[n]);return r}function yu(t){Ja.call(this,null,t)}const vu=[{key:{function:\"normal\"},params:[{name:\"mean\",type:\"number\",default:0},{name:\"stdev\",type:\"number\",default:1}]},{key:{function:\"lognormal\"},params:[{name:\"mean\",type:\"number\",default:0},{name:\"stdev\",type:\"number\",default:1}]},{key:{function:\"uniform\"},params:[{name:\"min\",type:\"number\",default:0},{name:\"max\",type:\"number\",default:1}]},{key:{function:\"kde\"},params:[{name:\"field\",type:\"field\",required:!0},{name:\"from\",type:\"data\"},{name:\"bandwidth\",type:\"number\",default:0}]}],_u={key:{function:\"mixture\"},params:[{name:\"distributions\",type:\"param\",array:!0,params:vu},{name:\"weights\",type:\"number\",array:!0}]};function xu(t,e){return t?t.map(((t,r)=>e[r]||n(t))):null}function bu(t,e,n){const r=[],i=t=>t(u);let o,a,s,u,l,c;if(null==e)r.push(t.map(n));else for(o={},a=0,s=t.length;a<s;++a)u=t[a],l=e.map(i),c=o[l],c||(o[l]="
-  , "c=[],c.dims=l,r.push(c)),c.push(n(u));return r}yu.Definition={type:\"Density\",metadata:{generates:!0},params:[{name:\"extent\",type:\"number\",array:!0,length:2},{name:\"steps\",type:\"number\"},{name:\"minsteps\",type:\"number\",default:25},{name:\"maxsteps\",type:\"number\",default:200},{name:\"method\",type:\"string\",default:\"pdf\",values:[\"pdf\",\"cdf\"]},{name:\"distribution\",type:\"param\",params:vu.concat(_u)},{name:\"as\",type:\"string\",array:!0,default:[\"value\",\"density\"]}]},dt(yu,Ja,{transform(t,e){const n=e.fork(e.NO_SOURCE|e.NO_FIELDS);if(!this.value||e.changed()||t.modified()){const r=mu(t.distribution,function(t){return()=>t.materialize(t.SOURCE).source}(e)),i=t.steps||t.minsteps||25,o=t.steps||t.maxsteps||200;let a=t.method||\"pdf\";\"pdf\"!==a&&\"cdf\"!==a&&s(\"Invalid density method: \"+a),t.extent||r.data||s(\"Missing density extent parameter.\"),a=r[a];const u=t.as||[\"value\",\"density\"],l=Is(a,t.extent||st(r.data()),i,o).map((t=>{const e={};return e[u[0]]=t[0],e[u[1]]=t[1],_a(e)}));this.value&&(n.rem=this.value),this.value=n.add=n.source=l}return n}});function wu(t){Ja.call(this,null,t)}wu.Definition={type:\"DotBin\",metadata:{modifies:!0},params:[{name:\"field\",type:\"field\",required:!0},{name:\"groupby\",type:\"field\",array:!0},{name:\"step\",type:\"number\"},{name:\"smooth\",type:\"boolean\",default:!1},{name:\"as\",type:\"string\",default:\"bin\"}]};function ku(t){Sa.call(this,null,Au,t),this.modified(!0)}function Au(t){const i=t.expr;return this.value&&!t.modified(\"expr\")?this.value:e((e=>i(e,t)),r(i),n(i))}function Mu(t){Ja.call(this,[void 0,void 0],t)}function Eu(t,e){Sa.call(this,t),this.parent=e,this.count=0}function Du(t){Ja.call(this,{},t),this._keys=ft();const e=this._targets=[];e.active=0,e.forEach=t=>{for(let n=0,r=e.active;n<r;++n)t(e[n],n,e)}}function Cu(t){Sa.call(this,null,Fu,t)}function Fu(t){return this.value&&!t.modified()?this.value:A(t.name)?X(t.name).map((t=>l(t))):l(t.name,t.as)}function Su(t){Ja.call(this,ft(),t)}function $u(t){Ja.call(this,[],t)}function Tu(t){Ja.call(this,[],t)}function Bu(t){Ja.call(this,null,t)}function Nu(t){Ja.call(this,[],t)}dt(wu,Ja,{transform(t,e){if(this.value&&!t.modified()&&!e.changed())return e;const n=e.materialize(e.SOURCE).source,r=bu(e.source,t.groupby,f),i=t.smooth||!1,o=t.field,a=t.step||((t,e)=>Dt(st(t,e))/30)(n,o),s=ka(((t,e)=>o(t)-o(e))),u=t.as||\"bin\",l=r.length;let c,h=1/0,d=-1/0,p=0;for(;p<l;++p){const t=r[p].sort(s);c=-1;for(const e of as(t,a,i,o))e<h&&(h=e),e>d&&(d=e),t[++c][u]=e}return this.value={start:h,stop:d,step:a},e.reflow(!0).modifies(u)}}),dt(ku,Sa),Mu.Definition={type:\"Extent\",metadata:{},params:[{name:\"field\",type:\"field\",required:!0}]},dt(Mu,Ja,{transform(t,e){const r=this.value,i=t.field,o=e.changed()||e.modified(i.fields)||t.modified(\"field\");let a=r[0],s=r[1];if((o||null==a)&&(a=1/0,s=-1/0),e.visit(o?e.SOURCE:e.ADD,(t=>{const e=$(i(t));null!=e&&(e<a&&(a=e),e>s&&(s=e))})),!Number.isFinite(a)||!Number.isFinite(s)){let t=n(i);t&&(t=` for field \"${t}\"`),e.dataflow.warn(`Infinite extent${t}: [${a}, ${s}]`),a=s=void 0}this.value=[a,s]}}),dt(Eu,Sa,{connect(t){return this.detachSubflow=t.detachSubflow,this.targets().add(t),t.source=this},add(t){this.count+=1,this.value.add.push(t)},rem(t){this.count-=1,this.value.rem.push(t)},mod(t){this.value.mod.push(t)},init(t){this.value.init(t,t.NO_SOURCE)},evaluate(){return this.value}}),dt(Du,Ja,{activate(t){this._targets[this._targets.active++]=t},subflow(t,e,n,r){const i=this.value;let o,a,s=lt(i,t)&&i[t];return s?s.value.stamp<n.stamp&&(s.init(n),this.activate(s)):(a=r||(a=this._group[t])&&a.tuple,o=n.dataflow,s=new Eu(n.fork(n.NO_SOURCE),this),o.add(s).connect(e(o,t,a)),i[t]=s,this.activate(s)),s},clean(){const t=this.value;let e=0;for(const n in t)if(0===t[n].count){const r=t[n].detachSubflow;r&&r(),delete t[n],++e}if(e){const t=this._targets.filter((t=>t&&t.count>0));this.initTargets(t)}},initTargets(t){const e=this._targets,n=e.length,r=t?t.length:0;let i=0;for(;i<r;++i)e[i]=t[i];for(;i<n&&null!=e[i];++i)e[i]=null;e.active=r},transform(t,e){const n=e.dataflow,r=t.key,i=t.subflow,o=this._keys,a=t.modified(\"key\"),s=t=>this.subflow(t,i,e);retur"
-  , "n this._group=t.group||{},this.initTargets(),e.visit(e.REM,(t=>{const e=ya(t),n=o.get(e);void 0!==n&&(o.delete(e),s(n).rem(t))})),e.visit(e.ADD,(t=>{const e=r(t);o.set(ya(t),e),s(e).add(t)})),a||e.modified(r.fields)?e.visit(e.MOD,(t=>{const e=ya(t),n=o.get(e),i=r(t);n===i?s(i).mod(t):(o.set(e,i),s(n).rem(t),s(i).add(t))})):e.changed(e.MOD)&&e.visit(e.MOD,(t=>{s(o.get(ya(t))).mod(t)})),a&&e.visit(e.REFLOW,(t=>{const e=ya(t),n=o.get(e),i=r(t);n!==i&&(o.set(e,i),s(n).rem(t),s(i).add(t))})),e.clean()?n.runAfter((()=>{this.clean(),o.clean()})):o.empty>n.cleanThreshold&&n.runAfter(o.clean),e}}),dt(Cu,Sa),Su.Definition={type:\"Filter\",metadata:{changes:!0},params:[{name:\"expr\",type:\"expr\",required:!0}]},dt(Su,Ja,{transform(t,e){const n=e.dataflow,r=this.value,i=e.fork(),o=i.add,a=i.rem,s=i.mod,u=t.expr;let l=!0;function c(e){const n=ya(e),i=u(e,t),c=r.get(n);i&&c?(r.delete(n),o.push(e)):i||c?l&&i&&!c&&s.push(e):(r.set(n,1),a.push(e))}return e.visit(e.REM,(t=>{const e=ya(t);r.has(e)?r.delete(e):a.push(t)})),e.visit(e.ADD,(e=>{u(e,t)?o.push(e):r.set(ya(e),1)})),e.visit(e.MOD,c),t.modified()&&(l=!1,e.visit(e.REFLOW,c)),r.empty>n.cleanThreshold&&n.runAfter(r.clean),i}}),$u.Definition={type:\"Flatten\",metadata:{generates:!0},params:[{name:\"fields\",type:\"field\",array:!0,required:!0},{name:\"index\",type:\"string\"},{name:\"as\",type:\"string\",array:!0}]},dt($u,Ja,{transform(t,e){const n=e.fork(e.NO_SOURCE),r=t.fields,i=xu(r,t.as||[]),o=t.index||null,a=i.length;return n.rem=this.value,e.visit(e.SOURCE,(t=>{const e=r.map((e=>e(t))),s=e.reduce(((t,e)=>Math.max(t,e.length)),0);let u,l,c,f=0;for(;f<s;++f){for(l=xa(t),u=0;u<a;++u)l[i[u]]=null==(c=e[u][f])?null:c;o&&(l[o]=f),n.add.push(l)}})),this.value=n.source=n.add,o&&n.modifies(o),n.modifies(i)}}),Tu.Definition={type:\"Fold\",metadata:{generates:!0},params:[{name:\"fields\",type:\"field\",array:!0,required:!0},{name:\"as\",type:\"string\",array:!0,length:2,default:[\"key\",\"value\"]}]},dt(Tu,Ja,{transform(t,e){const r=e.fork(e.NO_SOURCE),i=t.fields,o=i.map(n),a=t.as||[\"key\",\"value\"],s=a[0],u=a[1],l=i.length;return r.rem=this.value,e.visit(e.SOURCE,(t=>{for(let e,n=0;n<l;++n)e=xa(t),e[s]=o[n],e[u]=i[n](t),r.add.push(e)})),this.value=r.source=r.add,r.modifies(a)}}),Bu.Definition={type:\"Formula\",metadata:{modifies:!0},params:[{name:\"expr\",type:\"expr\",required:!0},{name:\"as\",type:\"string\",required:!0},{name:\"initonly\",type:\"boolean\"}]},dt(Bu,Ja,{transform(t,e){const n=t.expr,r=t.as,i=t.modified(),o=t.initonly?e.ADD:i?e.SOURCE:e.modified(n.fields)||e.modified(r)?e.ADD_MOD:e.ADD;return i&&(e=e.materialize().reflow(!0)),t.initonly||e.modifies(r),e.visit(o,(e=>e[r]=n(e,t)))}}),dt(Nu,Ja,{transform(t,e){const n=e.fork(e.ALL),r=t.generator;let i,o,a,s=this.value,u=t.size-s.length;if(u>0){for(i=[];--u>=0;)i.push(a=_a(r(t))),s.push(a);n.add=n.add.length?n.materialize(n.ADD).add.concat(i):i}else o=s.slice(0,-u),n.rem=n.rem.length?n.materialize(n.REM).rem.concat(o):o,s=s.slice(-u);return n.source=this.value=s,n}});const zu={value:\"value\",median:Ce,mean:function(t,e){let n=0,r=0;if(void 0===e)for(let e of t)null!=e&&(e=+e)>=e&&(++n,r+=e);else{let i=-1;for(let o of t)null!=(o=e(o,++i,t))&&(o=+o)>=o&&(++n,r+=o)}if(n)return r/n},min:ke,max:we},Ou=[];function Ru(t){Ja.call(this,[],t)}function Lu(t){au.call(this,t)}function Uu(t){Ja.call(this,null,t)}function qu(t){Sa.call(this,null,Pu,t)}function Pu(t){return this.value&&!t.modified()?this.value:bt(t.fields,t.flat)}function ju(t){Ja.call(this,[],t),this._pending=null}function Iu(t,e,n){n.forEach(_a);const r=e.fork(e.NO_FIELDS&e.NO_SOURCE);return r.rem=t.value,t.value=r.source=r.add=n,t._pending=null,r.rem.length&&r.clean(!0),r}function Wu(t){Ja.call(this,{},t)}function Hu(t){Sa.call(this,null,Yu,t)}function Yu(t){if(this.value&&!t.modified())return this.value;const e=t.extents,n=e.length;let r,i,o=1/0,a=-1/0;for(r=0;r<n;++r)i=e[r],i[0]<o&&(o=i[0]),i[1]>a&&(a=i[1]);return[o,a]}function Gu(t){Sa.call(this,null,Vu,t)}function Vu(t){return this.value&&!t.modified()?this.value:t.values.reduce(((t,e)=>t.concat(e)),[])}function Xu(t){Ja.call(this,null,t)}function Ju(t){au.call(this,t)"
-  , "}function Zu(t){Du.call(this,t)}function Qu(t){Ja.call(this,null,t)}function Ku(t){Ja.call(this,null,t)}function tl(t){Ja.call(this,null,t)}Ru.Definition={type:\"Impute\",metadata:{changes:!0},params:[{name:\"field\",type:\"field\",required:!0},{name:\"key\",type:\"field\",required:!0},{name:\"keyvals\",array:!0},{name:\"groupby\",type:\"field\",array:!0},{name:\"method\",type:\"enum\",default:\"value\",values:[\"value\",\"mean\",\"median\",\"max\",\"min\"]},{name:\"value\",default:0}]},dt(Ru,Ja,{transform(t,e){var r,i,o,a,u,l,c,f,h,d,p=e.fork(e.ALL),g=function(t){var e,n=t.method||zu.value;if(null!=zu[n])return n===zu.value?(e=void 0!==t.value?t.value:0,()=>e):zu[n];s(\"Unrecognized imputation method: \"+n)}(t),m=function(t){const e=t.field;return t=>t?e(t):NaN}(t),y=n(t.field),v=n(t.key),_=(t.groupby||[]).map(n),x=function(t,e,n,r){var i,o,a,s,u,l,c,f,h=t=>t(f),d=[],p=r?r.slice():[],g={},m={};for(p.forEach(((t,e)=>g[t]=e+1)),s=0,c=t.length;s<c;++s)l=n(f=t[s]),u=g[l]||(g[l]=p.push(l)),(a=m[o=(i=e?e.map(h):Ou)+\"\"])||(a=m[o]=[],d.push(a),a.values=i),a[u-1]=f;return d.domain=p,d}(e.source,t.groupby,t.key,t.keyvals),b=[],w=this.value,k=x.domain.length;for(u=0,f=x.length;u<f;++u)for(o=(r=x[u]).values,i=NaN,c=0;c<k;++c)if(null==r[c]){for(a=x.domain[c],d={_impute:!0},l=0,h=o.length;l<h;++l)d[_[l]]=o[l];d[v]=a,d[y]=Number.isNaN(i)?i=g(r,m):i,b.push(_a(d))}return b.length&&(p.add=p.materialize(p.ADD).add.concat(b)),w.length&&(p.rem=p.materialize(p.REM).rem.concat(w)),this.value=b,p}}),Lu.Definition={type:\"JoinAggregate\",metadata:{modifies:!0},params:[{name:\"groupby\",type:\"field\",array:!0},{name:\"fields\",type:\"field\",null:!0,array:!0},{name:\"ops\",type:\"enum\",array:!0,values:Js},{name:\"as\",type:\"string\",null:!0,array:!0},{name:\"key\",type:\"field\"}]},dt(Lu,au,{transform(t,e){const n=this,r=t.modified();let i;return n.value&&(r||e.modified(n._inputs,!0))?(i=n.value=r?n.init(t):{},e.visit(e.SOURCE,(t=>n.add(t)))):(i=n.value=n.value||this.init(t),e.visit(e.REM,(t=>n.rem(t))),e.visit(e.ADD,(t=>n.add(t)))),n.changes(),e.visit(e.SOURCE,(t=>{at(t,i[n.cellkey(t)].tuple)})),e.reflow(r).modifies(this._outputs)},changes(){const t=this._adds,e=this._mods;let n,r;for(n=0,r=this._alen;n<r;++n)this.celltuple(t[n]),t[n]=null;for(n=0,r=this._mlen;n<r;++n)this.celltuple(e[n]),e[n]=null;this._alen=this._mlen=0}}),Uu.Definition={type:\"KDE\",metadata:{generates:!0},params:[{name:\"groupby\",type:\"field\",array:!0},{name:\"field\",type:\"field\",required:!0},{name:\"cumulative\",type:\"boolean\",default:!1},{name:\"counts\",type:\"boolean\",default:!1},{name:\"bandwidth\",type:\"number\",default:0},{name:\"extent\",type:\"number\",array:!0,length:2},{name:\"resolve\",type:\"enum\",values:[\"shared\",\"independent\"],default:\"independent\"},{name:\"steps\",type:\"number\"},{name:\"minsteps\",type:\"number\",default:25},{name:\"maxsteps\",type:\"number\",default:200},{name:\"as\",type:\"string\",array:!0,default:[\"value\",\"density\"]}]},dt(Uu,Ja,{transform(t,e){const r=e.fork(e.NO_SOURCE|e.NO_FIELDS);if(!this.value||e.changed()||t.modified()){const i=e.materialize(e.SOURCE).source,o=bu(i,t.groupby,t.field),a=(t.groupby||[]).map(n),u=t.bandwidth,l=t.cumulative?\"cdf\":\"pdf\",c=t.as||[\"value\",\"density\"],f=[];let h=t.extent,d=t.steps||t.minsteps||25,p=t.steps||t.maxsteps||200;\"pdf\"!==l&&\"cdf\"!==l&&s(\"Invalid density method: \"+l),\"shared\"===t.resolve&&(h||(h=st(i,t.field)),d=p=t.steps||p),o.forEach((e=>{const n=gs(e,u)[l],r=t.counts?e.length:1;Is(n,h||st(e),d,p).forEach((t=>{const n={};for(let t=0;t<a.length;++t)n[a[t]]=e.dims[t];n[c[0]]=t[0],n[c[1]]=t[1]*r,f.push(_a(n))}))})),this.value&&(r.rem=this.value),this.value=r.add=r.source=f}return r}}),dt(qu,Sa),dt(ju,Ja,{transform(t,e){const n=e.dataflow;if(this._pending)return Iu(this,e,this._pending);if(function(t){return t.modified(\"async\")&&!(t.modified(\"values\")||t.modified(\"url\")||t.modified(\"format\"))}(t))return e.StopPropagation;if(t.values)return Iu(this,e,n.parse(t.values,t.format));if(t.async){const e=n.request(t.url,t.format).then((t=>(this._pending=X(t.data),t=>t.touch(this))));return{async:e}}return n.request(t.url,t.format).then((t=>Iu(this,e,X(t.data))))}}),Wu.Definition={type:\"Lookup\",m"
-  , "etadata:{modifies:!0},params:[{name:\"index\",type:\"index\",params:[{name:\"from\",type:\"data\",required:!0},{name:\"key\",type:\"field\",required:!0}]},{name:\"values\",type:\"field\",array:!0},{name:\"fields\",type:\"field\",array:!0,required:!0},{name:\"as\",type:\"string\",array:!0},{name:\"default\",default:null}]},dt(Wu,Ja,{transform(t,e){const r=t.fields,i=t.index,o=t.values,a=null==t.default?null:t.default,u=t.modified(),l=r.length;let c,f,h,d=u?e.SOURCE:e.ADD,p=e,g=t.as;return o?(f=o.length,l>1&&!g&&s('Multi-field lookup requires explicit \"as\" parameter.'),g&&g.length!==l*f&&s('The \"as\" parameter has too few output field names.'),g=g||o.map(n),c=function(t){for(var e,n,s=0,u=0;s<l;++s)if(null==(n=i.get(r[s](t))))for(e=0;e<f;++e,++u)t[g[u]]=a;else for(e=0;e<f;++e,++u)t[g[u]]=o[e](n)}):(g||s(\"Missing output field names.\"),c=function(t){for(var e,n=0;n<l;++n)e=i.get(r[n](t)),t[g[n]]=null==e?a:e}),u?p=e.reflow(!0):(h=r.some((t=>e.modified(t.fields))),d|=h?e.MOD:0),e.visit(d,c),p.modifies(g)}}),dt(Hu,Sa),dt(Gu,Sa),dt(Xu,Ja,{transform(t,e){return this.modified(t.modified()),this.value=t,e.fork(e.NO_SOURCE|e.NO_FIELDS)}}),Ju.Definition={type:\"Pivot\",metadata:{generates:!0,changes:!0},params:[{name:\"groupby\",type:\"field\",array:!0},{name:\"field\",type:\"field\",required:!0},{name:\"value\",type:\"field\",required:!0},{name:\"op\",type:\"enum\",values:Js,default:\"sum\"},{name:\"limit\",type:\"number\",default:0},{name:\"key\",type:\"field\"}]},dt(Ju,au,{_transform:au.prototype.transform,transform(t,n){return this._transform(function(t,n){const i=t.field,o=t.value,a=(\"count\"===t.op?\"__count__\":t.op)||\"sum\",s=r(i).concat(r(o)),u=function(t,e,n){const r={},i=[];return n.visit(n.SOURCE,(e=>{const n=t(e);r[n]||(r[n]=1,i.push(n))})),i.sort(tt),e?i.slice(0,e):i}(i,t.limit||0,n);n.changed()&&t.set(\"__pivot__\",null,null,!0);return{key:t.key,groupby:t.groupby,ops:u.map((()=>a)),fields:u.map((t=>function(t,n,r,i){return e((e=>n(e)===t?r(e):NaN),i,t+\"\")}(t,i,o,s))),as:u.map((t=>t+\"\")),modified:t.modified.bind(t)}}(t,n),n)}}),dt(Zu,Du,{transform(t,e){const n=t.subflow,i=t.field,o=t=>this.subflow(ya(t),n,e,t);return(t.modified(\"field\")||i&&e.modified(r(i)))&&s(\"PreFacet does not support field modification.\"),this.initTargets(),i?(e.visit(e.MOD,(t=>{const e=o(t);i(t).forEach((t=>e.mod(t)))})),e.visit(e.ADD,(t=>{const e=o(t);i(t).forEach((t=>e.add(_a(t))))})),e.visit(e.REM,(t=>{const e=o(t);i(t).forEach((t=>e.rem(t)))}))):(e.visit(e.MOD,(t=>o(t).mod(t))),e.visit(e.ADD,(t=>o(t).add(t))),e.visit(e.REM,(t=>o(t).rem(t)))),e.clean()&&e.runAfter((()=>this.clean())),e}}),Qu.Definition={type:\"Project\",metadata:{generates:!0,changes:!0},params:[{name:\"fields\",type:\"field\",array:!0},{name:\"as\",type:\"string\",null:!0,array:!0}]},dt(Qu,Ja,{transform(t,e){const n=e.fork(e.NO_SOURCE),r=t.fields,i=xu(t.fields,t.as||[]),o=r?(t,e)=>function(t,e,n,r){for(let i=0,o=n.length;i<o;++i)e[r[i]]=n[i](t);return e}(t,e,r,i):ba;let a;return this.value?a=this.value:(e=e.addAll(),a=this.value={}),e.visit(e.REM,(t=>{const e=ya(t);n.rem.push(a[e]),a[e]=null})),e.visit(e.ADD,(t=>{const e=o(t,_a({}));a[ya(t)]=e,n.add.push(e)})),e.visit(e.MOD,(t=>{n.mod.push(o(t,a[ya(t)]))})),n}}),dt(Ku,Ja,{transform(t,e){return this.value=t.value,t.modified(\"value\")?e.fork(e.NO_SOURCE|e.NO_FIELDS):e.StopPropagation}}),tl.Definition={type:\"Quantile\",metadata:{generates:!0,changes:!0},params:[{name:\"groupby\",type:\"field\",array:!0},{name:\"field\",type:\"field\",required:!0},{name:\"probs\",type:\"number\",array:!0},{name:\"step\",type:\"number\",default:.01},{name:\"as\",type:\"string\",array:!0,default:[\"prob\",\"value\"]}]};function el(t){Ja.call(this,null,t)}function nl(t){Ja.call(this,[],t),this.count=0}function rl(t){Ja.call(this,null,t)}function il(t){Ja.call(this,null,t),this.modified(!0)}function ol(t){Ja.call(this,null,t)}dt(tl,Ja,{transform(t,e){const r=e.fork(e.NO_SOURCE|e.NO_FIELDS),i=t.as||[\"prob\",\"value\"];if(this.value&&!t.modified()&&!e.changed())return r.source=this.value,r;const o=bu(e.materialize(e.SOURCE).source,t.groupby,t.field),a=(t.groupby||[]).map(n),s=[],u=t.step||.01,l=t.probs||Se(u/2,1-1e-14,u),c=l.length;return o.forEach((t="
-  , ">{const e=es(t,l);for(let n=0;n<c;++n){const r={};for(let e=0;e<a.length;++e)r[a[e]]=t.dims[e];r[i[0]]=l[n],r[i[1]]=e[n],s.push(_a(r))}})),this.value&&(r.rem=this.value),this.value=r.add=r.source=s,r}}),dt(el,Ja,{transform(t,e){let n,r;return this.value?r=this.value:(n=e=e.addAll(),r=this.value={}),t.derive&&(n=e.fork(e.NO_SOURCE),e.visit(e.REM,(t=>{const e=ya(t);n.rem.push(r[e]),r[e]=null})),e.visit(e.ADD,(t=>{const e=xa(t);r[ya(t)]=e,n.add.push(e)})),e.visit(e.MOD,(t=>{const e=r[ya(t)];for(const r in t)e[r]=t[r],n.modifies(r);n.mod.push(e)}))),n}}),nl.Definition={type:\"Sample\",metadata:{},params:[{name:\"size\",type:\"number\",default:1e3}]},dt(nl,Ja,{transform(e,n){const r=n.fork(n.NO_SOURCE),i=e.modified(\"size\"),o=e.size,a=this.value.reduce(((t,e)=>(t[ya(e)]=1,t)),{});let s=this.value,u=this.count,l=0;function c(e){let n,i;s.length<o?s.push(e):(i=~~((u+1)*t.random()),i<s.length&&i>=l&&(n=s[i],a[ya(n)]&&r.rem.push(n),s[i]=e)),++u}if(n.rem.length&&(n.visit(n.REM,(t=>{const e=ya(t);a[e]&&(a[e]=-1,r.rem.push(t)),--u})),s=s.filter((t=>-1!==a[ya(t)]))),(n.rem.length||i)&&s.length<o&&n.source&&(l=u=s.length,n.visit(n.SOURCE,(t=>{a[ya(t)]||c(t)})),l=-1),i&&s.length>o){const t=s.length-o;for(let e=0;e<t;++e)a[ya(s[e])]=-1,r.rem.push(s[e]);s=s.slice(t)}return n.mod.length&&n.visit(n.MOD,(t=>{a[ya(t)]&&r.mod.push(t)})),n.add.length&&n.visit(n.ADD,c),(n.add.length||l<0)&&(r.add=s.filter((t=>!a[ya(t)]))),this.count=u,this.value=r.source=s,r}}),rl.Definition={type:\"Sequence\",metadata:{generates:!0,changes:!0},params:[{name:\"start\",type:\"number\",required:!0},{name:\"stop\",type:\"number\",required:!0},{name:\"step\",type:\"number\",default:1},{name:\"as\",type:\"string\",default:\"data\"}]},dt(rl,Ja,{transform(t,e){if(this.value&&!t.modified())return;const n=e.materialize().fork(e.MOD),r=t.as||\"data\";return n.rem=this.value?e.rem.concat(this.value):e.rem,this.value=Se(t.start,t.stop,t.step||1).map((t=>{const e={};return e[r]=t,_a(e)})),n.add=e.add.concat(this.value),n}}),dt(il,Ja,{transform(t,e){return this.value=e.source,e.changed()?e.fork(e.NO_SOURCE|e.NO_FIELDS):e.StopPropagation}});const al=[\"unit0\",\"unit1\"];function sl(t){Ja.call(this,ft(),t)}function ul(t){Ja.call(this,null,t)}ol.Definition={type:\"TimeUnit\",metadata:{modifies:!0},params:[{name:\"field\",type:\"field\",required:!0},{name:\"interval\",type:\"boolean\",default:!0},{name:\"units\",type:\"enum\",values:Kn,array:!0},{name:\"step\",type:\"number\",default:1},{name:\"maxbins\",type:\"number\",default:40},{name:\"extent\",type:\"date\",array:!0},{name:\"timezone\",type:\"enum\",default:\"local\",values:[\"local\",\"utc\"]},{name:\"as\",type:\"string\",array:!0,length:2,default:al}]},dt(ol,Ja,{transform(t,e){const n=t.field,i=!1!==t.interval,o=\"utc\"===t.timezone,a=this._floor(t,e),s=(o?Fr:Cr)(a.unit).offset,u=t.as||al,l=u[0],c=u[1],f=a.step;let h=a.start||1/0,d=a.stop||-1/0,p=e.ADD;return(t.modified()||e.changed(e.REM)||e.modified(r(n)))&&(p=(e=e.reflow(!0)).SOURCE,h=1/0,d=-1/0),e.visit(p,(t=>{const e=n(t);let r,o;null==e?(t[l]=null,i&&(t[c]=null)):(t[l]=r=o=a(e),i&&(t[c]=o=s(r,f)),r<h&&(h=r),o>d&&(d=o))})),a.start=h,a.stop=d,e.modifies(i?u:l)},_floor(t,e){const n=\"utc\"===t.timezone,{units:r,step:i}=t.units?{units:t.units,step:t.step||1}:Jr({extent:t.extent||st(e.materialize(e.SOURCE).source,t.field),maxbins:t.maxbins}),o=er(r),a=this.value||{},s=(n?Mr:wr)(o,i);return s.unit=S(o),s.units=o,s.step=i,s.start=a.start,s.stop=a.stop,this.value=s}}),dt(sl,Ja,{transform(t,e){const n=e.dataflow,r=t.field,i=this.value,o=t=>i.set(r(t),t);let a=!0;return t.modified(\"field\")||e.modified(r.fields)?(i.clear(),e.visit(e.SOURCE,o)):e.changed()?(e.visit(e.REM,(t=>i.delete(r(t)))),e.visit(e.ADD,o)):a=!1,this.modified(a),i.empty>n.cleanThreshold&&n.runAfter(i.clean),e.fork()}}),dt(ul,Ja,{transform(t,e){(!this.value||t.modified(\"field\")||t.modified(\"sort\")||e.changed()||t.sort&&e.modified(t.sort.fields))&&(this.value=(t.sort?e.source.slice().sort(ka(t.sort)):e.source).map(t.field))}});const ll={row_number:function(){return{next:t=>t.index+1}},rank:function(){let t;return{init:()=>t=1,next:e=>{const n=e.index,r=e.data;return n&&e.compare(r[n-1]"
-  , ",r[n])?t=n+1:t}}},dense_rank:function(){let t;return{init:()=>t=1,next:e=>{const n=e.index,r=e.data;return n&&e.compare(r[n-1],r[n])?++t:t}}},percent_rank:function(){const t=ll.rank(),e=t.next;return{init:t.init,next:t=>(e(t)-1)/(t.data.length-1)}},cume_dist:function(){let t;return{init:()=>t=0,next:e=>{const n=e.data,r=e.compare;let i=e.index;if(t<i){for(;i+1<n.length&&!r(n[i],n[i+1]);)++i;t=i}return(1+t)/n.length}}},ntile:function(t,e){(e=+e)>0||s(\"ntile num must be greater than zero.\");const n=ll.cume_dist(),r=n.next;return{init:n.init,next:t=>Math.ceil(e*r(t))}},lag:function(t,e){return e=+e||1,{next:n=>{const r=n.index-e;return r>=0?t(n.data[r]):null}}},lead:function(t,e){return e=+e||1,{next:n=>{const r=n.index+e,i=n.data;return r<i.length?t(i[r]):null}}},first_value:function(t){return{next:e=>t(e.data[e.i0])}},last_value:function(t){return{next:e=>t(e.data[e.i1-1])}},nth_value:function(t,e){return(e=+e)>0||s(\"nth_value nth must be greater than zero.\"),{next:n=>{const r=n.i0+(e-1);return r<n.i1?t(n.data[r]):null}}},prev_value:function(t){let e;return{init:()=>e=null,next:n=>{const r=t(n.data[n.index]);return null!=r?e=r:e}}},next_value:function(t){let e,n;return{init:()=>(e=null,n=-1),next:r=>{const i=r.data;return r.index<=n?e:(n=function(t,e,n){for(let r=e.length;n<r;++n){if(null!=t(e[n]))return n}return-1}(t,i,r.index))<0?(n=i.length,e=null):e=t(i[n])}}}};const cl=Object.keys(ll);function fl(t){const e=X(t.ops),i=X(t.fields),o=X(t.params),a=X(t.aggregate_params),u=X(t.as),l=this.outputs=[],c=this.windows=[],f={},d={},p=[],g=[];let m=!0;function y(t){X(r(t)).forEach((t=>f[t]=1))}y(t.sort),e.forEach(((t,e)=>{const r=i[e],f=o[e],v=a[e]||null,_=n(r),x=Ys(t,_,u[e]);if(y(r),l.push(x),lt(ll,t))c.push(function(t,e,n,r){const i=ll[t](e,n);return{init:i.init||h,update:function(t,e){e[r]=i.next(t)}}}(t,r,f,x));else{if(null==r&&\"count\"!==t&&s(\"Null aggregate field specified.\"),\"count\"===t)return void p.push(x);m=!1;let e=d[_];e||(e=d[_]=[],e.field=r,g.push(e)),e.push(Zs(t,v,x))}})),(p.length||g.length)&&(this.cell=function(t,e,n){t=t.map((t=>ru(t,t.field)));const r={num:0,agg:null,store:!1,count:e};if(!n)for(var i=t.length,o=r.agg=Array(i),a=0;a<i;++a)o[a]=new t[a](r);if(r.store)var s=r.data=new iu;return r.add=function(t){if(r.num+=1,!n){s&&s.add(t);for(let e=0;e<i;++e)o[e].add(o[e].get(t),t)}},r.rem=function(t){if(r.num-=1,!n){s&&s.rem(t);for(let e=0;e<i;++e)o[e].rem(o[e].get(t),t)}},r.set=function(t){let i,a;for(s&&s.values(),i=0,a=e.length;i<a;++i)t[e[i]]=r.num;if(!n)for(i=0,a=o.length;i<a;++i)o[i].set(t)},r.init=function(){r.num=0,s&&s.reset();for(let t=0;t<i;++t)o[t].init()},r}(g,p,m)),this.inputs=Object.keys(f)}const hl=fl.prototype;function dl(t){Ja.call(this,{},t),this._mlen=0,this._mods=[]}function pl(t,e,n,r){const i=r.sort,o=i&&!r.ignorePeers,a=r.frame||[null,0],s=t.data(n),u=s.length,l=o?ee(i):null,c={i0:0,i1:0,p0:0,p1:0,index:0,data:s,compare:i||it(-1)};e.init();for(let t=0;t<u;++t)gl(c,a,t,u),o&&ml(c,l),e.update(c,s[t])}function gl(t,e,n,r){t.p0=t.i0,t.p1=t.i1,t.i0=null==e[0]?0:Math.max(0,n-Math.abs(e[0])),t.i1=null==e[1]?r:Math.min(r,n+Math.abs(e[1])+1),t.index=n}function ml(t,e){const n=t.i0,r=t.i1-1,i=t.compare,o=t.data,a=o.length-1;n>0&&!i(o[n],o[n-1])&&(t.i0=e.left(o,o[n])),r<a&&!i(o[r],o[r+1])&&(t.i1=e.right(o,o[r]))}hl.init=function(){this.windows.forEach((t=>t.init())),this.cell&&this.cell.init()},hl.update=function(t,e){const n=this.cell,r=this.windows,i=t.data,o=r&&r.length;let a;if(n){for(a=t.p0;a<t.i0;++a)n.rem(i[a]);for(a=t.p1;a<t.i1;++a)n.add(i[a]);n.set(e)}for(a=0;a<o;++a)r[a].update(t,e)},dl.Definition={type:\"Window\",metadata:{modifies:!0},params:[{name:\"sort\",type:\"compare\"},{name:\"groupby\",type:\"field\",array:!0},{name:\"ops\",type:\"enum\",array:!0,values:cl.concat(Js)},{name:\"params\",type:\"number\",null:!0,array:!0},{name:\"aggregate_params\",type:\"number\",null:!0,array:!0},{name:\"fields\",type:\"field\",null:!0,array:!0},{name:\"as\",type:\"string\",null:!0,array:!0},{name:\"frame\",type:\"number\",null:!0,array:!0,length:2,default:[null,0]},{name:\"ignorePeers\",type:\"boolean\",default:!1}]},dt(dl,Ja,{transfo"
-  , "rm(t,e){this.stamp=e.stamp;const n=t.modified(),r=ka(t.sort),i=Hs(t.groupby),o=t=>this.group(i(t));let a=this.state;a&&!n||(a=this.state=new fl(t)),n||e.modified(a.inputs)?(this.value={},e.visit(e.SOURCE,(t=>o(t).add(t)))):(e.visit(e.REM,(t=>o(t).remove(t))),e.visit(e.ADD,(t=>o(t).add(t))));for(let e=0,n=this._mlen;e<n;++e)pl(this._mods[e],a,r,t);return this._mlen=0,this._mods=[],e.reflow(n).modifies(a.outputs)},group(t){let e=this.value[t];return e||(e=this.value[t]=uu(ya),e.stamp=-1),e.stamp<this.stamp&&(e.stamp=this.stamp,this._mods[this._mlen++]=e),e}});var yl=Object.freeze({__proto__:null,aggregate:au,bin:su,collect:lu,compare:cu,countpattern:hu,cross:du,density:yu,dotbin:wu,expression:ku,extent:Mu,facet:Du,field:Cu,filter:Su,flatten:$u,fold:Tu,formula:Bu,generate:Nu,impute:Ru,joinaggregate:Lu,kde:Uu,key:qu,load:ju,lookup:Wu,multiextent:Hu,multivalues:Gu,params:Xu,pivot:Ju,prefacet:Zu,project:Qu,proxy:Ku,quantile:tl,relay:el,sample:nl,sequence:rl,sieve:il,subflow:Eu,timeunit:ol,tupleindex:sl,values:ul,window:dl});function vl(t){return function(){return t}}const _l=Math.abs,xl=Math.atan2,bl=Math.cos,wl=Math.max,kl=Math.min,Al=Math.sin,Ml=Math.sqrt,El=1e-12,Dl=Math.PI,Cl=Dl/2,Fl=2*Dl;function Sl(t){return t>=1?Cl:t<=-1?-Cl:Math.asin(t)}const $l=Math.PI,Tl=2*$l,Bl=1e-6,Nl=Tl-Bl;function zl(t){this._+=t[0];for(let e=1,n=t.length;e<n;++e)this._+=arguments[e]+t[e]}let Ol=class{constructor(t){this._x0=this._y0=this._x1=this._y1=null,this._=\"\",this._append=null==t?zl:function(t){let e=Math.floor(t);if(!(e>=0))throw new Error(`invalid digits: ${t}`);if(e>15)return zl;const n=10**e;return function(t){this._+=t[0];for(let e=1,r=t.length;e<r;++e)this._+=Math.round(arguments[e]*n)/n+t[e]}}(t)}moveTo(t,e){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+e}`}closePath(){null!==this._x1&&(this._x1=this._x0,this._y1=this._y0,this._append`Z`)}lineTo(t,e){this._append`L${this._x1=+t},${this._y1=+e}`}quadraticCurveTo(t,e,n,r){this._append`Q${+t},${+e},${this._x1=+n},${this._y1=+r}`}bezierCurveTo(t,e,n,r,i,o){this._append`C${+t},${+e},${+n},${+r},${this._x1=+i},${this._y1=+o}`}arcTo(t,e,n,r,i){if(t=+t,e=+e,n=+n,r=+r,(i=+i)<0)throw new Error(`negative radius: ${i}`);let o=this._x1,a=this._y1,s=n-t,u=r-e,l=o-t,c=a-e,f=l*l+c*c;if(null===this._x1)this._append`M${this._x1=t},${this._y1=e}`;else if(f>Bl)if(Math.abs(c*s-u*l)>Bl&&i){let h=n-o,d=r-a,p=s*s+u*u,g=h*h+d*d,m=Math.sqrt(p),y=Math.sqrt(f),v=i*Math.tan(($l-Math.acos((p+f-g)/(2*m*y)))/2),_=v/y,x=v/m;Math.abs(_-1)>Bl&&this._append`L${t+_*l},${e+_*c}`,this._append`A${i},${i},0,0,${+(c*h>l*d)},${this._x1=t+x*s},${this._y1=e+x*u}`}else this._append`L${this._x1=t},${this._y1=e}`;else;}arc(t,e,n,r,i,o){if(t=+t,e=+e,o=!!o,(n=+n)<0)throw new Error(`negative radius: ${n}`);let a=n*Math.cos(r),s=n*Math.sin(r),u=t+a,l=e+s,c=1^o,f=o?r-i:i-r;null===this._x1?this._append`M${u},${l}`:(Math.abs(this._x1-u)>Bl||Math.abs(this._y1-l)>Bl)&&this._append`L${u},${l}`,n&&(f<0&&(f=f%Tl+Tl),f>Nl?this._append`A${n},${n},0,1,${c},${t-a},${e-s}A${n},${n},0,1,${c},${this._x1=u},${this._y1=l}`:f>Bl&&this._append`A${n},${n},0,${+(f>=$l)},${c},${this._x1=t+n*Math.cos(i)},${this._y1=e+n*Math.sin(i)}`)}rect(t,e,n,r){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+e}h${n=+n}v${+r}h${-n}Z`}toString(){return this._}};function Rl(){return new Ol}function Ll(t){let e=3;return t.digits=function(n){if(!arguments.length)return e;if(null==n)e=null;else{const t=Math.floor(n);if(!(t>=0))throw new RangeError(`invalid digits: ${n}`);e=t}return t},()=>new Ol(e)}function Ul(t){return t.innerRadius}function ql(t){return t.outerRadius}function Pl(t){return t.startAngle}function jl(t){return t.endAngle}function Il(t){return t&&t.padAngle}function Wl(t,e,n,r,i,o,a){var s=t-n,u=e-r,l=(a?o:-o)/Ml(s*s+u*u),c=l*u,f=-l*s,h=t+c,d=e+f,p=n+c,g=r+f,m=(h+p)/2,y=(d+g)/2,v=p-h,_=g-d,x=v*v+_*_,b=i-o,w=h*g-p*d,k=(_<0?-1:1)*Ml(wl(0,b*b*x-w*w)),A=(w*_-v*k)/x,M=(-w*v-_*k)/x,E=(w*_+v*k)/x,D=(-w*v+_*k)/x,C=A-m,F=M-y,S=E-m,$=D-y;return C*C+F*F>S*S+$*$&&(A=E,M=D),{cx:A,cy:M,x01:-c,y01:-f,x11:A*(i/b-1),y11:M*(i/b-1)}}function Hl(t){return\"ob"
-  , "ject\"==typeof t&&\"length\"in t?t:Array.from(t)}function Yl(t){this._context=t}function Gl(t){return new Yl(t)}function Vl(t){return t[0]}function Xl(t){return t[1]}function Jl(t,e){var n=vl(!0),r=null,i=Gl,o=null,a=Ll(s);function s(s){var u,l,c,f=(s=Hl(s)).length,h=!1;for(null==r&&(o=i(c=a())),u=0;u<=f;++u)!(u<f&&n(l=s[u],u,s))===h&&((h=!h)?o.lineStart():o.lineEnd()),h&&o.point(+t(l,u,s),+e(l,u,s));if(c)return o=null,c+\"\"||null}return t=\"function\"==typeof t?t:void 0===t?Vl:vl(t),e=\"function\"==typeof e?e:void 0===e?Xl:vl(e),s.x=function(e){return arguments.length?(t=\"function\"==typeof e?e:vl(+e),s):t},s.y=function(t){return arguments.length?(e=\"function\"==typeof t?t:vl(+t),s):e},s.defined=function(t){return arguments.length?(n=\"function\"==typeof t?t:vl(!!t),s):n},s.curve=function(t){return arguments.length?(i=t,null!=r&&(o=i(r)),s):i},s.context=function(t){return arguments.length?(null==t?r=o=null:o=i(r=t),s):r},s}function Zl(t,e,n){var r=null,i=vl(!0),o=null,a=Gl,s=null,u=Ll(l);function l(l){var c,f,h,d,p,g=(l=Hl(l)).length,m=!1,y=new Array(g),v=new Array(g);for(null==o&&(s=a(p=u())),c=0;c<=g;++c){if(!(c<g&&i(d=l[c],c,l))===m)if(m=!m)f=c,s.areaStart(),s.lineStart();else{for(s.lineEnd(),s.lineStart(),h=c-1;h>=f;--h)s.point(y[h],v[h]);s.lineEnd(),s.areaEnd()}m&&(y[c]=+t(d,c,l),v[c]=+e(d,c,l),s.point(r?+r(d,c,l):y[c],n?+n(d,c,l):v[c]))}if(p)return s=null,p+\"\"||null}function c(){return Jl().defined(i).curve(a).context(o)}return t=\"function\"==typeof t?t:void 0===t?Vl:vl(+t),e=\"function\"==typeof e?e:vl(void 0===e?0:+e),n=\"function\"==typeof n?n:void 0===n?Xl:vl(+n),l.x=function(e){return arguments.length?(t=\"function\"==typeof e?e:vl(+e),r=null,l):t},l.x0=function(e){return arguments.length?(t=\"function\"==typeof e?e:vl(+e),l):t},l.x1=function(t){return arguments.length?(r=null==t?null:\"function\"==typeof t?t:vl(+t),l):r},l.y=function(t){return arguments.length?(e=\"function\"==typeof t?t:vl(+t),n=null,l):e},l.y0=function(t){return arguments.length?(e=\"function\"==typeof t?t:vl(+t),l):e},l.y1=function(t){return arguments.length?(n=null==t?null:\"function\"==typeof t?t:vl(+t),l):n},l.lineX0=l.lineY0=function(){return c().x(t).y(e)},l.lineY1=function(){return c().x(t).y(n)},l.lineX1=function(){return c().x(r).y(e)},l.defined=function(t){return arguments.length?(i=\"function\"==typeof t?t:vl(!!t),l):i},l.curve=function(t){return arguments.length?(a=t,null!=o&&(s=a(o)),l):a},l.context=function(t){return arguments.length?(null==t?o=s=null:s=a(o=t),l):o},l}Rl.prototype=Ol.prototype,Yl.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2;default:this._context.lineTo(t,e)}}};var Ql={draw(t,e){const n=Ml(e/Dl);t.moveTo(n,0),t.arc(0,0,n,0,Fl)}};function Kl(){}function tc(t,e,n){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+e)/6,(t._y0+4*t._y1+n)/6)}function ec(t){this._context=t}function nc(t){this._context=t}function rc(t){this._context=t}function ic(t,e){this._basis=new ec(t),this._beta=e}ec.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:tc(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:tc(this,t,e)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=e}},nc.prototype={areaStart:Kl,areaEnd:Kl,lineStart:functio"
-  , "n(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._x2=t,this._y2=e;break;case 1:this._point=2,this._x3=t,this._y3=e;break;case 2:this._point=3,this._x4=t,this._y4=e,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+e)/6);break;default:tc(this,t,e)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=e}},rc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var n=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+e)/6;this._line?this._context.lineTo(n,r):this._context.moveTo(n,r);break;case 3:this._point=4;default:tc(this,t,e)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=e}},ic.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,e=this._y,n=t.length-1;if(n>0)for(var r,i=t[0],o=e[0],a=t[n]-i,s=e[n]-o,u=-1;++u<=n;)r=u/n,this._basis.point(this._beta*t[u]+(1-this._beta)*(i+r*a),this._beta*e[u]+(1-this._beta)*(o+r*s));this._x=this._y=null,this._basis.lineEnd()},point:function(t,e){this._x.push(+t),this._y.push(+e)}};var oc=function t(e){function n(t){return 1===e?new ec(t):new ic(t,e)}return n.beta=function(e){return t(+e)},n}(.85);function ac(t,e,n){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-e),t._y2+t._k*(t._y1-n),t._x2,t._y2)}function sc(t,e){this._context=t,this._k=(1-e)/6}sc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:ac(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2,this._x1=t,this._y1=e;break;case 2:this._point=3;default:ac(this,t,e)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};var uc=function t(e){function n(t){return new sc(t,e)}return n.tension=function(e){return t(+e)},n}(0);function lc(t,e){this._context=t,this._k=(1-e)/6}lc.prototype={areaStart:Kl,areaEnd:Kl,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._x3=t,this._y3=e;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=e);break;case 2:this._point=3,this._x5=t,this._y5=e;break;default:ac(this,t,e)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};var cc=function t(e){function n(t){return new lc(t,e)}return n.tension=function(e){return t(+e)},n}(0);function fc(t,e){this._context=t,this._k=(1-e)/6}fc.prototype={areaStart:function"
-  , "(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:ac(this,t,e)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};var hc=function t(e){function n(t){return new fc(t,e)}return n.tension=function(e){return t(+e)},n}(0);function dc(t,e,n){var r=t._x1,i=t._y1,o=t._x2,a=t._y2;if(t._l01_a>El){var s=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,u=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*s-t._x0*t._l12_2a+t._x2*t._l01_2a)/u,i=(i*s-t._y0*t._l12_2a+t._y2*t._l01_2a)/u}if(t._l23_a>El){var l=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,c=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*l+t._x1*t._l23_2a-e*t._l12_2a)/c,a=(a*l+t._y1*t._l23_2a-n*t._l12_2a)/c}t._context.bezierCurveTo(r,i,o,a,t._x2,t._y2)}function pc(t,e){this._context=t,this._alpha=e}pc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){if(t=+t,e=+e,this._point){var n=this._x2-t,r=this._y2-e;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(n*n+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2;break;case 2:this._point=3;default:dc(this,t,e)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};var gc=function t(e){function n(t){return e?new pc(t,e):new sc(t,0)}return n.alpha=function(e){return t(+e)},n}(.5);function mc(t,e){this._context=t,this._alpha=e}mc.prototype={areaStart:Kl,areaEnd:Kl,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,e){if(t=+t,e=+e,this._point){var n=this._x2-t,r=this._y2-e;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(n*n+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=e;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=e);break;case 2:this._point=3,this._x5=t,this._y5=e;break;default:dc(this,t,e)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};var yc=function t(e){function n(t){return e?new mc(t,e):new lc(t,0)}return n.alpha=function(e){return t(+e)},n}(.5);function vc(t,e){this._context=t,this._alpha=e}vc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){if(t=+t,e=+e,this._point){var n=this._x2-t,r=this."
-  , "_y2-e;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(n*n+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:dc(this,t,e)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};var _c=function t(e){function n(t){return e?new vc(t,e):new fc(t,0)}return n.alpha=function(e){return t(+e)},n}(.5);function xc(t){this._context=t}function bc(t){return t<0?-1:1}function wc(t,e,n){var r=t._x1-t._x0,i=e-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),a=(n-t._y1)/(i||r<0&&-0),s=(o*i+a*r)/(r+i);return(bc(o)+bc(a))*Math.min(Math.abs(o),Math.abs(a),.5*Math.abs(s))||0}function kc(t,e){var n=t._x1-t._x0;return n?(3*(t._y1-t._y0)/n-e)/2:e}function Ac(t,e,n){var r=t._x0,i=t._y0,o=t._x1,a=t._y1,s=(o-r)/3;t._context.bezierCurveTo(r+s,i+s*e,o-s,a-s*n,o,a)}function Mc(t){this._context=t}function Ec(t){this._context=new Dc(t)}function Dc(t){this._context=t}function Cc(t){this._context=t}function Fc(t){var e,n,r=t.length-1,i=new Array(r),o=new Array(r),a=new Array(r);for(i[0]=0,o[0]=2,a[0]=t[0]+2*t[1],e=1;e<r-1;++e)i[e]=1,o[e]=4,a[e]=4*t[e]+2*t[e+1];for(i[r-1]=2,o[r-1]=7,a[r-1]=8*t[r-1]+t[r],e=1;e<r;++e)n=i[e]/o[e-1],o[e]-=n,a[e]-=n*a[e-1];for(i[r-1]=a[r-1]/o[r-1],e=r-2;e>=0;--e)i[e]=(a[e]-i[e+1])/o[e];for(o[r-1]=(t[r]+i[r-1])/2,e=0;e<r-1;++e)o[e]=2*t[e+1]-i[e+1];return[i,o]}function Sc(t,e){this._context=t,this._t=e}function $c(t,e){if(\"undefined\"!=typeof document&&document.createElement){const n=document.createElement(\"canvas\");if(n&&n.getContext)return n.width=t,n.height=e,n}return null}xc.prototype={areaStart:Kl,areaEnd:Kl,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(t,e){t=+t,e=+e,this._point?this._context.lineTo(t,e):(this._point=1,this._context.moveTo(t,e))}},Mc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:Ac(this,this._t0,kc(this,this._t0))}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){var n=NaN;if(e=+e,(t=+t)!==this._x1||e!==this._y1){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2;break;case 2:this._point=3,Ac(this,kc(this,n=wc(this,t,e)),n);break;default:Ac(this,this._t0,n=wc(this,t,e))}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=e,this._t0=n}}},(Ec.prototype=Object.create(Mc.prototype)).point=function(t,e){Mc.prototype.point.call(this,e,t)},Dc.prototype={moveTo:function(t,e){this._context.moveTo(e,t)},closePath:function(){this._context.closePath()},lineTo:function(t,e){this._context.lineTo(e,t)},bezierCurveTo:function(t,e,n,r,i,o){this._context.bezierCurveTo(e,t,r,n,o,i)}},Cc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[],this._y=[]},lineEnd:function(){var t=this._x,e=this._y,n=t.length;if(n)if(this._line?this._context.lineTo(t[0],e[0]):this._context.moveTo(t[0],e[0]),2===n)this._context.lineTo(t[1],e[1]);else for(var r=Fc(t),i=Fc(e),o=0,a=1;a<n;++o,++a)this._context.bezierCurveTo(r[0][o],i[0][o],r[1][o],i[1][o],t[a],e[a]);(this._line||0!==this._line&&1===n)&&this._context.closePath(),this._line=1-this._line,this._x=this._y=null},point:function(t,e){this._x.push(+t),this._y.push(+e)}},Sc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=this._y=NaN,this._point=0},lineEnd:function(){0<this._t&&this._t<1&&2===this._point&&this._context.lineTo(this._x,this._y),(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line>=0&&(this._t=1-this._"
-  , "t,this._line=1-this._line)},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,e),this._context.lineTo(t,e);else{var n=this._x*(1-this._t)+t*this._t;this._context.lineTo(n,this._y),this._context.lineTo(n,e)}}this._x=t,this._y=e}};const Tc=()=>\"undefined\"!=typeof Image?Image:null;function Bc(t,e){switch(arguments.length){case 0:break;case 1:this.range(t);break;default:this.range(e).domain(t)}return this}function Nc(t,e){switch(arguments.length){case 0:break;case 1:\"function\"==typeof t?this.interpolator(t):this.range(t);break;default:this.domain(t),\"function\"==typeof e?this.interpolator(e):this.range(e)}return this}const zc=Symbol(\"implicit\");function Oc(){var t=new ue,e=[],n=[],r=zc;function i(i){let o=t.get(i);if(void 0===o){if(r!==zc)return r;t.set(i,o=e.push(i)-1)}return n[o%n.length]}return i.domain=function(n){if(!arguments.length)return e.slice();e=[],t=new ue;for(const r of n)t.has(r)||t.set(r,e.push(r)-1);return i},i.range=function(t){return arguments.length?(n=Array.from(t),i):n.slice()},i.unknown=function(t){return arguments.length?(r=t,i):r},i.copy=function(){return Oc(e,n).unknown(r)},Bc.apply(i,arguments),i}function Rc(t,e,n){t.prototype=e.prototype=n,n.constructor=t}function Lc(t,e){var n=Object.create(t.prototype);for(var r in e)n[r]=e[r];return n}function Uc(){}var qc=.7,Pc=1/qc,jc=\"\\\\s*([+-]?\\\\d+)\\\\s*\",Ic=\"\\\\s*([+-]?(?:\\\\d*\\\\.)?\\\\d+(?:[eE][+-]?\\\\d+)?)\\\\s*\",Wc=\"\\\\s*([+-]?(?:\\\\d*\\\\.)?\\\\d+(?:[eE][+-]?\\\\d+)?)%\\\\s*\",Hc=/^#([0-9a-f]{3,8})$/,Yc=new RegExp(`^rgb\\\\(${jc},${jc},${jc}\\\\)$`),Gc=new RegExp(`^rgb\\\\(${Wc},${Wc},${Wc}\\\\)$`),Vc=new RegExp(`^rgba\\\\(${jc},${jc},${jc},${Ic}\\\\)$`),Xc=new RegExp(`^rgba\\\\(${Wc},${Wc},${Wc},${Ic}\\\\)$`),Jc=new RegExp(`^hsl\\\\(${Ic},${Wc},${Wc}\\\\)$`),Zc=new RegExp(`^hsla\\\\(${Ic},${Wc},${Wc},${Ic}\\\\)$`),Qc={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:1"
-  , "4524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074};function Kc(){return this.rgb().formatHex()}function tf(){return this.rgb().formatRgb()}function ef(t){var e,n;return t=(t+\"\").trim().toLowerCase(),(e=Hc.exec(t))?(n=e[1].length,e=parseInt(e[1],16),6===n?nf(e):3===n?new sf(e>>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===n?rf(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===n?rf(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=Yc.exec(t))?new sf(e[1],e[2],e[3],1):(e=Gc.exec(t))?new sf(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=Vc.exec(t))?rf(e[1],e[2],e[3],e[4]):(e=Xc.exec(t))?rf(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=Jc.exec(t))?df(e[1],e[2]/100,e[3]/100,1):(e=Zc.exec(t))?df(e[1],e[2]/100,e[3]/100,e[4]):Qc.hasOwnProperty(t)?nf(Qc[t]):\"transparent\"===t?new sf(NaN,NaN,NaN,0):null}function nf(t){return new sf(t>>16&255,t>>8&255,255&t,1)}function rf(t,e,n,r){return r<=0&&(t=e=n=NaN),new sf(t,e,n,r)}function of(t){return t instanceof Uc||(t=ef(t)),t?new sf((t=t.rgb()).r,t.g,t.b,t.opacity):new sf}function af(t,e,n,r){return 1===arguments.length?of(t):new sf(t,e,n,null==r?1:r)}function sf(t,e,n,r){this.r=+t,this.g=+e,this.b=+n,this.opacity=+r}function uf(){return`#${hf(this.r)}${hf(this.g)}${hf(this.b)}`}function lf(){const t=cf(this.opacity);return`${1===t?\"rgb(\":\"rgba(\"}${ff(this.r)}, ${ff(this.g)}, ${ff(this.b)}${1===t?\")\":`, ${t})`}`}function cf(t){return isNaN(t)?1:Math.max(0,Math.min(1,t))}function ff(t){return Math.max(0,Math.min(255,Math.round(t)||0))}function hf(t){return((t=ff(t))<16?\"0\":\"\")+t.toString(16)}function df(t,e,n,r){return r<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new mf(t,e,n,r)}function pf(t){if(t instanceof mf)return new mf(t.h,t.s,t.l,t.opacity);if(t instanceof Uc||(t=ef(t)),!t)return new mf;if(t instanceof mf)return t;var e=(t=t.rgb()).r/255,n=t.g/255,r=t.b/255,i=Math.min(e,n,r),o=Math.max(e,n,r),a=NaN,s=o-i,u=(o+i)/2;return s?(a=e===o?(n-r)/s+6*(n<r):n===o?(r-e)/s+2:(e-n)/s+4,s/=u<.5?o+i:2-o-i,a*=60):s=u>0&&u<1?0:a,new mf(a,s,u,t.opacity)}function gf(t,e,n,r){return 1===arguments.length?pf(t):new mf(t,e,n,null==r?1:r)}function mf(t,e,n,r){this.h=+t,this.s=+e,this.l=+n,this.opacity=+r}function yf(t){return(t=(t||0)%360)<0?t+360:t}function vf(t){return Math.max(0,Math.min(1,t||0))}function _f(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}Rc(Uc,ef,{copy(t){return Object.assign(new this.constructor,this,t)},displayable(){return this.rgb().displayable()},hex:Kc,formatHex:Kc,formatHex8:function(){return this.rgb().formatHex8()},formatHsl:function(){return pf(this).formatHsl()},formatRgb:tf,toString:tf}),Rc(sf,af,Lc(Uc,{brighter(t){return t=null==t?Pc:Math.pow(Pc,t),new sf(this.r*t,this.g*t,this.b*t,this.opacity)},darker(t){return t=null==t?qc:Math.pow(qc,t),new sf(this.r*t,this.g*t,this.b*t,this.opacity)},rgb(){return this},clamp(){return new sf(ff(this.r),ff(this.g),ff(this.b),cf(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:uf,formatHex:uf,formatHex8:function(){return`#${hf(this.r)}${hf(this.g)}${hf(this.b)}${hf(255*(isNaN(this.opacity)?1:this.opacity))}`},formatRgb:lf,toString:lf})),Rc(mf,gf,Lc(Uc,{brighter(t){return t=null==t?Pc:Math.pow(Pc,t),new mf(this.h,this.s,this.l*t,this.opacity)},darker(t){return t=null==t?qc:Math.pow(qc,t),new mf(this.h,this.s,this.l*t,this.opacity)},rgb(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*e,i=2*n-r;return new sf(_f(t>=240?t-240:t+120,i,r),_f(t,i,r),_f(t<120?t+"
-  , "240:t-120,i,r),this.opacity)},clamp(){return new mf(yf(this.h),vf(this.s),vf(this.l),cf(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const t=cf(this.opacity);return`${1===t?\"hsl(\":\"hsla(\"}${yf(this.h)}, ${100*vf(this.s)}%, ${100*vf(this.l)}%${1===t?\")\":`, ${t})`}`}}));const xf=Math.PI/180,bf=180/Math.PI,wf=.96422,kf=1,Af=.82521,Mf=4/29,Ef=6/29,Df=3*Ef*Ef,Cf=Ef*Ef*Ef;function Ff(t){if(t instanceof $f)return new $f(t.l,t.a,t.b,t.opacity);if(t instanceof Rf)return Lf(t);t instanceof sf||(t=of(t));var e,n,r=zf(t.r),i=zf(t.g),o=zf(t.b),a=Tf((.2225045*r+.7168786*i+.0606169*o)/kf);return r===i&&i===o?e=n=a:(e=Tf((.4360747*r+.3850649*i+.1430804*o)/wf),n=Tf((.0139322*r+.0971045*i+.7141733*o)/Af)),new $f(116*a-16,500*(e-a),200*(a-n),t.opacity)}function Sf(t,e,n,r){return 1===arguments.length?Ff(t):new $f(t,e,n,null==r?1:r)}function $f(t,e,n,r){this.l=+t,this.a=+e,this.b=+n,this.opacity=+r}function Tf(t){return t>Cf?Math.pow(t,1/3):t/Df+Mf}function Bf(t){return t>Ef?t*t*t:Df*(t-Mf)}function Nf(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function zf(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Of(t,e,n,r){return 1===arguments.length?function(t){if(t instanceof Rf)return new Rf(t.h,t.c,t.l,t.opacity);if(t instanceof $f||(t=Ff(t)),0===t.a&&0===t.b)return new Rf(NaN,0<t.l&&t.l<100?0:NaN,t.l,t.opacity);var e=Math.atan2(t.b,t.a)*bf;return new Rf(e<0?e+360:e,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}(t):new Rf(t,e,n,null==r?1:r)}function Rf(t,e,n,r){this.h=+t,this.c=+e,this.l=+n,this.opacity=+r}function Lf(t){if(isNaN(t.h))return new $f(t.l,0,0,t.opacity);var e=t.h*xf;return new $f(t.l,Math.cos(e)*t.c,Math.sin(e)*t.c,t.opacity)}Rc($f,Sf,Lc(Uc,{brighter(t){return new $f(this.l+18*(null==t?1:t),this.a,this.b,this.opacity)},darker(t){return new $f(this.l-18*(null==t?1:t),this.a,this.b,this.opacity)},rgb(){var t=(this.l+16)/116,e=isNaN(this.a)?t:t+this.a/500,n=isNaN(this.b)?t:t-this.b/200;return new sf(Nf(3.1338561*(e=wf*Bf(e))-1.6168667*(t=kf*Bf(t))-.4906146*(n=Af*Bf(n))),Nf(-.9787684*e+1.9161415*t+.033454*n),Nf(.0719453*e-.2289914*t+1.4052427*n),this.opacity)}})),Rc(Rf,Of,Lc(Uc,{brighter(t){return new Rf(this.h,this.c,this.l+18*(null==t?1:t),this.opacity)},darker(t){return new Rf(this.h,this.c,this.l-18*(null==t?1:t),this.opacity)},rgb(){return Lf(this).rgb()}}));var Uf=-.14861,qf=1.78277,Pf=-.29227,jf=-.90649,If=1.97294,Wf=If*jf,Hf=If*qf,Yf=qf*Pf-jf*Uf;function Gf(t,e,n,r){return 1===arguments.length?function(t){if(t instanceof Vf)return new Vf(t.h,t.s,t.l,t.opacity);t instanceof sf||(t=of(t));var e=t.r/255,n=t.g/255,r=t.b/255,i=(Yf*r+Wf*e-Hf*n)/(Yf+Wf-Hf),o=r-i,a=(If*(n-i)-Pf*o)/jf,s=Math.sqrt(a*a+o*o)/(If*i*(1-i)),u=s?Math.atan2(a,o)*bf-120:NaN;return new Vf(u<0?u+360:u,s,i,t.opacity)}(t):new Vf(t,e,n,null==r?1:r)}function Vf(t,e,n,r){this.h=+t,this.s=+e,this.l=+n,this.opacity=+r}function Xf(t,e,n,r,i){var o=t*t,a=o*t;return((1-3*t+3*o-a)*e+(4-6*o+3*a)*n+(1+3*t+3*o-3*a)*r+a*i)/6}function Jf(t){var e=t.length-1;return function(n){var r=n<=0?n=0:n>=1?(n=1,e-1):Math.floor(n*e),i=t[r],o=t[r+1],a=r>0?t[r-1]:2*i-o,s=r<e-1?t[r+2]:2*o-i;return Xf((n-r/e)*e,a,i,o,s)}}function Zf(t){var e=t.length;return function(n){var r=Math.floor(((n%=1)<0?++n:n)*e),i=t[(r+e-1)%e],o=t[r%e],a=t[(r+1)%e],s=t[(r+2)%e];return Xf((n-r/e)*e,i,o,a,s)}}Rc(Vf,Gf,Lc(Uc,{brighter(t){return t=null==t?Pc:Math.pow(Pc,t),new Vf(this.h,this.s,this.l*t,this.opacity)},darker(t){return t=null==t?qc:Math.pow(qc,t),new Vf(this.h,this.s,this.l*t,this.opacity)},rgb(){var t=isNaN(this.h)?0:(this.h+120)*xf,e=+this.l,n=isNaN(this.s)?0:this.s*e*(1-e),r=Math.cos(t),i=Math.sin(t);return new sf(255*(e+n*(Uf*r+qf*i)),255*(e+n*(Pf*r+jf*i)),255*(e+n*(If*r)),this.opacity)}}));var Qf=t=>()=>t;function Kf(t,e){return function(n){return t+n*e}}function th(t,e){var n=e-t;return n?Kf(t,n>180||n<-180?n-360*Math.round(n/360):n):Qf(isNaN(t)?e:t)}function eh(t){return 1==(t=+t)?nh:function(e,n){return n-e?function(t,e,n){return t=Math.pow(t,n),e=Math.pow(e,n)-"
-  , "t,n=1/n,function(r){return Math.pow(t+r*e,n)}}(e,n,t):Qf(isNaN(e)?n:e)}}function nh(t,e){var n=e-t;return n?Kf(t,n):Qf(isNaN(t)?e:t)}var rh=function t(e){var n=eh(e);function r(t,e){var r=n((t=af(t)).r,(e=af(e)).r),i=n(t.g,e.g),o=n(t.b,e.b),a=nh(t.opacity,e.opacity);return function(e){return t.r=r(e),t.g=i(e),t.b=o(e),t.opacity=a(e),t+\"\"}}return r.gamma=t,r}(1);function ih(t){return function(e){var n,r,i=e.length,o=new Array(i),a=new Array(i),s=new Array(i);for(n=0;n<i;++n)r=af(e[n]),o[n]=r.r||0,a[n]=r.g||0,s[n]=r.b||0;return o=t(o),a=t(a),s=t(s),r.opacity=1,function(t){return r.r=o(t),r.g=a(t),r.b=s(t),r+\"\"}}}var oh=ih(Jf),ah=ih(Zf);function sh(t,e){e||(e=[]);var n,r=t?Math.min(e.length,t.length):0,i=e.slice();return function(o){for(n=0;n<r;++n)i[n]=t[n]*(1-o)+e[n]*o;return i}}function uh(t){return ArrayBuffer.isView(t)&&!(t instanceof DataView)}function lh(t,e){var n,r=e?e.length:0,i=t?Math.min(r,t.length):0,o=new Array(i),a=new Array(r);for(n=0;n<i;++n)o[n]=mh(t[n],e[n]);for(;n<r;++n)a[n]=e[n];return function(t){for(n=0;n<i;++n)a[n]=o[n](t);return a}}function ch(t,e){var n=new Date;return t=+t,e=+e,function(r){return n.setTime(t*(1-r)+e*r),n}}function fh(t,e){return t=+t,e=+e,function(n){return t*(1-n)+e*n}}function hh(t,e){var n,r={},i={};for(n in null!==t&&\"object\"==typeof t||(t={}),null!==e&&\"object\"==typeof e||(e={}),e)n in t?r[n]=mh(t[n],e[n]):i[n]=e[n];return function(t){for(n in r)i[n]=r[n](t);return i}}var dh=/[-+]?(?:\\d+\\.?\\d*|\\.?\\d+)(?:[eE][-+]?\\d+)?/g,ph=new RegExp(dh.source,\"g\");function gh(t,e){var n,r,i,o=dh.lastIndex=ph.lastIndex=0,a=-1,s=[],u=[];for(t+=\"\",e+=\"\";(n=dh.exec(t))&&(r=ph.exec(e));)(i=r.index)>o&&(i=e.slice(o,i),s[a]?s[a]+=i:s[++a]=i),(n=n[0])===(r=r[0])?s[a]?s[a]+=r:s[++a]=r:(s[++a]=null,u.push({i:a,x:fh(n,r)})),o=ph.lastIndex;return o<e.length&&(i=e.slice(o),s[a]?s[a]+=i:s[++a]=i),s.length<2?u[0]?function(t){return function(e){return t(e)+\"\"}}(u[0].x):function(t){return function(){return t}}(e):(e=u.length,function(t){for(var n,r=0;r<e;++r)s[(n=u[r]).i]=n.x(t);return s.join(\"\")})}function mh(t,e){var n,r=typeof e;return null==e||\"boolean\"===r?Qf(e):(\"number\"===r?fh:\"string\"===r?(n=ef(e))?(e=n,rh):gh:e instanceof ef?rh:e instanceof Date?ch:uh(e)?sh:Array.isArray(e)?lh:\"function\"!=typeof e.valueOf&&\"function\"!=typeof e.toString||isNaN(e)?hh:fh)(t,e)}function yh(t,e){return t=+t,e=+e,function(n){return Math.round(t*(1-n)+e*n)}}var vh,_h=180/Math.PI,xh={translateX:0,translateY:0,rotate:0,skewX:0,scaleX:1,scaleY:1};function bh(t,e,n,r,i,o){var a,s,u;return(a=Math.sqrt(t*t+e*e))&&(t/=a,e/=a),(u=t*n+e*r)&&(n-=t*u,r-=e*u),(s=Math.sqrt(n*n+r*r))&&(n/=s,r/=s,u/=s),t*r<e*n&&(t=-t,e=-e,u=-u,a=-a),{translateX:i,translateY:o,rotate:Math.atan2(e,t)*_h,skewX:Math.atan(u)*_h,scaleX:a,scaleY:s}}function wh(t,e,n,r){function i(t){return t.length?t.pop()+\" \":\"\"}return function(o,a){var s=[],u=[];return o=t(o),a=t(a),function(t,r,i,o,a,s){if(t!==i||r!==o){var u=a.push(\"translate(\",null,e,null,n);s.push({i:u-4,x:fh(t,i)},{i:u-2,x:fh(r,o)})}else(i||o)&&a.push(\"translate(\"+i+e+o+n)}(o.translateX,o.translateY,a.translateX,a.translateY,s,u),function(t,e,n,o){t!==e?(t-e>180?e+=360:e-t>180&&(t+=360),o.push({i:n.push(i(n)+\"rotate(\",null,r)-2,x:fh(t,e)})):e&&n.push(i(n)+\"rotate(\"+e+r)}(o.rotate,a.rotate,s,u),function(t,e,n,o){t!==e?o.push({i:n.push(i(n)+\"skewX(\",null,r)-2,x:fh(t,e)}):e&&n.push(i(n)+\"skewX(\"+e+r)}(o.skewX,a.skewX,s,u),function(t,e,n,r,o,a){if(t!==n||e!==r){var s=o.push(i(o)+\"scale(\",null,\",\",null,\")\");a.push({i:s-4,x:fh(t,n)},{i:s-2,x:fh(e,r)})}else 1===n&&1===r||o.push(i(o)+\"scale(\"+n+\",\"+r+\")\")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,s,u),o=a=null,function(t){for(var e,n=-1,r=u.length;++n<r;)s[(e=u[n]).i]=e.x(t);return s.join(\"\")}}}var kh=wh((function(t){const e=new(\"function\"==typeof DOMMatrix?DOMMatrix:WebKitCSSMatrix)(t+\"\");return e.isIdentity?xh:bh(e.a,e.b,e.c,e.d,e.e,e.f)}),\"px, \",\"px)\",\"deg)\"),Ah=wh((function(t){return null==t?xh:(vh||(vh=document.createElementNS(\"http://www.w3.org/2000/svg\",\"g\")),vh.setAttribute(\"transform\",t),(t=vh.transform.baseVal.consolidate())?bh((t=t.matrix).a,t.b,t.c,t.d,t."
-  , "e,t.f):xh)}),\", \",\")\",\")\");function Mh(t){return((t=Math.exp(t))+1/t)/2}var Eh=function t(e,n,r){function i(t,i){var o,a,s=t[0],u=t[1],l=t[2],c=i[0],f=i[1],h=i[2],d=c-s,p=f-u,g=d*d+p*p;if(g<1e-12)a=Math.log(h/l)/e,o=function(t){return[s+t*d,u+t*p,l*Math.exp(e*t*a)]};else{var m=Math.sqrt(g),y=(h*h-l*l+r*g)/(2*l*n*m),v=(h*h-l*l-r*g)/(2*h*n*m),_=Math.log(Math.sqrt(y*y+1)-y),x=Math.log(Math.sqrt(v*v+1)-v);a=(x-_)/e,o=function(t){var r=t*a,i=Mh(_),o=l/(n*m)*(i*function(t){return((t=Math.exp(2*t))-1)/(t+1)}(e*r+_)-function(t){return((t=Math.exp(t))-1/t)/2}(_));return[s+o*d,u+o*p,l*i/Mh(e*r+_)]}}return o.duration=1e3*a*e/Math.SQRT2,o}return i.rho=function(e){var n=Math.max(.001,+e),r=n*n;return t(n,r,r*r)},i}(Math.SQRT2,2,4);function Dh(t){return function(e,n){var r=t((e=gf(e)).h,(n=gf(n)).h),i=nh(e.s,n.s),o=nh(e.l,n.l),a=nh(e.opacity,n.opacity);return function(t){return e.h=r(t),e.s=i(t),e.l=o(t),e.opacity=a(t),e+\"\"}}}var Ch=Dh(th),Fh=Dh(nh);function Sh(t){return function(e,n){var r=t((e=Of(e)).h,(n=Of(n)).h),i=nh(e.c,n.c),o=nh(e.l,n.l),a=nh(e.opacity,n.opacity);return function(t){return e.h=r(t),e.c=i(t),e.l=o(t),e.opacity=a(t),e+\"\"}}}var $h=Sh(th),Th=Sh(nh);function Bh(t){return function e(n){function r(e,r){var i=t((e=Gf(e)).h,(r=Gf(r)).h),o=nh(e.s,r.s),a=nh(e.l,r.l),s=nh(e.opacity,r.opacity);return function(t){return e.h=i(t),e.s=o(t),e.l=a(Math.pow(t,n)),e.opacity=s(t),e+\"\"}}return n=+n,r.gamma=e,r}(1)}var Nh=Bh(th),zh=Bh(nh);function Oh(t,e){void 0===e&&(e=t,t=mh);for(var n=0,r=e.length-1,i=e[0],o=new Array(r<0?0:r);n<r;)o[n]=t(i,i=e[++n]);return function(t){var e=Math.max(0,Math.min(r-1,Math.floor(t*=r)));return o[e](t-e)}}var Rh=Object.freeze({__proto__:null,interpolate:mh,interpolateArray:function(t,e){return(uh(e)?sh:lh)(t,e)},interpolateBasis:Jf,interpolateBasisClosed:Zf,interpolateCubehelix:Nh,interpolateCubehelixLong:zh,interpolateDate:ch,interpolateDiscrete:function(t){var e=t.length;return function(n){return t[Math.max(0,Math.min(e-1,Math.floor(n*e)))]}},interpolateHcl:$h,interpolateHclLong:Th,interpolateHsl:Ch,interpolateHslLong:Fh,interpolateHue:function(t,e){var n=th(+t,+e);return function(t){var e=n(t);return e-360*Math.floor(e/360)}},interpolateLab:function(t,e){var n=nh((t=Sf(t)).l,(e=Sf(e)).l),r=nh(t.a,e.a),i=nh(t.b,e.b),o=nh(t.opacity,e.opacity);return function(e){return t.l=n(e),t.a=r(e),t.b=i(e),t.opacity=o(e),t+\"\"}},interpolateNumber:fh,interpolateNumberArray:sh,interpolateObject:hh,interpolateRgb:rh,interpolateRgbBasis:oh,interpolateRgbBasisClosed:ah,interpolateRound:yh,interpolateString:gh,interpolateTransformCss:kh,interpolateTransformSvg:Ah,interpolateZoom:Eh,piecewise:Oh,quantize:function(t,e){for(var n=new Array(e),r=0;r<e;++r)n[r]=t(r/(e-1));return n}});function Lh(t){return+t}var Uh=[0,1];function qh(t){return t}function Ph(t,e){return(e-=t=+t)?function(n){return(n-t)/e}:function(t){return function(){return t}}(isNaN(e)?NaN:.5)}function jh(t,e,n){var r=t[0],i=t[1],o=e[0],a=e[1];return i<r?(r=Ph(i,r),o=n(a,o)):(r=Ph(r,i),o=n(o,a)),function(t){return o(r(t))}}function Ih(t,e,n){var r=Math.min(t.length,e.length)-1,i=new Array(r),o=new Array(r),a=-1;for(t[r]<t[0]&&(t=t.slice().reverse(),e=e.slice().reverse());++a<r;)i[a]=Ph(t[a],t[a+1]),o[a]=n(e[a],e[a+1]);return function(e){var n=oe(t,e,1,r)-1;return o[n](i[n](e))}}function Wh(t,e){return e.domain(t.domain()).range(t.range()).interpolate(t.interpolate()).clamp(t.clamp()).unknown(t.unknown())}function Hh(){var t,e,n,r,i,o,a=Uh,s=Uh,u=mh,l=qh;function c(){var t=Math.min(a.length,s.length);return l!==qh&&(l=function(t,e){var n;return t>e&&(n=t,t=e,e=n),function(n){return Math.max(t,Math.min(e,n))}}(a[0],a[t-1])),r=t>2?Ih:jh,i=o=null,f}function f(e){return null==e||isNaN(e=+e)?n:(i||(i=r(a.map(t),s,u)))(t(l(e)))}return f.invert=function(n){return l(e((o||(o=r(s,a.map(t),fh)))(n)))},f.domain=function(t){return arguments.length?(a=Array.from(t,Lh),c()):a.slice()},f.range=function(t){return arguments.length?(s=Array.from(t),c()):s.slice()},f.rangeRound=function(t){return s=Array.from(t),u=yh,c()},f.clamp=function(t){return arguments.length?(l=!!t||qh,c("
-  , ")):l!==qh},f.interpolate=function(t){return arguments.length?(u=t,c()):u},f.unknown=function(t){return arguments.length?(n=t,f):n},function(n,r){return t=n,e=r,c()}}function Yh(){return Hh()(qh,qh)}function Gh(t,e,n,r){var i,o=be(t,e,n);switch((r=Re(null==r?\",f\":r)).type){case\"s\":var a=Math.max(Math.abs(t),Math.abs(e));return null!=r.precision||isNaN(i=Xe(o,a))||(r.precision=i),We(r,a);case\"\":case\"e\":case\"g\":case\"p\":case\"r\":null!=r.precision||isNaN(i=Je(o,Math.max(Math.abs(t),Math.abs(e))))||(r.precision=i-(\"e\"===r.type));break;case\"f\":case\"%\":null!=r.precision||isNaN(i=Ve(o))||(r.precision=i-2*(\"%\"===r.type))}return Ie(r)}function Vh(t){var e=t.domain;return t.ticks=function(t){var n=e();return _e(n[0],n[n.length-1],null==t?10:t)},t.tickFormat=function(t,n){var r=e();return Gh(r[0],r[r.length-1],null==t?10:t,n)},t.nice=function(n){null==n&&(n=10);var r,i,o=e(),a=0,s=o.length-1,u=o[a],l=o[s],c=10;for(l<u&&(i=u,u=l,l=i,i=a,a=s,s=i);c-- >0;){if((i=xe(u,l,n))===r)return o[a]=u,o[s]=l,e(o);if(i>0)u=Math.floor(u/i)*i,l=Math.ceil(l/i)*i;else{if(!(i<0))break;u=Math.ceil(u*i)/i,l=Math.floor(l*i)/i}r=i}return t},t}function Xh(t,e){var n,r=0,i=(t=t.slice()).length-1,o=t[r],a=t[i];return a<o&&(n=r,r=i,i=n,n=o,o=a,a=n),t[r]=e.floor(o),t[i]=e.ceil(a),t}function Jh(t){return Math.log(t)}function Zh(t){return Math.exp(t)}function Qh(t){return-Math.log(-t)}function Kh(t){return-Math.exp(-t)}function td(t){return isFinite(t)?+(\"1e\"+t):t<0?0:t}function ed(t){return(e,n)=>-t(-e,n)}function nd(t){const e=t(Jh,Zh),n=e.domain;let r,i,o=10;function a(){return r=function(t){return t===Math.E?Math.log:10===t&&Math.log10||2===t&&Math.log2||(t=Math.log(t),e=>Math.log(e)/t)}(o),i=function(t){return 10===t?td:t===Math.E?Math.exp:e=>Math.pow(t,e)}(o),n()[0]<0?(r=ed(r),i=ed(i),t(Qh,Kh)):t(Jh,Zh),e}return e.base=function(t){return arguments.length?(o=+t,a()):o},e.domain=function(t){return arguments.length?(n(t),a()):n()},e.ticks=t=>{const e=n();let a=e[0],s=e[e.length-1];const u=s<a;u&&([a,s]=[s,a]);let l,c,f=r(a),h=r(s);const d=null==t?10:+t;let p=[];if(!(o%1)&&h-f<d){if(f=Math.floor(f),h=Math.ceil(h),a>0){for(;f<=h;++f)for(l=1;l<o;++l)if(c=f<0?l/i(-f):l*i(f),!(c<a)){if(c>s)break;p.push(c)}}else for(;f<=h;++f)for(l=o-1;l>=1;--l)if(c=f>0?l/i(-f):l*i(f),!(c<a)){if(c>s)break;p.push(c)}2*p.length<d&&(p=_e(a,s,d))}else p=_e(f,h,Math.min(h-f,d)).map(i);return u?p.reverse():p},e.tickFormat=(t,n)=>{if(null==t&&(t=10),null==n&&(n=10===o?\"s\":\",\"),\"function\"!=typeof n&&(o%1||null!=(n=Re(n)).precision||(n.trim=!0),n=Ie(n)),t===1/0)return n;const a=Math.max(1,o*t/e.ticks().length);return t=>{let e=t/i(Math.round(r(t)));return e*o<o-.5&&(e*=o),e<=a?n(t):\"\"}},e.nice=()=>n(Xh(n(),{floor:t=>i(Math.floor(r(t))),ceil:t=>i(Math.ceil(r(t)))})),e}function rd(t){return function(e){return Math.sign(e)*Math.log1p(Math.abs(e/t))}}function id(t){return function(e){return Math.sign(e)*Math.expm1(Math.abs(e))*t}}function od(t){var e=1,n=t(rd(e),id(e));return n.constant=function(n){return arguments.length?t(rd(e=+n),id(e)):e},Vh(n)}function ad(t){return function(e){return e<0?-Math.pow(-e,t):Math.pow(e,t)}}function sd(t){return t<0?-Math.sqrt(-t):Math.sqrt(t)}function ud(t){return t<0?-t*t:t*t}function ld(t){var e=t(qh,qh),n=1;return e.exponent=function(e){return arguments.length?1===(n=+e)?t(qh,qh):.5===n?t(sd,ud):t(ad(n),ad(1/n)):n},Vh(e)}function cd(){var t=ld(Hh());return t.copy=function(){return Wh(t,cd()).exponent(t.exponent())},Bc.apply(t,arguments),t}function fd(t){return new Date(t)}function hd(t){return t instanceof Date?+t:+new Date(+t)}function dd(t,e,n,r,i,o,a,s,u,l){var c=Yh(),f=c.invert,h=c.domain,d=l(\".%L\"),p=l(\":%S\"),g=l(\"%I:%M\"),m=l(\"%I %p\"),y=l(\"%a %d\"),v=l(\"%b %d\"),_=l(\"%B\"),x=l(\"%Y\");function b(t){return(u(t)<t?d:s(t)<t?p:a(t)<t?g:o(t)<t?m:r(t)<t?i(t)<t?y:v:n(t)<t?_:x)(t)}return c.invert=function(t){return new Date(f(t))},c.domain=function(t){return arguments.length?h(Array.from(t,hd)):h().map(fd)},c.ticks=function(e){var n=h();return t(n[0],n[n.length-1],null==e?10:e)},c.tickFormat=function(t,e){return null==e?b:l(e)},c.nice=function(t){var n=h();return t&&\"f"
-  , "unction\"==typeof t.range||(t=e(n[0],n[n.length-1],null==t?10:t)),t?h(Xh(n,t)):c},c.copy=function(){return Wh(c,dd(t,e,n,r,i,o,a,s,u,l))},c}function pd(){var t,e,n,r,i,o=0,a=1,s=qh,u=!1;function l(e){return null==e||isNaN(e=+e)?i:s(0===n?.5:(e=(r(e)-t)*n,u?Math.max(0,Math.min(1,e)):e))}function c(t){return function(e){var n,r;return arguments.length?([n,r]=e,s=t(n,r),l):[s(0),s(1)]}}return l.domain=function(i){return arguments.length?([o,a]=i,t=r(o=+o),e=r(a=+a),n=t===e?0:1/(e-t),l):[o,a]},l.clamp=function(t){return arguments.length?(u=!!t,l):u},l.interpolator=function(t){return arguments.length?(s=t,l):s},l.range=c(mh),l.rangeRound=c(yh),l.unknown=function(t){return arguments.length?(i=t,l):i},function(i){return r=i,t=i(o),e=i(a),n=t===e?0:1/(e-t),l}}function gd(t,e){return e.domain(t.domain()).interpolator(t.interpolator()).clamp(t.clamp()).unknown(t.unknown())}function md(){var t=Vh(pd()(qh));return t.copy=function(){return gd(t,md())},Nc.apply(t,arguments)}function yd(){var t=ld(pd());return t.copy=function(){return gd(t,yd()).exponent(t.exponent())},Nc.apply(t,arguments)}function vd(){var t,e,n,r,i,o,a,s=0,u=.5,l=1,c=1,f=qh,h=!1;function d(t){return isNaN(t=+t)?a:(t=.5+((t=+o(t))-e)*(c*t<c*e?r:i),f(h?Math.max(0,Math.min(1,t)):t))}function p(t){return function(e){var n,r,i;return arguments.length?([n,r,i]=e,f=Oh(t,[n,r,i]),d):[f(0),f(.5),f(1)]}}return d.domain=function(a){return arguments.length?([s,u,l]=a,t=o(s=+s),e=o(u=+u),n=o(l=+l),r=t===e?0:.5/(e-t),i=e===n?0:.5/(n-e),c=e<t?-1:1,d):[s,u,l]},d.clamp=function(t){return arguments.length?(h=!!t,d):h},d.interpolator=function(t){return arguments.length?(f=t,d):f},d.range=p(mh),d.rangeRound=p(yh),d.unknown=function(t){return arguments.length?(a=t,d):a},function(a){return o=a,t=a(s),e=a(u),n=a(l),r=t===e?0:.5/(e-t),i=e===n?0:.5/(n-e),c=e<t?-1:1,d}}function _d(){var t=ld(vd());return t.copy=function(){return gd(t,_d()).exponent(t.exponent())},Nc.apply(t,arguments)}function xd(t){for(var e=t.length/6|0,n=new Array(e),r=0;r<e;)n[r]=\"#\"+t.slice(6*r,6*++r);return n}var bd=xd(\"1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf\"),wd=xd(\"7fc97fbeaed4fdc086ffff99386cb0f0027fbf5b17666666\"),kd=xd(\"1b9e77d95f027570b3e7298a66a61ee6ab02a6761d666666\"),Ad=xd(\"4269d0efb118ff725c6cc5b03ca951ff8ab7a463f297bbf59c6b4e9498a0\"),Md=xd(\"a6cee31f78b4b2df8a33a02cfb9a99e31a1cfdbf6fff7f00cab2d66a3d9affff99b15928\"),Ed=xd(\"fbb4aeb3cde3ccebc5decbe4fed9a6ffffcce5d8bdfddaecf2f2f2\"),Dd=xd(\"b3e2cdfdcdaccbd5e8f4cae4e6f5c9fff2aef1e2cccccccc\"),Cd=xd(\"e41a1c377eb84daf4a984ea3ff7f00ffff33a65628f781bf999999\"),Fd=xd(\"66c2a5fc8d628da0cbe78ac3a6d854ffd92fe5c494b3b3b3\"),Sd=xd(\"8dd3c7ffffb3bebadafb807280b1d3fdb462b3de69fccde5d9d9d9bc80bdccebc5ffed6f\");function $d(t,e,n){const r=t-e+2*n;return t?r>0?r:1:0}const Td=\"linear\",Bd=\"log\",Nd=\"pow\",zd=\"sqrt\",Od=\"symlog\",Rd=\"time\",Ld=\"utc\",Ud=\"sequential\",qd=\"diverging\",Pd=\"quantile\",jd=\"quantize\",Id=\"threshold\",Wd=\"ordinal\",Hd=\"point\",Yd=\"band\",Gd=\"bin-ordinal\",Vd=\"continuous\",Xd=\"discrete\",Jd=\"discretizing\",Zd=\"interpolating\",Qd=\"temporal\";function Kd(){const t=Oc().unknown(void 0),e=t.domain,n=t.range;let r,i,o=[0,1],a=!1,s=0,u=0,l=.5;function c(){const t=e().length,c=o[1]<o[0],f=o[1-c],h=$d(t,s,u);let d=o[c-0];r=(f-d)/(h||1),a&&(r=Math.floor(r)),d+=(f-d-r*(t-s))*l,i=r*(1-s),a&&(d=Math.round(d),i=Math.round(i));const p=Se(t).map((t=>d+r*t));return n(c?p.reverse():p)}return delete t.unknown,t.domain=function(t){return arguments.length?(e(t),c()):e()},t.range=function(t){return arguments.length?(o=[+t[0],+t[1]],c()):o.slice()},t.rangeRound=function(t){return o=[+t[0],+t[1]],a=!0,c()},t.bandwidth=function(){return i},t.step=function(){return r},t.round=function(t){return arguments.length?(a=!!t,c()):a},t.padding=function(t){return arguments.length?(u=Math.max(0,Math.min(1,t)),s=u,c()):s},t.paddingInner=function(t){return arguments.length?(s=Math.max(0,Math.min(1,t)),c()):s},t.paddingOuter=function(t){return arguments.length?(u=Math.max(0,Math.min(1,t)),c()):u},t.align=function(t){return arguments.length?(l=Math.max(0,Math.min(1,t)),c()):l},t.invertRange=function(t){i"
-  , "f(null==t[0]||null==t[1])return;const r=o[1]<o[0],a=r?n().reverse():n(),s=a.length-1;let u,l,c,f=+t[0],h=+t[1];return f!=f||h!=h||(h<f&&(c=f,f=h,h=c),h<a[0]||f>o[1-r])?void 0:(u=Math.max(0,oe(a,f)-1),l=f===h?u:oe(a,h)-1,f-a[u]>i+1e-10&&++u,r&&(c=u,u=s-l,l=s-c),u>l?void 0:e().slice(u,l+1))},t.invert=function(e){const n=t.invertRange([e,e]);return n?n[0]:n},t.copy=function(){return Kd().domain(e()).range(o).round(a).paddingInner(s).paddingOuter(u).align(l)},c()}function tp(t){const e=t.copy;return t.padding=t.paddingOuter,delete t.paddingInner,t.copy=function(){return tp(e())},t}var ep=Array.prototype.map;const np=Array.prototype.slice;const rp=new Map,ip=Symbol(\"vega_scale\");function op(t){return t[ip]=!0,t}function ap(t){return t&&!0===t[ip]}function sp(t,e,n){return arguments.length>1?(rp.set(t,function(t,e,n){const r=function(){const n=e();return n.invertRange||(n.invertRange=n.invert?function(t){return function(e){let n,r=e[0],i=e[1];return i<r&&(n=r,r=i,i=n),[t.invert(r),t.invert(i)]}}(n):n.invertExtent?function(t){return function(e){const n=t.range();let r,i,o,a,s=e[0],u=e[1],l=-1;for(u<s&&(i=s,s=u,u=i),o=0,a=n.length;o<a;++o)n[o]>=s&&n[o]<=u&&(l<0&&(l=o),r=o);if(!(l<0))return s=t.invertExtent(n[l]),u=t.invertExtent(n[r]),[void 0===s[0]?s[1]:s[0],void 0===u[1]?u[0]:u[1]]}}(n):void 0),n.type=t,op(n)};return r.metadata=Bt(X(n)),r}(t,e,n)),this):up(t)?rp.get(t):void 0}function up(t){return rp.has(t)}function lp(t,e){const n=rp.get(t);return n&&n.metadata[e]}function cp(t){return lp(t,Vd)}function fp(t){return lp(t,Xd)}function hp(t){return lp(t,Jd)}function dp(t){return lp(t,Bd)}function pp(t){return lp(t,Zd)}function gp(t){return lp(t,Pd)}sp(\"identity\",(function t(e){var n;function r(t){return null==t||isNaN(t=+t)?n:t}return r.invert=r,r.domain=r.range=function(t){return arguments.length?(e=Array.from(t,Lh),r):e.slice()},r.unknown=function(t){return arguments.length?(n=t,r):n},r.copy=function(){return t(e).unknown(n)},e=arguments.length?Array.from(e,Lh):[0,1],Vh(r)})),sp(Td,(function t(){var e=Yh();return e.copy=function(){return Wh(e,t())},Bc.apply(e,arguments),Vh(e)}),Vd),sp(Bd,(function t(){const e=nd(Hh()).domain([1,10]);return e.copy=()=>Wh(e,t()).base(e.base()),Bc.apply(e,arguments),e}),[Vd,Bd]),sp(Nd,cd,Vd),sp(zd,(function(){return cd.apply(null,arguments).exponent(.5)}),Vd),sp(Od,(function t(){var e=od(Hh());return e.copy=function(){return Wh(e,t()).constant(e.constant())},Bc.apply(e,arguments)}),Vd),sp(Rd,(function(){return Bc.apply(dd(qn,Pn,zn,Bn,vn,pn,hn,cn,ln,ni).domain([new Date(2e3,0,1),new Date(2e3,0,2)]),arguments)}),[Vd,Qd]),sp(Ld,(function(){return Bc.apply(dd(Ln,Un,On,Nn,En,gn,dn,fn,ln,ii).domain([Date.UTC(2e3,0,1),Date.UTC(2e3,0,2)]),arguments)}),[Vd,Qd]),sp(Ud,md,[Vd,Zd]),sp(`${Ud}-${Td}`,md,[Vd,Zd]),sp(`${Ud}-${Bd}`,(function t(){var e=nd(pd()).domain([1,10]);return e.copy=function(){return gd(e,t()).base(e.base())},Nc.apply(e,arguments)}),[Vd,Zd,Bd]),sp(`${Ud}-${Nd}`,yd,[Vd,Zd]),sp(`${Ud}-${zd}`,(function(){return yd.apply(null,arguments).exponent(.5)}),[Vd,Zd]),sp(`${Ud}-${Od}`,(function t(){var e=od(pd());return e.copy=function(){return gd(e,t()).constant(e.constant())},Nc.apply(e,arguments)}),[Vd,Zd]),sp(`${qd}-${Td}`,(function t(){var e=Vh(vd()(qh));return e.copy=function(){return gd(e,t())},Nc.apply(e,arguments)}),[Vd,Zd]),sp(`${qd}-${Bd}`,(function t(){var e=nd(vd()).domain([.1,1,10]);return e.copy=function(){return gd(e,t()).base(e.base())},Nc.apply(e,arguments)}),[Vd,Zd,Bd]),sp(`${qd}-${Nd}`,_d,[Vd,Zd]),sp(`${qd}-${zd}`,(function(){return _d.apply(null,arguments).exponent(.5)}),[Vd,Zd]),sp(`${qd}-${Od}`,(function t(){var e=od(vd());return e.copy=function(){return gd(e,t()).constant(e.constant())},Nc.apply(e,arguments)}),[Vd,Zd]),sp(Pd,(function t(){var e,n=[],r=[],i=[];function o(){var t=0,e=Math.max(1,r.length);for(i=new Array(e-1);++t<e;)i[t-1]=De(n,t/e);return a}function a(t){return null==t||isNaN(t=+t)?e:r[oe(i,t)]}return a.invertExtent=function(t){var e=r.indexOf(t);return e<0?[NaN,NaN]:[e>0?i[e-1]:n[0],e<i.length?i[e]:n[n.length-1]]},a.domain=function(t){if(!arguments.length)return"
-  , " n.slice();n=[];for(let e of t)null==e||isNaN(e=+e)||n.push(e);return n.sort(Kt),o()},a.range=function(t){return arguments.length?(r=Array.from(t),o()):r.slice()},a.unknown=function(t){return arguments.length?(e=t,a):e},a.quantiles=function(){return i.slice()},a.copy=function(){return t().domain(n).range(r).unknown(e)},Bc.apply(a,arguments)}),[Jd,Pd]),sp(jd,(function t(){var e,n=0,r=1,i=1,o=[.5],a=[0,1];function s(t){return null!=t&&t<=t?a[oe(o,t,0,i)]:e}function u(){var t=-1;for(o=new Array(i);++t<i;)o[t]=((t+1)*r-(t-i)*n)/(i+1);return s}return s.domain=function(t){return arguments.length?([n,r]=t,n=+n,r=+r,u()):[n,r]},s.range=function(t){return arguments.length?(i=(a=Array.from(t)).length-1,u()):a.slice()},s.invertExtent=function(t){var e=a.indexOf(t);return e<0?[NaN,NaN]:e<1?[n,o[0]]:e>=i?[o[i-1],r]:[o[e-1],o[e]]},s.unknown=function(t){return arguments.length?(e=t,s):s},s.thresholds=function(){return o.slice()},s.copy=function(){return t().domain([n,r]).range(a).unknown(e)},Bc.apply(Vh(s),arguments)}),Jd),sp(Id,(function t(){var e,n=[.5],r=[0,1],i=1;function o(t){return null!=t&&t<=t?r[oe(n,t,0,i)]:e}return o.domain=function(t){return arguments.length?(n=Array.from(t),i=Math.min(n.length,r.length-1),o):n.slice()},o.range=function(t){return arguments.length?(r=Array.from(t),i=Math.min(n.length,r.length-1),o):r.slice()},o.invertExtent=function(t){var e=r.indexOf(t);return[n[e-1],n[e]]},o.unknown=function(t){return arguments.length?(e=t,o):e},o.copy=function(){return t().domain(n).range(r).unknown(e)},Bc.apply(o,arguments)}),Jd),sp(Gd,(function t(){let e=[],n=[];function r(t){return null==t||t!=t?void 0:n[(oe(e,t)-1)%n.length]}return r.domain=function(t){return arguments.length?(e=function(t){return ep.call(t,$)}(t),r):e.slice()},r.range=function(t){return arguments.length?(n=np.call(t),r):n.slice()},r.tickFormat=function(t,n){return Gh(e[0],S(e),null==t?10:t,n)},r.copy=function(){return t().domain(r.domain()).range(r.range())},r}),[Xd,Jd]),sp(Wd,Oc,Xd),sp(Yd,Kd,Xd),sp(Hd,(function(){return tp(Kd().paddingInner(1))}),Xd);const mp=[\"clamp\",\"base\",\"constant\",\"exponent\"];function yp(t,e){const n=e[0],r=S(e)-n;return function(e){return t(n+e*r)}}function vp(t,e,n){return Oh(bp(e||\"rgb\",n),t)}function _p(t,e){const n=new Array(e),r=e+1;for(let i=0;i<e;)n[i]=t(++i/r);return n}function xp(t,e,n){const r=n-e;let i,o,a;return r&&Number.isFinite(r)?(i=(o=t.type).indexOf(\"-\"),o=i<0?o:o.slice(i+1),a=sp(o)().domain([e,n]).range([0,1]),mp.forEach((e=>t[e]?a[e](t[e]()):0)),a):it(.5)}function bp(t,e){const n=Rh[function(t){return\"interpolate\"+t.toLowerCase().split(\"-\").map((t=>t[0].toUpperCase()+t.slice(1))).join(\"\")}(t)];return null!=e&&n&&n.gamma?n.gamma(e):n}function wp(t){if(A(t))return t;const e=t.length/6|0,n=new Array(e);for(let r=0;r<e;)n[r]=\"#\"+t.slice(6*r,6*++r);return n}function kp(t,e){for(const n in t)Mp(n,e(t[n]))}const Ap={};function Mp(t,e){return t=t&&t.toLowerCase(),arguments.length>1?(Ap[t]=e,this):Ap[t]}kp({accent:wd,category10:bd,category20:\"1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5\",category20b:\"393b795254a36b6ecf9c9ede6379398ca252b5cf6bcedb9c8c6d31bd9e39e7ba52e7cb94843c39ad494ad6616be7969c7b4173a55194ce6dbdde9ed6\",category20c:\"3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9\",dark2:kd,observable10:Ad,paired:Md,pastel1:Ed,pastel2:Dd,set1:Cd,set2:Fd,set3:Sd,tableau10:\"4c78a8f58518e4575672b7b254a24beeca3bb279a2ff9da69d755dbab0ac\",tableau20:\"4c78a89ecae9f58518ffbf7954a24b88d27ab79a20f2cf5b43989483bcb6e45756ff9d9879706ebab0acd67195fcbfd2b279a2d6a5c99e765fd8b5a5\"},wp),kp({blues:\"cfe1f2bed8eca8cee58fc1de74b2d75ba3cf4592c63181bd206fb2125ca40a4a90\",greens:\"d3eecdc0e6baabdda594d3917bc77d60ba6c46ab5e329a512089430e7735036429\",greys:\"e2e2e2d4d4d4c4c4c4b1b1b19d9d9d8888887575756262624d4d4d3535351e1e1e\",oranges:\"fdd8b3fdc998fdb87bfda55efc9244f87f2cf06b18e4580bd14904b93d029f3303\",purples:\"e2e1efd4d4e8c4c5e0b4b3d6a3a0cc928ec3827cb97566ae684ea25c3696501f8c\",reds:\"fdc9b4fcb49afc9e80fc8"
-  , "767fa7051f6573fec3f2fdc2a25c81b1db21218970b13\",blueGreen:\"d5efedc1e8e0a7ddd18bd2be70c6a958ba9144ad77319c5d2089460e7736036429\",bluePurple:\"ccddecbad0e4a8c2dd9ab0d4919cc98d85be8b6db28a55a6873c99822287730f71\",greenBlue:\"d3eecec5e8c3b1e1bb9bd8bb82cec269c2ca51b2cd3c9fc7288abd1675b10b60a1\",orangeRed:\"fddcaffdcf9bfdc18afdad77fb9562f67d53ee6545e24932d32d1ebf130da70403\",purpleBlue:\"dbdaebc8cee4b1c3de97b7d87bacd15b9fc93a90c01e7fb70b70ab056199045281\",purpleBlueGreen:\"dbd8eac8cee4b0c3de93b7d872acd1549fc83892bb1c88a3097f8702736b016353\",purpleRed:\"dcc9e2d3b3d7ce9eccd186c0da6bb2e14da0e23189d91e6fc61159ab07498f023a\",redPurple:\"fccfccfcbec0faa9b8f98faff571a5ec539ddb3695c41b8aa908808d0179700174\",yellowGreen:\"e4f4acd1eca0b9e2949ed68880c97c62bb6e47aa5e3297502083440e723b036034\",yellowOrangeBrown:\"feeaa1fedd84fecc63feb746fca031f68921eb7215db5e0bc54c05ab3d038f3204\",yellowOrangeRed:\"fee087fed16ffebd59fea849fd903efc7335f9522bee3423de1b20ca0b22af0225\",blueOrange:\"134b852f78b35da2cb9dcae1d2e5eff2f0ebfce0bafbbf74e8932fc5690d994a07\",brownBlueGreen:\"704108a0651ac79548e3c78af3e6c6eef1eac9e9e48ed1c74da79e187a72025147\",purpleGreen:\"5b1667834792a67fb6c9aed3e6d6e8eff0efd9efd5aedda971bb75368e490e5e29\",purpleOrange:\"4114696647968f83b7b9b4d6dadbebf3eeeafce0bafbbf74e8932fc5690d994a07\",redBlue:\"8c0d25bf363adf745ef4ae91fbdbc9f2efeed2e5ef9dcae15da2cb2f78b3134b85\",redGrey:\"8c0d25bf363adf745ef4ae91fcdccbfaf4f1e2e2e2c0c0c0969696646464343434\",yellowGreenBlue:\"eff9bddbf1b4bde5b594d5b969c5be45b4c22c9ec02182b82163aa23479c1c3185\",redYellowBlue:\"a50026d4322cf16e43fcac64fedd90faf8c1dcf1ecabd6e875abd04a74b4313695\",redYellowGreen:\"a50026d4322cf16e43fcac63fedd8df9f7aed7ee8ea4d86e64bc6122964f006837\",pinkYellowGreen:\"8e0152c0267edd72adf0b3d6faddedf5f3efe1f2cab6de8780bb474f9125276419\",spectral:\"9e0142d13c4bf0704afcac63fedd8dfbf8b0e0f3a1a9dda269bda94288b55e4fa2\",viridis:\"440154470e61481a6c482575472f7d443a834144873d4e8a39568c35608d31688e2d708e2a788e27818e23888e21918d1f988b1fa08822a8842ab07f35b77943bf7154c56866cc5d7ad1518fd744a5db36bcdf27d2e21be9e51afde725\",magma:\"0000040404130b0924150e3720114b2c11603b0f704a107957157e651a80721f817f24828c29819a2e80a8327db6377ac43c75d1426fde4968e95462f1605df76f5cfa7f5efc8f65fe9f6dfeaf78febf84fece91fddea0fcedaffcfdbf\",inferno:\"0000040403130c0826170c3b240c4f330a5f420a68500d6c5d126e6b176e781c6d86216b932667a12b62ae305cbb3755c73e4cd24644dd513ae65c30ed6925f3771af8850ffb9506fca50afcb519fac62df6d645f2e661f3f484fcffa4\",plasma:\"0d088723069033059742039d5002a25d01a66a00a87801a88405a7900da49c179ea72198b12a90ba3488c33d80cb4779d35171da5a69e16462e76e5bed7953f2834cf68f44fa9a3dfca636fdb32ffec029fcce25f9dc24f5ea27f0f921\",cividis:\"00205100235800265d002961012b65042e670831690d346b11366c16396d1c3c6e213f6e26426e2c456e31476e374a6e3c4d6e42506e47536d4c566d51586e555b6e5a5e6e5e616e62646f66676f6a6a706e6d717270717573727976737c79747f7c75827f758682768985778c8877908b78938e789691789a94789e9778a19b78a59e77a9a177aea575b2a874b6ab73bbaf71c0b26fc5b66dc9b96acebd68d3c065d8c462ddc85fe2cb5ce7cf58ebd355f0d652f3da4ff7de4cfae249fce647\",rainbow:\"6e40aa883eb1a43db3bf3cafd83fa4ee4395fe4b83ff576eff6659ff7847ff8c38f3a130e2b72fcfcc36bee044aff05b8ff4576ff65b52f6673af27828ea8d1ddfa319d0b81cbecb23abd82f96e03d82e14c6edb5a5dd0664dbf6e40aa\",sinebow:\"ff4040fc582af47218e78d0bd5a703bfbf00a7d5038de70b72f41858fc2a40ff402afc5818f4720be78d03d5a700bfbf03a7d50b8de71872f42a58fc4040ff582afc7218f48d0be7a703d5bf00bfd503a7e70b8df41872fc2a58ff4040\",turbo:\"23171b32204a3e2a71453493493eae4b49c54a53d7485ee44569ee4074f53c7ff8378af93295f72e9ff42ba9ef28b3e926bce125c5d925cdcf27d5c629dcbc2de3b232e9a738ee9d3ff39347f68950f9805afc7765fd6e70fe667cfd5e88fc5795fb51a1f84badf545b9f140c5ec3cd0e637dae034e4d931ecd12ef4c92bfac029ffb626ffad24ffa223ff9821ff8d1fff821dff771cfd6c1af76118f05616e84b14df4111d5380fcb2f0dc0260ab61f07ac1805a313029b0f00950c00910b00\",browns:\"eedbbdecca96e9b97ae4a865dc9856d18954c7784cc0673fb85536ad44339f3632\",tealBlues:\"bce4d89dd3d181c3cb65b3c245a2b9368fae347da0306a932c5985\",teals:\"bbdfdfa2d4d58ac9c975bcbb61b0af4da5a43799982b8b8c1e7f7f127273006667\",warmGreys:\"dcd4d0cec5c1c0b8b4b3aaa7a59c9998908c8b827f7e7673726866665c5a5950"
-  , "4e\",goldGreen:\"f4d166d5ca60b6c35c98bb597cb25760a6564b9c533f8f4f33834a257740146c36\",goldOrange:\"f4d166f8be5cf8aa4cf5983bf3852aef701be2621fd65322c54923b142239e3a26\",goldRed:\"f4d166f6be59f9aa51fc964ef6834bee734ae56249db5247cf4244c43141b71d3e\",lightGreyRed:\"efe9e6e1dad7d5cbc8c8bdb9bbaea9cd967ddc7b43e15f19df4011dc000b\",lightGreyTeal:\"e4eaead6dcddc8ced2b7c2c7a6b4bc64b0bf22a6c32295c11f85be1876bc\",lightMulti:\"e0f1f2c4e9d0b0de9fd0e181f6e072f6c053f3993ef77440ef4a3c\",lightOrange:\"f2e7daf7d5baf9c499fab184fa9c73f68967ef7860e8645bde515bd43d5b\",lightTealBlue:\"e3e9e0c0dccf9aceca7abfc859afc0389fb9328dad2f7ca0276b95255988\",darkBlue:\"3232322d46681a5c930074af008cbf05a7ce25c0dd38daed50f3faffffff\",darkGold:\"3c3c3c584b37725e348c7631ae8b2bcfa424ecc31ef9de30fff184ffffff\",darkGreen:\"3a3a3a215748006f4d048942489e4276b340a6c63dd2d836ffeb2cffffaa\",darkMulti:\"3737371f5287197d8c29a86995ce3fffe800ffffff\",darkRed:\"3434347036339e3c38cc4037e75d1eec8620eeab29f0ce32ffeb2c\"},(t=>vp(wp(t))));const Ep=\"symbol\",Dp=\"discrete\",Cp=t=>A(t)?t.map((t=>String(t))):String(t),Fp=(t,e)=>t[1]-e[1],Sp=(t,e)=>e[1]-t[1];function $p(t,e,n){let r;return vt(e)&&(t.bins&&(e=Math.max(e,t.bins.length)),null!=n&&(e=Math.min(e,Math.floor(Dt(t.domain())/n||1)+1))),M(e)&&(r=e.step,e=e.interval),xt(e)&&(e=t.type===Rd?Cr(e):t.type==Ld?Fr(e):s(\"Only time and utc scales accept interval strings.\"),r&&(e=e.every(r))),e}function Tp(t,e,n){let r=t.range(),i=r[0],o=S(r),a=Fp;if(i>o&&(r=o,o=i,i=r,a=Sp),i=Math.floor(i),o=Math.ceil(o),e=e.map((e=>[e,t(e)])).filter((t=>i<=t[1]&&t[1]<=o)).sort(a).map((t=>t[0])),n>0&&e.length>1){const t=[e[0],S(e)];for(;e.length>n&&e.length>=3;)e=e.filter(((t,e)=>!(e%2)));e.length<3&&(e=t)}return e}function Bp(t,e){return t.bins?Tp(t,t.bins,e):t.ticks?t.ticks(e):t.domain()}function Np(t,e,n,r,i,o){const a=e.type;let s=Cp;if(a===Rd||i===Rd)s=t.timeFormat(r);else if(a===Ld||i===Ld)s=t.utcFormat(r);else if(dp(a)){const i=t.formatFloat(r);if(o||e.bins)s=i;else{const t=zp(e,n,!1);s=e=>t(e)?i(e):\"\"}}else if(e.tickFormat){const i=e.domain();s=t.formatSpan(i[0],i[i.length-1],n,r)}else r&&(s=t.format(r));return s}function zp(t,e,n){const r=Bp(t,e),i=t.base(),o=Math.log(i),a=Math.max(1,i*e/r.length),s=t=>{let e=t/Math.pow(i,Math.round(Math.log(t)/o));return e*i<i-.5&&(e*=i),e<=a};return n?r.filter(s):s}const Op={[Pd]:\"quantiles\",[jd]:\"thresholds\",[Id]:\"domain\"},Rp={[Pd]:\"quantiles\",[jd]:\"domain\"};function Lp(t,e){return t.bins?function(t){const e=t.slice(0,-1);return e.max=S(t),e}(t.bins):t.type===Bd?zp(t,e,!0):Op[t.type]?function(t){const e=[-1/0].concat(t);return e.max=1/0,e}(t[Op[t.type]]()):Bp(t,e)}const Up=t=>Op[t.type]||t.bins;function qp(t,e,n,r,i,o,a){const s=Rp[e.type]&&o!==Rd&&o!==Ld?function(t,e,n){const r=e[Rp[e.type]](),i=r.length;let o,a=i>1?r[1]-r[0]:r[0];for(o=1;o<i;++o)a=Math.min(a,r[o]-r[o-1]);return t.formatSpan(0,a,30,n)}(t,e,i):Np(t,e,n,i,o,a);return r===Ep&&Up(e)?Pp(s):r===Dp?Ip(s):Wp(s)}const Pp=t=>(e,n,r)=>{const i=jp(r[n+1],jp(r.max,1/0)),o=Hp(e,t),a=Hp(i,t);return o&&a?o+\" – \"+a:a?\"< \"+a:\"≥ \"+o},jp=(t,e)=>null!=t?t:e,Ip=t=>(e,n)=>n?t(e):null,Wp=t=>e=>t(e),Hp=(t,e)=>Number.isFinite(t)?e(t):null;function Yp(t,e,n,r){const i=r||e.type;return xt(n)&&function(t){return lp(t,Qd)}(i)&&(n=n.replace(/%a/g,\"%A\").replace(/%b/g,\"%B\")),n||i!==Rd?n||i!==Ld?qp(t,e,5,null,n,r,!0):t.utcFormat(\"%A, %d %B %Y, %X UTC\"):t.timeFormat(\"%A, %d %B %Y, %X\")}function Gp(t,e,n){n=n||{};const r=Math.max(3,n.maxlen||7),i=Yp(t,e,n.format,n.formatType);if(hp(e.type)){const t=Lp(e).slice(1).map(i),n=t.length;return`${n} boundar${1===n?\"y\":\"ies\"}: ${t.join(\", \")}`}if(fp(e.type)){const t=e.domain(),n=t.length;return`${n} value${1===n?\"\":\"s\"}: ${n>r?t.slice(0,r-2).map(i).join(\", \")+\", ending with \"+t.slice(-1).map(i):t.map(i).join(\", \")}`}{const t=e.domain();return`values from ${i(t[0])} to ${i(S(t))}`}}let Vp=0;const Xp=\"p_\";function Jp(t){return t&&t.gradient}function Zp(t,e,n){const r=t.gradient;let i=t.id,o=\"radial\"===r?Xp:\"\";return i||(i=t.id=\"gradient_\"+Vp++,\"radial\"===r?(t.x1=Qp(t.x1,.5),t.y1=Qp(t.y1,.5),t.r1=Qp(t.r1,0),t.x2=Qp(t.x2,.5),t.y2=Qp(t.y2,.5),t.r2=Qp(t.r2,.5),o=Xp):(t."
-  , "x1=Qp(t.x1,0),t.y1=Qp(t.y1,0),t.x2=Qp(t.x2,1),t.y2=Qp(t.y2,0))),e[i]=t,\"url(\"+(n||\"\")+\"#\"+o+i+\")\"}function Qp(t,e){return null!=t?t:e}function Kp(t,e){var n,r=[];return n={gradient:\"linear\",x1:t?t[0]:0,y1:t?t[1]:0,x2:e?e[0]:1,y2:e?e[1]:0,stops:r,stop:function(t,e){return r.push({offset:t,color:e}),n}}}const tg={basis:{curve:function(t){return new ec(t)}},\"basis-closed\":{curve:function(t){return new nc(t)}},\"basis-open\":{curve:function(t){return new rc(t)}},bundle:{curve:oc,tension:\"beta\",value:.85},cardinal:{curve:uc,tension:\"tension\",value:0},\"cardinal-open\":{curve:hc,tension:\"tension\",value:0},\"cardinal-closed\":{curve:cc,tension:\"tension\",value:0},\"catmull-rom\":{curve:gc,tension:\"alpha\",value:.5},\"catmull-rom-closed\":{curve:yc,tension:\"alpha\",value:.5},\"catmull-rom-open\":{curve:_c,tension:\"alpha\",value:.5},linear:{curve:Gl},\"linear-closed\":{curve:function(t){return new xc(t)}},monotone:{horizontal:function(t){return new Ec(t)},vertical:function(t){return new Mc(t)}},natural:{curve:function(t){return new Cc(t)}},step:{curve:function(t){return new Sc(t,.5)}},\"step-after\":{curve:function(t){return new Sc(t,1)}},\"step-before\":{curve:function(t){return new Sc(t,0)}}};function eg(t,e,n){var r=lt(tg,t)&&tg[t],i=null;return r&&(i=r.curve||r[e||\"vertical\"],r.tension&&null!=n&&(i=i[r.tension](n))),i}const ng={m:2,l:2,h:1,v:1,z:0,c:6,s:4,q:4,t:2,a:7},rg=/[mlhvzcsqta]([^mlhvzcsqta]+|$)/gi,ig=/^[+-]?(([0-9]*\\.[0-9]+)|([0-9]+\\.)|([0-9]+))([eE][+-]?[0-9]+)?/,og=/^((\\s+,?\\s*)|(,\\s*))/,ag=/^[01]/;function sg(t){const e=[];return(t.match(rg)||[]).forEach((t=>{let n=t[0];const r=n.toLowerCase(),i=ng[r],o=function(t,e,n){const r=[];for(let i=0;e&&i<n.length;)for(let o=0;o<e;++o){const e=\"a\"!==t||3!==o&&4!==o?ig:ag,a=n.slice(i).match(e);if(null===a)throw Error(\"Invalid SVG path, incorrect parameter type\");i+=a[0].length,r.push(+a[0]);const s=n.slice(i).match(og);null!==s&&(i+=s[0].length)}return r}(r,i,t.slice(1).trim()),a=o.length;if(a<i||a&&a%i!=0)throw Error(\"Invalid SVG path, incorrect parameter count\");if(e.push([n,...o.slice(0,i)]),a!==i){\"m\"===r&&(n=\"M\"===n?\"L\":\"l\");for(let t=i;t<a;t+=i)e.push([n,...o.slice(t,t+i)])}})),e}const ug=Math.PI/180,lg=Math.PI/2,cg=2*Math.PI,fg=Math.sqrt(3)/2;var hg={},dg={},pg=[].join;function gg(t){const e=pg.call(t);if(dg[e])return dg[e];var n=t[0],r=t[1],i=t[2],o=t[3],a=t[4],s=t[5],u=t[6],l=t[7];const c=l*a,f=-u*s,h=u*a,d=l*s,p=Math.cos(i),g=Math.sin(i),m=Math.cos(o),y=Math.sin(o),v=.5*(o-i),_=Math.sin(.5*v),x=8/3*_*_/Math.sin(v),b=n+p-x*g,w=r+g+x*p,k=n+m,A=r+y,M=k+x*y,E=A-x*m;return dg[e]=[c*b+f*w,h*b+d*w,c*M+f*E,h*M+d*E,c*k+f*A,h*k+d*A]}const mg=[\"l\",0,0,0,0,0,0,0];function yg(t,e,n){const r=mg[0]=t[0];if(\"a\"===r||\"A\"===r)mg[1]=e*t[1],mg[2]=n*t[2],mg[3]=t[3],mg[4]=t[4],mg[5]=t[5],mg[6]=e*t[6],mg[7]=n*t[7];else if(\"h\"===r||\"H\"===r)mg[1]=e*t[1];else if(\"v\"===r||\"V\"===r)mg[1]=n*t[1];else for(var i=1,o=t.length;i<o;++i)mg[i]=(i%2==1?e:n)*t[i];return mg}function vg(t,e,n,r,i,o){var a,s,u,l,c,f=null,h=0,d=0,p=0,g=0,m=0,y=0;null==n&&(n=0),null==r&&(r=0),null==i&&(i=1),null==o&&(o=i),t.beginPath&&t.beginPath();for(var v=0,_=e.length;v<_;++v){switch(a=e[v],1===i&&1===o||(a=yg(a,i,o)),a[0]){case\"l\":h+=a[1],d+=a[2],t.lineTo(h+n,d+r);break;case\"L\":h=a[1],d=a[2],t.lineTo(h+n,d+r);break;case\"h\":h+=a[1],t.lineTo(h+n,d+r);break;case\"H\":h=a[1],t.lineTo(h+n,d+r);break;case\"v\":d+=a[1],t.lineTo(h+n,d+r);break;case\"V\":d=a[1],t.lineTo(h+n,d+r);break;case\"m\":m=h+=a[1],y=d+=a[2],t.moveTo(h+n,d+r);break;case\"M\":m=h=a[1],y=d=a[2],t.moveTo(h+n,d+r);break;case\"c\":s=h+a[5],u=d+a[6],p=h+a[3],g=d+a[4],t.bezierCurveTo(h+a[1]+n,d+a[2]+r,p+n,g+r,s+n,u+r),h=s,d=u;break;case\"C\":h=a[5],d=a[6],p=a[3],g=a[4],t.bezierCurveTo(a[1]+n,a[2]+r,p+n,g+r,h+n,d+r);break;case\"s\":s=h+a[3],u=d+a[4],p=2*h-p,g=2*d-g,t.bezierCurveTo(p+n,g+r,h+a[1]+n,d+a[2]+r,s+n,u+r),p=h+a[1],g=d+a[2],h=s,d=u;break;case\"S\":s=a[3],u=a[4],p=2*h-p,g=2*d-g,t.bezierCurveTo(p+n,g+r,a[1]+n,a[2]+r,s+n,u+r),h=s,d=u,p=a[1],g=a[2];break;case\"q\":s=h+a[3],u=d+a[4],p=h+a[1],g=d+a[2],t.quadraticCurveTo(p+n,g+r,s+n,u+r),h=s,d=u;break;case\"Q\":s=a[3],u=a[4],t.quadraticCurveTo(a[1]+n,a[2]+"
-  , "r,s+n,u+r),h=s,d=u,p=a[1],g=a[2];break;case\"t\":s=h+a[1],u=d+a[2],null===f[0].match(/[QqTt]/)?(p=h,g=d):\"t\"===f[0]?(p=2*h-l,g=2*d-c):\"q\"===f[0]&&(p=2*h-p,g=2*d-g),l=p,c=g,t.quadraticCurveTo(p+n,g+r,s+n,u+r),d=u,p=(h=s)+a[1],g=d+a[2];break;case\"T\":s=a[1],u=a[2],p=2*h-p,g=2*d-g,t.quadraticCurveTo(p+n,g+r,s+n,u+r),h=s,d=u;break;case\"a\":_g(t,h+n,d+r,[a[1],a[2],a[3],a[4],a[5],a[6]+h+n,a[7]+d+r]),h+=a[6],d+=a[7];break;case\"A\":_g(t,h+n,d+r,[a[1],a[2],a[3],a[4],a[5],a[6]+n,a[7]+r]),h=a[6],d=a[7];break;case\"z\":case\"Z\":h=m,d=y,t.closePath()}f=a}}function _g(t,e,n,r){const i=function(t,e,n,r,i,o,a,s,u){const l=pg.call(arguments);if(hg[l])return hg[l];const c=a*ug,f=Math.sin(c),h=Math.cos(c),d=h*(s-t)*.5+f*(u-e)*.5,p=h*(u-e)*.5-f*(s-t)*.5;let g=d*d/((n=Math.abs(n))*n)+p*p/((r=Math.abs(r))*r);g>1&&(g=Math.sqrt(g),n*=g,r*=g);const m=h/n,y=f/n,v=-f/r,_=h/r,x=m*s+y*u,b=v*s+_*u,w=m*t+y*e,k=v*t+_*e;let A=1/((w-x)*(w-x)+(k-b)*(k-b))-.25;A<0&&(A=0);let M=Math.sqrt(A);o==i&&(M=-M);const E=.5*(x+w)-M*(k-b),D=.5*(b+k)+M*(w-x),C=Math.atan2(b-D,x-E);let F=Math.atan2(k-D,w-E)-C;F<0&&1===o?F+=cg:F>0&&0===o&&(F-=cg);const S=Math.ceil(Math.abs(F/(lg+.001))),$=[];for(let t=0;t<S;++t){const e=C+t*F/S,i=C+(t+1)*F/S;$[t]=[E,D,e,i,n,r,f,h]}return hg[l]=$}(r[5],r[6],r[0],r[1],r[3],r[4],r[2],e,n);for(let e=0;e<i.length;++e){const n=gg(i[e]);t.bezierCurveTo(n[0],n[1],n[2],n[3],n[4],n[5])}}const xg=.5773502691896257,bg={circle:{draw:function(t,e){const n=Math.sqrt(e)/2;t.moveTo(n,0),t.arc(0,0,n,0,cg)}},cross:{draw:function(t,e){var n=Math.sqrt(e)/2,r=n/2.5;t.moveTo(-n,-r),t.lineTo(-n,r),t.lineTo(-r,r),t.lineTo(-r,n),t.lineTo(r,n),t.lineTo(r,r),t.lineTo(n,r),t.lineTo(n,-r),t.lineTo(r,-r),t.lineTo(r,-n),t.lineTo(-r,-n),t.lineTo(-r,-r),t.closePath()}},diamond:{draw:function(t,e){const n=Math.sqrt(e)/2;t.moveTo(-n,0),t.lineTo(0,-n),t.lineTo(n,0),t.lineTo(0,n),t.closePath()}},square:{draw:function(t,e){var n=Math.sqrt(e),r=-n/2;t.rect(r,r,n,n)}},arrow:{draw:function(t,e){var n=Math.sqrt(e)/2,r=n/7,i=n/2.5,o=n/8;t.moveTo(-r,n),t.lineTo(r,n),t.lineTo(r,-o),t.lineTo(i,-o),t.lineTo(0,-n),t.lineTo(-i,-o),t.lineTo(-r,-o),t.closePath()}},wedge:{draw:function(t,e){var n=Math.sqrt(e)/2,r=fg*n,i=r-n*xg,o=n/4;t.moveTo(0,-r-i),t.lineTo(-o,r-i),t.lineTo(o,r-i),t.closePath()}},triangle:{draw:function(t,e){var n=Math.sqrt(e)/2,r=fg*n,i=r-n*xg;t.moveTo(0,-r-i),t.lineTo(-n,r-i),t.lineTo(n,r-i),t.closePath()}},\"triangle-up\":{draw:function(t,e){var n=Math.sqrt(e)/2,r=fg*n;t.moveTo(0,-r),t.lineTo(-n,r),t.lineTo(n,r),t.closePath()}},\"triangle-down\":{draw:function(t,e){var n=Math.sqrt(e)/2,r=fg*n;t.moveTo(0,r),t.lineTo(-n,-r),t.lineTo(n,-r),t.closePath()}},\"triangle-right\":{draw:function(t,e){var n=Math.sqrt(e)/2,r=fg*n;t.moveTo(r,0),t.lineTo(-r,-n),t.lineTo(-r,n),t.closePath()}},\"triangle-left\":{draw:function(t,e){var n=Math.sqrt(e)/2,r=fg*n;t.moveTo(-r,0),t.lineTo(r,-n),t.lineTo(r,n),t.closePath()}},stroke:{draw:function(t,e){const n=Math.sqrt(e)/2;t.moveTo(-n,0),t.lineTo(n,0)}}};function wg(t){return lt(bg,t)?bg[t]:function(t){if(!lt(kg,t)){const e=sg(t);kg[t]={draw:function(t,n){vg(t,e,0,0,Math.sqrt(n)/2)}}}return kg[t]}(t)}var kg={};const Ag=.448084975506;function Mg(t){return t.x}function Eg(t){return t.y}function Dg(t){return t.width}function Cg(t){return t.height}function Fg(t){return\"function\"==typeof t?t:()=>+t}function Sg(t,e,n){return Math.max(e,Math.min(t,n))}function $g(){var t=Mg,e=Eg,n=Dg,r=Cg,i=Fg(0),o=i,a=i,s=i,u=null;function l(l,c,f){var h,d=null!=c?c:+t.call(this,l),p=null!=f?f:+e.call(this,l),g=+n.call(this,l),m=+r.call(this,l),y=Math.min(g,m)/2,v=Sg(+i.call(this,l),0,y),_=Sg(+o.call(this,l),0,y),x=Sg(+a.call(this,l),0,y),b=Sg(+s.call(this,l),0,y);if(u||(u=h=Rl()),v<=0&&_<=0&&x<=0&&b<=0)u.rect(d,p,g,m);else{var w=d+g,k=p+m;u.moveTo(d+v,p),u.lineTo(w-_,p),u.bezierCurveTo(w-Ag*_,p,w,p+Ag*_,w,p+_),u.lineTo(w,k-b),u.bezierCurveTo(w,k-Ag*b,w-Ag*b,k,w-b,k),u.lineTo(d+x,k),u.bezierCurveTo(d+Ag*x,k,d,k-Ag*x,d,k-x),u.lineTo(d,p+v),u.bezierCurveTo(d,p+Ag*v,d+Ag*v,p,d+v,p),u.closePath()}if(h)return u=null,h+\"\"||null}return l.x=function(e){return arguments.length?(t=Fg(e),l):"
-  , "t},l.y=function(t){return arguments.length?(e=Fg(t),l):e},l.width=function(t){return arguments.length?(n=Fg(t),l):n},l.height=function(t){return arguments.length?(r=Fg(t),l):r},l.cornerRadius=function(t,e,n,r){return arguments.length?(i=Fg(t),o=null!=e?Fg(e):i,s=null!=n?Fg(n):i,a=null!=r?Fg(r):o,l):i},l.context=function(t){return arguments.length?(u=null==t?null:t,l):u},l}function Tg(){var t,e,n,r,i,o,a,s,u=null;function l(t,e,n){const r=n/2;if(i){var l=a-e,c=t-o;if(l||c){var f=Math.hypot(l,c),h=(l/=f)*s,d=(c/=f)*s,p=Math.atan2(c,l);u.moveTo(o-h,a-d),u.lineTo(t-l*r,e-c*r),u.arc(t,e,r,p-Math.PI,p),u.lineTo(o+h,a+d),u.arc(o,a,s,p,p+Math.PI)}else u.arc(t,e,r,0,cg);u.closePath()}else i=1;o=t,a=e,s=r}function c(o){var a,s,c,f=o.length,h=!1;for(null==u&&(u=c=Rl()),a=0;a<=f;++a)!(a<f&&r(s=o[a],a,o))===h&&(h=!h)&&(i=0),h&&l(+t(s,a,o),+e(s,a,o),+n(s,a,o));if(c)return u=null,c+\"\"||null}return c.x=function(e){return arguments.length?(t=e,c):t},c.y=function(t){return arguments.length?(e=t,c):e},c.size=function(t){return arguments.length?(n=t,c):n},c.defined=function(t){return arguments.length?(r=t,c):r},c.context=function(t){return arguments.length?(u=null==t?null:t,c):u},c}function Bg(t,e){return null!=t?t:e}const Ng=t=>t.x||0,zg=t=>t.y||0,Og=t=>!(!1===t.defined),Rg=function(){var t=Ul,e=ql,n=vl(0),r=null,i=Pl,o=jl,a=Il,s=null,u=Ll(l);function l(){var l,c,f=+t.apply(this,arguments),h=+e.apply(this,arguments),d=i.apply(this,arguments)-Cl,p=o.apply(this,arguments)-Cl,g=_l(p-d),m=p>d;if(s||(s=l=u()),h<f&&(c=h,h=f,f=c),h>El)if(g>Fl-El)s.moveTo(h*bl(d),h*Al(d)),s.arc(0,0,h,d,p,!m),f>El&&(s.moveTo(f*bl(p),f*Al(p)),s.arc(0,0,f,p,d,m));else{var y,v,_=d,x=p,b=d,w=p,k=g,A=g,M=a.apply(this,arguments)/2,E=M>El&&(r?+r.apply(this,arguments):Ml(f*f+h*h)),D=kl(_l(h-f)/2,+n.apply(this,arguments)),C=D,F=D;if(E>El){var S=Sl(E/f*Al(M)),$=Sl(E/h*Al(M));(k-=2*S)>El?(b+=S*=m?1:-1,w-=S):(k=0,b=w=(d+p)/2),(A-=2*$)>El?(_+=$*=m?1:-1,x-=$):(A=0,_=x=(d+p)/2)}var T=h*bl(_),B=h*Al(_),N=f*bl(w),z=f*Al(w);if(D>El){var O,R=h*bl(x),L=h*Al(x),U=f*bl(b),q=f*Al(b);if(g<Dl)if(O=function(t,e,n,r,i,o,a,s){var u=n-t,l=r-e,c=a-i,f=s-o,h=f*u-c*l;if(!(h*h<El))return[t+(h=(c*(e-o)-f*(t-i))/h)*u,e+h*l]}(T,B,U,q,R,L,N,z)){var P=T-O[0],j=B-O[1],I=R-O[0],W=L-O[1],H=1/Al(function(t){return t>1?0:t<-1?Dl:Math.acos(t)}((P*I+j*W)/(Ml(P*P+j*j)*Ml(I*I+W*W)))/2),Y=Ml(O[0]*O[0]+O[1]*O[1]);C=kl(D,(f-Y)/(H-1)),F=kl(D,(h-Y)/(H+1))}else C=F=0}A>El?F>El?(y=Wl(U,q,T,B,h,F,m),v=Wl(R,L,N,z,h,F,m),s.moveTo(y.cx+y.x01,y.cy+y.y01),F<D?s.arc(y.cx,y.cy,F,xl(y.y01,y.x01),xl(v.y01,v.x01),!m):(s.arc(y.cx,y.cy,F,xl(y.y01,y.x01),xl(y.y11,y.x11),!m),s.arc(0,0,h,xl(y.cy+y.y11,y.cx+y.x11),xl(v.cy+v.y11,v.cx+v.x11),!m),s.arc(v.cx,v.cy,F,xl(v.y11,v.x11),xl(v.y01,v.x01),!m))):(s.moveTo(T,B),s.arc(0,0,h,_,x,!m)):s.moveTo(T,B),f>El&&k>El?C>El?(y=Wl(N,z,R,L,f,-C,m),v=Wl(T,B,U,q,f,-C,m),s.lineTo(y.cx+y.x01,y.cy+y.y01),C<D?s.arc(y.cx,y.cy,C,xl(y.y01,y.x01),xl(v.y01,v.x01),!m):(s.arc(y.cx,y.cy,C,xl(y.y01,y.x01),xl(y.y11,y.x11),!m),s.arc(0,0,f,xl(y.cy+y.y11,y.cx+y.x11),xl(v.cy+v.y11,v.cx+v.x11),m),s.arc(v.cx,v.cy,C,xl(v.y11,v.x11),xl(v.y01,v.x01),!m))):s.arc(0,0,f,w,b,m):s.lineTo(N,z)}else s.moveTo(0,0);if(s.closePath(),l)return s=null,l+\"\"||null}return l.centroid=function(){var n=(+t.apply(this,arguments)+ +e.apply(this,arguments))/2,r=(+i.apply(this,arguments)+ +o.apply(this,arguments))/2-Dl/2;return[bl(r)*n,Al(r)*n]},l.innerRadius=function(e){return arguments.length?(t=\"function\"==typeof e?e:vl(+e),l):t},l.outerRadius=function(t){return arguments.length?(e=\"function\"==typeof t?t:vl(+t),l):e},l.cornerRadius=function(t){return arguments.length?(n=\"function\"==typeof t?t:vl(+t),l):n},l.padRadius=function(t){return arguments.length?(r=null==t?null:\"function\"==typeof t?t:vl(+t),l):r},l.startAngle=function(t){return arguments.length?(i=\"function\"==typeof t?t:vl(+t),l):i},l.endAngle=function(t){return arguments.length?(o=\"function\"==typeof t?t:vl(+t),l):o},l.padAngle=function(t){return arguments.length?(a=\"function\"==typeof t?t:vl(+t),l):a},l.context=function(t){return arguments.length?(s=null==t?null:t,l):s},l}().startAngl"
-  , "e((t=>t.startAngle||0)).endAngle((t=>t.endAngle||0)).padAngle((t=>t.padAngle||0)).innerRadius((t=>t.innerRadius||0)).outerRadius((t=>t.outerRadius||0)).cornerRadius((t=>t.cornerRadius||0)),Lg=Zl().x(Ng).y1(zg).y0((t=>(t.y||0)+(t.height||0))).defined(Og),Ug=Zl().y(zg).x1(Ng).x0((t=>(t.x||0)+(t.width||0))).defined(Og),qg=Jl().x(Ng).y(zg).defined(Og),Pg=$g().x(Ng).y(zg).width((t=>t.width||0)).height((t=>t.height||0)).cornerRadius((t=>Bg(t.cornerRadiusTopLeft,t.cornerRadius)||0),(t=>Bg(t.cornerRadiusTopRight,t.cornerRadius)||0),(t=>Bg(t.cornerRadiusBottomRight,t.cornerRadius)||0),(t=>Bg(t.cornerRadiusBottomLeft,t.cornerRadius)||0)),jg=function(t,e){let n=null,r=Ll(i);function i(){let i;if(n||(n=i=r()),t.apply(this,arguments).draw(n,+e.apply(this,arguments)),i)return n=null,i+\"\"||null}return t=\"function\"==typeof t?t:vl(t||Ql),e=\"function\"==typeof e?e:vl(void 0===e?64:+e),i.type=function(e){return arguments.length?(t=\"function\"==typeof e?e:vl(e),i):t},i.size=function(t){return arguments.length?(e=\"function\"==typeof t?t:vl(+t),i):e},i.context=function(t){return arguments.length?(n=null==t?null:t,i):n},i}().type((t=>wg(t.shape||\"circle\"))).size((t=>Bg(t.size,64))),Ig=Tg().x(Ng).y(zg).defined(Og).size((t=>t.size||1));function Wg(t){return t.cornerRadius||t.cornerRadiusTopLeft||t.cornerRadiusTopRight||t.cornerRadiusBottomRight||t.cornerRadiusBottomLeft}function Hg(t,e,n,r){return Pg.context(t)(e,n,r)}var Yg=1;function Gg(){Yg=1}function Vg(t,e,n){var r=e.clip,i=t._defs,o=e.clip_id||(e.clip_id=\"clip\"+Yg++),a=i.clipping[o]||(i.clipping[o]={id:o});return Z(r)?a.path=r(null):Wg(n)?a.path=Hg(null,n,0,0):(a.width=n.width||0,a.height=n.height||0),\"url(#\"+o+\")\"}function Xg(t){this.clear(),t&&this.union(t)}function Jg(t){this.mark=t,this.bounds=this.bounds||new Xg}function Zg(t){Jg.call(this,t),this.items=this.items||[]}Xg.prototype={clone(){return new Xg(this)},clear(){return this.x1=+Number.MAX_VALUE,this.y1=+Number.MAX_VALUE,this.x2=-Number.MAX_VALUE,this.y2=-Number.MAX_VALUE,this},empty(){return this.x1===+Number.MAX_VALUE&&this.y1===+Number.MAX_VALUE&&this.x2===-Number.MAX_VALUE&&this.y2===-Number.MAX_VALUE},equals(t){return this.x1===t.x1&&this.y1===t.y1&&this.x2===t.x2&&this.y2===t.y2},set(t,e,n,r){return n<t?(this.x2=t,this.x1=n):(this.x1=t,this.x2=n),r<e?(this.y2=e,this.y1=r):(this.y1=e,this.y2=r),this},add(t,e){return t<this.x1&&(this.x1=t),e<this.y1&&(this.y1=e),t>this.x2&&(this.x2=t),e>this.y2&&(this.y2=e),this},expand(t){return this.x1-=t,this.y1-=t,this.x2+=t,this.y2+=t,this},round(){return this.x1=Math.floor(this.x1),this.y1=Math.floor(this.y1),this.x2=Math.ceil(this.x2),this.y2=Math.ceil(this.y2),this},scale(t){return this.x1*=t,this.y1*=t,this.x2*=t,this.y2*=t,this},translate(t,e){return this.x1+=t,this.x2+=t,this.y1+=e,this.y2+=e,this},rotate(t,e,n){const r=this.rotatedPoints(t,e,n);return this.clear().add(r[0],r[1]).add(r[2],r[3]).add(r[4],r[5]).add(r[6],r[7])},rotatedPoints(t,e,n){var{x1:r,y1:i,x2:o,y2:a}=this,s=Math.cos(t),u=Math.sin(t),l=e-e*s+n*u,c=n-e*u-n*s;return[s*r-u*i+l,u*r+s*i+c,s*r-u*a+l,u*r+s*a+c,s*o-u*i+l,u*o+s*i+c,s*o-u*a+l,u*o+s*a+c]},union(t){return t.x1<this.x1&&(this.x1=t.x1),t.y1<this.y1&&(this.y1=t.y1),t.x2>this.x2&&(this.x2=t.x2),t.y2>this.y2&&(this.y2=t.y2),this},intersect(t){return t.x1>this.x1&&(this.x1=t.x1),t.y1>this.y1&&(this.y1=t.y1),t.x2<this.x2&&(this.x2=t.x2),t.y2<this.y2&&(this.y2=t.y2),this},encloses(t){return t&&this.x1<=t.x1&&this.x2>=t.x2&&this.y1<=t.y1&&this.y2>=t.y2},alignsWith(t){return t&&(this.x1==t.x1||this.x2==t.x2||this.y1==t.y1||this.y2==t.y2)},intersects(t){return t&&!(this.x2<t.x1||this.x1>t.x2||this.y2<t.y1||this.y1>t.y2)},contains(t,e){return!(t<this.x1||t>this.x2||e<this.y1||e>this.y2)},width(){return this.x2-this.x1},height(){return this.y2-this.y1}},dt(Zg,Jg);class Qg{constructor(t){this._pending=0,this._loader=t||fa()}pending(){return this._pending}sanitizeURL(t){const e=this;return Kg(e),e._loader.sanitize(t,{context:\"href\"}).then((t=>(tm(e),t))).catch((()=>(tm(e),null)))}loadImage(t){const e=this,n=Tc();return Kg(e),e._loader.sanitize(t,{context:\"image\"}).then((t=>{c"
-  , "onst r=t.href;if(!r||!n)throw{url:r};const i=new n,o=lt(t,\"crossOrigin\")?t.crossOrigin:\"anonymous\";return null!=o&&(i.crossOrigin=o),i.onload=()=>tm(e),i.onerror=()=>tm(e),i.src=r,i})).catch((t=>(tm(e),{complete:!1,width:0,height:0,src:t&&t.url||\"\"})))}ready(){const t=this;return new Promise((e=>{!function n(r){t.pending()?setTimeout((()=>{n(!0)}),10):e(r)}(!1)}))}}function Kg(t){t._pending+=1}function tm(t){t._pending-=1}function em(t,e,n){if(e.stroke&&0!==e.opacity&&0!==e.strokeOpacity){const r=null!=e.strokeWidth?+e.strokeWidth:1;t.expand(r+(n?function(t,e){return t.strokeJoin&&\"miter\"!==t.strokeJoin?0:e}(e,r):0))}return t}const nm=cg-1e-8;let rm,im,om,am,sm,um,lm,cm;const fm=(t,e)=>rm.add(t,e),hm=(t,e)=>fm(im=t,om=e),dm=t=>fm(t,rm.y1),pm=t=>fm(rm.x1,t),gm=(t,e)=>sm*t+lm*e,mm=(t,e)=>um*t+cm*e,ym=(t,e)=>fm(gm(t,e),mm(t,e)),vm=(t,e)=>hm(gm(t,e),mm(t,e));function _m(t,e){return rm=t,e?(am=e*ug,sm=cm=Math.cos(am),um=Math.sin(am),lm=-um):(sm=cm=1,am=um=lm=0),xm}const xm={beginPath(){},closePath(){},moveTo:vm,lineTo:vm,rect(t,e,n,r){am?(ym(t+n,e),ym(t+n,e+r),ym(t,e+r),vm(t,e)):(fm(t+n,e+r),hm(t,e))},quadraticCurveTo(t,e,n,r){const i=gm(t,e),o=mm(t,e),a=gm(n,r),s=mm(n,r);bm(im,i,a,dm),bm(om,o,s,pm),hm(a,s)},bezierCurveTo(t,e,n,r,i,o){const a=gm(t,e),s=mm(t,e),u=gm(n,r),l=mm(n,r),c=gm(i,o),f=mm(i,o);wm(im,a,u,c,dm),wm(om,s,l,f,pm),hm(c,f)},arc(t,e,n,r,i,o){if(r+=am,i+=am,im=n*Math.cos(i)+t,om=n*Math.sin(i)+e,Math.abs(i-r)>nm)fm(t-n,e-n),fm(t+n,e+n);else{const a=r=>fm(n*Math.cos(r)+t,n*Math.sin(r)+e);let s,u;if(a(r),a(i),i!==r)if((r%=cg)<0&&(r+=cg),(i%=cg)<0&&(i+=cg),i<r&&(o=!o,s=r,r=i,i=s),o)for(i-=cg,s=r-r%lg,u=0;u<4&&s>i;++u,s-=lg)a(s);else for(s=r-r%lg+lg,u=0;u<4&&s<i;++u,s+=lg)a(s)}}};function bm(t,e,n,r){const i=(t-e)/(t+n-2*e);0<i&&i<1&&r(t+(e-t)*i)}function wm(t,e,n,r,i){const o=r-t+3*e-3*n,a=t+n-2*e,s=t-e;let u,l=0,c=0;Math.abs(o)>1e-14?(u=a*a+s*o,u>=0&&(u=Math.sqrt(u),l=(-a+u)/o,c=(-a-u)/o)):l=.5*s/a,0<l&&l<1&&i(km(l,t,e,n,r)),0<c&&c<1&&i(km(c,t,e,n,r))}function km(t,e,n,r,i){const o=1-t,a=o*o,s=t*t;return a*o*e+3*a*t*n+3*o*s*r+s*t*i}var Am=(Am=$c(1,1))?Am.getContext(\"2d\"):null;const Mm=new Xg;function Em(t){return function(e,n){if(!Am)return!0;t(Am,e),Mm.clear().union(e.bounds).intersect(n).round();const{x1:r,y1:i,x2:o,y2:a}=Mm;for(let t=i;t<=a;++t)for(let e=r;e<=o;++e)if(Am.isPointInPath(e,t))return!0;return!1}}function Dm(t,e){return e.contains(t.x||0,t.y||0)}function Cm(t,e){const n=t.x||0,r=t.y||0,i=t.width||0,o=t.height||0;return e.intersects(Mm.set(n,r,n+i,r+o))}function Fm(t,e){const n=t.x||0,r=t.y||0;return Sm(e,n,r,null!=t.x2?t.x2:n,null!=t.y2?t.y2:r)}function Sm(t,e,n,r,i){const{x1:o,y1:a,x2:s,y2:u}=t,l=r-e,c=i-n;let f,h,d,p,g=0,m=1;for(p=0;p<4;++p){if(0===p&&(f=-l,h=-(o-e)),1===p&&(f=l,h=s-e),2===p&&(f=-c,h=-(a-n)),3===p&&(f=c,h=u-n),Math.abs(f)<1e-10&&h<0)return!1;if(d=h/f,f<0){if(d>m)return!1;d>g&&(g=d)}else if(f>0){if(d<g)return!1;d<m&&(m=d)}}return!0}function $m(t,e){t.globalCompositeOperation=e.blend||\"source-over\"}function Tm(t,e){return null==t?e:t}function Bm(t,e){const n=e.length;for(let r=0;r<n;++r)t.addColorStop(e[r].offset,e[r].color);return t}function Nm(t,e,n){return Jp(n)?function(t,e,n){const r=n.width(),i=n.height();let o;if(\"radial\"===e.gradient)o=t.createRadialGradient(n.x1+Tm(e.x1,.5)*r,n.y1+Tm(e.y1,.5)*i,Math.max(r,i)*Tm(e.r1,0),n.x1+Tm(e.x2,.5)*r,n.y1+Tm(e.y2,.5)*i,Math.max(r,i)*Tm(e.r2,.5));else{const a=Tm(e.x1,0),s=Tm(e.y1,0),u=Tm(e.x2,1),l=Tm(e.y2,0);if(a!==u&&s!==l&&r!==i){const n=$c(Math.ceil(r),Math.ceil(i)),o=n.getContext(\"2d\");return o.scale(r,i),o.fillStyle=Bm(o.createLinearGradient(a,s,u,l),e.stops),o.fillRect(0,0,r,i),t.createPattern(n,\"no-repeat\")}o=t.createLinearGradient(n.x1+a*r,n.y1+s*i,n.x1+u*r,n.y1+l*i)}return Bm(o,e.stops)}(t,n,e.bounds):n}function zm(t,e,n){return(n*=null==e.fillOpacity?1:e.fillOpacity)>0&&(t.globalAlpha=n,t.fillStyle=Nm(t,e,e.fill),!0)}var Om=[];function Rm(t,e,n){var r=null!=(r=e.strokeWidth)?r:1;return!(r<=0)&&((n*=null==e.strokeOpacity?1:e.strokeOpacity)>0&&(t.globalAlpha=n,t.strokeStyle=Nm(t,e,e.stroke),t.lineWidth=r,t.lineCap=e.strokeCap||\"bu"
-  , "tt\",t.lineJoin=e.strokeJoin||\"miter\",t.miterLimit=e.strokeMiterLimit||10,t.setLineDash&&(t.setLineDash(e.strokeDash||Om),t.lineDashOffset=e.strokeDashOffset||0),!0))}function Lm(t,e){return t.zindex-e.zindex||t.index-e.index}function Um(t){if(!t.zdirty)return t.zitems;var e,n,r,i=t.items,o=[];for(n=0,r=i.length;n<r;++n)(e=i[n]).index=n,e.zindex&&o.push(e);return t.zdirty=!1,t.zitems=o.sort(Lm)}function qm(t,e){var n,r,i=t.items;if(!i||!i.length)return;const o=Um(t);if(o&&o.length){for(n=0,r=i.length;n<r;++n)i[n].zindex||e(i[n]);i=o}for(n=0,r=i.length;n<r;++n)e(i[n])}function Pm(t,e){var n,r,i=t.items;if(!i||!i.length)return null;const o=Um(t);for(o&&o.length&&(i=o),r=i.length;--r>=0;)if(n=e(i[r]))return n;if(i===o)for(r=(i=t.items).length;--r>=0;)if(!i[r].zindex&&(n=e(i[r])))return n;return null}function jm(t){return function(e,n,r){qm(n,(n=>{r&&!r.intersects(n.bounds)||Wm(t,e,n,n)}))}}function Im(t){return function(e,n,r){!n.items.length||r&&!r.intersects(n.bounds)||Wm(t,e,n.items[0],n.items)}}function Wm(t,e,n,r){var i=null==n.opacity?1:n.opacity;0!==i&&(t(e,r)||($m(e,n),n.fill&&zm(e,n,i)&&e.fill(),n.stroke&&Rm(e,n,i)&&e.stroke()))}function Hm(t){return t=t||p,function(e,n,r,i,o,a){return r*=e.pixelRatio,i*=e.pixelRatio,Pm(n,(n=>{const s=n.bounds;if((!s||s.contains(o,a))&&s)return t(e,n,r,i,o,a)?n:void 0}))}}function Ym(t,e){return function(n,r,i,o){var a,s,u=Array.isArray(r)?r[0]:r,l=null==e?u.fill:e,c=u.stroke&&n.isPointInStroke;return c&&(a=u.strokeWidth,s=u.strokeCap,n.lineWidth=null!=a?a:1,n.lineCap=null!=s?s:\"butt\"),!t(n,r)&&(l&&n.isPointInPath(i,o)||c&&n.isPointInStroke(i,o))}}function Gm(t){return Hm(Ym(t))}function Vm(t,e){return\"translate(\"+t+\",\"+e+\")\"}function Xm(t){return\"rotate(\"+t+\")\"}function Jm(t){return Vm(t.x||0,t.y||0)}function Zm(t,e,n){function r(t,n){var r=n.x||0,i=n.y||0,o=n.angle||0;t.translate(r,i),o&&t.rotate(o*=ug),t.beginPath(),e(t,n),o&&t.rotate(-o),t.translate(-r,-i)}return{type:t,tag:\"path\",nested:!1,attr:function(t,n){t(\"transform\",function(t){return Vm(t.x||0,t.y||0)+(t.angle?\" \"+Xm(t.angle):\"\")}(n)),t(\"d\",e(null,n))},bound:function(t,n){return e(_m(t,n.angle),n),em(t,n).translate(n.x||0,n.y||0)},draw:jm(r),pick:Gm(r),isect:n||Em(r)}}var Qm=Zm(\"arc\",(function(t,e){return Rg.context(t)(e)}));function Km(t,e,n){function r(t,n){t.beginPath(),e(t,n)}const i=Ym(r);return{type:t,tag:\"path\",nested:!0,attr:function(t,n){var r=n.mark.items;r.length&&t(\"d\",e(null,r))},bound:function(t,n){var r=n.items;return 0===r.length?t:(e(_m(t),r),em(t,r[0]))},draw:Im(r),pick:function(t,e,n,r,o,a){var s=e.items,u=e.bounds;return!s||!s.length||u&&!u.contains(o,a)?null:(n*=t.pixelRatio,r*=t.pixelRatio,i(t,s,n,r)?s[0]:null)},isect:Dm,tip:n}}var ty=Km(\"area\",(function(t,e){const n=e[0],r=n.interpolate||\"linear\";return(\"horizontal\"===n.orient?Ug:Lg).curve(eg(r,n.orient,n.tension)).context(t)(e)}),(function(t,e){for(var n,r,i=\"horizontal\"===t[0].orient?e[1]:e[0],o=\"horizontal\"===t[0].orient?\"y\":\"x\",a=t.length,s=1/0;--a>=0;)!1!==t[a].defined&&(r=Math.abs(t[a][o]-i))<s&&(s=r,n=t[a]);return n}));function ey(t,e){t.beginPath(),Wg(e)?Hg(t,e,0,0):t.rect(0,0,e.width||0,e.height||0),t.clip()}function ny(t){const e=Tm(t.strokeWidth,1);return null!=t.strokeOffset?t.strokeOffset:t.stroke&&e>.5&&e<1.5?.5-Math.abs(e-1):0}function ry(t,e){const n=ny(e);t(\"d\",Hg(null,e,n,n))}function iy(t,e,n,r){const i=ny(e);t.beginPath(),Hg(t,e,(n||0)+i,(r||0)+i)}const oy=Ym(iy),ay=Ym(iy,!1),sy=Ym(iy,!0);var uy={type:\"group\",tag:\"g\",nested:!1,attr:function(t,e){t(\"transform\",Jm(e))},bound:function(t,e){if(!e.clip&&e.items){const n=e.items,r=n.length;for(let e=0;e<r;++e)t.union(n[e].bounds)}return(e.clip||e.width||e.height)&&!e.noBound&&t.add(0,0).add(e.width||0,e.height||0),em(t,e),t.translate(e.x||0,e.y||0)},draw:function(t,e,n,r){qm(e,(e=>{const i=e.x||0,o=e.y||0,a=e.strokeForeground,s=null==e.opacity?1:e.opacity;(e.stroke||e.fill)&&s&&(iy(t,e,i,o),$m(t,e),e.fill&&zm(t,e,s)&&t.fill(),e.stroke&&!a&&Rm(t,e,s)&&t.stroke()),t.save(),t.translate(i,o),e.clip&&ey(t,e),n&&n.translate(-i,-o),qm(e,(e=>{(\"group\"===e.marktype||null==r||r.includes(e.markt"
-  , "ype))&&this.draw(t,e,n,r)})),n&&n.translate(i,o),t.restore(),a&&e.stroke&&s&&(iy(t,e,i,o),$m(t,e),Rm(t,e,s)&&t.stroke())}))},pick:function(t,e,n,r,i,o){if(e.bounds&&!e.bounds.contains(i,o)||!e.items)return null;const a=n*t.pixelRatio,s=r*t.pixelRatio;return Pm(e,(u=>{let l,c,f;const h=u.bounds;if(h&&!h.contains(i,o))return;c=u.x||0,f=u.y||0;const d=c+(u.width||0),p=f+(u.height||0),g=u.clip;if(g&&(i<c||i>d||o<f||o>p))return;if(t.save(),t.translate(c,f),c=i-c,f=o-f,g&&Wg(u)&&!sy(t,u,a,s))return t.restore(),null;const m=u.strokeForeground,y=!1!==e.interactive;return y&&m&&u.stroke&&ay(t,u,a,s)?(t.restore(),u):(l=Pm(u,(t=>function(t,e,n){return(!1!==t.interactive||\"group\"===t.marktype)&&t.bounds&&t.bounds.contains(e,n)}(t,c,f)?this.pick(t,n,r,c,f):null)),!l&&y&&(u.fill||!m&&u.stroke)&&oy(t,u,a,s)&&(l=u),t.restore(),l||null)}))},isect:Cm,content:function(t,e,n){t(\"clip-path\",e.clip?Vg(n,e,e):null)},background:function(t,e){t(\"class\",\"background\"),t(\"aria-hidden\",!0),ry(t,e)},foreground:function(t,e){t(\"class\",\"foreground\"),t(\"aria-hidden\",!0),e.strokeForeground?ry(t,e):t(\"d\",\"\")}},ly={xmlns:\"http://www.w3.org/2000/svg\",\"xmlns:xlink\":\"http://www.w3.org/1999/xlink\",version:\"1.1\"};function cy(t,e){var n=t.image;return(!n||t.url&&t.url!==n.url)&&(n={complete:!1,width:0,height:0},e.loadImage(t.url).then((e=>{t.image=e,t.image.url=t.url}))),n}function fy(t,e){return null!=t.width?t.width:e&&e.width?!1!==t.aspect&&t.height?t.height*e.width/e.height:e.width:0}function hy(t,e){return null!=t.height?t.height:e&&e.height?!1!==t.aspect&&t.width?t.width*e.height/e.width:e.height:0}function dy(t,e){return\"center\"===t?e/2:\"right\"===t?e:0}function py(t,e){return\"middle\"===t?e/2:\"bottom\"===t?e:0}var gy={type:\"image\",tag:\"image\",nested:!1,attr:function(t,e,n){const r=cy(e,n),i=fy(e,r),o=hy(e,r),a=(e.x||0)-dy(e.align,i),s=(e.y||0)-py(e.baseline,o);t(\"href\",!r.src&&r.toDataURL?r.toDataURL():r.src||\"\",ly[\"xmlns:xlink\"],\"xlink:href\"),t(\"transform\",Vm(a,s)),t(\"width\",i),t(\"height\",o),t(\"preserveAspectRatio\",!1===e.aspect?\"none\":\"xMidYMid\")},bound:function(t,e){const n=e.image,r=fy(e,n),i=hy(e,n),o=(e.x||0)-dy(e.align,r),a=(e.y||0)-py(e.baseline,i);return t.set(o,a,o+r,a+i)},draw:function(t,e,n){qm(e,(e=>{if(n&&!n.intersects(e.bounds))return;const r=cy(e,this);let i=fy(e,r),o=hy(e,r);if(0===i||0===o)return;let a,s,u,l,c=(e.x||0)-dy(e.align,i),f=(e.y||0)-py(e.baseline,o);!1!==e.aspect&&(s=r.width/r.height,u=e.width/e.height,s==s&&u==u&&s!==u&&(u<s?(l=i/s,f+=(o-l)/2,o=l):(l=o*s,c+=(i-l)/2,i=l))),(r.complete||r.toDataURL)&&($m(t,e),t.globalAlpha=null!=(a=e.opacity)?a:1,t.imageSmoothingEnabled=!1!==e.smooth,t.drawImage(r,c,f,i,o))}))},pick:Hm(),isect:p,get:cy,xOffset:dy,yOffset:py},my=Km(\"line\",(function(t,e){const n=e[0],r=n.interpolate||\"linear\";return qg.curve(eg(r,n.orient,n.tension)).context(t)(e)}),(function(t,e){for(var n,r,i=Math.pow(t[0].strokeWidth||1,2),o=t.length;--o>=0;)if(!1!==t[o].defined&&(n=t[o].x-e[0])*n+(r=t[o].y-e[1])*r<i)return t[o];return null}));function yy(t,e){var n=e.path;if(null==n)return!0;var r=e.x||0,i=e.y||0,o=e.scaleX||1,a=e.scaleY||1,s=(e.angle||0)*ug,u=e.pathCache;u&&u.path===n||((e.pathCache=u=sg(n)).path=n),s&&t.rotate&&t.translate?(t.translate(r,i),t.rotate(s),vg(t,u,0,0,o,a),t.rotate(-s),t.translate(-r,-i)):vg(t,u,r,i,o,a)}var vy={type:\"path\",tag:\"path\",nested:!1,attr:function(t,e){var n=e.scaleX||1,r=e.scaleY||1;1===n&&1===r||t(\"vector-effect\",\"non-scaling-stroke\"),t(\"transform\",function(t){return Vm(t.x||0,t.y||0)+(t.angle?\" \"+Xm(t.angle):\"\")+(t.scaleX||t.scaleY?\" \"+function(t,e){return\"scale(\"+t+\",\"+e+\")\"}(t.scaleX||1,t.scaleY||1):\"\")}(e)),t(\"d\",e.path)},bound:function(t,e){return yy(_m(t,e.angle),e)?t.set(0,0,0,0):em(t,e,!0)},draw:jm(yy),pick:Gm(yy),isect:Em(yy)};function _y(t,e){t.beginPath(),Hg(t,e)}var xy={type:\"rect\",tag:\"path\",nested:!1,attr:function(t,e){t(\"d\",Hg(null,e))},bound:function(t,e){var n,r;return em(t.set(n=e.x||0,r=e.y||0,n+e.width||0,r+e.height||0),e)},draw:jm(_y),pick:Gm(_y),isect:Cm};function by(t,e,n){var r,i,o,a;return!(!e.stroke||!Rm(t,e,n))&&(r=e.x||0,i=e.y||0,o=null!=e.x2?e.x2:r,a=null!"
-  , "=e.y2?e.y2:i,t.beginPath(),t.moveTo(r,i),t.lineTo(o,a),!0)}var wy={type:\"rule\",tag:\"line\",nested:!1,attr:function(t,e){t(\"transform\",Jm(e)),t(\"x2\",null!=e.x2?e.x2-(e.x||0):0),t(\"y2\",null!=e.y2?e.y2-(e.y||0):0)},bound:function(t,e){var n,r;return em(t.set(n=e.x||0,r=e.y||0,null!=e.x2?e.x2:n,null!=e.y2?e.y2:r),e)},draw:function(t,e,n){qm(e,(e=>{if(!n||n.intersects(e.bounds)){var r=null==e.opacity?1:e.opacity;r&&by(t,e,r)&&($m(t,e),t.stroke())}}))},pick:Hm((function(t,e,n,r){return!!t.isPointInStroke&&(by(t,e,1)&&t.isPointInStroke(n,r))})),isect:Fm},ky=Zm(\"shape\",(function(t,e){return(e.mark.shape||e.shape).context(t)(e)})),Ay=Zm(\"symbol\",(function(t,e){return jg.context(t)(e)}),Dm);const My=kt();var Ey={height:Ty,measureWidth:Sy,estimateWidth:Cy,width:Cy,canvas:Dy};function Dy(t){Ey.width=t&&Am?Sy:Cy}function Cy(t,e){return Fy(Oy(t,e),Ty(t))}function Fy(t,e){return~~(.8*t.length*e)}function Sy(t,e){return Ty(t)<=0||!(e=Oy(t,e))?0:$y(e,Ly(t))}function $y(t,e){const n=`(${e}) ${t}`;let r=My.get(n);return void 0===r&&(Am.font=e,r=Am.measureText(t).width,My.set(n,r)),r}function Ty(t){return null!=t.fontSize?+t.fontSize||0:11}function By(t){return null!=t.lineHeight?t.lineHeight:Ty(t)+2}function Ny(t){return e=t.lineBreak&&t.text&&!A(t.text)?t.text.split(t.lineBreak):t.text,A(e)?e.length>1?e:e[0]:e;var e}function zy(t){const e=Ny(t);return(A(e)?e.length-1:0)*By(t)}function Oy(t,e){const n=null==e?\"\":(e+\"\").trim();return t.limit>0&&n.length?function(t,e){var n=+t.limit,r=function(t){if(Ey.width===Sy){const e=Ly(t);return t=>$y(t,e)}if(Ey.width===Cy){const e=Ty(t);return t=>Fy(t,e)}return e=>Ey.width(t,e)}(t);if(r(e)<n)return e;var i,o=t.ellipsis||\"…\",a=\"rtl\"===t.dir,s=0,u=e.length;if(n-=r(o),a){for(;s<u;)i=s+u>>>1,r(e.slice(i))>n?s=i+1:u=i;return o+e.slice(s)}for(;s<u;)i=1+(s+u>>>1),r(e.slice(0,i))<n?s=i:u=i-1;return e.slice(0,s)+o}(t,n):n}function Ry(t,e){var n=t.font;return(e&&n?String(n).replace(/\"/g,\"'\"):n)||\"sans-serif\"}function Ly(t,e){return(t.fontStyle?t.fontStyle+\" \":\"\")+(t.fontVariant?t.fontVariant+\" \":\"\")+(t.fontWeight?t.fontWeight+\" \":\"\")+Ty(t)+\"px \"+Ry(t,e)}function Uy(t){var e=t.baseline,n=Ty(t);return Math.round(\"top\"===e?.79*n:\"middle\"===e?.3*n:\"bottom\"===e?-.21*n:\"line-top\"===e?.29*n+.5*By(t):\"line-bottom\"===e?.29*n-.5*By(t):0)}Dy(!0);const qy={left:\"start\",center:\"middle\",right:\"end\"},Py=new Xg;function jy(t){var e,n=t.x||0,r=t.y||0,i=t.radius||0;return i&&(e=(t.theta||0)-lg,n+=i*Math.cos(e),r+=i*Math.sin(e)),Py.x1=n,Py.y1=r,Py}function Iy(t,e,n){var r,i=Ey.height(e),o=e.align,a=jy(e),s=a.x1,u=a.y1,l=e.dx||0,c=(e.dy||0)+Uy(e)-Math.round(.8*i),f=Ny(e);if(A(f)?(i+=By(e)*(f.length-1),r=f.reduce(((t,n)=>Math.max(t,Ey.width(e,n))),0)):r=Ey.width(e,f),\"center\"===o?l-=r/2:\"right\"===o&&(l-=r),t.set(l+=s,c+=u,l+r,c+i),e.angle&&!n)t.rotate(e.angle*ug,s,u);else if(2===n)return t.rotatedPoints(e.angle*ug,s,u);return t}var Wy={type:\"text\",tag:\"text\",nested:!1,attr:function(t,e){var n,r=e.dx||0,i=(e.dy||0)+Uy(e),o=jy(e),a=o.x1,s=o.y1,u=e.angle||0;t(\"text-anchor\",qy[e.align]||\"start\"),u?(n=Vm(a,s)+\" \"+Xm(u),(r||i)&&(n+=\" \"+Vm(r,i))):n=Vm(a+r,s+i),t(\"transform\",n)},bound:Iy,draw:function(t,e,n){qm(e,(e=>{var r,i,o,a,s,u,l,c=null==e.opacity?1:e.opacity;if(!(n&&!n.intersects(e.bounds)||0===c||e.fontSize<=0||null==e.text||0===e.text.length)){if(t.font=Ly(e),t.textAlign=e.align||\"left\",i=(r=jy(e)).x1,o=r.y1,e.angle&&(t.save(),t.translate(i,o),t.rotate(e.angle*ug),i=o=0),i+=e.dx||0,o+=(e.dy||0)+Uy(e),u=Ny(e),$m(t,e),A(u))for(s=By(e),a=0;a<u.length;++a)l=Oy(e,u[a]),e.fill&&zm(t,e,c)&&t.fillText(l,i,o),e.stroke&&Rm(t,e,c)&&t.strokeText(l,i,o),o+=s;else l=Oy(e,u),e.fill&&zm(t,e,c)&&t.fillText(l,i,o),e.stroke&&Rm(t,e,c)&&t.strokeText(l,i,o);e.angle&&t.restore()}}))},pick:Hm((function(t,e,n,r,i,o){if(e.fontSize<=0)return!1;if(!e.angle)return!0;var a=jy(e),s=a.x1,u=a.y1,l=Iy(Py,e,1),c=-e.angle*ug,f=Math.cos(c),h=Math.sin(c),d=f*i-h*o+(s-f*s+h*u),p=h*i+f*o+(u-h*s-f*u);return l.contains(d,p)})),isect:function(t,e){const n=Iy(Py,t,2);return Sm(e,n[0],n[1],n[2],n[3])||Sm(e,n[0],n[1],n[4],n[5])||Sm(e,n[4],n[5],n[6],n[7])||Sm(e,n[2],n[3],n[6],"
-  , "n[7])}},Hy=Km(\"trail\",(function(t,e){return Ig.context(t)(e)}),(function(t,e){for(var n,r,i=t.length;--i>=0;)if(!1!==t[i].defined&&(n=t[i].x-e[0])*n+(r=t[i].y-e[1])*r<(n=t[i].size||1)*n)return t[i];return null})),Yy={arc:Qm,area:ty,group:uy,image:gy,line:my,path:vy,rect:xy,rule:wy,shape:ky,symbol:Ay,text:Wy,trail:Hy};function Gy(t,e,n){var r=Yy[t.mark.marktype],i=e||r.bound;return r.nested&&(t=t.mark),i(t.bounds||(t.bounds=new Xg),t,n)}var Vy={mark:null};function Xy(t,e,n){var r,i,o,a,s=Yy[t.marktype],u=s.bound,l=t.items,c=l&&l.length;if(s.nested)return c?o=l[0]:(Vy.mark=t,o=Vy),a=Gy(o,u,n),e=e&&e.union(a)||a;if(e=e||t.bounds&&t.bounds.clear()||new Xg,c)for(r=0,i=l.length;r<i;++r)e.union(Gy(l[r],u,n));return t.bounds=e}const Jy=[\"marktype\",\"name\",\"role\",\"interactive\",\"clip\",\"items\",\"zindex\",\"x\",\"y\",\"width\",\"height\",\"align\",\"baseline\",\"fill\",\"fillOpacity\",\"opacity\",\"blend\",\"stroke\",\"strokeOpacity\",\"strokeWidth\",\"strokeCap\",\"strokeDash\",\"strokeDashOffset\",\"strokeForeground\",\"strokeOffset\",\"startAngle\",\"endAngle\",\"innerRadius\",\"outerRadius\",\"cornerRadius\",\"padAngle\",\"cornerRadiusTopLeft\",\"cornerRadiusTopRight\",\"cornerRadiusBottomLeft\",\"cornerRadiusBottomRight\",\"interpolate\",\"tension\",\"orient\",\"defined\",\"url\",\"aspect\",\"smooth\",\"path\",\"scaleX\",\"scaleY\",\"x2\",\"y2\",\"size\",\"shape\",\"text\",\"angle\",\"theta\",\"radius\",\"dir\",\"dx\",\"dy\",\"ellipsis\",\"limit\",\"lineBreak\",\"lineHeight\",\"font\",\"fontSize\",\"fontWeight\",\"fontStyle\",\"fontVariant\",\"description\",\"aria\",\"ariaRole\",\"ariaRoleDescription\"];function Zy(t,e){return JSON.stringify(t,Jy,e)}function Qy(t){return Ky(\"string\"==typeof t?JSON.parse(t):t)}function Ky(t){var e,n,r,i=t.marktype,o=t.items;if(o)for(n=0,r=o.length;n<r;++n)e=i?\"mark\":\"group\",o[n][e]=t,o[n].zindex&&(o[n][e].zdirty=!0),\"group\"===(i||e)&&Ky(o[n]);return i&&Xy(t),t}class tv{constructor(t){arguments.length?this.root=Qy(t):(this.root=ev({marktype:\"group\",name:\"root\",role:\"frame\"}),this.root.items=[new Zg(this.root)])}toJSON(t){return Zy(this.root,t||0)}mark(t,e,n){const r=ev(t,e=e||this.root.items[0]);return e.items[n]=r,r.zindex&&(r.group.zdirty=!0),r}}function ev(t,e){const n={bounds:new Xg,clip:!!t.clip,group:e,interactive:!1!==t.interactive,items:[],marktype:t.marktype,name:t.name||void 0,role:t.role||void 0,zindex:t.zindex||0};return null!=t.aria&&(n.aria=t.aria),t.description&&(n.description=t.description),n}function nv(t,e,n){return!t&&\"undefined\"!=typeof document&&document.createElement&&(t=document),t?n?t.createElementNS(n,e):t.createElement(e):null}function rv(t,e){e=e.toLowerCase();for(var n=t.childNodes,r=0,i=n.length;r<i;++r)if(n[r].tagName.toLowerCase()===e)return n[r]}function iv(t,e,n,r){var i,o=t.childNodes[e];return o&&o.tagName.toLowerCase()===n.toLowerCase()||(i=o||null,o=nv(t.ownerDocument,n,r),t.insertBefore(o,i)),o}function ov(t,e){for(var n=t.childNodes,r=n.length;r>e;)t.removeChild(n[--r]);return t}function av(t){return\"mark-\"+t.marktype+(t.role?\" role-\"+t.role:\"\")+(t.name?\" \"+t.name:\"\")}function sv(t,e){const n=e.getBoundingClientRect();return[t.clientX-n.left-(e.clientLeft||0),t.clientY-n.top-(e.clientTop||0)]}class uv{constructor(t,e){this._active=null,this._handlers={},this._loader=t||fa(),this._tooltip=e||lv}initialize(t,e,n){return this._el=t,this._obj=n||null,this.origin(e)}element(){return this._el}canvas(){return this._el&&this._el.firstChild}origin(t){return arguments.length?(this._origin=t||[0,0],this):this._origin.slice()}scene(t){return arguments.length?(this._scene=t,this):this._scene}on(){}off(){}_handlerIndex(t,e,n){for(let r=t?t.length:0;--r>=0;)if(t[r].type===e&&(!n||t[r].handler===n))return r;return-1}handlers(t){const e=this._handlers,n=[];if(t)n.push(...e[this.eventName(t)]);else for(const t in e)n.push(...e[t]);return n}eventName(t){const e=t.indexOf(\".\");return e<0?t:t.slice(0,e)}handleHref(t,e,n){this._loader.sanitize(n,{context:\"href\"}).then((e=>{const n=new MouseEvent(t.type,t),r=nv(null,\"a\");for(const t in e)r.setAttribute(t,e[t]);r.dispatchEvent(n)})).catch((()=>{}))}handleTooltip(t,e,n){if(e&&null!=e.tooltip){e=function(t,e,n,r){var i,o,a=t&&t.mark;if(a&&(i=Yy[a.marktype])."
-  , "tip){for((o=sv(e,n))[0]-=r[0],o[1]-=r[1];t=t.mark.group;)o[0]-=t.x||0,o[1]-=t.y||0;t=i.tip(a.items,o)}return t}(e,t,this.canvas(),this._origin);const r=n&&e&&e.tooltip||null;this._tooltip.call(this._obj,this,t,e,r)}}getItemBoundingClientRect(t){const e=this.canvas();if(!e)return;const n=e.getBoundingClientRect(),r=this._origin,i=t.bounds,o=i.width(),a=i.height();let s=i.x1+r[0]+n.left,u=i.y1+r[1]+n.top;for(;t.mark&&(t=t.mark.group);)s+=t.x||0,u+=t.y||0;return{x:s,y:u,width:o,height:a,left:s,top:u,right:s+o,bottom:u+a}}}function lv(t,e,n,r){t.element().setAttribute(\"title\",r||\"\")}class cv{constructor(t){this._el=null,this._bgcolor=null,this._loader=new Qg(t)}initialize(t,e,n,r,i){return this._el=t,this.resize(e,n,r,i)}element(){return this._el}canvas(){return this._el&&this._el.firstChild}background(t){return 0===arguments.length?this._bgcolor:(this._bgcolor=t,this)}resize(t,e,n,r){return this._width=t,this._height=e,this._origin=n||[0,0],this._scale=r||1,this}dirty(){}render(t,e){const n=this;return n._call=function(){n._render(t,e)},n._call(),n._call=null,n}_render(){}renderAsync(t,e){const n=this.render(t,e);return this._ready?this._ready.then((()=>n)):Promise.resolve(n)}_load(t,e){var n=this,r=n._loader[t](e);if(!n._ready){const t=n._call;n._ready=n._loader.ready().then((e=>{e&&t(),n._ready=null}))}return r}sanitizeURL(t){return this._load(\"sanitizeURL\",t)}loadImage(t){return this._load(\"loadImage\",t)}}const fv=\"dragenter\",hv=\"dragleave\",dv=\"dragover\",pv=\"pointerdown\",gv=\"pointermove\",mv=\"pointerout\",yv=\"pointerover\",vv=\"mousedown\",_v=\"mousemove\",xv=\"mouseout\",bv=\"mouseover\",wv=\"click\",kv=\"mousewheel\",Av=\"touchstart\",Mv=\"touchmove\",Ev=\"touchend\",Dv=[\"keydown\",\"keypress\",\"keyup\",fv,hv,dv,pv,\"pointerup\",gv,mv,yv,vv,\"mouseup\",_v,xv,bv,wv,\"dblclick\",\"wheel\",kv,Av,Mv,Ev],Cv=gv,Fv=xv,Sv=wv;class $v extends uv{constructor(t,e){super(t,e),this._down=null,this._touch=null,this._first=!0,this._events={},this.events=Dv,this.pointermove=zv([gv,_v],[yv,bv],[mv,xv]),this.dragover=zv([dv],[fv],[hv]),this.pointerout=Ov([mv,xv]),this.dragleave=Ov([hv])}initialize(t,e,n){return this._canvas=t&&rv(t,\"canvas\"),[wv,vv,pv,gv,mv,hv].forEach((t=>Bv(this,t))),super.initialize(t,e,n)}canvas(){return this._canvas}context(){return this._canvas.getContext(\"2d\")}DOMMouseScroll(t){this.fire(kv,t)}pointerdown(t){this._down=this._active,this.fire(pv,t)}mousedown(t){this._down=this._active,this.fire(vv,t)}click(t){this._down===this._active&&(this.fire(wv,t),this._down=null)}touchstart(t){this._touch=this.pickEvent(t.changedTouches[0]),this._first&&(this._active=this._touch,this._first=!1),this.fire(Av,t,!0)}touchmove(t){this.fire(Mv,t,!0)}touchend(t){this.fire(Ev,t,!0),this._touch=null}fire(t,e,n){const r=n?this._touch:this._active,i=this._handlers[t];if(e.vegaType=t,t===Sv&&r&&r.href?this.handleHref(e,r,r.href):t!==Cv&&t!==Fv||this.handleTooltip(e,r,t!==Fv),i)for(let t=0,n=i.length;t<n;++t)i[t].handler.call(this._obj,e,r)}on(t,e){const n=this.eventName(t),r=this._handlers;return this._handlerIndex(r[n],t,e)<0&&(Bv(this,t),(r[n]||(r[n]=[])).push({type:t,handler:e})),this}off(t,e){const n=this.eventName(t),r=this._handlers[n],i=this._handlerIndex(r,t,e);return i>=0&&r.splice(i,1),this}pickEvent(t){const e=sv(t,this._canvas),n=this._origin;return this.pick(this._scene,e[0],e[1],e[0]-n[0],e[1]-n[1])}pick(t,e,n,r,i){const o=this.context();return Yy[t.marktype].pick.call(this,o,t,e,n,r,i)}}const Tv=t=>t===Av||t===Mv||t===Ev?[Av,Mv,Ev]:[t];function Bv(t,e){Tv(e).forEach((e=>function(t,e){const n=t.canvas();n&&!t._events[e]&&(t._events[e]=1,n.addEventListener(e,t[e]?n=>t[e](n):n=>t.fire(e,n)))}(t,e)))}function Nv(t,e,n){e.forEach((e=>t.fire(e,n)))}function zv(t,e,n){return function(r){const i=this._active,o=this.pickEvent(r);o===i||(i&&i.exit||Nv(this,n,r),this._active=o,Nv(this,e,r)),Nv(this,t,r)}}function Ov(t){return function(e){Nv(this,t,e),this._active=null}}function Rv(t,e,n,r,i,o){const a=\"undefined\"!=typeof HTMLElement&&t instanceof HTMLElement&&null!=t.parentNode,s=t.getContext(\"2d\"),u=a?\"undefined\"!=typeof window&&window.devicePixelRatio||1:i;t.wid"
-  , "th=e*u,t.height=n*u;for(const t in o)s[t]=o[t];return a&&1!==u&&(t.style.width=e+\"px\",t.style.height=n+\"px\"),s.pixelRatio=u,s.setTransform(u,0,0,u,u*r[0],u*r[1]),t}class Lv extends cv{constructor(t){super(t),this._options={},this._redraw=!1,this._dirty=new Xg,this._tempb=new Xg}initialize(t,e,n,r,i,o){return this._options=o||{},this._canvas=this._options.externalContext?null:$c(1,1,this._options.type),t&&this._canvas&&(ov(t,0).appendChild(this._canvas),this._canvas.setAttribute(\"class\",\"marks\")),super.initialize(t,e,n,r,i)}resize(t,e,n,r){if(super.resize(t,e,n,r),this._canvas)Rv(this._canvas,this._width,this._height,this._origin,this._scale,this._options.context);else{const t=this._options.externalContext;t||s(\"CanvasRenderer is missing a valid canvas or context\"),t.scale(this._scale,this._scale),t.translate(this._origin[0],this._origin[1])}return this._redraw=!0,this}canvas(){return this._canvas}context(){return this._options.externalContext||(this._canvas?this._canvas.getContext(\"2d\"):null)}dirty(t){const e=this._tempb.clear().union(t.bounds);let n=t.mark.group;for(;n;)e.translate(n.x||0,n.y||0),n=n.mark.group;this._dirty.union(e)}_render(t,e){const n=this.context(),r=this._origin,i=this._width,o=this._height,a=this._dirty,s=Uv(r,i,o);n.save();const u=this._redraw||a.empty()?(this._redraw=!1,s.expand(1)):function(t,e,n){e.expand(1).round(),t.pixelRatio%1&&e.scale(t.pixelRatio).round().scale(1/t.pixelRatio);return e.translate(-n[0]%1,-n[1]%1),t.beginPath(),t.rect(e.x1,e.y1,e.width(),e.height()),t.clip(),e}(n,s.intersect(a),r);return this.clear(-r[0],-r[1],i,o),this.draw(n,t,u,e),n.restore(),a.clear(),this}draw(t,e,n,r){if(\"group\"!==e.marktype&&null!=r&&!r.includes(e.marktype))return;const i=Yy[e.marktype];e.clip&&function(t,e){var n=e.clip;t.save(),Z(n)?(t.beginPath(),n(t),t.clip()):ey(t,e.group)}(t,e),i.draw.call(this,t,e,n,r),e.clip&&t.restore()}clear(t,e,n,r){const i=this._options,o=this.context();\"pdf\"===i.type||i.externalContext||o.clearRect(t,e,n,r),null!=this._bgcolor&&(o.fillStyle=this._bgcolor,o.fillRect(t,e,n,r))}}const Uv=(t,e,n)=>(new Xg).set(0,0,e,n).translate(-t[0],-t[1]);class qv extends uv{constructor(t,e){super(t,e);const n=this;n._hrefHandler=Pv(n,((t,e)=>{e&&e.href&&n.handleHref(t,e,e.href)})),n._tooltipHandler=Pv(n,((t,e)=>{n.handleTooltip(t,e,t.type!==Fv)}))}initialize(t,e,n){let r=this._svg;return r&&(r.removeEventListener(Sv,this._hrefHandler),r.removeEventListener(Cv,this._tooltipHandler),r.removeEventListener(Fv,this._tooltipHandler)),this._svg=r=t&&rv(t,\"svg\"),r&&(r.addEventListener(Sv,this._hrefHandler),r.addEventListener(Cv,this._tooltipHandler),r.addEventListener(Fv,this._tooltipHandler)),super.initialize(t,e,n)}canvas(){return this._svg}on(t,e){const n=this.eventName(t),r=this._handlers;if(this._handlerIndex(r[n],t,e)<0){const i={type:t,handler:e,listener:Pv(this,e)};(r[n]||(r[n]=[])).push(i),this._svg&&this._svg.addEventListener(n,i.listener)}return this}off(t,e){const n=this.eventName(t),r=this._handlers[n],i=this._handlerIndex(r,t,e);return i>=0&&(this._svg&&this._svg.removeEventListener(n,r[i].listener),r.splice(i,1)),this}}const Pv=(t,e)=>n=>{let r=n.target.__data__;r=Array.isArray(r)?r[0]:r,n.vegaType=n.type,e.call(t._obj,n,r)},jv=\"aria-hidden\",Iv=\"aria-label\",Wv=\"role\",Hv=\"aria-roledescription\",Yv=\"graphics-object\",Gv=\"graphics-symbol\",Vv=(t,e,n)=>({[Wv]:t,[Hv]:e,[Iv]:n||void 0}),Xv=Bt([\"axis-domain\",\"axis-grid\",\"axis-label\",\"axis-tick\",\"axis-title\",\"legend-band\",\"legend-entry\",\"legend-gradient\",\"legend-label\",\"legend-title\",\"legend-symbol\",\"title\"]),Jv={axis:{desc:\"axis\",caption:function(t){const e=t.datum,n=t.orient,r=e.title?e_(t):null,i=t.context,o=i.scales[e.scale].value,a=i.dataflow.locale(),s=o.type;return(\"left\"===n||\"right\"===n?\"Y\":\"X\")+\"-axis\"+(r?` titled '${r}'`:\"\")+` for a ${fp(s)?\"discrete\":s} scale`+` with ${Gp(a,o,t)}`}},legend:{desc:\"legend\",caption:function(t){const e=t.datum,n=e.title?e_(t):null,r=`${e.type||\"\"} legend`.trim(),i=e.scales,o=Object.keys(i),a=t.context,s=a.scales[i[o[0]]].value,u=a.dataflow.locale();return l=r,(l.length?l[0].toUpperCase()+l.slice(1):l)"
-  , "+(n?` titled '${n}'`:\"\")+` for ${function(t){return t=t.map((t=>t+(\"fill\"===t||\"stroke\"===t?\" color\":\"\"))),t.length<2?t[0]:t.slice(0,-1).join(\", \")+\" and \"+S(t)}(o)}`+` with ${Gp(u,s,t)}`;var l}},\"title-text\":{desc:\"title\",caption:t=>`Title text '${t_(t)}'`},\"title-subtitle\":{desc:\"subtitle\",caption:t=>`Subtitle text '${t_(t)}'`}},Zv={ariaRole:Wv,ariaRoleDescription:Hv,description:Iv};function Qv(t,e){const n=!1===e.aria;if(t(jv,n||void 0),n||null==e.description)for(const e in Zv)t(Zv[e],void 0);else{const n=e.mark.marktype;t(Iv,e.description),t(Wv,e.ariaRole||(\"group\"===n?Yv:Gv)),t(Hv,e.ariaRoleDescription||`${n} mark`)}}function Kv(t){return!1===t.aria?{[jv]:!0}:Xv[t.role]?null:Jv[t.role]?function(t,e){try{const n=t.items[0],r=e.caption||(()=>\"\");return Vv(e.role||Gv,e.desc,n.description||r(n))}catch(t){return null}}(t,Jv[t.role]):function(t){const e=t.marktype,n=\"group\"===e||\"text\"===e||t.items.some((t=>null!=t.description&&!1!==t.aria));return Vv(n?Yv:Gv,`${e} mark container`,t.description)}(t)}function t_(t){return X(t.text).join(\" \")}function e_(t){try{return X(S(t.items).items[0].text).join(\" \")}catch(t){return null}}const n_=t=>(t+\"\").replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\");function r_(){let t=\"\",e=\"\",n=\"\";const r=[],i=()=>e=n=\"\",o=(t,n)=>{var r;return null!=n&&(e+=` ${t}=\"${r=n,n_(r).replace(/\"/g,\"&quot;\").replace(/\\t/g,\"&#x9;\").replace(/\\n/g,\"&#xA;\").replace(/\\r/g,\"&#xD;\")}\"`),a},a={open(s){(o=>{e&&(t+=`${e}>${n}`,i()),r.push(o)})(s),e=\"<\"+s;for(var u=arguments.length,l=new Array(u>1?u-1:0),c=1;c<u;c++)l[c-1]=arguments[c];for(const t of l)for(const e in t)o(e,t[e]);return a},close(){const o=r.pop();return t+=e?e+(n?`>${n}</${o}>`:\"/>\"):`</${o}>`,i(),a},attr:o,text:t=>(n+=n_(t),a),toString:()=>t};return a}const i_=t=>o_(r_(),t)+\"\";function o_(t,e){if(t.open(e.tagName),e.hasAttributes()){const n=e.attributes,r=n.length;for(let e=0;e<r;++e)t.attr(n[e].name,n[e].value)}if(e.hasChildNodes()){const n=e.childNodes;for(const e of n)3===e.nodeType?t.text(e.nodeValue):o_(t,e)}return t.close()}const a_={fill:\"fill\",fillOpacity:\"fill-opacity\",stroke:\"stroke\",strokeOpacity:\"stroke-opacity\",strokeWidth:\"stroke-width\",strokeCap:\"stroke-linecap\",strokeJoin:\"stroke-linejoin\",strokeDash:\"stroke-dasharray\",strokeDashOffset:\"stroke-dashoffset\",strokeMiterLimit:\"stroke-miterlimit\",opacity:\"opacity\"},s_={blend:\"mix-blend-mode\"},u_={fill:\"none\",\"stroke-miterlimit\":10},l_=\"http://www.w3.org/2000/xmlns/\",c_=ly.xmlns;class f_ extends cv{constructor(t){super(t),this._dirtyID=0,this._dirty=[],this._svg=null,this._root=null,this._defs=null}initialize(t,e,n,r,i){return this._defs={},this._clearDefs(),t&&(this._svg=iv(t,0,\"svg\",c_),this._svg.setAttributeNS(l_,\"xmlns\",c_),this._svg.setAttributeNS(l_,\"xmlns:xlink\",ly[\"xmlns:xlink\"]),this._svg.setAttribute(\"version\",ly.version),this._svg.setAttribute(\"class\",\"marks\"),ov(t,1),this._root=iv(this._svg,0,\"g\",c_),b_(this._root,u_),ov(this._svg,1)),this.background(this._bgcolor),super.initialize(t,e,n,r,i)}background(t){return arguments.length&&this._svg&&this._svg.style.setProperty(\"background-color\",t),super.background(...arguments)}resize(t,e,n,r){return super.resize(t,e,n,r),this._svg&&(b_(this._svg,{width:this._width*this._scale,height:this._height*this._scale,viewBox:`0 0 ${this._width} ${this._height}`}),this._root.setAttribute(\"transform\",`translate(${this._origin})`)),this._dirty=[],this}canvas(){return this._svg}svg(){const t=this._svg,e=this._bgcolor;if(!t)return null;let n;e&&(t.removeAttribute(\"style\"),n=iv(t,0,\"rect\",c_),b_(n,{width:this._width,height:this._height,fill:e}));const r=i_(t);return e&&(t.removeChild(n),this._svg.style.setProperty(\"background-color\",e)),r}_render(t,e){return this._dirtyCheck()&&(this._dirtyAll&&this._clearDefs(),this.mark(this._root,t,void 0,e),ov(this._root,1)),this.defs(),this._dirty=[],++this._dirtyID,this}dirty(t){t.dirty!==this._dirtyID&&(t.dirty=this._dirtyID,this._dirty.push(t))}isDirty(t){return this._dirtyAll||!t._svg||!t._svg.ownerSVGElement||t.dirty===this._dirtyID}_dirtyCheck(){this._dirtyAll=!0;const t=this._dirty;if(!t.l"
-  , "ength||!this._dirtyID)return!0;const e=++this._dirtyID;let n,r,i,o,a,s,u;for(a=0,s=t.length;a<s;++a)n=t[a],r=n.mark,r.marktype!==i&&(i=r.marktype,o=Yy[i]),r.zdirty&&r.dirty!==e&&(this._dirtyAll=!1,h_(n,e),r.items.forEach((t=>{t.dirty=e}))),r.zdirty||(n.exit?(o.nested&&r.items.length?(u=r.items[0],u._svg&&this._update(o,u._svg,u)):n._svg&&(u=n._svg.parentNode,u&&u.removeChild(n._svg)),n._svg=null):(n=o.nested?r.items[0]:n,n._update!==e&&(n._svg&&n._svg.ownerSVGElement?this._update(o,n._svg,n):(this._dirtyAll=!1,h_(n,e)),n._update=e)));return!this._dirtyAll}mark(t,e,n,r){if(!this.isDirty(e))return e._svg;const i=this._svg,o=e.marktype,a=Yy[o],s=!1===e.interactive?\"none\":null,u=\"g\"===a.tag,l=g_(e,t,n,\"g\",i);if(\"group\"!==o&&null!=r&&!r.includes(o))return ov(l,0),e._svg;l.setAttribute(\"class\",av(e));const c=Kv(e);for(const t in c)w_(l,t,c[t]);u||w_(l,\"pointer-events\",s),w_(l,\"clip-path\",e.clip?Vg(this,e,e.group):null);let f=null,h=0;const d=t=>{const e=this.isDirty(t),n=g_(t,l,f,a.tag,i);e&&(this._update(a,n,t),u&&function(t,e,n,r){e=e.lastChild.previousSibling;let i,o=0;qm(n,(n=>{i=t.mark(e,n,i,r),++o})),ov(e,1+o)}(this,n,t,r)),f=n,++h};return a.nested?e.items.length&&d(e.items[0]):qm(e,d),ov(l,h),l}_update(t,e,n){m_=e,y_=e.__values__,Qv(__,n),t.attr(__,n,this);const r=v_[t.type];r&&r.call(this,t,e,n),m_&&this.style(m_,n)}style(t,e){if(null!=e){for(const n in a_){let r=\"font\"===n?Ry(e):e[n];if(r===y_[n])continue;const i=a_[n];null==r?t.removeAttribute(i):(Jp(r)&&(r=Zp(r,this._defs.gradient,k_())),t.setAttribute(i,r+\"\")),y_[n]=r}for(const n in s_)x_(t,s_[n],e[n])}}defs(){const t=this._svg,e=this._defs;let n=e.el,r=0;for(const i in e.gradient)n||(e.el=n=iv(t,1,\"defs\",c_)),r=d_(n,e.gradient[i],r);for(const i in e.clipping)n||(e.el=n=iv(t,1,\"defs\",c_)),r=p_(n,e.clipping[i],r);n&&(0===r?(t.removeChild(n),e.el=null):ov(n,r))}_clearDefs(){const t=this._defs;t.gradient={},t.clipping={}}}function h_(t,e){for(;t&&t.dirty!==e;t=t.mark.group){if(t.dirty=e,!t.mark||t.mark.dirty===e)return;t.mark.dirty=e}}function d_(t,e,n){let r,i,o;if(\"radial\"===e.gradient){let r=iv(t,n++,\"pattern\",c_);b_(r,{id:Xp+e.id,viewBox:\"0,0,1,1\",width:\"100%\",height:\"100%\",preserveAspectRatio:\"xMidYMid slice\"}),r=iv(r,0,\"rect\",c_),b_(r,{width:1,height:1,fill:`url(${k_()}#${e.id})`}),b_(t=iv(t,n++,\"radialGradient\",c_),{id:e.id,fx:e.x1,fy:e.y1,fr:e.r1,cx:e.x2,cy:e.y2,r:e.r2})}else b_(t=iv(t,n++,\"linearGradient\",c_),{id:e.id,x1:e.x1,x2:e.x2,y1:e.y1,y2:e.y2});for(r=0,i=e.stops.length;r<i;++r)o=iv(t,r,\"stop\",c_),o.setAttribute(\"offset\",e.stops[r].offset),o.setAttribute(\"stop-color\",e.stops[r].color);return ov(t,r),n}function p_(t,e,n){let r;return(t=iv(t,n,\"clipPath\",c_)).setAttribute(\"id\",e.id),e.path?(r=iv(t,0,\"path\",c_),r.setAttribute(\"d\",e.path)):(r=iv(t,0,\"rect\",c_),b_(r,{x:0,y:0,width:e.width,height:e.height})),ov(t,1),n+1}function g_(t,e,n,r,i){let o,a=t._svg;if(!a&&(o=e.ownerDocument,a=nv(o,r,c_),t._svg=a,t.mark&&(a.__data__=t,a.__values__={fill:\"default\"},\"g\"===r))){const e=nv(o,\"path\",c_);a.appendChild(e),e.__data__=t;const n=nv(o,\"g\",c_);a.appendChild(n),n.__data__=t;const r=nv(o,\"path\",c_);a.appendChild(r),r.__data__=t,r.__values__={fill:\"default\"}}return(a.ownerSVGElement!==i||function(t,e){return t.parentNode&&t.parentNode.childNodes.length>1&&t.previousSibling!=e}(a,n))&&e.insertBefore(a,n?n.nextSibling:e.firstChild),a}let m_=null,y_=null;const v_={group(t,e,n){const r=m_=e.childNodes[2];y_=r.__values__,t.foreground(__,n,this),y_=e.__values__,m_=e.childNodes[1],t.content(__,n,this);const i=m_=e.childNodes[0];t.background(__,n,this);const o=!1===n.mark.interactive?\"none\":null;if(o!==y_.events&&(w_(r,\"pointer-events\",o),w_(i,\"pointer-events\",o),y_.events=o),n.strokeForeground&&n.stroke){const t=n.fill;w_(r,\"display\",null),this.style(i,n),w_(i,\"stroke\",null),t&&(n.fill=null),y_=r.__values__,this.style(r,n),t&&(n.fill=t),m_=null}else w_(r,\"display\",\"none\")},image(t,e,n){!1===n.smooth?(x_(e,\"image-rendering\",\"optimizeSpeed\"),x_(e,\"image-rendering\",\"pixelated\")):x_(e,\"image-rendering\",null)},text(t,e,n){const r=Ny(n);let i,o,a,s;A(r)?(o=r.map((t=>Oy(n,t))),i="
-  , "o.join(\"\\n\"),i!==y_.text&&(ov(e,0),a=e.ownerDocument,s=By(n),o.forEach(((t,r)=>{const i=nv(a,\"tspan\",c_);i.__data__=n,i.textContent=t,r&&(i.setAttribute(\"x\",0),i.setAttribute(\"dy\",s)),e.appendChild(i)})),y_.text=i)):(o=Oy(n,r),o!==y_.text&&(e.textContent=o,y_.text=o)),w_(e,\"font-family\",Ry(n)),w_(e,\"font-size\",Ty(n)+\"px\"),w_(e,\"font-style\",n.fontStyle),w_(e,\"font-variant\",n.fontVariant),w_(e,\"font-weight\",n.fontWeight)}};function __(t,e,n){e!==y_[t]&&(n?function(t,e,n,r){null!=n?t.setAttributeNS(r,e,n):t.removeAttributeNS(r,e)}(m_,t,e,n):w_(m_,t,e),y_[t]=e)}function x_(t,e,n){n!==y_[e]&&(null==n?t.style.removeProperty(e):t.style.setProperty(e,n+\"\"),y_[e]=n)}function b_(t,e){for(const n in e)w_(t,n,e[n])}function w_(t,e,n){null!=n?t.setAttribute(e,n):t.removeAttribute(e)}function k_(){let t;return\"undefined\"==typeof window?\"\":(t=window.location).hash?t.href.slice(0,-t.hash.length):t.href}class A_ extends cv{constructor(t){super(t),this._text=null,this._defs={gradient:{},clipping:{}}}svg(){return this._text}_render(t){const e=r_();e.open(\"svg\",at({},ly,{class:\"marks\",width:this._width*this._scale,height:this._height*this._scale,viewBox:`0 0 ${this._width} ${this._height}`}));const n=this._bgcolor;return n&&\"transparent\"!==n&&\"none\"!==n&&e.open(\"rect\",{width:this._width,height:this._height,fill:n}).close(),e.open(\"g\",u_,{transform:\"translate(\"+this._origin+\")\"}),this.mark(e,t),e.close(),this.defs(e),this._text=e.close()+\"\",this}mark(t,e){const n=Yy[e.marktype],r=n.tag,i=[Qv,n.attr];t.open(\"g\",{class:av(e),\"clip-path\":e.clip?Vg(this,e,e.group):null},Kv(e),{\"pointer-events\":\"g\"!==r&&!1===e.interactive?\"none\":null});const o=o=>{const a=this.href(o);if(a&&t.open(\"a\",a),t.open(r,this.attr(e,o,i,\"g\"!==r?r:null)),\"text\"===r){const e=Ny(o);if(A(e)){const n={x:0,dy:By(o)};for(let r=0;r<e.length;++r)t.open(\"tspan\",r?n:null).text(Oy(o,e[r])).close()}else t.text(Oy(o,e))}else if(\"g\"===r){const r=o.strokeForeground,i=o.fill,a=o.stroke;r&&a&&(o.stroke=null),t.open(\"path\",this.attr(e,o,n.background,\"bgrect\")).close(),t.open(\"g\",this.attr(e,o,n.content)),qm(o,(e=>this.mark(t,e))),t.close(),r&&a?(i&&(o.fill=null),o.stroke=a,t.open(\"path\",this.attr(e,o,n.foreground,\"bgrect\")).close(),i&&(o.fill=i)):t.open(\"path\",this.attr(e,o,n.foreground,\"bgfore\")).close()}t.close(),a&&t.close()};return n.nested?e.items&&e.items.length&&o(e.items[0]):qm(e,o),t.close()}href(t){const e=t.href;let n;if(e){if(n=this._hrefs&&this._hrefs[e])return n;this.sanitizeURL(e).then((t=>{t[\"xlink:href\"]=t.href,t.href=null,(this._hrefs||(this._hrefs={}))[e]=t}))}return null}attr(t,e,n,r){const i={},o=(t,e,n,r)=>{i[r||t]=e};return Array.isArray(n)?n.forEach((t=>t(o,e,this))):n(o,e,this),r&&function(t,e,n,r,i){let o;if(null==e)return t;\"bgrect\"===r&&!1===n.interactive&&(t[\"pointer-events\"]=\"none\");if(\"bgfore\"===r&&(!1===n.interactive&&(t[\"pointer-events\"]=\"none\"),t.display=\"none\",null!==e.fill))return t;\"image\"===r&&!1===e.smooth&&(o=[\"image-rendering: optimizeSpeed;\",\"image-rendering: pixelated;\"]);\"text\"===r&&(t[\"font-family\"]=Ry(e),t[\"font-size\"]=Ty(e)+\"px\",t[\"font-style\"]=e.fontStyle,t[\"font-variant\"]=e.fontVariant,t[\"font-weight\"]=e.fontWeight);for(const n in a_){let r=e[n];const o=a_[n];(\"transparent\"!==r||\"fill\"!==o&&\"stroke\"!==o)&&null!=r&&(Jp(r)&&(r=Zp(r,i.gradient,\"\")),t[o]=r)}for(const t in s_){const n=e[t];null!=n&&(o=o||[],o.push(`${s_[t]}: ${n};`))}o&&(t.style=o.join(\" \"))}(i,e,t,r,this._defs),i}defs(t){const e=this._defs.gradient,n=this._defs.clipping;if(0!==Object.keys(e).length+Object.keys(n).length){t.open(\"defs\");for(const n in e){const r=e[n],i=r.stops;\"radial\"===r.gradient?(t.open(\"pattern\",{id:Xp+n,viewBox:\"0,0,1,1\",width:\"100%\",height:\"100%\",preserveAspectRatio:\"xMidYMid slice\"}),t.open(\"rect\",{width:\"1\",height:\"1\",fill:\"url(#\"+n+\")\"}).close(),t.close(),t.open(\"radialGradient\",{id:n,fx:r.x1,fy:r.y1,fr:r.r1,cx:r.x2,cy:r.y2,r:r.r2})):t.open(\"linearGradient\",{id:n,x1:r.x1,x2:r.x2,y1:r.y1,y2:r.y2});for(let e=0;e<i.length;++e)t.open(\"stop\",{offset:i[e].offset,\"stop-color\":i[e].color}).close();t.close()}for(const e in n){const r=n[e];t.open(\"clipPath\",{id:e}"
-  , "),r.path?t.open(\"path\",{d:r.path}).close():t.open(\"rect\",{x:0,y:0,width:r.width,height:r.height}).close(),t.close()}t.close()}}}const M_={svgMarkTypes:[\"text\"],svgOnTop:!0,debug:!1};class E_ extends cv{constructor(t){super(t),this._svgRenderer=new f_(t),this._canvasRenderer=new Lv(t)}initialize(t,e,n,r,i){this._root_el=iv(t,0,\"div\");const o=iv(this._root_el,0,\"div\"),a=iv(this._root_el,1,\"div\");return this._root_el.style.position=\"relative\",M_.debug||(o.style.height=\"100%\",a.style.position=\"absolute\",a.style.top=\"0\",a.style.left=\"0\",a.style.height=\"100%\",a.style.width=\"100%\"),this._svgEl=M_.svgOnTop?a:o,this._canvasEl=M_.svgOnTop?o:a,this._svgEl.style.pointerEvents=\"none\",this._canvasRenderer.initialize(this._canvasEl,e,n,r,i),this._svgRenderer.initialize(this._svgEl,e,n,r,i),super.initialize(t,e,n,r,i)}dirty(t){return M_.svgMarkTypes.includes(t.mark.marktype)?this._svgRenderer.dirty(t):this._canvasRenderer.dirty(t),this}_render(t,e){const n=(e??[\"arc\",\"area\",\"image\",\"line\",\"path\",\"rect\",\"rule\",\"shape\",\"symbol\",\"text\",\"trail\"]).filter((t=>!M_.svgMarkTypes.includes(t)));this._svgRenderer.render(t,M_.svgMarkTypes),this._canvasRenderer.render(t,n)}resize(t,e,n,r){return super.resize(t,e,n,r),this._svgRenderer.resize(t,e,n,r),this._canvasRenderer.resize(t,e,n,r),this}background(t){return M_.svgOnTop?this._canvasRenderer.background(t):this._svgRenderer.background(t),this}}class D_ extends $v{constructor(t,e){super(t,e)}initialize(t,e,n){const r=iv(iv(t,0,\"div\"),M_.svgOnTop?0:1,\"div\");return super.initialize(r,e,n)}}const C_=\"canvas\",F_=\"hybrid\",S_=\"none\",$_={Canvas:C_,PNG:\"png\",SVG:\"svg\",Hybrid:F_,None:S_},T_={};function B_(t,e){return t=String(t||\"\").toLowerCase(),arguments.length>1?(T_[t]=e,this):T_[t]}function N_(t,e,n){const r=[],i=(new Xg).union(e),o=t.marktype;return o?z_(t,i,n,r):\"group\"===o?O_(t,i,n,r):s(\"Intersect scene must be mark node or group item.\")}function z_(t,e,n,r){if(function(t,e,n){return t.bounds&&e.intersects(t.bounds)&&(\"group\"===t.marktype||!1!==t.interactive&&(!n||n(t)))}(t,e,n)){const i=t.items,o=t.marktype,a=i.length;let s=0;if(\"group\"===o)for(;s<a;++s)O_(i[s],e,n,r);else for(const t=Yy[o].isect;s<a;++s){const n=i[s];R_(n,e,t)&&r.push(n)}}return r}function O_(t,e,n,r){n&&n(t.mark)&&R_(t,e,Yy.group.isect)&&r.push(t);const i=t.items,o=i&&i.length;if(o){const a=t.x||0,s=t.y||0;e.translate(-a,-s);for(let t=0;t<o;++t)z_(i[t],e,n,r);e.translate(a,s)}return r}function R_(t,e,n){const r=t.bounds;return e.encloses(r)||e.intersects(r)&&n(t,e)}T_[C_]=T_.png={renderer:Lv,headless:Lv,handler:$v},T_.svg={renderer:f_,headless:A_,handler:qv},T_[F_]={renderer:E_,headless:E_,handler:D_},T_[S_]={};const L_=new Xg;function U_(t){const e=t.clip;if(Z(e))e(_m(L_.clear()));else{if(!e)return;L_.set(0,0,t.group.width,t.group.height)}t.bounds.intersect(L_)}const q_=1e-9;function P_(t,e,n){return t===e||(\"path\"===n?j_(t,e):t instanceof Date&&e instanceof Date?+t==+e:vt(t)&&vt(e)?Math.abs(t-e)<=q_:t&&e&&(M(t)||M(e))?function(t,e){var n,r,i=Object.keys(t),o=Object.keys(e);if(i.length!==o.length)return!1;for(i.sort(),o.sort(),r=i.length-1;r>=0;r--)if(i[r]!=o[r])return!1;for(r=i.length-1;r>=0;r--)if(!P_(t[n=i[r]],e[n],n))return!1;return typeof t==typeof e}(t,e):t==e)}function j_(t,e){return P_(sg(t),sg(e))}const I_=\"top\",W_=\"left\",H_=\"right\",Y_=\"bottom\",G_=\"top-left\",V_=\"top-right\",X_=\"bottom-left\",J_=\"bottom-right\",Z_=\"start\",Q_=\"middle\",K_=\"end\",tx=\"x\",ex=\"y\",nx=\"group\",rx=\"axis\",ix=\"title\",ox=\"frame\",ax=\"scope\",sx=\"legend\",ux=\"row-header\",lx=\"row-footer\",cx=\"row-title\",fx=\"column-header\",hx=\"column-footer\",dx=\"column-title\",px=\"padding\",gx=\"symbol\",mx=\"fit\",yx=\"fit-x\",vx=\"fit-y\",_x=\"pad\",xx=\"none\",bx=\"all\",wx=\"each\",kx=\"flush\",Ax=\"column\",Mx=\"row\";function Ex(t){Ja.call(this,null,t)}function Dx(t,e,n){return e(t.bounds.clear(),t,n)}dt(Ex,Ja,{transform(t,e){const n=e.dataflow,r=t.mark,i=r.marktype,o=Yy[i],a=o.bound;let s,u=r.bounds;if(o.nested)r.items.length&&n.dirty(r.items[0]),u=Dx(r,a),r.items.forEach((t=>{t.bounds.clear().union(u)}));else if(i===nx||t.modified())switch(e.visit(e.MOD,(t=>n.dirty(t))),u.clear(),r.items.forEach"
-  , "((t=>u.union(Dx(t,a)))),r.role){case rx:case sx:case ix:e.reflow()}else s=e.changed(e.REM),e.visit(e.ADD,(t=>{u.union(Dx(t,a))})),e.visit(e.MOD,(t=>{s=s||u.alignsWith(t.bounds),n.dirty(t),u.union(Dx(t,a))})),s&&(u.clear(),r.items.forEach((t=>u.union(t.bounds))));return U_(r),e.modifies(\"bounds\")}});const Cx=\":vega_identifier:\";function Fx(t){Ja.call(this,0,t)}function Sx(t){Ja.call(this,null,t)}function $x(t){Ja.call(this,null,t)}Fx.Definition={type:\"Identifier\",metadata:{modifies:!0},params:[{name:\"as\",type:\"string\",required:!0}]},dt(Fx,Ja,{transform(t,e){const n=(i=e.dataflow)._signals[Cx]||(i._signals[Cx]=i.add(0)),r=t.as;var i;let o=n.value;return e.visit(e.ADD,(t=>t[r]=t[r]||++o)),n.set(this.value=o),e}}),dt(Sx,Ja,{transform(t,e){let n=this.value;n||(n=e.dataflow.scenegraph().mark(t.markdef,function(t){const e=t.groups,n=t.parent;return e&&1===e.size?e.get(Object.keys(e.object)[0]):e&&n?e.lookup(n):null}(t),t.index),n.group.context=t.context,t.context.group||(t.context.group=n.group),n.source=this.source,n.clip=t.clip,n.interactive=t.interactive,this.value=n);const r=n.marktype===nx?Zg:Jg;return e.visit(e.ADD,(t=>r.call(t,n))),(t.modified(\"clip\")||t.modified(\"interactive\"))&&(n.clip=t.clip,n.interactive=!!t.interactive,n.zdirty=!0,e.reflow()),n.items=e.source,e}});const Tx={parity:t=>t.filter(((t,e)=>e%2?t.opacity=0:1)),greedy:(t,e)=>{let n;return t.filter(((t,r)=>r&&Bx(n.bounds,t.bounds,e)?t.opacity=0:(n=t,1)))}},Bx=(t,e,n)=>n>Math.max(e.x1-t.x2,t.x1-e.x2,e.y1-t.y2,t.y1-e.y2),Nx=(t,e)=>{for(var n,r=1,i=t.length,o=t[0].bounds;r<i;o=n,++r)if(Bx(o,n=t[r].bounds,e))return!0},zx=t=>{const e=t.bounds;return e.width()>1&&e.height()>1},Ox=t=>(t.forEach((t=>t.opacity=1)),t),Rx=(t,e)=>t.reflow(e.modified()).modifies(\"opacity\");function Lx(t){Ja.call(this,null,t)}dt($x,Ja,{transform(t,e){const n=Tx[t.method]||Tx.parity,r=t.separation||0;let i,o,a=e.materialize(e.SOURCE).source;if(!a||!a.length)return;if(!t.method)return t.modified(\"method\")&&(Ox(a),e=Rx(e,t)),e;if(a=a.filter(zx),!a.length)return;if(t.sort&&(a=a.slice().sort(t.sort)),i=Ox(a),e=Rx(e,t),i.length>=3&&Nx(i,r)){do{i=n(i,r)}while(i.length>=3&&Nx(i,r));i.length<3&&!S(a).opacity&&(i.length>1&&(S(i).opacity=0),S(a).opacity=1)}t.boundScale&&t.boundTolerance>=0&&(o=((t,e,n)=>{var r=t.range(),i=new Xg;return e===I_||e===Y_?i.set(r[0],-1/0,r[1],1/0):i.set(-1/0,r[0],1/0,r[1]),i.expand(n||1),t=>i.encloses(t.bounds)})(t.boundScale,t.boundOrient,+t.boundTolerance),a.forEach((t=>{o(t)||(t.opacity=0)})));const s=i[0].mark.bounds.clear();return a.forEach((t=>{t.opacity&&s.union(t.bounds)})),e}}),dt(Lx,Ja,{transform(t,e){const n=e.dataflow;if(e.visit(e.ALL,(t=>n.dirty(t))),e.fields&&e.fields.zindex){const t=e.source&&e.source[0];t&&(t.mark.zdirty=!0)}}});const Ux=new Xg;function qx(t,e,n){return t[e]===n?0:(t[e]=n,1)}function Px(t){var e=t.items[0].orient;return e===W_||e===H_}function jx(t,e,n,r){var i,o,a=e.items[0],s=a.datum,u=null!=a.translate?a.translate:.5,l=a.orient,c=function(t){let e=+t.grid;return[t.ticks?e++:-1,t.labels?e++:-1,e+ +t.domain]}(s),f=a.range,h=a.offset,d=a.position,p=a.minExtent,g=a.maxExtent,m=s.title&&a.items[c[2]].items[0],y=a.titlePadding,v=a.bounds,_=m&&zy(m),x=0,b=0;switch(Ux.clear().union(v),v.clear(),(i=c[0])>-1&&v.union(a.items[i].bounds),(i=c[1])>-1&&v.union(a.items[i].bounds),l){case I_:x=d||0,b=-h,o=Math.max(p,Math.min(g,-v.y1)),v.add(0,-o).add(f,0),m&&Ix(t,m,o,y,_,0,-1,v);break;case W_:x=-h,b=d||0,o=Math.max(p,Math.min(g,-v.x1)),v.add(-o,0).add(0,f),m&&Ix(t,m,o,y,_,1,-1,v);break;case H_:x=n+h,b=d||0,o=Math.max(p,Math.min(g,v.x2)),v.add(0,0).add(o,f),m&&Ix(t,m,o,y,_,1,1,v);break;case Y_:x=d||0,b=r+h,o=Math.max(p,Math.min(g,v.y2)),v.add(0,0).add(f,o),m&&Ix(t,m,o,y,0,0,1,v);break;default:x=a.x,b=a.y}return em(v.translate(x,b),a),qx(a,\"x\",x+u)|qx(a,\"y\",b+u)&&(a.bounds=Ux,t.dirty(a),a.bounds=v,t.dirty(a)),a.mark.bounds.clear().union(v)}function Ix(t,e,n,r,i,o,a,s){const u=e.bounds;if(e.auto){const s=a*(n+i+r);let l=0,c=0;t.dirty(e),o?l=(e.x||0)-(e.x=s):c=(e.y||0)-(e.y=s),e.mark.bounds.clear().union(u.translate(-l,-c)),t.dirty(e)}s.union(u)}const Wx="
-  , "(t,e)=>Math.floor(Math.min(t,e)),Hx=(t,e)=>Math.ceil(Math.max(t,e));function Yx(t){return(new Xg).set(0,0,t.width||0,t.height||0)}function Gx(t){const e=t.bounds.clone();return e.empty()?e.set(0,0,0,0):e.translate(-(t.x||0),-(t.y||0))}function Vx(t,e,n){const r=M(t)?t[e]:t;return null!=r?r:void 0!==n?n:0}function Xx(t){return t<0?Math.ceil(-t):0}function Jx(t,e,n){var r,i,o,a,s,u,l,c,f,h,d,p=!n.nodirty,g=n.bounds===kx?Yx:Gx,m=Ux.set(0,0,0,0),y=Vx(n.align,Ax),v=Vx(n.align,Mx),_=Vx(n.padding,Ax),x=Vx(n.padding,Mx),b=n.columns||e.length,w=b<=0?1:Math.ceil(e.length/b),k=e.length,A=Array(k),M=Array(b),E=0,D=Array(k),C=Array(w),F=0,S=Array(k),$=Array(k),T=Array(k);for(i=0;i<b;++i)M[i]=0;for(i=0;i<w;++i)C[i]=0;for(i=0;i<k;++i)u=e[i],s=T[i]=g(u),u.x=u.x||0,S[i]=0,u.y=u.y||0,$[i]=0,o=i%b,a=~~(i/b),E=Math.max(E,l=Math.ceil(s.x2)),F=Math.max(F,c=Math.ceil(s.y2)),M[o]=Math.max(M[o],l),C[a]=Math.max(C[a],c),A[i]=_+Xx(s.x1),D[i]=x+Xx(s.y1),p&&t.dirty(e[i]);for(i=0;i<k;++i)i%b==0&&(A[i]=0),i<b&&(D[i]=0);if(y===wx)for(o=1;o<b;++o){for(d=0,i=o;i<k;i+=b)d<A[i]&&(d=A[i]);for(i=o;i<k;i+=b)A[i]=d+M[o-1]}else if(y===bx){for(d=0,i=0;i<k;++i)i%b&&d<A[i]&&(d=A[i]);for(i=0;i<k;++i)i%b&&(A[i]=d+E)}else for(y=!1,o=1;o<b;++o)for(i=o;i<k;i+=b)A[i]+=M[o-1];if(v===wx)for(a=1;a<w;++a){for(d=0,r=(i=a*b)+b;i<r;++i)d<D[i]&&(d=D[i]);for(i=a*b;i<r;++i)D[i]=d+C[a-1]}else if(v===bx){for(d=0,i=b;i<k;++i)d<D[i]&&(d=D[i]);for(i=b;i<k;++i)D[i]=d+F}else for(v=!1,a=1;a<w;++a)for(r=(i=a*b)+b;i<r;++i)D[i]+=C[a-1];for(f=0,i=0;i<k;++i)f=A[i]+(i%b?f:0),S[i]+=f-e[i].x;for(o=0;o<b;++o)for(h=0,i=o;i<k;i+=b)h+=D[i],$[i]+=h-e[i].y;if(y&&Vx(n.center,Ax)&&w>1)for(i=0;i<k;++i)(f=(s=y===bx?E:M[i%b])-T[i].x2-e[i].x-S[i])>0&&(S[i]+=f/2);if(v&&Vx(n.center,Mx)&&1!==b)for(i=0;i<k;++i)(h=(s=v===bx?F:C[~~(i/b)])-T[i].y2-e[i].y-$[i])>0&&($[i]+=h/2);for(i=0;i<k;++i)m.union(T[i].translate(S[i],$[i]));switch(f=Vx(n.anchor,tx),h=Vx(n.anchor,ex),Vx(n.anchor,Ax)){case K_:f-=m.width();break;case Q_:f-=m.width()/2}switch(Vx(n.anchor,Mx)){case K_:h-=m.height();break;case Q_:h-=m.height()/2}for(f=Math.round(f),h=Math.round(h),m.clear(),i=0;i<k;++i)e[i].mark.bounds.clear();for(i=0;i<k;++i)(u=e[i]).x+=S[i]+=f,u.y+=$[i]+=h,m.union(u.mark.bounds.union(u.bounds.translate(S[i],$[i]))),p&&t.dirty(u);return m}function Zx(t,e,n){var r,i,o,a,s,u,l,c=function(t){var e,n,r=t.items,i=r.length,o=0;const a={marks:[],rowheaders:[],rowfooters:[],colheaders:[],colfooters:[],rowtitle:null,coltitle:null};for(;o<i;++o)if(n=(e=r[o]).items,e.marktype===nx)switch(e.role){case rx:case sx:case ix:break;case ux:a.rowheaders.push(...n);break;case lx:a.rowfooters.push(...n);break;case fx:a.colheaders.push(...n);break;case hx:a.colfooters.push(...n);break;case cx:a.rowtitle=n[0];break;case dx:a.coltitle=n[0];break;default:a.marks.push(...n)}return a}(e),f=c.marks,h=n.bounds===kx?Qx:Kx,d=n.offset,p=n.columns||f.length,g=p<=0?1:Math.ceil(f.length/p),m=g*p;const y=Jx(t,f,n);y.empty()&&y.set(0,0,0,0),c.rowheaders&&(u=Vx(n.headerBand,Mx,null),r=tb(t,c.rowheaders,f,p,g,-Vx(d,\"rowHeader\"),Wx,0,h,\"x1\",0,p,1,u)),c.colheaders&&(u=Vx(n.headerBand,Ax,null),i=tb(t,c.colheaders,f,p,p,-Vx(d,\"columnHeader\"),Wx,1,h,\"y1\",0,1,p,u)),c.rowfooters&&(u=Vx(n.footerBand,Mx,null),o=tb(t,c.rowfooters,f,p,g,Vx(d,\"rowFooter\"),Hx,0,h,\"x2\",p-1,p,1,u)),c.colfooters&&(u=Vx(n.footerBand,Ax,null),a=tb(t,c.colfooters,f,p,p,Vx(d,\"columnFooter\"),Hx,1,h,\"y2\",m-p,1,p,u)),c.rowtitle&&(s=Vx(n.titleAnchor,Mx),l=Vx(d,\"rowTitle\"),l=s===K_?o+l:r-l,u=Vx(n.titleBand,Mx,.5),eb(t,c.rowtitle,l,0,y,u)),c.coltitle&&(s=Vx(n.titleAnchor,Ax),l=Vx(d,\"columnTitle\"),l=s===K_?a+l:i-l,u=Vx(n.titleBand,Ax,.5),eb(t,c.coltitle,l,1,y,u))}function Qx(t,e){return\"x1\"===e?t.x||0:\"y1\"===e?t.y||0:\"x2\"===e?(t.x||0)+(t.width||0):\"y2\"===e?(t.y||0)+(t.height||0):void 0}function Kx(t,e){return t.bounds[e]}function tb(t,e,n,r,i,o,a,s,u,l,c,f,h,d){var p,g,m,y,v,_,x,b,w,k=n.length,A=0,M=0;if(!k)return A;for(p=c;p<k;p+=f)n[p]&&(A=a(A,u(n[p],l)));if(!e.length)return A;for(e.length>i&&(t.warn(\"Grid headers exceed limit: \"+i),e=e.slice(0,i)),A+=o,g=0,y=e.length;g<y;++g)t.dirty(e[g]),e[g].mark.bounds.clear()"
-  , ";for(p=c,g=0,y=e.length;g<y;++g,p+=f){for(v=(_=e[g]).mark.bounds,m=p;m>=0&&null==(x=n[m]);m-=h);s?(b=null==d?x.x:Math.round(x.bounds.x1+d*x.bounds.width()),w=A):(b=A,w=null==d?x.y:Math.round(x.bounds.y1+d*x.bounds.height())),v.union(_.bounds.translate(b-(_.x||0),w-(_.y||0))),_.x=b,_.y=w,t.dirty(_),M=a(M,v[l])}return M}function eb(t,e,n,r,i,o){if(e){t.dirty(e);var a=n,s=n;r?a=Math.round(i.x1+o*i.width()):s=Math.round(i.y1+o*i.height()),e.bounds.translate(a-(e.x||0),s-(e.y||0)),e.mark.bounds.clear().union(e.bounds),e.x=a,e.y=s,t.dirty(e)}}function nb(t,e,n,r,i,o,a){const s=function(t,e){const n=t[e]||{};return(e,r)=>null!=n[e]?n[e]:null!=t[e]?t[e]:r}(n,e),u=function(t,e){let n=-1/0;return t.forEach((t=>{null!=t.offset&&(n=Math.max(n,t.offset))})),n>-1/0?n:e}(t,s(\"offset\",0)),l=s(\"anchor\",Z_),c=l===K_?1:l===Q_?.5:0,f={align:wx,bounds:s(\"bounds\",kx),columns:\"vertical\"===s(\"direction\")?1:t.length,padding:s(\"margin\",8),center:s(\"center\"),nodirty:!0};switch(e){case W_:f.anchor={x:Math.floor(r.x1)-u,column:K_,y:c*(a||r.height()+2*r.y1),row:l};break;case H_:f.anchor={x:Math.ceil(r.x2)+u,y:c*(a||r.height()+2*r.y1),row:l};break;case I_:f.anchor={y:Math.floor(i.y1)-u,row:K_,x:c*(o||i.width()+2*i.x1),column:l};break;case Y_:f.anchor={y:Math.ceil(i.y2)+u,x:c*(o||i.width()+2*i.x1),column:l};break;case G_:f.anchor={x:u,y:u};break;case V_:f.anchor={x:o-u,y:u,column:K_};break;case X_:f.anchor={x:u,y:a-u,row:K_};break;case J_:f.anchor={x:o-u,y:a-u,column:K_,row:K_}}return f}function rb(t,e){var n,r,i=e.items[0],o=i.datum,a=i.orient,s=i.bounds,u=i.x,l=i.y;return i._bounds?i._bounds.clear().union(s):i._bounds=s.clone(),s.clear(),function(t,e,n){var r=e.padding,i=r-n.x,o=r-n.y;if(e.datum.title){var a=e.items[1].items[0],s=a.anchor,u=e.titlePadding||0,l=r-a.x,c=r-a.y;switch(a.orient){case W_:i+=Math.ceil(a.bounds.width())+u;break;case H_:case Y_:break;default:o+=a.bounds.height()+u}switch((i||o)&&ob(t,n,i,o),a.orient){case W_:c+=ib(e,n,a,s,1,1);break;case H_:l+=ib(e,n,a,K_,0,0)+u,c+=ib(e,n,a,s,1,1);break;case Y_:l+=ib(e,n,a,s,0,0),c+=ib(e,n,a,K_,-1,0,1)+u;break;default:l+=ib(e,n,a,s,0,0)}(l||c)&&ob(t,a,l,c),(l=Math.round(a.bounds.x1-r))<0&&(ob(t,n,-l,0),ob(t,a,-l,0))}else(i||o)&&ob(t,n,i,o)}(t,i,i.items[0].items[0]),s=function(t,e){return t.items.forEach((t=>e.union(t.bounds))),e.x1=t.padding,e.y1=t.padding,e}(i,s),n=2*i.padding,r=2*i.padding,s.empty()||(n=Math.ceil(s.width()+n),r=Math.ceil(s.height()+r)),o.type===gx&&function(t){const e=t.reduce(((t,e)=>(t[e.column]=Math.max(e.bounds.x2-e.x,t[e.column]||0),t)),{});t.forEach((t=>{t.width=e[t.column],t.height=t.bounds.y2-t.y}))}(i.items[0].items[0].items[0].items),a!==xx&&(i.x=u=0,i.y=l=0),i.width=n,i.height=r,em(s.set(u,l,u+n,l+r),i),i.mark.bounds.clear().union(s),i}function ib(t,e,n,r,i,o,a){const s=\"symbol\"!==t.datum.type,u=n.datum.vgrad,l=(!s||!o&&u||a?e:e.items[0]).bounds[i?\"y2\":\"x2\"]-t.padding,c=u&&o?l:0,f=u&&o?0:l,h=i<=0?0:zy(n);return Math.round(r===Z_?c:r===K_?f-h:.5*(l-h))}function ob(t,e,n,r){e.x+=n,e.y+=r,e.bounds.translate(n,r),e.mark.bounds.translate(n,r),t.dirty(e)}function ab(t){Ja.call(this,null,t)}dt(ab,Ja,{transform(t,e){const n=e.dataflow;return t.mark.items.forEach((e=>{t.layout&&Zx(n,e,t.layout),function(t,e,n){var r,i,o,a,s,u=e.items,l=Math.max(0,e.width||0),c=Math.max(0,e.height||0),f=(new Xg).set(0,0,l,c),h=f.clone(),d=f.clone(),p=[];for(a=0,s=u.length;a<s;++a)switch((i=u[a]).role){case rx:(Px(i)?h:d).union(jx(t,i,l,c));break;case ix:r=i;break;case sx:p.push(rb(t,i));break;case ox:case ax:case ux:case lx:case cx:case fx:case hx:case dx:h.union(i.bounds),d.union(i.bounds);break;default:f.union(i.bounds)}if(p.length){const e={};p.forEach((t=>{(o=t.orient||H_)!==xx&&(e[o]||(e[o]=[])).push(t)}));for(const r in e){const i=e[r];Jx(t,i,nb(i,r,n.legends,h,d,l,c))}p.forEach((e=>{const r=e.bounds;if(r.equals(e._bounds)||(e.bounds=e._bounds,t.dirty(e),e.bounds=r,t.dirty(e)),!n.autosize||n.autosize.type!==mx&&n.autosize.type!==yx&&n.autosize.type!==vx)f.union(r);else switch(e.orient){case W_:case H_:f.add(r.x1,0).add(r.x2,0);break;case I_:case Y_:f.add(0,r.y1).add(0,r.y2)}}))}f.uni"
-  , "on(h).union(d),r&&f.union(function(t,e,n,r,i){var o,a=e.items[0],s=a.frame,u=a.orient,l=a.anchor,c=a.offset,f=a.padding,h=a.items[0].items[0],d=a.items[1]&&a.items[1].items[0],p=u===W_||u===H_?r:n,g=0,m=0,y=0,v=0,_=0;if(s!==nx?u===W_?(g=i.y2,p=i.y1):u===H_?(g=i.y1,p=i.y2):(g=i.x1,p=i.x2):u===W_&&(g=r,p=0),o=l===Z_?g:l===K_?p:(g+p)/2,d&&d.text){switch(u){case I_:case Y_:_=h.bounds.height()+f;break;case W_:v=h.bounds.width()+f;break;case H_:v=-h.bounds.width()-f}Ux.clear().union(d.bounds),Ux.translate(v-(d.x||0),_-(d.y||0)),qx(d,\"x\",v)|qx(d,\"y\",_)&&(t.dirty(d),d.bounds.clear().union(Ux),d.mark.bounds.clear().union(Ux),t.dirty(d)),Ux.clear().union(d.bounds)}else Ux.clear();switch(Ux.union(h.bounds),u){case I_:m=o,y=i.y1-Ux.height()-c;break;case W_:m=i.x1-Ux.width()-c,y=o;break;case H_:m=i.x2+Ux.width()+c,y=o;break;case Y_:m=o,y=i.y2+c;break;default:m=a.x,y=a.y}return qx(a,\"x\",m)|qx(a,\"y\",y)&&(Ux.translate(m,y),t.dirty(a),a.bounds.clear().union(Ux),e.bounds.clear().union(Ux),t.dirty(a)),a.bounds}(t,r,l,c,f));e.clip&&f.set(0,0,e.width||0,e.height||0);!function(t,e,n,r){const i=r.autosize||{},o=i.type;if(t._autosize<1||!o)return;let a=t._width,s=t._height,u=Math.max(0,e.width||0),l=Math.max(0,Math.ceil(-n.x1)),c=Math.max(0,e.height||0),f=Math.max(0,Math.ceil(-n.y1));const h=Math.max(0,Math.ceil(n.x2-u)),d=Math.max(0,Math.ceil(n.y2-c));if(i.contains===px){const e=t.padding();a-=e.left+e.right,s-=e.top+e.bottom}o===xx?(l=0,f=0,u=a,c=s):o===mx?(u=Math.max(0,a-l-h),c=Math.max(0,s-f-d)):o===yx?(u=Math.max(0,a-l-h),s=c+f+d):o===vx?(a=u+l+h,c=Math.max(0,s-f-d)):o===_x&&(a=u+l+h,s=c+f+d);t._resizeView(a,s,u,c,[l,f],i.resize)}(t,e,f,n)}(n,e,t)})),function(t){return t&&\"legend-entry\"!==t.mark.role}(t.mark.group)?e.reflow():e}});var sb=Object.freeze({__proto__:null,bound:Ex,identifier:Fx,mark:Sx,overlap:$x,render:Lx,viewlayout:ab});function ub(t){Ja.call(this,null,t)}function lb(t){Ja.call(this,null,t)}function cb(){return _a({})}function fb(t){Ja.call(this,null,t)}function hb(t){Ja.call(this,[],t)}dt(ub,Ja,{transform(t,e){if(this.value&&!t.modified())return e.StopPropagation;var n=e.dataflow.locale(),r=e.fork(e.NO_SOURCE|e.NO_FIELDS),i=this.value,o=t.scale,a=$p(o,null==t.count?t.values?t.values.length:10:t.count,t.minstep),s=t.format||Np(n,o,a,t.formatSpecifier,t.formatType,!!t.values),u=t.values?Tp(o,t.values,a):Bp(o,a);return i&&(r.rem=i),i=u.map(((t,e)=>_a({index:e/(u.length-1||1),value:t,label:s(t)}))),t.extra&&i.length&&i.push(_a({index:-1,extra:{value:i[0].value},label:\"\"})),r.source=i,r.add=i,this.value=i,r}}),dt(lb,Ja,{transform(t,e){var n=e.dataflow,r=e.fork(e.NO_SOURCE|e.NO_FIELDS),i=t.item||cb,o=t.key||ya,a=this.value;return A(r.encode)&&(r.encode=null),a&&(t.modified(\"key\")||e.modified(o))&&s(\"DataJoin does not support modified key function or fields.\"),a||(e=e.addAll(),this.value=a=function(t){const e=ft().test((t=>t.exit));return e.lookup=n=>e.get(t(n)),e}(o)),e.visit(e.ADD,(t=>{const e=o(t);let n=a.get(e);n?n.exit?(a.empty--,r.add.push(n)):r.mod.push(n):(n=i(t),a.set(e,n),r.add.push(n)),n.datum=t,n.exit=!1})),e.visit(e.MOD,(t=>{const e=o(t),n=a.get(e);n&&(n.datum=t,r.mod.push(n))})),e.visit(e.REM,(t=>{const e=o(t),n=a.get(e);t!==n.datum||n.exit||(r.rem.push(n),n.exit=!0,++a.empty)})),e.changed(e.ADD_MOD)&&r.modifies(\"datum\"),(e.clean()||t.clean&&a.empty>n.cleanThreshold)&&n.runAfter(a.clean),r}}),dt(fb,Ja,{transform(t,e){var n=e.fork(e.ADD_REM),r=t.mod||!1,i=t.encoders,o=e.encode;if(A(o)){if(!n.changed()&&!o.every((t=>i[t])))return e.StopPropagation;o=o[0],n.encode=null}var a=\"enter\"===o,s=i.update||g,u=i.enter||g,l=i.exit||g,c=(o&&!a?i[o]:s)||g;if(e.changed(e.ADD)&&(e.visit(e.ADD,(e=>{u(e,t),s(e,t)})),n.modifies(u.output),n.modifies(s.output),c!==g&&c!==s&&(e.visit(e.ADD,(e=>{c(e,t)})),n.modifies(c.output))),e.changed(e.REM)&&l!==g&&(e.visit(e.REM,(e=>{l(e,t)})),n.modifies(l.output)),a||c!==g){const i=e.MOD|(t.modified()?e.REFLOW:0);a?(e.visit(i,(e=>{const i=u(e,t)||r;(c(e,t)||i)&&n.mod.push(e)})),n.mod.length&&n.modifies(u.output)):e.visit(i,(e=>{(c(e,t)||r)&&n.mod.push(e)})),n.mod.length&&n.modifies(c.output)}return n."
-  , "changed()?n:e.StopPropagation}}),dt(hb,Ja,{transform(t,e){if(null!=this.value&&!t.modified())return e.StopPropagation;var n,r,i,o,a,s=e.dataflow.locale(),u=e.fork(e.NO_SOURCE|e.NO_FIELDS),l=this.value,c=t.type||Ep,f=t.scale,h=+t.limit,d=$p(f,null==t.count?5:t.count,t.minstep),p=!!t.values||c===Ep,g=t.format||qp(s,f,d,c,t.formatSpecifier,t.formatType,p),m=t.values||Lp(f,d);return l&&(u.rem=l),c===Ep?(h&&m.length>h?(e.dataflow.warn(\"Symbol legend count exceeds limit, filtering items.\"),l=m.slice(0,h-1),a=!0):l=m,Z(i=t.size)?(t.values||0!==f(l[0])||(l=l.slice(1)),o=l.reduce(((e,n)=>Math.max(e,i(n,t))),0)):i=it(o=i||8),l=l.map(((e,n)=>_a({index:n,label:g(e,n,l),value:e,offset:o,size:i(e,t)}))),a&&(a=m[l.length],l.push(_a({index:l.length,label:`…${m.length-l.length} entries`,value:a,offset:o,size:i(a,t)})))):\"gradient\"===c?(n=f.domain(),r=xp(f,n[0],S(n)),m.length<3&&!t.values&&n[0]!==S(n)&&(m=[n[0],S(n)]),l=m.map(((t,e)=>_a({index:e,label:g(t,e,m),value:t,perc:r(t)})))):(i=m.length-1,r=function(t){const e=t.domain(),n=e.length-1;let r=+e[0],i=+S(e),o=i-r;if(t.type===Id){const t=n?o/n:.1;r-=t,i+=t,o=i-r}return t=>(t-r)/o}(f),l=m.map(((t,e)=>_a({index:e,label:g(t,e,m),value:t,perc:e?r(t):0,perc2:e===i?1:r(m[e+1])})))),u.source=l,u.add=l,this.value=l,u}});const db=t=>t.source.x,pb=t=>t.source.y,gb=t=>t.target.x,mb=t=>t.target.y;function yb(t){Ja.call(this,{},t)}yb.Definition={type:\"LinkPath\",metadata:{modifies:!0},params:[{name:\"sourceX\",type:\"field\",default:\"source.x\"},{name:\"sourceY\",type:\"field\",default:\"source.y\"},{name:\"targetX\",type:\"field\",default:\"target.x\"},{name:\"targetY\",type:\"field\",default:\"target.y\"},{name:\"orient\",type:\"enum\",default:\"vertical\",values:[\"horizontal\",\"vertical\",\"radial\"]},{name:\"shape\",type:\"enum\",default:\"line\",values:[\"line\",\"arc\",\"curve\",\"diagonal\",\"orthogonal\"]},{name:\"require\",type:\"signal\"},{name:\"as\",type:\"string\",default:\"path\"}]},dt(yb,Ja,{transform(t,e){var n=t.sourceX||db,r=t.sourceY||pb,i=t.targetX||gb,o=t.targetY||mb,a=t.as||\"path\",u=t.orient||\"vertical\",l=t.shape||\"line\",c=bb.get(l+\"-\"+u)||bb.get(l);return c||s(\"LinkPath unsupported type: \"+t.shape+(t.orient?\"-\"+t.orient:\"\")),e.visit(e.SOURCE,(t=>{t[a]=c(n(t),r(t),i(t),o(t))})),e.reflow(t.modified()).modifies(a)}});const vb=(t,e,n,r)=>\"M\"+t+\",\"+e+\"L\"+n+\",\"+r,_b=(t,e,n,r)=>{var i=n-t,o=r-e,a=Math.hypot(i,o)/2;return\"M\"+t+\",\"+e+\"A\"+a+\",\"+a+\" \"+180*Math.atan2(o,i)/Math.PI+\" 0 1 \"+n+\",\"+r},xb=(t,e,n,r)=>{const i=n-t,o=r-e,a=.2*(i+o),s=.2*(o-i);return\"M\"+t+\",\"+e+\"C\"+(t+a)+\",\"+(e+s)+\" \"+(n+s)+\",\"+(r-a)+\" \"+n+\",\"+r},bb=ft({line:vb,\"line-radial\":(t,e,n,r)=>vb(e*Math.cos(t),e*Math.sin(t),r*Math.cos(n),r*Math.sin(n)),arc:_b,\"arc-radial\":(t,e,n,r)=>_b(e*Math.cos(t),e*Math.sin(t),r*Math.cos(n),r*Math.sin(n)),curve:xb,\"curve-radial\":(t,e,n,r)=>xb(e*Math.cos(t),e*Math.sin(t),r*Math.cos(n),r*Math.sin(n)),\"orthogonal-horizontal\":(t,e,n,r)=>\"M\"+t+\",\"+e+\"V\"+r+\"H\"+n,\"orthogonal-vertical\":(t,e,n,r)=>\"M\"+t+\",\"+e+\"H\"+n+\"V\"+r,\"orthogonal-radial\":(t,e,n,r)=>{const i=Math.cos(t),o=Math.sin(t),a=Math.cos(n),s=Math.sin(n);return\"M\"+e*i+\",\"+e*o+\"A\"+e+\",\"+e+\" 0 0,\"+((Math.abs(n-t)>Math.PI?n<=t:n>t)?1:0)+\" \"+e*a+\",\"+e*s+\"L\"+r*a+\",\"+r*s},\"diagonal-horizontal\":(t,e,n,r)=>{const i=(t+n)/2;return\"M\"+t+\",\"+e+\"C\"+i+\",\"+e+\" \"+i+\",\"+r+\" \"+n+\",\"+r},\"diagonal-vertical\":(t,e,n,r)=>{const i=(e+r)/2;return\"M\"+t+\",\"+e+\"C\"+t+\",\"+i+\" \"+n+\",\"+i+\" \"+n+\",\"+r},\"diagonal-radial\":(t,e,n,r)=>{const i=Math.cos(t),o=Math.sin(t),a=Math.cos(n),s=Math.sin(n),u=(e+r)/2;return\"M\"+e*i+\",\"+e*o+\"C\"+u*i+\",\"+u*o+\" \"+u*a+\",\"+u*s+\" \"+r*a+\",\"+r*s}});function wb(t){Ja.call(this,null,t)}wb.Definition={type:\"Pie\",metadata:{modifies:!0},params:[{name:\"field\",type:\"field\"},{name:\"startAngle\",type:\"number\",default:0},{name:\"endAngle\",type:\"number\",default:6.283185307179586},{name:\"sort\",type:\"boolean\",default:!1},{name:\"as\",type:\"string\",array:!0,length:2,default:[\"startAngle\",\"endAngle\"]}]},dt(wb,Ja,{transform(t,e){var n,r,i,o=t.as||[\"startAngle\",\"endAngle\"],a=o[0],s=o[1],u=t.field||d,l=t.startAngle||0,c=null!=t.endAngle?t.endAngle:2*Math.PI,f=e.source,h=f.map(u),p=h.length,g=l,m=(c-l)/$e(h),y=Se(p);for(t.s"
-  , "ort&&y.sort(((t,e)=>h[t]-h[e])),n=0;n<p;++n)i=h[y[n]],(r=f[y[n]])[a]=g,r[s]=g+=i*m;return this.value=h,e.reflow(t.modified()).modifies(o)}});const kb=5;function Ab(t){return cp(t)&&t!==Ud}const Mb=Bt([\"set\",\"modified\",\"clear\",\"type\",\"scheme\",\"schemeExtent\",\"schemeCount\",\"domain\",\"domainMin\",\"domainMid\",\"domainMax\",\"domainRaw\",\"domainImplicit\",\"nice\",\"zero\",\"bins\",\"range\",\"rangeStep\",\"round\",\"reverse\",\"interpolate\",\"interpolateGamma\"]);function Eb(t){Ja.call(this,null,t),this.modified(!0)}function Db(t,e,n){dp(t)&&(Math.abs(e.reduce(((t,e)=>t+(e<0?-1:e>0?1:0)),0))!==e.length&&n.warn(\"Log scale domain includes zero: \"+Ct(e)));return e}function Cb(t,e,n){return Z(t)&&(e||n)?yp(t,Fb(e||[0,1],n)):t}function Fb(t,e){return e?t.slice().reverse():t}function Sb(t){Ja.call(this,null,t)}dt(Eb,Ja,{transform(t,e){var n=e.dataflow,r=this.value,i=function(t){var e,n=t.type,r=\"\";if(n===Ud)return Ud+\"-\"+Td;(function(t){const e=t.type;return cp(e)&&e!==Rd&&e!==Ld&&(t.scheme||t.range&&t.range.length&&t.range.every(xt))})(t)&&(r=2===(e=t.rawDomain?t.rawDomain.length:t.domain?t.domain.length+ +(null!=t.domainMid):0)?Ud+\"-\":3===e?qd+\"-\":\"\");return(r+n||Td).toLowerCase()}(t);for(i in r&&i===r.type||(this.value=r=sp(i)()),t)if(!Mb[i]){if(\"padding\"===i&&Ab(r.type))continue;Z(r[i])?r[i](t[i]):n.warn(\"Unsupported scale property: \"+i)}return function(t,e,n){var r=t.type,i=e.round||!1,o=e.range;if(null!=e.rangeStep)o=function(t,e,n){t!==Yd&&t!==Hd&&s(\"Only band and point scales support rangeStep.\");var r=(null!=e.paddingOuter?e.paddingOuter:e.padding)||0,i=t===Hd?1:(null!=e.paddingInner?e.paddingInner:e.padding)||0;return[0,e.rangeStep*$d(n,i,r)]}(r,e,n);else if(e.scheme&&(o=function(t,e,n){var r,i=e.schemeExtent;A(e.scheme)?r=vp(e.scheme,e.interpolate,e.interpolateGamma):(r=Mp(e.scheme.toLowerCase()))||s(`Unrecognized scheme name: ${e.scheme}`);return n=t===Id?n+1:t===Gd?n-1:t===Pd||t===jd?+e.schemeCount||kb:n,pp(t)?Cb(r,i,e.reverse):Z(r)?_p(Cb(r,i),n):t===Wd?r:r.slice(0,n)}(r,e,n),Z(o))){if(t.interpolator)return t.interpolator(o);s(`Scale type ${r} does not support interpolating color schemes.`)}if(o&&pp(r))return t.interpolator(vp(Fb(o,e.reverse),e.interpolate,e.interpolateGamma));o&&e.interpolate&&t.interpolate?t.interpolate(bp(e.interpolate,e.interpolateGamma)):Z(t.round)?t.round(i):Z(t.rangeRound)&&t.interpolate(i?yh:mh);o&&t.range(Fb(o,e.reverse))}(r,t,function(t,e,n){let r=e.bins;if(r&&!A(r)){const e=t.domain(),n=e[0],i=S(e),o=r.step;let a=null==r.start?n:r.start,u=null==r.stop?i:r.stop;o||s(\"Scale bins parameter missing step property.\"),a<n&&(a=o*Math.ceil(n/o)),u>i&&(u=o*Math.floor(i/o)),r=Se(a,u+o/2,o)}r?t.bins=r:t.bins&&delete t.bins;t.type===Gd&&(r?e.domain||e.domainRaw||(t.domain(r),n=r.length):t.bins=t.domain());return n}(r,t,function(t,e,n){const r=function(t,e,n){return e?(t.domain(Db(t.type,e,n)),e.length):-1}(t,e.domainRaw,n);if(r>-1)return r;var i,o,a=e.domain,s=t.type,u=e.zero||void 0===e.zero&&function(t){const e=t.type;return!t.bins&&(e===Td||e===Nd||e===zd)}(t);if(!a)return 0;if((u||null!=e.domainMin||null!=e.domainMax||null!=e.domainMid)&&(i=(a=a.slice()).length-1||1,u&&(a[0]>0&&(a[0]=0),a[i]<0&&(a[i]=0)),null!=e.domainMin&&(a[0]=e.domainMin),null!=e.domainMax&&(a[i]=e.domainMax),null!=e.domainMid)){const t=(o=e.domainMid)>a[i]?i+1:o<a[0]?0:i;t!==i&&n.warn(\"Scale domainMid exceeds domain min or max.\",o),a.splice(t,0,o)}Ab(s)&&e.padding&&a[0]!==S(a)&&(a=function(t,e,n,r,i,o){var a=Math.abs(S(n)-n[0]),s=a/(a-2*r),u=t===Bd?W(e,null,s):t===zd?H(e,null,s,.5):t===Nd?H(e,null,s,i||1):t===Od?Y(e,null,s,o||1):I(e,null,s);return e=e.slice(),e[0]=u[0],e[e.length-1]=u[1],e}(s,a,e.range,e.padding,e.exponent,e.constant));t.domain(Db(s,a,n)),s===Wd&&t.unknown(e.domainImplicit?zc:void 0);e.nice&&t.nice&&t.nice(!0!==e.nice&&$p(t,e.nice)||null);return a.length}(r,t,n))),e.fork(e.NO_SOURCE|e.NO_FIELDS)}}),dt(Sb,Ja,{transform(t,e){const n=t.modified(\"sort\")||e.changed(e.ADD)||e.modified(t.sort.fields)||e.modified(\"datum\");return n&&e.source.sort(ka(t.sort)),this.modified(n),e}});const $b=\"zero\",Tb=\"center\",Bb=\"normalize\",Nb=[\"y0\",\"y1\"];function"
-  , " zb(t){Ja.call(this,null,t)}function Ob(t,e,n,r,i){for(var o,a=(e-t.sum)/2,s=t.length,u=0;u<s;++u)(o=t[u])[r]=a,o[i]=a+=Math.abs(n(o))}function Rb(t,e,n,r,i){for(var o,a=1/t.sum,s=0,u=t.length,l=0,c=0;l<u;++l)(o=t[l])[r]=s,o[i]=s=a*(c+=Math.abs(n(o)))}function Lb(t,e,n,r,i){for(var o,a,s=0,u=0,l=t.length,c=0;c<l;++c)(o=+n(a=t[c]))<0?(a[r]=u,a[i]=u+=o):(a[r]=s,a[i]=s+=o)}zb.Definition={type:\"Stack\",metadata:{modifies:!0},params:[{name:\"field\",type:\"field\"},{name:\"groupby\",type:\"field\",array:!0},{name:\"sort\",type:\"compare\"},{name:\"offset\",type:\"enum\",default:$b,values:[$b,Tb,Bb]},{name:\"as\",type:\"string\",array:!0,length:2,default:Nb}]},dt(zb,Ja,{transform(t,e){var n,r,i,o,a=t.as||Nb,s=a[0],u=a[1],l=ka(t.sort),c=t.field||d,f=t.offset===Tb?Ob:t.offset===Bb?Rb:Lb;for(n=function(t,e,n,r){var i,o,a,s,u,l,c,f,h,d=[],p=t=>t(u);if(null==e)d.push(t.slice());else for(i={},o=0,a=t.length;o<a;++o)u=t[o],(c=i[l=e.map(p)])||(i[l]=c=[],d.push(c)),c.push(u);for(l=0,h=0,s=d.length;l<s;++l){for(o=0,f=0,a=(c=d[l]).length;o<a;++o)f+=Math.abs(r(c[o]));c.sum=f,f>h&&(h=f),n&&c.sort(n)}return d.max=h,d}(e.source,t.groupby,l,c),r=0,i=n.length,o=n.max;r<i;++r)f(n[r],o,c,s,u);return e.reflow(t.modified()).modifies(a)}});var Ub=Object.freeze({__proto__:null,axisticks:ub,datajoin:lb,encode:fb,legendentries:hb,linkpath:yb,pie:wb,scale:Eb,sortitems:Sb,stack:zb}),qb=1e-6,Pb=1e-12,jb=Math.PI,Ib=jb/2,Wb=jb/4,Hb=2*jb,Yb=180/jb,Gb=jb/180,Vb=Math.abs,Xb=Math.atan,Jb=Math.atan2,Zb=Math.cos,Qb=Math.ceil,Kb=Math.exp,tw=Math.hypot,ew=Math.log,nw=Math.pow,rw=Math.sin,iw=Math.sign||function(t){return t>0?1:t<0?-1:0},ow=Math.sqrt,aw=Math.tan;function sw(t){return t>1?0:t<-1?jb:Math.acos(t)}function uw(t){return t>1?Ib:t<-1?-Ib:Math.asin(t)}function lw(){}function cw(t,e){t&&hw.hasOwnProperty(t.type)&&hw[t.type](t,e)}var fw={Feature:function(t,e){cw(t.geometry,e)},FeatureCollection:function(t,e){for(var n=t.features,r=-1,i=n.length;++r<i;)cw(n[r].geometry,e)}},hw={Sphere:function(t,e){e.sphere()},Point:function(t,e){t=t.coordinates,e.point(t[0],t[1],t[2])},MultiPoint:function(t,e){for(var n=t.coordinates,r=-1,i=n.length;++r<i;)t=n[r],e.point(t[0],t[1],t[2])},LineString:function(t,e){dw(t.coordinates,e,0)},MultiLineString:function(t,e){for(var n=t.coordinates,r=-1,i=n.length;++r<i;)dw(n[r],e,0)},Polygon:function(t,e){pw(t.coordinates,e)},MultiPolygon:function(t,e){for(var n=t.coordinates,r=-1,i=n.length;++r<i;)pw(n[r],e)},GeometryCollection:function(t,e){for(var n=t.geometries,r=-1,i=n.length;++r<i;)cw(n[r],e)}};function dw(t,e,n){var r,i=-1,o=t.length-n;for(e.lineStart();++i<o;)r=t[i],e.point(r[0],r[1],r[2]);e.lineEnd()}function pw(t,e){var n=-1,r=t.length;for(e.polygonStart();++n<r;)dw(t[n],e,1);e.polygonEnd()}function gw(t,e){t&&fw.hasOwnProperty(t.type)?fw[t.type](t,e):cw(t,e)}var mw,yw,vw,_w,xw,bw,ww,kw,Aw,Mw,Ew,Dw,Cw,Fw,Sw,$w,Tw=new se,Bw=new se,Nw={point:lw,lineStart:lw,lineEnd:lw,polygonStart:function(){Tw=new se,Nw.lineStart=zw,Nw.lineEnd=Ow},polygonEnd:function(){var t=+Tw;Bw.add(t<0?Hb+t:t),this.lineStart=this.lineEnd=this.point=lw},sphere:function(){Bw.add(Hb)}};function zw(){Nw.point=Rw}function Ow(){Lw(mw,yw)}function Rw(t,e){Nw.point=Lw,mw=t,yw=e,vw=t*=Gb,_w=Zb(e=(e*=Gb)/2+Wb),xw=rw(e)}function Lw(t,e){var n=(t*=Gb)-vw,r=n>=0?1:-1,i=r*n,o=Zb(e=(e*=Gb)/2+Wb),a=rw(e),s=xw*a,u=_w*o+s*Zb(i),l=s*r*rw(i);Tw.add(Jb(l,u)),vw=t,_w=o,xw=a}function Uw(t){return[Jb(t[1],t[0]),uw(t[2])]}function qw(t){var e=t[0],n=t[1],r=Zb(n);return[r*Zb(e),r*rw(e),rw(n)]}function Pw(t,e){return t[0]*e[0]+t[1]*e[1]+t[2]*e[2]}function jw(t,e){return[t[1]*e[2]-t[2]*e[1],t[2]*e[0]-t[0]*e[2],t[0]*e[1]-t[1]*e[0]]}function Iw(t,e){t[0]+=e[0],t[1]+=e[1],t[2]+=e[2]}function Ww(t,e){return[t[0]*e,t[1]*e,t[2]*e]}function Hw(t){var e=ow(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=e,t[1]/=e,t[2]/=e}var Yw,Gw,Vw,Xw,Jw,Zw,Qw,Kw,tk,ek,nk,rk,ik,ok,ak,sk,uk={point:lk,lineStart:fk,lineEnd:hk,polygonStart:function(){uk.point=dk,uk.lineStart=pk,uk.lineEnd=gk,Fw=new se,Nw.polygonStart()},polygonEnd:function(){Nw.polygonEnd(),uk.point=lk,uk.lineStart=fk,uk.lineEnd=hk,Tw<0?(bw=-(kw=180),ww=-(Aw=90)):Fw>qb?Aw=90"
-  , ":Fw<-1e-6&&(ww=-90),$w[0]=bw,$w[1]=kw},sphere:function(){bw=-(kw=180),ww=-(Aw=90)}};function lk(t,e){Sw.push($w=[bw=t,kw=t]),e<ww&&(ww=e),e>Aw&&(Aw=e)}function ck(t,e){var n=qw([t*Gb,e*Gb]);if(Cw){var r=jw(Cw,n),i=jw([r[1],-r[0],0],r);Hw(i),i=Uw(i);var o,a=t-Mw,s=a>0?1:-1,u=i[0]*Yb*s,l=Vb(a)>180;l^(s*Mw<u&&u<s*t)?(o=i[1]*Yb)>Aw&&(Aw=o):l^(s*Mw<(u=(u+360)%360-180)&&u<s*t)?(o=-i[1]*Yb)<ww&&(ww=o):(e<ww&&(ww=e),e>Aw&&(Aw=e)),l?t<Mw?mk(bw,t)>mk(bw,kw)&&(kw=t):mk(t,kw)>mk(bw,kw)&&(bw=t):kw>=bw?(t<bw&&(bw=t),t>kw&&(kw=t)):t>Mw?mk(bw,t)>mk(bw,kw)&&(kw=t):mk(t,kw)>mk(bw,kw)&&(bw=t)}else Sw.push($w=[bw=t,kw=t]);e<ww&&(ww=e),e>Aw&&(Aw=e),Cw=n,Mw=t}function fk(){uk.point=ck}function hk(){$w[0]=bw,$w[1]=kw,uk.point=lk,Cw=null}function dk(t,e){if(Cw){var n=t-Mw;Fw.add(Vb(n)>180?n+(n>0?360:-360):n)}else Ew=t,Dw=e;Nw.point(t,e),ck(t,e)}function pk(){Nw.lineStart()}function gk(){dk(Ew,Dw),Nw.lineEnd(),Vb(Fw)>qb&&(bw=-(kw=180)),$w[0]=bw,$w[1]=kw,Cw=null}function mk(t,e){return(e-=t)<0?e+360:e}function yk(t,e){return t[0]-e[0]}function vk(t,e){return t[0]<=t[1]?t[0]<=e&&e<=t[1]:e<t[0]||t[1]<e}var _k={sphere:lw,point:xk,lineStart:wk,lineEnd:Mk,polygonStart:function(){_k.lineStart=Ek,_k.lineEnd=Dk},polygonEnd:function(){_k.lineStart=wk,_k.lineEnd=Mk}};function xk(t,e){t*=Gb;var n=Zb(e*=Gb);bk(n*Zb(t),n*rw(t),rw(e))}function bk(t,e,n){++Yw,Vw+=(t-Vw)/Yw,Xw+=(e-Xw)/Yw,Jw+=(n-Jw)/Yw}function wk(){_k.point=kk}function kk(t,e){t*=Gb;var n=Zb(e*=Gb);ok=n*Zb(t),ak=n*rw(t),sk=rw(e),_k.point=Ak,bk(ok,ak,sk)}function Ak(t,e){t*=Gb;var n=Zb(e*=Gb),r=n*Zb(t),i=n*rw(t),o=rw(e),a=Jb(ow((a=ak*o-sk*i)*a+(a=sk*r-ok*o)*a+(a=ok*i-ak*r)*a),ok*r+ak*i+sk*o);Gw+=a,Zw+=a*(ok+(ok=r)),Qw+=a*(ak+(ak=i)),Kw+=a*(sk+(sk=o)),bk(ok,ak,sk)}function Mk(){_k.point=xk}function Ek(){_k.point=Ck}function Dk(){Fk(rk,ik),_k.point=xk}function Ck(t,e){rk=t,ik=e,t*=Gb,e*=Gb,_k.point=Fk;var n=Zb(e);ok=n*Zb(t),ak=n*rw(t),sk=rw(e),bk(ok,ak,sk)}function Fk(t,e){t*=Gb;var n=Zb(e*=Gb),r=n*Zb(t),i=n*rw(t),o=rw(e),a=ak*o-sk*i,s=sk*r-ok*o,u=ok*i-ak*r,l=tw(a,s,u),c=uw(l),f=l&&-c/l;tk.add(f*a),ek.add(f*s),nk.add(f*u),Gw+=c,Zw+=c*(ok+(ok=r)),Qw+=c*(ak+(ak=i)),Kw+=c*(sk+(sk=o)),bk(ok,ak,sk)}function Sk(t,e){function n(n,r){return n=t(n,r),e(n[0],n[1])}return t.invert&&e.invert&&(n.invert=function(n,r){return(n=e.invert(n,r))&&t.invert(n[0],n[1])}),n}function $k(t,e){return Vb(t)>jb&&(t-=Math.round(t/Hb)*Hb),[t,e]}function Tk(t,e,n){return(t%=Hb)?e||n?Sk(Nk(t),zk(e,n)):Nk(t):e||n?zk(e,n):$k}function Bk(t){return function(e,n){return Vb(e+=t)>jb&&(e-=Math.round(e/Hb)*Hb),[e,n]}}function Nk(t){var e=Bk(t);return e.invert=Bk(-t),e}function zk(t,e){var n=Zb(t),r=rw(t),i=Zb(e),o=rw(e);function a(t,e){var a=Zb(e),s=Zb(t)*a,u=rw(t)*a,l=rw(e),c=l*n+s*r;return[Jb(u*i-c*o,s*n-l*r),uw(c*i+u*o)]}return a.invert=function(t,e){var a=Zb(e),s=Zb(t)*a,u=rw(t)*a,l=rw(e),c=l*i-u*o;return[Jb(u*i+l*o,s*n+c*r),uw(c*n-s*r)]},a}function Ok(t,e){(e=qw(e))[0]-=t,Hw(e);var n=sw(-e[1]);return((-e[2]<0?-n:n)+Hb-qb)%Hb}function Rk(){var t,e=[];return{point:function(e,n,r){t.push([e,n,r])},lineStart:function(){e.push(t=[])},lineEnd:lw,rejoin:function(){e.length>1&&e.push(e.pop().concat(e.shift()))},result:function(){var n=e;return e=[],t=null,n}}}function Lk(t,e){return Vb(t[0]-e[0])<qb&&Vb(t[1]-e[1])<qb}function Uk(t,e,n,r){this.x=t,this.z=e,this.o=n,this.e=r,this.v=!1,this.n=this.p=null}function qk(t,e,n,r,i){var o,a,s=[],u=[];if(t.forEach((function(t){if(!((e=t.length-1)<=0)){var e,n,r=t[0],a=t[e];if(Lk(r,a)){if(!r[2]&&!a[2]){for(i.lineStart(),o=0;o<e;++o)i.point((r=t[o])[0],r[1]);return void i.lineEnd()}a[0]+=2*qb}s.push(n=new Uk(r,t,null,!0)),u.push(n.o=new Uk(r,null,n,!1)),s.push(n=new Uk(a,t,null,!1)),u.push(n.o=new Uk(a,null,n,!0))}})),s.length){for(u.sort(e),Pk(s),Pk(u),o=0,a=u.length;o<a;++o)u[o].e=n=!n;for(var l,c,f=s[0];;){for(var h=f,d=!0;h.v;)if((h=h.n)===f)return;l=h.z,i.lineStart();do{if(h.v=h.o.v=!0,h.e){if(d)for(o=0,a=l.length;o<a;++o)i.point((c=l[o])[0],c[1]);else r(h.x,h.n.x,1,i);h=h.n}else{if(d)for(l=h.p.z,o=l.length-1;o>=0;--o)i.point((c=l[o])[0],c[1]);else r(h.x,h.p.x,-1,i);h=h.p}l=(h=h.o).z,d=!d}while(!"
-  , "h.v);i.lineEnd()}}}function Pk(t){if(e=t.length){for(var e,n,r=0,i=t[0];++r<e;)i.n=n=t[r],n.p=i,i=n;i.n=n=t[0],n.p=i}}function jk(t){return Vb(t[0])<=jb?t[0]:iw(t[0])*((Vb(t[0])+jb)%Hb-jb)}function Ik(t,e,n,r){return function(i){var o,a,s,u=e(i),l=Rk(),c=e(l),f=!1,h={point:d,lineStart:g,lineEnd:m,polygonStart:function(){h.point=y,h.lineStart=v,h.lineEnd=_,a=[],o=[]},polygonEnd:function(){h.point=d,h.lineStart=g,h.lineEnd=m,a=Fe(a);var t=function(t,e){var n=jk(e),r=e[1],i=rw(r),o=[rw(n),-Zb(n),0],a=0,s=0,u=new se;1===i?r=Ib+qb:-1===i&&(r=-Ib-qb);for(var l=0,c=t.length;l<c;++l)if(h=(f=t[l]).length)for(var f,h,d=f[h-1],p=jk(d),g=d[1]/2+Wb,m=rw(g),y=Zb(g),v=0;v<h;++v,p=x,m=w,y=k,d=_){var _=f[v],x=jk(_),b=_[1]/2+Wb,w=rw(b),k=Zb(b),A=x-p,M=A>=0?1:-1,E=M*A,D=E>jb,C=m*w;if(u.add(Jb(C*M*rw(E),y*k+C*Zb(E))),a+=D?A+M*Hb:A,D^p>=n^x>=n){var F=jw(qw(d),qw(_));Hw(F);var S=jw(o,F);Hw(S);var $=(D^A>=0?-1:1)*uw(S[2]);(r>$||r===$&&(F[0]||F[1]))&&(s+=D^A>=0?1:-1)}}return(a<-1e-6||a<qb&&u<-1e-12)^1&s}(o,r);a.length?(f||(i.polygonStart(),f=!0),qk(a,Hk,t,n,i)):t&&(f||(i.polygonStart(),f=!0),i.lineStart(),n(null,null,1,i),i.lineEnd()),f&&(i.polygonEnd(),f=!1),a=o=null},sphere:function(){i.polygonStart(),i.lineStart(),n(null,null,1,i),i.lineEnd(),i.polygonEnd()}};function d(e,n){t(e,n)&&i.point(e,n)}function p(t,e){u.point(t,e)}function g(){h.point=p,u.lineStart()}function m(){h.point=d,u.lineEnd()}function y(t,e){s.push([t,e]),c.point(t,e)}function v(){c.lineStart(),s=[]}function _(){y(s[0][0],s[0][1]),c.lineEnd();var t,e,n,r,u=c.clean(),h=l.result(),d=h.length;if(s.pop(),o.push(s),s=null,d)if(1&u){if((e=(n=h[0]).length-1)>0){for(f||(i.polygonStart(),f=!0),i.lineStart(),t=0;t<e;++t)i.point((r=n[t])[0],r[1]);i.lineEnd()}}else d>1&&2&u&&h.push(h.pop().concat(h.shift())),a.push(h.filter(Wk))}return h}}function Wk(t){return t.length>1}function Hk(t,e){return((t=t.x)[0]<0?t[1]-Ib-qb:Ib-t[1])-((e=e.x)[0]<0?e[1]-Ib-qb:Ib-e[1])}$k.invert=$k;var Yk=Ik((function(){return!0}),(function(t){var e,n=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),e=1},point:function(o,a){var s=o>0?jb:-jb,u=Vb(o-n);Vb(u-jb)<qb?(t.point(n,r=(r+a)/2>0?Ib:-Ib),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(s,r),t.point(o,r),e=0):i!==s&&u>=jb&&(Vb(n-i)<qb&&(n-=i*qb),Vb(o-s)<qb&&(o-=s*qb),r=function(t,e,n,r){var i,o,a=rw(t-n);return Vb(a)>qb?Xb((rw(e)*(o=Zb(r))*rw(n)-rw(r)*(i=Zb(e))*rw(t))/(i*o*a)):(e+r)/2}(n,r,o,a),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(s,r),e=0),t.point(n=o,r=a),i=s},lineEnd:function(){t.lineEnd(),n=r=NaN},clean:function(){return 2-e}}}),(function(t,e,n,r){var i;if(null==t)i=n*Ib,r.point(-jb,i),r.point(0,i),r.point(jb,i),r.point(jb,0),r.point(jb,-i),r.point(0,-i),r.point(-jb,-i),r.point(-jb,0),r.point(-jb,i);else if(Vb(t[0]-e[0])>qb){var o=t[0]<e[0]?jb:-jb;i=n*o/2,r.point(-o,i),r.point(0,i),r.point(o,i)}else r.point(e[0],e[1])}),[-jb,-Ib]);function Gk(t){var e=Zb(t),n=6*Gb,r=e>0,i=Vb(e)>qb;function o(t,n){return Zb(t)*Zb(n)>e}function a(t,n,r){var i=[1,0,0],o=jw(qw(t),qw(n)),a=Pw(o,o),s=o[0],u=a-s*s;if(!u)return!r&&t;var l=e*a/u,c=-e*s/u,f=jw(i,o),h=Ww(i,l);Iw(h,Ww(o,c));var d=f,p=Pw(h,d),g=Pw(d,d),m=p*p-g*(Pw(h,h)-1);if(!(m<0)){var y=ow(m),v=Ww(d,(-p-y)/g);if(Iw(v,h),v=Uw(v),!r)return v;var _,x=t[0],b=n[0],w=t[1],k=n[1];b<x&&(_=x,x=b,b=_);var A=b-x,M=Vb(A-jb)<qb;if(!M&&k<w&&(_=w,w=k,k=_),M||A<qb?M?w+k>0^v[1]<(Vb(v[0]-x)<qb?w:k):w<=v[1]&&v[1]<=k:A>jb^(x<=v[0]&&v[0]<=b)){var E=Ww(d,(-p+y)/g);return Iw(E,h),[v,Uw(E)]}}}function s(e,n){var i=r?t:jb-t,o=0;return e<-i?o|=1:e>i&&(o|=2),n<-i?o|=4:n>i&&(o|=8),o}return Ik(o,(function(t){var e,n,u,l,c;return{lineStart:function(){l=u=!1,c=1},point:function(f,h){var d,p=[f,h],g=o(f,h),m=r?g?0:s(f,h):g?s(f+(f<0?jb:-jb),h):0;if(!e&&(l=u=g)&&t.lineStart(),g!==u&&(!(d=a(e,p))||Lk(e,d)||Lk(p,d))&&(p[2]=1),g!==u)c=0,g?(t.lineStart(),d=a(p,e),t.point(d[0],d[1])):(d=a(e,p),t.point(d[0],d[1],2),t.lineEnd()),e=d;else if(i&&e&&r^g){var y;m&n||!(y=a(p,e,!0))||(c=0,r?(t.lineStart(),t.point(y[0][0],y[0][1]),t.point(y[1][0],y[1][1]),t.lineEnd()):(t.point(y[1][0],y[1][1]),t.lineEnd(),t.lineStart(),t.point(y[0][0],y[0][1],"
-  , "3)))}!g||e&&Lk(e,p)||t.point(p[0],p[1]),e=p,u=g,n=m},lineEnd:function(){u&&t.lineEnd(),e=null},clean:function(){return c|(l&&u)<<1}}}),(function(e,r,i,o){!function(t,e,n,r,i,o){if(n){var a=Zb(e),s=rw(e),u=r*n;null==i?(i=e+r*Hb,o=e-u/2):(i=Ok(a,i),o=Ok(a,o),(r>0?i<o:i>o)&&(i+=r*Hb));for(var l,c=i;r>0?c>o:c<o;c-=u)l=Uw([a,-s*Zb(c),-s*rw(c)]),t.point(l[0],l[1])}}(o,t,n,i,e,r)}),r?[0,-t]:[-jb,t-jb])}var Vk=1e9,Xk=-1e9;function Jk(t,e,n,r){function i(i,o){return t<=i&&i<=n&&e<=o&&o<=r}function o(i,o,s,l){var c=0,f=0;if(null==i||(c=a(i,s))!==(f=a(o,s))||u(i,o)<0^s>0)do{l.point(0===c||3===c?t:n,c>1?r:e)}while((c=(c+s+4)%4)!==f);else l.point(o[0],o[1])}function a(r,i){return Vb(r[0]-t)<qb?i>0?0:3:Vb(r[0]-n)<qb?i>0?2:1:Vb(r[1]-e)<qb?i>0?1:0:i>0?3:2}function s(t,e){return u(t.x,e.x)}function u(t,e){var n=a(t,1),r=a(e,1);return n!==r?n-r:0===n?e[1]-t[1]:1===n?t[0]-e[0]:2===n?t[1]-e[1]:e[0]-t[0]}return function(a){var u,l,c,f,h,d,p,g,m,y,v,_=a,x=Rk(),b={point:w,lineStart:function(){b.point=k,l&&l.push(c=[]);y=!0,m=!1,p=g=NaN},lineEnd:function(){u&&(k(f,h),d&&m&&x.rejoin(),u.push(x.result()));b.point=w,m&&_.lineEnd()},polygonStart:function(){_=x,u=[],l=[],v=!0},polygonEnd:function(){var e=function(){for(var e=0,n=0,i=l.length;n<i;++n)for(var o,a,s=l[n],u=1,c=s.length,f=s[0],h=f[0],d=f[1];u<c;++u)o=h,a=d,h=(f=s[u])[0],d=f[1],a<=r?d>r&&(h-o)*(r-a)>(d-a)*(t-o)&&++e:d<=r&&(h-o)*(r-a)<(d-a)*(t-o)&&--e;return e}(),n=v&&e,i=(u=Fe(u)).length;(n||i)&&(a.polygonStart(),n&&(a.lineStart(),o(null,null,1,a),a.lineEnd()),i&&qk(u,s,e,o,a),a.polygonEnd());_=a,u=l=c=null}};function w(t,e){i(t,e)&&_.point(t,e)}function k(o,a){var s=i(o,a);if(l&&c.push([o,a]),y)f=o,h=a,d=s,y=!1,s&&(_.lineStart(),_.point(o,a));else if(s&&m)_.point(o,a);else{var u=[p=Math.max(Xk,Math.min(Vk,p)),g=Math.max(Xk,Math.min(Vk,g))],x=[o=Math.max(Xk,Math.min(Vk,o)),a=Math.max(Xk,Math.min(Vk,a))];!function(t,e,n,r,i,o){var a,s=t[0],u=t[1],l=0,c=1,f=e[0]-s,h=e[1]-u;if(a=n-s,f||!(a>0)){if(a/=f,f<0){if(a<l)return;a<c&&(c=a)}else if(f>0){if(a>c)return;a>l&&(l=a)}if(a=i-s,f||!(a<0)){if(a/=f,f<0){if(a>c)return;a>l&&(l=a)}else if(f>0){if(a<l)return;a<c&&(c=a)}if(a=r-u,h||!(a>0)){if(a/=h,h<0){if(a<l)return;a<c&&(c=a)}else if(h>0){if(a>c)return;a>l&&(l=a)}if(a=o-u,h||!(a<0)){if(a/=h,h<0){if(a>c)return;a>l&&(l=a)}else if(h>0){if(a<l)return;a<c&&(c=a)}return l>0&&(t[0]=s+l*f,t[1]=u+l*h),c<1&&(e[0]=s+c*f,e[1]=u+c*h),!0}}}}}(u,x,t,e,n,r)?s&&(_.lineStart(),_.point(o,a),v=!1):(m||(_.lineStart(),_.point(u[0],u[1])),_.point(x[0],x[1]),s||_.lineEnd(),v=!1)}p=o,g=a,m=s}return b}}function Zk(t,e,n){var r=Se(t,e-qb,n).concat(e);return function(t){return r.map((function(e){return[t,e]}))}}function Qk(t,e,n){var r=Se(t,e-qb,n).concat(e);return function(t){return r.map((function(e){return[e,t]}))}}var Kk,tA,eA,nA,rA=t=>t,iA=new se,oA=new se,aA={point:lw,lineStart:lw,lineEnd:lw,polygonStart:function(){aA.lineStart=sA,aA.lineEnd=cA},polygonEnd:function(){aA.lineStart=aA.lineEnd=aA.point=lw,iA.add(Vb(oA)),oA=new se},result:function(){var t=iA/2;return iA=new se,t}};function sA(){aA.point=uA}function uA(t,e){aA.point=lA,Kk=eA=t,tA=nA=e}function lA(t,e){oA.add(nA*t-eA*e),eA=t,nA=e}function cA(){lA(Kk,tA)}var fA=1/0,hA=fA,dA=-fA,pA=dA,gA={point:function(t,e){t<fA&&(fA=t);t>dA&&(dA=t);e<hA&&(hA=e);e>pA&&(pA=e)},lineStart:lw,lineEnd:lw,polygonStart:lw,polygonEnd:lw,result:function(){var t=[[fA,hA],[dA,pA]];return dA=pA=-(hA=fA=1/0),t}};var mA,yA,vA,_A,xA=0,bA=0,wA=0,kA=0,AA=0,MA=0,EA=0,DA=0,CA=0,FA={point:SA,lineStart:$A,lineEnd:NA,polygonStart:function(){FA.lineStart=zA,FA.lineEnd=OA},polygonEnd:function(){FA.point=SA,FA.lineStart=$A,FA.lineEnd=NA},result:function(){var t=CA?[EA/CA,DA/CA]:MA?[kA/MA,AA/MA]:wA?[xA/wA,bA/wA]:[NaN,NaN];return xA=bA=wA=kA=AA=MA=EA=DA=CA=0,t}};function SA(t,e){xA+=t,bA+=e,++wA}function $A(){FA.point=TA}function TA(t,e){FA.point=BA,SA(vA=t,_A=e)}function BA(t,e){var n=t-vA,r=e-_A,i=ow(n*n+r*r);kA+=i*(vA+t)/2,AA+=i*(_A+e)/2,MA+=i,SA(vA=t,_A=e)}function NA(){FA.point=SA}function zA(){FA.point=RA}function OA(){LA(mA,yA)}function RA(t,e){FA.point=LA,SA(mA=vA=t,yA=_A=e)}function LA(t,e){va"
-  , "r n=t-vA,r=e-_A,i=ow(n*n+r*r);kA+=i*(vA+t)/2,AA+=i*(_A+e)/2,MA+=i,EA+=(i=_A*t-vA*e)*(vA+t),DA+=i*(_A+e),CA+=3*i,SA(vA=t,_A=e)}function UA(t){this._context=t}UA.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,e){switch(this._point){case 0:this._context.moveTo(t,e),this._point=1;break;case 1:this._context.lineTo(t,e);break;default:this._context.moveTo(t+this._radius,e),this._context.arc(t,e,this._radius,0,Hb)}},result:lw};var qA,PA,jA,IA,WA,HA=new se,YA={point:lw,lineStart:function(){YA.point=GA},lineEnd:function(){qA&&VA(PA,jA),YA.point=lw},polygonStart:function(){qA=!0},polygonEnd:function(){qA=null},result:function(){var t=+HA;return HA=new se,t}};function GA(t,e){YA.point=VA,PA=IA=t,jA=WA=e}function VA(t,e){IA-=t,WA-=e,HA.add(ow(IA*IA+WA*WA)),IA=t,WA=e}let XA,JA,ZA,QA;class KA{constructor(t){this._append=null==t?tM:function(t){const e=Math.floor(t);if(!(e>=0))throw new RangeError(`invalid digits: ${t}`);if(e>15)return tM;if(e!==XA){const t=10**e;XA=e,JA=function(e){let n=1;this._+=e[0];for(const r=e.length;n<r;++n)this._+=Math.round(arguments[n]*t)/t+e[n]}}return JA}(t),this._radius=4.5,this._=\"\"}pointRadius(t){return this._radius=+t,this}polygonStart(){this._line=0}polygonEnd(){this._line=NaN}lineStart(){this._point=0}lineEnd(){0===this._line&&(this._+=\"Z\"),this._point=NaN}point(t,e){switch(this._point){case 0:this._append`M${t},${e}`,this._point=1;break;case 1:this._append`L${t},${e}`;break;default:if(this._append`M${t},${e}`,this._radius!==ZA||this._append!==JA){const t=this._radius,e=this._;this._=\"\",this._append`m0,${t}a${t},${t} 0 1,1 0,${-2*t}a${t},${t} 0 1,1 0,${2*t}z`,ZA=t,JA=this._append,QA=this._,this._=e}this._+=QA}}result(){const t=this._;return this._=\"\",t.length?t:null}}function tM(t){let e=1;this._+=t[0];for(const n=t.length;e<n;++e)this._+=arguments[e]+t[e]}function eM(t,e){let n,r,i=3,o=4.5;function a(t){return t&&(\"function\"==typeof o&&r.pointRadius(+o.apply(this,arguments)),gw(t,n(r))),r.result()}return a.area=function(t){return gw(t,n(aA)),aA.result()},a.measure=function(t){return gw(t,n(YA)),YA.result()},a.bounds=function(t){return gw(t,n(gA)),gA.result()},a.centroid=function(t){return gw(t,n(FA)),FA.result()},a.projection=function(e){return arguments.length?(n=null==e?(t=null,rA):(t=e).stream,a):t},a.context=function(t){return arguments.length?(r=null==t?(e=null,new KA(i)):new UA(e=t),\"function\"!=typeof o&&r.pointRadius(o),a):e},a.pointRadius=function(t){return arguments.length?(o=\"function\"==typeof t?t:(r.pointRadius(+t),+t),a):o},a.digits=function(t){if(!arguments.length)return i;if(null==t)i=null;else{const e=Math.floor(t);if(!(e>=0))throw new RangeError(`invalid digits: ${t}`);i=e}return null===e&&(r=new KA(i)),a},a.projection(t).digits(i).context(e)}function nM(t){return function(e){var n=new rM;for(var r in t)n[r]=t[r];return n.stream=e,n}}function rM(){}function iM(t,e,n){var r=t.clipExtent&&t.clipExtent();return t.scale(150).translate([0,0]),null!=r&&t.clipExtent(null),gw(n,t.stream(gA)),e(gA.result()),null!=r&&t.clipExtent(r),t}function oM(t,e,n){return iM(t,(function(n){var r=e[1][0]-e[0][0],i=e[1][1]-e[0][1],o=Math.min(r/(n[1][0]-n[0][0]),i/(n[1][1]-n[0][1])),a=+e[0][0]+(r-o*(n[1][0]+n[0][0]))/2,s=+e[0][1]+(i-o*(n[1][1]+n[0][1]))/2;t.scale(150*o).translate([a,s])}),n)}function aM(t,e,n){return oM(t,[[0,0],e],n)}function sM(t,e,n){return iM(t,(function(n){var r=+e,i=r/(n[1][0]-n[0][0]),o=(r-i*(n[1][0]+n[0][0]))/2,a=-i*n[0][1];t.scale(150*i).translate([o,a])}),n)}function uM(t,e,n){return iM(t,(function(n){var r=+e,i=r/(n[1][1]-n[0][1]),o=-i*n[0][0],a=(r-i*(n[1][1]+n[0][1]))/2;t.scale(150*i).translate([o,a])}),n)}rM.prototype={constructor:rM,point:function(t,e){this.stream.point(t,e)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart"
-  , "()},polygonEnd:function(){this.stream.polygonEnd()}};var lM=16,cM=Zb(30*Gb);function fM(t,e){return+e?function(t,e){function n(r,i,o,a,s,u,l,c,f,h,d,p,g,m){var y=l-r,v=c-i,_=y*y+v*v;if(_>4*e&&g--){var x=a+h,b=s+d,w=u+p,k=ow(x*x+b*b+w*w),A=uw(w/=k),M=Vb(Vb(w)-1)<qb||Vb(o-f)<qb?(o+f)/2:Jb(b,x),E=t(M,A),D=E[0],C=E[1],F=D-r,S=C-i,$=v*F-y*S;($*$/_>e||Vb((y*F+v*S)/_-.5)>.3||a*h+s*d+u*p<cM)&&(n(r,i,o,a,s,u,D,C,M,x/=k,b/=k,w,g,m),m.point(D,C),n(D,C,M,x,b,w,l,c,f,h,d,p,g,m))}}return function(e){var r,i,o,a,s,u,l,c,f,h,d,p,g={point:m,lineStart:y,lineEnd:_,polygonStart:function(){e.polygonStart(),g.lineStart=x},polygonEnd:function(){e.polygonEnd(),g.lineStart=y}};function m(n,r){n=t(n,r),e.point(n[0],n[1])}function y(){c=NaN,g.point=v,e.lineStart()}function v(r,i){var o=qw([r,i]),a=t(r,i);n(c,f,l,h,d,p,c=a[0],f=a[1],l=r,h=o[0],d=o[1],p=o[2],lM,e),e.point(c,f)}function _(){g.point=m,e.lineEnd()}function x(){y(),g.point=b,g.lineEnd=w}function b(t,e){v(r=t,e),i=c,o=f,a=h,s=d,u=p,g.point=v}function w(){n(c,f,l,h,d,p,i,o,r,a,s,u,lM,e),g.lineEnd=_,_()}return g}}(t,e):function(t){return nM({point:function(e,n){e=t(e,n),this.stream.point(e[0],e[1])}})}(t)}var hM=nM({point:function(t,e){this.stream.point(t*Gb,e*Gb)}});function dM(t,e,n,r,i,o){if(!o)return function(t,e,n,r,i){function o(o,a){return[e+t*(o*=r),n-t*(a*=i)]}return o.invert=function(o,a){return[(o-e)/t*r,(n-a)/t*i]},o}(t,e,n,r,i);var a=Zb(o),s=rw(o),u=a*t,l=s*t,c=a/t,f=s/t,h=(s*n-a*e)/t,d=(s*e+a*n)/t;function p(t,o){return[u*(t*=r)-l*(o*=i)+e,n-l*t-u*o]}return p.invert=function(t,e){return[r*(c*t-f*e+h),i*(d-f*t-c*e)]},p}function pM(t){return gM((function(){return t}))()}function gM(t){var e,n,r,i,o,a,s,u,l,c,f=150,h=480,d=250,p=0,g=0,m=0,y=0,v=0,_=0,x=1,b=1,w=null,k=Yk,A=null,M=rA,E=.5;function D(t){return u(t[0]*Gb,t[1]*Gb)}function C(t){return(t=u.invert(t[0],t[1]))&&[t[0]*Yb,t[1]*Yb]}function F(){var t=dM(f,0,0,x,b,_).apply(null,e(p,g)),r=dM(f,h-t[0],d-t[1],x,b,_);return n=Tk(m,y,v),s=Sk(e,r),u=Sk(n,s),a=fM(s,E),S()}function S(){return l=c=null,D}return D.stream=function(t){return l&&c===t?l:l=hM(function(t){return nM({point:function(e,n){var r=t(e,n);return this.stream.point(r[0],r[1])}})}(n)(k(a(M(c=t)))))},D.preclip=function(t){return arguments.length?(k=t,w=void 0,S()):k},D.postclip=function(t){return arguments.length?(M=t,A=r=i=o=null,S()):M},D.clipAngle=function(t){return arguments.length?(k=+t?Gk(w=t*Gb):(w=null,Yk),S()):w*Yb},D.clipExtent=function(t){return arguments.length?(M=null==t?(A=r=i=o=null,rA):Jk(A=+t[0][0],r=+t[0][1],i=+t[1][0],o=+t[1][1]),S()):null==A?null:[[A,r],[i,o]]},D.scale=function(t){return arguments.length?(f=+t,F()):f},D.translate=function(t){return arguments.length?(h=+t[0],d=+t[1],F()):[h,d]},D.center=function(t){return arguments.length?(p=t[0]%360*Gb,g=t[1]%360*Gb,F()):[p*Yb,g*Yb]},D.rotate=function(t){return arguments.length?(m=t[0]%360*Gb,y=t[1]%360*Gb,v=t.length>2?t[2]%360*Gb:0,F()):[m*Yb,y*Yb,v*Yb]},D.angle=function(t){return arguments.length?(_=t%360*Gb,F()):_*Yb},D.reflectX=function(t){return arguments.length?(x=t?-1:1,F()):x<0},D.reflectY=function(t){return arguments.length?(b=t?-1:1,F()):b<0},D.precision=function(t){return arguments.length?(a=fM(s,E=t*t),S()):ow(E)},D.fitExtent=function(t,e){return oM(D,t,e)},D.fitSize=function(t,e){return aM(D,t,e)},D.fitWidth=function(t,e){return sM(D,t,e)},D.fitHeight=function(t,e){return uM(D,t,e)},function(){return e=t.apply(this,arguments),D.invert=e.invert&&C,F()}}function mM(t){var e=0,n=jb/3,r=gM(t),i=r(e,n);return i.parallels=function(t){return arguments.length?r(e=t[0]*Gb,n=t[1]*Gb):[e*Yb,n*Yb]},i}function yM(t,e){var n=rw(t),r=(n+rw(e))/2;if(Vb(r)<qb)return function(t){var e=Zb(t);function n(t,n){return[t*e,rw(n)/e]}return n.invert=function(t,n){return[t/e,uw(n*e)]},n}(t);var i=1+n*(2*r-n),o=ow(i)/r;function a(t,e){var n=ow(i-2*r*rw(e))/r;return[n*rw(t*=r),o-n*Zb(t)]}return a.invert=function(t,e){var n=o-e,a=Jb(t,Vb(n))*iw(n);return n*r<0&&(a-=jb*iw(t)*iw(n)),[a/r,uw((i-(t*t+n*n)*r*r)/(2*r))]},a}function vM(){return mM(yM).scale(155.424).center([0,33.6442])}function _M(){return vM().parallel"
-  , "s([29.5,45.5]).scale(1070).translate([480,250]).rotate([96,0]).center([-.6,38.7])}function xM(t){return function(e,n){var r=Zb(e),i=Zb(n),o=t(r*i);return o===1/0?[2,0]:[o*i*rw(e),o*rw(n)]}}function bM(t){return function(e,n){var r=ow(e*e+n*n),i=t(r),o=rw(i),a=Zb(i);return[Jb(e*o,r*a),uw(r&&n*o/r)]}}var wM=xM((function(t){return ow(2/(1+t))}));wM.invert=bM((function(t){return 2*uw(t/2)}));var kM=xM((function(t){return(t=sw(t))&&t/rw(t)}));function AM(t,e){return[t,ew(aw((Ib+e)/2))]}function MM(t){var e,n,r,i=pM(t),o=i.center,a=i.scale,s=i.translate,u=i.clipExtent,l=null;function c(){var o=jb*a(),s=i(function(t){function e(e){return(e=t(e[0]*Gb,e[1]*Gb))[0]*=Yb,e[1]*=Yb,e}return t=Tk(t[0]*Gb,t[1]*Gb,t.length>2?t[2]*Gb:0),e.invert=function(e){return(e=t.invert(e[0]*Gb,e[1]*Gb))[0]*=Yb,e[1]*=Yb,e},e}(i.rotate()).invert([0,0]));return u(null==l?[[s[0]-o,s[1]-o],[s[0]+o,s[1]+o]]:t===AM?[[Math.max(s[0]-o,l),e],[Math.min(s[0]+o,n),r]]:[[l,Math.max(s[1]-o,e)],[n,Math.min(s[1]+o,r)]])}return i.scale=function(t){return arguments.length?(a(t),c()):a()},i.translate=function(t){return arguments.length?(s(t),c()):s()},i.center=function(t){return arguments.length?(o(t),c()):o()},i.clipExtent=function(t){return arguments.length?(null==t?l=e=n=r=null:(l=+t[0][0],e=+t[0][1],n=+t[1][0],r=+t[1][1]),c()):null==l?null:[[l,e],[n,r]]},c()}function EM(t){return aw((Ib+t)/2)}function DM(t,e){var n=Zb(t),r=t===e?rw(t):ew(n/Zb(e))/ew(EM(e)/EM(t)),i=n*nw(EM(t),r)/r;if(!r)return AM;function o(t,e){i>0?e<-Ib+qb&&(e=-Ib+qb):e>Ib-qb&&(e=Ib-qb);var n=i/nw(EM(e),r);return[n*rw(r*t),i-n*Zb(r*t)]}return o.invert=function(t,e){var n=i-e,o=iw(r)*ow(t*t+n*n),a=Jb(t,Vb(n))*iw(n);return n*r<0&&(a-=jb*iw(t)*iw(n)),[a/r,2*Xb(nw(i/o,1/r))-Ib]},o}function CM(t,e){return[t,e]}function FM(t,e){var n=Zb(t),r=t===e?rw(t):(n-Zb(e))/(e-t),i=n/r+t;if(Vb(r)<qb)return CM;function o(t,e){var n=i-e,o=r*t;return[n*rw(o),i-n*Zb(o)]}return o.invert=function(t,e){var n=i-e,o=Jb(t,Vb(n))*iw(n);return n*r<0&&(o-=jb*iw(t)*iw(n)),[o/r,i-iw(r)*ow(t*t+n*n)]},o}kM.invert=bM((function(t){return t})),AM.invert=function(t,e){return[t,2*Xb(Kb(e))-Ib]},CM.invert=CM;var SM=1.340264,$M=-.081106,TM=893e-6,BM=.003796,NM=ow(3)/2;function zM(t,e){var n=uw(NM*rw(e)),r=n*n,i=r*r*r;return[t*Zb(n)/(NM*(SM+3*$M*r+i*(7*TM+9*BM*r))),n*(SM+$M*r+i*(TM+BM*r))]}function OM(t,e){var n=Zb(e),r=Zb(t)*n;return[n*rw(t)/r,rw(e)/r]}function RM(t,e){var n=e*e,r=n*n;return[t*(.8707-.131979*n+r*(r*(.003971*n-.001529*r)-.013791)),e*(1.007226+n*(.015085+r*(.028874*n-.044475-.005916*r)))]}function LM(t,e){return[Zb(e)*rw(t),rw(e)]}function UM(t,e){var n=Zb(e),r=1+Zb(t)*n;return[n*rw(t)/r,rw(e)/r]}function qM(t,e){return[ew(aw((Ib+e)/2)),-t]}zM.invert=function(t,e){for(var n,r=e,i=r*r,o=i*i*i,a=0;a<12&&(o=(i=(r-=n=(r*(SM+$M*i+o*(TM+BM*i))-e)/(SM+3*$M*i+o*(7*TM+9*BM*i)))*r)*i*i,!(Vb(n)<Pb));++a);return[NM*t*(SM+3*$M*i+o*(7*TM+9*BM*i))/Zb(r),uw(rw(r)/NM)]},OM.invert=bM(Xb),RM.invert=function(t,e){var n,r=e,i=25;do{var o=r*r,a=o*o;r-=n=(r*(1.007226+o*(.015085+a*(.028874*o-.044475-.005916*a)))-e)/(1.007226+o*(.045255+a*(.259866*o-.311325-.005916*11*a)))}while(Vb(n)>qb&&--i>0);return[t/(.8707+(o=r*r)*(o*(o*o*o*(.003971-.001529*o)-.013791)-.131979)),r]},LM.invert=bM(uw),UM.invert=bM((function(t){return 2*Xb(t)})),qM.invert=function(t,e){return[-e,2*Xb(Kb(t))-Ib]};var PM=Math.abs,jM=Math.cos,IM=Math.sin,WM=1e-6,HM=Math.PI,YM=HM/2,GM=function(t){return t>0?Math.sqrt(t):0}(2);function VM(t){return t>1?YM:t<-1?-YM:Math.asin(t)}function XM(t,e){var n,r=t*IM(e),i=30;do{e-=n=(e+IM(e)-r)/(1+jM(e))}while(PM(n)>WM&&--i>0);return e/2}var JM=function(t,e,n){function r(r,i){return[t*r*jM(i=XM(n,i)),e*IM(i)]}return r.invert=function(r,i){return i=VM(i/e),[r/(t*jM(i)),VM((2*i+IM(2*i))/n)]},r}(GM/YM,GM,HM);const ZM=eM(),QM=[\"clipAngle\",\"clipExtent\",\"scale\",\"translate\",\"center\",\"rotate\",\"parallels\",\"precision\",\"reflectX\",\"reflectY\",\"coefficient\",\"distance\",\"fraction\",\"lobes\",\"parallel\",\"radius\",\"ratio\",\"spacing\",\"tilt\"];function KM(t,e){if(!t||\"string\"!=typeof t)throw new Error(\"Projection type must be a name string.\");return t=t.toLowerCase(),ar"
-  , "guments.length>1?(eE[t]=function(t,e){return function n(){const r=e();return r.type=t,r.path=eM().projection(r),r.copy=r.copy||function(){const t=n();return QM.forEach((e=>{r[e]&&t[e](r[e]())})),t.path.pointRadius(r.path.pointRadius()),t},op(r)}}(t,e),this):eE[t]||null}function tE(t){return t&&t.path||ZM}const eE={albers:_M,albersusa:function(){var t,e,n,r,i,o,a=_M(),s=vM().rotate([154,0]).center([-2,58.5]).parallels([55,65]),u=vM().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(t,e){o=[t,e]}};function c(t){var e=t[0],a=t[1];return o=null,n.point(e,a),o||(r.point(e,a),o)||(i.point(e,a),o)}function f(){return t=e=null,c}return c.invert=function(t){var e=a.scale(),n=a.translate(),r=(t[0]-n[0])/e,i=(t[1]-n[1])/e;return(i>=.12&&i<.234&&r>=-.425&&r<-.214?s:i>=.166&&i<.234&&r>=-.214&&r<-.115?u:a).invert(t)},c.stream=function(n){return t&&e===n?t:(r=[a.stream(e=n),s.stream(n),u.stream(n)],i=r.length,t={point:function(t,e){for(var n=-1;++n<i;)r[n].point(t,e)},sphere:function(){for(var t=-1;++t<i;)r[t].sphere()},lineStart:function(){for(var t=-1;++t<i;)r[t].lineStart()},lineEnd:function(){for(var t=-1;++t<i;)r[t].lineEnd()},polygonStart:function(){for(var t=-1;++t<i;)r[t].polygonStart()},polygonEnd:function(){for(var t=-1;++t<i;)r[t].polygonEnd()}});var r,i},c.precision=function(t){return arguments.length?(a.precision(t),s.precision(t),u.precision(t),f()):a.precision()},c.scale=function(t){return arguments.length?(a.scale(t),s.scale(.35*t),u.scale(t),c.translate(a.translate())):a.scale()},c.translate=function(t){if(!arguments.length)return a.translate();var e=a.scale(),o=+t[0],c=+t[1];return n=a.translate(t).clipExtent([[o-.455*e,c-.238*e],[o+.455*e,c+.238*e]]).stream(l),r=s.translate([o-.307*e,c+.201*e]).clipExtent([[o-.425*e+qb,c+.12*e+qb],[o-.214*e-qb,c+.234*e-qb]]).stream(l),i=u.translate([o-.205*e,c+.212*e]).clipExtent([[o-.214*e+qb,c+.166*e+qb],[o-.115*e-qb,c+.234*e-qb]]).stream(l),f()},c.fitExtent=function(t,e){return oM(c,t,e)},c.fitSize=function(t,e){return aM(c,t,e)},c.fitWidth=function(t,e){return sM(c,t,e)},c.fitHeight=function(t,e){return uM(c,t,e)},c.scale(1070)},azimuthalequalarea:function(){return pM(wM).scale(124.75).clipAngle(179.999)},azimuthalequidistant:function(){return pM(kM).scale(79.4188).clipAngle(179.999)},conicconformal:function(){return mM(DM).scale(109.5).parallels([30,30])},conicequalarea:vM,conicequidistant:function(){return mM(FM).scale(131.154).center([0,13.9389])},equalEarth:function(){return pM(zM).scale(177.158)},equirectangular:function(){return pM(CM).scale(152.63)},gnomonic:function(){return pM(OM).scale(144.049).clipAngle(60)},identity:function(){var t,e,n,r,i,o,a,s=1,u=0,l=0,c=1,f=1,h=0,d=null,p=1,g=1,m=nM({point:function(t,e){var n=_([t,e]);this.stream.point(n[0],n[1])}}),y=rA;function v(){return p=s*c,g=s*f,o=a=null,_}function _(n){var r=n[0]*p,i=n[1]*g;if(h){var o=i*t-r*e;r=r*t+i*e,i=o}return[r+u,i+l]}return _.invert=function(n){var r=n[0]-u,i=n[1]-l;if(h){var o=i*t+r*e;r=r*t-i*e,i=o}return[r/p,i/g]},_.stream=function(t){return o&&a===t?o:o=m(y(a=t))},_.postclip=function(t){return arguments.length?(y=t,d=n=r=i=null,v()):y},_.clipExtent=function(t){return arguments.length?(y=null==t?(d=n=r=i=null,rA):Jk(d=+t[0][0],n=+t[0][1],r=+t[1][0],i=+t[1][1]),v()):null==d?null:[[d,n],[r,i]]},_.scale=function(t){return arguments.length?(s=+t,v()):s},_.translate=function(t){return arguments.length?(u=+t[0],l=+t[1],v()):[u,l]},_.angle=function(n){return arguments.length?(e=rw(h=n%360*Gb),t=Zb(h),v()):h*Yb},_.reflectX=function(t){return arguments.length?(c=t?-1:1,v()):c<0},_.reflectY=function(t){return arguments.length?(f=t?-1:1,v()):f<0},_.fitExtent=function(t,e){return oM(_,t,e)},_.fitSize=function(t,e){return aM(_,t,e)},_.fitWidth=function(t,e){return sM(_,t,e)},_.fitHeight=function(t,e){return uM(_,t,e)},_},mercator:function(){return MM(AM).scale(961/Hb)},mollweide:function(){return pM(JM).scale(169.529)},naturalEarth1:function(){return pM(RM).scale(175.295)},orthographic:function(){return pM(LM).scale(249.5).clipAngle(90+qb)},stereographic:function(){return pM(UM).scale(250)"
-  , ".clipAngle(142)},transversemercator:function(){var t=MM(qM),e=t.center,n=t.rotate;return t.center=function(t){return arguments.length?e([-t[1],t[0]]):[(t=e())[1],-t[0]]},t.rotate=function(t){return arguments.length?n([t[0],t[1],t.length>2?t[2]+90:90]):[(t=n())[0],t[1],t[2]-90]},n([0,0,90]).scale(159.155)}};for(const t in eE)KM(t,eE[t]);function nE(){}const rE=[[],[[[1,1.5],[.5,1]]],[[[1.5,1],[1,1.5]]],[[[1.5,1],[.5,1]]],[[[1,.5],[1.5,1]]],[[[1,1.5],[.5,1]],[[1,.5],[1.5,1]]],[[[1,.5],[1,1.5]]],[[[1,.5],[.5,1]]],[[[.5,1],[1,.5]]],[[[1,1.5],[1,.5]]],[[[.5,1],[1,.5]],[[1.5,1],[1,1.5]]],[[[1.5,1],[1,.5]]],[[[.5,1],[1.5,1]]],[[[1,1.5],[1.5,1]]],[[[.5,1],[1,1.5]]],[]];function iE(){var t=1,e=1,n=a;function r(t,e){return e.map((e=>i(t,e)))}function i(r,i){var a=[],s=[];return function(n,r,i){var a,s,u,l,c,f,h=[],d=[];a=s=-1,l=n[0]>=r,rE[l<<1].forEach(p);for(;++a<t-1;)u=l,l=n[a+1]>=r,rE[u|l<<1].forEach(p);rE[l<<0].forEach(p);for(;++s<e-1;){for(a=-1,l=n[s*t+t]>=r,c=n[s*t]>=r,rE[l<<1|c<<2].forEach(p);++a<t-1;)u=l,l=n[s*t+t+a+1]>=r,f=c,c=n[s*t+a+1]>=r,rE[u|l<<1|c<<2|f<<3].forEach(p);rE[l|c<<3].forEach(p)}a=-1,c=n[s*t]>=r,rE[c<<2].forEach(p);for(;++a<t-1;)f=c,c=n[s*t+a+1]>=r,rE[c<<2|f<<3].forEach(p);function p(t){var e,n,r=[t[0][0]+a,t[0][1]+s],u=[t[1][0]+a,t[1][1]+s],l=o(r),c=o(u);(e=d[l])?(n=h[c])?(delete d[e.end],delete h[n.start],e===n?(e.ring.push(u),i(e.ring)):h[e.start]=d[n.end]={start:e.start,end:n.end,ring:e.ring.concat(n.ring)}):(delete d[e.end],e.ring.push(u),d[e.end=c]=e):(e=h[c])?(n=d[l])?(delete h[e.start],delete d[n.end],e===n?(e.ring.push(u),i(e.ring)):h[n.start]=d[e.end]={start:n.start,end:e.end,ring:n.ring.concat(e.ring)}):(delete h[e.start],e.ring.unshift(r),h[e.start=l]=e):h[l]=d[c]={start:l,end:c,ring:[r,u]}}rE[c<<3].forEach(p)}(r,i,(t=>{n(t,r,i),function(t){var e=0,n=t.length,r=t[n-1][1]*t[0][0]-t[n-1][0]*t[0][1];for(;++e<n;)r+=t[e-1][1]*t[e][0]-t[e-1][0]*t[e][1];return r}(t)>0?a.push([t]):s.push(t)})),s.forEach((t=>{for(var e,n=0,r=a.length;n<r;++n)if(-1!==oE((e=a[n])[0],t))return void e.push(t)})),{type:\"MultiPolygon\",value:i,coordinates:a}}function o(e){return 2*e[0]+e[1]*(t+1)*4}function a(n,r,i){n.forEach((n=>{var o,a=n[0],s=n[1],u=0|a,l=0|s,c=r[l*t+u];a>0&&a<t&&u===a&&(o=r[l*t+u-1],n[0]=a+(i-o)/(c-o)-.5),s>0&&s<e&&l===s&&(o=r[(l-1)*t+u],n[1]=s+(i-o)/(c-o)-.5)}))}return r.contour=i,r.size=function(n){if(!arguments.length)return[t,e];var i=Math.floor(n[0]),o=Math.floor(n[1]);return i>=0&&o>=0||s(\"invalid size\"),t=i,e=o,r},r.smooth=function(t){return arguments.length?(n=t?a:nE,r):n===a},r}function oE(t,e){for(var n,r=-1,i=e.length;++r<i;)if(n=aE(t,e[r]))return n;return 0}function aE(t,e){for(var n=e[0],r=e[1],i=-1,o=0,a=t.length,s=a-1;o<a;s=o++){var u=t[o],l=u[0],c=u[1],f=t[s],h=f[0],d=f[1];if(sE(u,f,e))return 0;c>r!=d>r&&n<(h-l)*(r-c)/(d-c)+l&&(i=-i)}return i}function sE(t,e,n){var r,i,o,a;return function(t,e,n){return(e[0]-t[0])*(n[1]-t[1])==(n[0]-t[0])*(e[1]-t[1])}(t,e,n)&&(i=t[r=+(t[0]===e[0])],o=n[r],a=e[r],i<=o&&o<=a||a<=o&&o<=i)}function uE(t,e,n){return function(r){var i=st(r),o=n?Math.min(i[0],0):i[0],a=i[1],s=a-o,u=e?be(o,a,t):s/(t+1);return Se(o+u,a,u)}}function lE(t){Ja.call(this,null,t)}function cE(t,e,n,r,i){const o=t.x1||0,a=t.y1||0,s=e*n<0;function u(t){t.forEach(l)}function l(t){s&&t.reverse(),t.forEach(c)}function c(t){t[0]=(t[0]-o)*e+r,t[1]=(t[1]-a)*n+i}return function(t){return t.coordinates.forEach(u),t}}function fE(t,e,n){const r=t>=0?t:rs(e,n);return Math.round((Math.sqrt(4*r*r+1)-1)/2)}function hE(t){return Z(t)?t:it(+t)}function dE(){var t=t=>t[0],e=t=>t[1],n=d,r=[-1,-1],i=960,o=500,a=2;function u(s,u){const l=fE(r[0],s,t)>>a,c=fE(r[1],s,e)>>a,f=l?l+2:0,h=c?c+2:0,d=2*f+(i>>a),p=2*h+(o>>a),g=new Float32Array(d*p),m=new Float32Array(d*p);let y=g;s.forEach((r=>{const i=f+(+t(r)>>a),o=h+(+e(r)>>a);i>=0&&i<d&&o>=0&&o<p&&(g[i+o*d]+=+n(r))})),l>0&&c>0?(pE(d,p,g,m,l),gE(d,p,m,g,c),pE(d,p,g,m,l),gE(d,p,m,g,c),pE(d,p,g,m,l),gE(d,p,m,g,c)):l>0?(pE(d,p,g,m,l),pE(d,p,m,g,l),pE(d,p,g,m,l),y=m):c>0&&(gE(d,p,g,m,c),gE(d,p,m,g,c),gE(d,p,g,m,c),y=m);const v=u?Math.pow(2,-2*a):1/$e(y);for(let t=0,e=d*p;"
-  , "t<e;++t)y[t]*=v;return{values:y,scale:1<<a,width:d,height:p,x1:f,y1:h,x2:f+(i>>a),y2:h+(o>>a)}}return u.x=function(e){return arguments.length?(t=hE(e),u):t},u.y=function(t){return arguments.length?(e=hE(t),u):e},u.weight=function(t){return arguments.length?(n=hE(t),u):n},u.size=function(t){if(!arguments.length)return[i,o];var e=+t[0],n=+t[1];return e>=0&&n>=0||s(\"invalid size\"),i=e,o=n,u},u.cellSize=function(t){return arguments.length?((t=+t)>=1||s(\"invalid cell size\"),a=Math.floor(Math.log(t)/Math.LN2),u):1<<a},u.bandwidth=function(t){return arguments.length?(1===(t=X(t)).length&&(t=[+t[0],+t[0]]),2!==t.length&&s(\"invalid bandwidth\"),r=t,u):r},u}function pE(t,e,n,r,i){const o=1+(i<<1);for(let a=0;a<e;++a)for(let e=0,s=0;e<t+i;++e)e<t&&(s+=n[e+a*t]),e>=i&&(e>=o&&(s-=n[e-o+a*t]),r[e-i+a*t]=s/Math.min(e+1,t-1+o-e,o))}function gE(t,e,n,r,i){const o=1+(i<<1);for(let a=0;a<t;++a)for(let s=0,u=0;s<e+i;++s)s<e&&(u+=n[a+s*t]),s>=i&&(s>=o&&(u-=n[a+(s-o)*t]),r[a+(s-i)*t]=u/Math.min(s+1,e-1+o-s,o))}function mE(t){Ja.call(this,null,t)}lE.Definition={type:\"Isocontour\",metadata:{generates:!0},params:[{name:\"field\",type:\"field\"},{name:\"thresholds\",type:\"number\",array:!0},{name:\"levels\",type:\"number\"},{name:\"nice\",type:\"boolean\",default:!1},{name:\"resolve\",type:\"enum\",values:[\"shared\",\"independent\"],default:\"independent\"},{name:\"zero\",type:\"boolean\",default:!0},{name:\"smooth\",type:\"boolean\",default:!0},{name:\"scale\",type:\"number\",expr:!0},{name:\"translate\",type:\"number\",array:!0,expr:!0},{name:\"as\",type:\"string\",null:!0,default:\"contour\"}]},dt(lE,Ja,{transform(t,e){if(this.value&&!e.changed()&&!t.modified())return e.StopPropagation;var n=e.fork(e.NO_SOURCE|e.NO_FIELDS),r=e.materialize(e.SOURCE).source,i=t.field||f,o=iE().smooth(!1!==t.smooth),a=t.thresholds||function(t,e,n){const r=uE(n.levels||10,n.nice,!1!==n.zero);return\"shared\"!==n.resolve?r:r(t.map((t=>we(e(t).values))))}(r,i,t),s=null===t.as?null:t.as||\"contour\",u=[];return r.forEach((e=>{const n=i(e),r=o.size([n.width,n.height])(n.values,A(a)?a:a(n.values));!function(t,e,n,r){let i=r.scale||e.scale,o=r.translate||e.translate;Z(i)&&(i=i(n,r));Z(o)&&(o=o(n,r));if((1===i||null==i)&&!o)return;const a=(vt(i)?i:i[0])||1,s=(vt(i)?i:i[1])||1,u=o&&o[0]||0,l=o&&o[1]||0;t.forEach(cE(e,a,s,u,l))}(r,n,e,t),r.forEach((t=>{u.push(ba(e,_a(null!=s?{[s]:t}:t)))}))})),this.value&&(n.rem=this.value),this.value=n.source=n.add=u,n}}),mE.Definition={type:\"KDE2D\",metadata:{generates:!0},params:[{name:\"size\",type:\"number\",array:!0,length:2,required:!0},{name:\"x\",type:\"field\",required:!0},{name:\"y\",type:\"field\",required:!0},{name:\"weight\",type:\"field\"},{name:\"groupby\",type:\"field\",array:!0},{name:\"cellSize\",type:\"number\"},{name:\"bandwidth\",type:\"number\",array:!0,length:2},{name:\"counts\",type:\"boolean\",default:!1},{name:\"as\",type:\"string\",default:\"grid\"}]};const yE=[\"x\",\"y\",\"weight\",\"size\",\"cellSize\",\"bandwidth\"];function vE(t,e){return yE.forEach((n=>null!=e[n]?t[n](e[n]):0)),t}function _E(t){Ja.call(this,null,t)}dt(mE,Ja,{transform(t,e){if(this.value&&!e.changed()&&!t.modified())return e.StopPropagation;var r,i=e.fork(e.NO_SOURCE|e.NO_FIELDS),o=function(t,e){var n,r,i,o,a,s,u=[],l=t=>t(o);if(null==e)u.push(t);else for(n={},r=0,i=t.length;r<i;++r)o=t[r],(s=n[a=e.map(l)])||(n[a]=s=[],s.dims=a,u.push(s)),s.push(o);return u}(e.materialize(e.SOURCE).source,t.groupby),a=(t.groupby||[]).map(n),s=vE(dE(),t),u=t.as||\"grid\";return r=o.map((e=>_a(function(t,e){for(let n=0;n<a.length;++n)t[a[n]]=e[n];return t}({[u]:s(e,t.counts)},e.dims)))),this.value&&(i.rem=this.value),this.value=i.source=i.add=r,i}}),_E.Definition={type:\"Contour\",metadata:{generates:!0},params:[{name:\"size\",type:\"number\",array:!0,length:2,required:!0},{name:\"values\",type:\"number\",array:!0},{name:\"x\",type:\"field\"},{name:\"y\",type:\"field\"},{name:\"weight\",type:\"field\"},{name:\"cellSize\",type:\"number\"},{name:\"bandwidth\",type:\"number\"},{name:\"count\",type:\"number\"},{name:\"nice\",type:\"boolean\",default:!1},{name:\"thresholds\",type:\"number\",array:!0},{name:\"smooth\",type:\"boolean\",default:!0}]},dt(_E,Ja,{transform(t,e){if(this.value&&!e.changed()&&!t.modified"
-  , "())return e.StopPropagation;var n,r,i=e.fork(e.NO_SOURCE|e.NO_FIELDS),o=iE().smooth(!1!==t.smooth),a=t.values,s=t.thresholds||uE(t.count||10,t.nice,!!a),u=t.size;return a||(a=e.materialize(e.SOURCE).source,r=cE(n=vE(dE(),t)(a,!0),n.scale||1,n.scale||1,0,0),u=[n.width,n.height],a=n.values),s=A(s)?s:s(a),a=o.size(u)(a,s),r&&a.forEach(r),this.value&&(i.rem=this.value),this.value=i.source=i.add=(a||[]).map(_a),i}});const xE=\"Feature\",bE=\"FeatureCollection\";function wE(t){Ja.call(this,null,t)}function kE(t){Ja.call(this,null,t)}function AE(t){Ja.call(this,null,t)}function ME(t){Ja.call(this,null,t)}function EE(t){Ja.call(this,[],t),this.generator=function(){var t,e,n,r,i,o,a,s,u,l,c,f,h=10,d=h,p=90,g=360,m=2.5;function y(){return{type:\"MultiLineString\",coordinates:v()}}function v(){return Se(Qb(r/p)*p,n,p).map(c).concat(Se(Qb(s/g)*g,a,g).map(f)).concat(Se(Qb(e/h)*h,t,h).filter((function(t){return Vb(t%p)>qb})).map(u)).concat(Se(Qb(o/d)*d,i,d).filter((function(t){return Vb(t%g)>qb})).map(l))}return y.lines=function(){return v().map((function(t){return{type:\"LineString\",coordinates:t}}))},y.outline=function(){return{type:\"Polygon\",coordinates:[c(r).concat(f(a).slice(1),c(n).reverse().slice(1),f(s).reverse().slice(1))]}},y.extent=function(t){return arguments.length?y.extentMajor(t).extentMinor(t):y.extentMinor()},y.extentMajor=function(t){return arguments.length?(r=+t[0][0],n=+t[1][0],s=+t[0][1],a=+t[1][1],r>n&&(t=r,r=n,n=t),s>a&&(t=s,s=a,a=t),y.precision(m)):[[r,s],[n,a]]},y.extentMinor=function(n){return arguments.length?(e=+n[0][0],t=+n[1][0],o=+n[0][1],i=+n[1][1],e>t&&(n=e,e=t,t=n),o>i&&(n=o,o=i,i=n),y.precision(m)):[[e,o],[t,i]]},y.step=function(t){return arguments.length?y.stepMajor(t).stepMinor(t):y.stepMinor()},y.stepMajor=function(t){return arguments.length?(p=+t[0],g=+t[1],y):[p,g]},y.stepMinor=function(t){return arguments.length?(h=+t[0],d=+t[1],y):[h,d]},y.precision=function(h){return arguments.length?(m=+h,u=Zk(o,i,90),l=Qk(e,t,m),c=Zk(s,a,90),f=Qk(r,n,m),y):m},y.extentMajor([[-180,-90+qb],[180,90-qb]]).extentMinor([[-180,-80-qb],[180,80+qb]])}()}function DE(t){Ja.call(this,null,t)}function CE(t){if(!Z(t))return!1;const e=Bt(r(t));return e.$x||e.$y||e.$value||e.$max}function FE(t){Ja.call(this,null,t),this.modified(!0)}function SE(t,e,n){Z(t[e])&&t[e](n)}wE.Definition={type:\"GeoJSON\",metadata:{},params:[{name:\"fields\",type:\"field\",array:!0,length:2},{name:\"geojson\",type:\"field\"}]},dt(wE,Ja,{transform(t,e){var n,i=this._features,o=this._points,a=t.fields,s=a&&a[0],u=a&&a[1],l=t.geojson||!a&&f,c=e.ADD;n=t.modified()||e.changed(e.REM)||e.modified(r(l))||s&&e.modified(r(s))||u&&e.modified(r(u)),this.value&&!n||(c=e.SOURCE,this._features=i=[],this._points=o=[]),l&&e.visit(c,(t=>i.push(l(t)))),s&&u&&(e.visit(c,(t=>{var e=s(t),n=u(t);null!=e&&null!=n&&(e=+e)===e&&(n=+n)===n&&o.push([e,n])})),i=i.concat({type:xE,geometry:{type:\"MultiPoint\",coordinates:o}})),this.value={type:bE,features:i}}}),kE.Definition={type:\"GeoPath\",metadata:{modifies:!0},params:[{name:\"projection\",type:\"projection\"},{name:\"field\",type:\"field\"},{name:\"pointRadius\",type:\"number\",expr:!0},{name:\"as\",type:\"string\",default:\"path\"}]},dt(kE,Ja,{transform(t,e){var n=e.fork(e.ALL),r=this.value,i=t.field||f,o=t.as||\"path\",a=n.SOURCE;!r||t.modified()?(this.value=r=tE(t.projection),n.materialize().reflow()):a=i===f||e.modified(i.fields)?n.ADD_MOD:n.ADD;const s=function(t,e){const n=t.pointRadius();t.context(null),null!=e&&t.pointRadius(e);return n}(r,t.pointRadius);return n.visit(a,(t=>t[o]=r(i(t)))),r.pointRadius(s),n.modifies(o)}}),AE.Definition={type:\"GeoPoint\",metadata:{modifies:!0},params:[{name:\"projection\",type:\"projection\",required:!0},{name:\"fields\",type:\"field\",array:!0,required:!0,length:2},{name:\"as\",type:\"string\",array:!0,length:2,default:[\"x\",\"y\"]}]},dt(AE,Ja,{transform(t,e){var n,r=t.projection,i=t.fields[0],o=t.fields[1],a=t.as||[\"x\",\"y\"],s=a[0],u=a[1];function l(t){const e=r([i(t),o(t)]);e?(t[s]=e[0],t[u]=e[1]):(t[s]=void 0,t[u]=void 0)}return t.modified()?e=e.materialize().reflow(!0).visit(e.SOURCE,l):(n=e.modified(i.fields)||e.modified(o.fields"
-  , "),e.visit(n?e.ADD_MOD:e.ADD,l)),e.modifies(a)}}),ME.Definition={type:\"GeoShape\",metadata:{modifies:!0,nomod:!0},params:[{name:\"projection\",type:\"projection\"},{name:\"field\",type:\"field\",default:\"datum\"},{name:\"pointRadius\",type:\"number\",expr:!0},{name:\"as\",type:\"string\",default:\"shape\"}]},dt(ME,Ja,{transform(t,e){var n=e.fork(e.ALL),r=this.value,i=t.as||\"shape\",o=n.ADD;return r&&!t.modified()||(this.value=r=function(t,e,n){const r=null==n?n=>t(e(n)):r=>{var i=t.pointRadius(),o=t.pointRadius(n)(e(r));return t.pointRadius(i),o};return r.context=e=>(t.context(e),r),r}(tE(t.projection),t.field||l(\"datum\"),t.pointRadius),n.materialize().reflow(),o=n.SOURCE),n.visit(o,(t=>t[i]=r)),n.modifies(i)}}),EE.Definition={type:\"Graticule\",metadata:{changes:!0,generates:!0},params:[{name:\"extent\",type:\"array\",array:!0,length:2,content:{type:\"number\",array:!0,length:2}},{name:\"extentMajor\",type:\"array\",array:!0,length:2,content:{type:\"number\",array:!0,length:2}},{name:\"extentMinor\",type:\"array\",array:!0,length:2,content:{type:\"number\",array:!0,length:2}},{name:\"step\",type:\"number\",array:!0,length:2},{name:\"stepMajor\",type:\"number\",array:!0,length:2,default:[90,360]},{name:\"stepMinor\",type:\"number\",array:!0,length:2,default:[10,10]},{name:\"precision\",type:\"number\",default:2.5}]},dt(EE,Ja,{transform(t,e){var n,r=this.value,i=this.generator;if(!r.length||t.modified())for(const e in t)Z(i[e])&&i[e](t[e]);return n=i(),r.length?e.mod.push(wa(r[0],n)):e.add.push(_a(n)),r[0]=n,e}}),DE.Definition={type:\"heatmap\",metadata:{modifies:!0},params:[{name:\"field\",type:\"field\"},{name:\"color\",type:\"string\",expr:!0},{name:\"opacity\",type:\"number\",expr:!0},{name:\"resolve\",type:\"enum\",values:[\"shared\",\"independent\"],default:\"independent\"},{name:\"as\",type:\"string\",default:\"image\"}]},dt(DE,Ja,{transform(t,e){if(!e.changed()&&!t.modified())return e.StopPropagation;var n=e.materialize(e.SOURCE).source,r=\"shared\"===t.resolve,i=t.field||f,o=function(t,e){let n;Z(t)?(n=n=>t(n,e),n.dep=CE(t)):t?n=it(t):(n=t=>t.$value/t.$max||0,n.dep=!0);return n}(t.opacity,t),a=function(t,e){let n;Z(t)?(n=n=>af(t(n,e)),n.dep=CE(t)):n=it(af(t||\"#888\"));return n}(t.color,t),s=t.as||\"image\",u={$x:0,$y:0,$value:0,$max:r?we(n.map((t=>we(i(t).values)))):0};return n.forEach((t=>{const e=i(t),n=at({},t,u);r||(n.$max=we(e.values||[])),t[s]=function(t,e,n,r){const i=t.width,o=t.height,a=t.x1||0,s=t.y1||0,u=t.x2||i,l=t.y2||o,c=t.values,f=c?t=>c[t]:h,d=$c(u-a,l-s),p=d.getContext(\"2d\"),g=p.getImageData(0,0,u-a,l-s),m=g.data;for(let t=s,o=0;t<l;++t){e.$y=t-s;for(let s=a,l=t*i;s<u;++s,o+=4){e.$x=s-a,e.$value=f(s+l);const t=n(e);m[o+0]=t.r,m[o+1]=t.g,m[o+2]=t.b,m[o+3]=~~(255*r(e))}}return p.putImageData(g,0,0),d}(e,n,a.dep?a:it(a(n)),o.dep?o:it(o(n)))})),e.reflow(!0).modifies(s)}}),dt(FE,Ja,{transform(t,e){let n=this.value;return!n||t.modified(\"type\")?(this.value=n=function(t){const e=KM((t||\"mercator\").toLowerCase());e||s(\"Unrecognized projection type: \"+t);return e()}(t.type),QM.forEach((e=>{null!=t[e]&&SE(n,e,t[e])}))):QM.forEach((e=>{t.modified(e)&&SE(n,e,t[e])})),null!=t.pointRadius&&n.path.pointRadius(t.pointRadius),t.fit&&function(t,e){const n=function(t){return t=X(t),1===t.length?t[0]:{type:bE,features:t.reduce(((t,e)=>t.concat(function(t){return t.type===bE?t.features:X(t).filter((t=>null!=t)).map((t=>t.type===xE?t:{type:xE,geometry:t}))}(e))),[])}}(e.fit);e.extent?t.fitExtent(e.extent,n):e.size&&t.fitSize(e.size,n)}(n,t),e.fork(e.NO_SOURCE|e.NO_FIELDS)}});var $E=Object.freeze({__proto__:null,contour:_E,geojson:wE,geopath:kE,geopoint:AE,geoshape:ME,graticule:EE,heatmap:DE,isocontour:lE,kde2d:mE,projection:FE});function TE(t,e,n,r){if(isNaN(e)||isNaN(n))return t;var i,o,a,s,u,l,c,f,h,d=t._root,p={data:r},g=t._x0,m=t._y0,y=t._x1,v=t._y1;if(!d)return t._root=p,t;for(;d.length;)if((l=e>=(o=(g+y)/2))?g=o:y=o,(c=n>=(a=(m+v)/2))?m=a:v=a,i=d,!(d=d[f=c<<1|l]))return i[f]=p,t;if(s=+t._x.call(null,d.data),u=+t._y.call(null,d.data),e===s&&n===u)return p.next=d,i?i[f]=p:t._root=p,t;do{i=i?i[f]=new Array(4):t._root=new Array(4),(l=e>=(o=(g+y)/2))?g=o:y=o,(c=n>=(a=(m+v)/2))?m=a:v=a}while((f=c<<1|l)==(h=(u>"
-  , "=a)<<1|s>=o));return i[h]=d,i[f]=p,t}function BE(t,e,n,r,i){this.node=t,this.x0=e,this.y0=n,this.x1=r,this.y1=i}function NE(t){return t[0]}function zE(t){return t[1]}function OE(t,e,n){var r=new RE(null==e?NE:e,null==n?zE:n,NaN,NaN,NaN,NaN);return null==t?r:r.addAll(t)}function RE(t,e,n,r,i,o){this._x=t,this._y=e,this._x0=n,this._y0=r,this._x1=i,this._y1=o,this._root=void 0}function LE(t){for(var e={data:t.data},n=e;t=t.next;)n=n.next={data:t.data};return e}var UE=OE.prototype=RE.prototype;function qE(t){return function(){return t}}function PE(t){return 1e-6*(t()-.5)}function jE(t){return t.x+t.vx}function IE(t){return t.y+t.vy}function WE(t){return t.index}function HE(t,e){var n=t.get(e);if(!n)throw new Error(\"node not found: \"+e);return n}UE.copy=function(){var t,e,n=new RE(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return n;if(!r.length)return n._root=LE(r),n;for(t=[{source:r,target:n._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(e=r.source[i])&&(e.length?t.push({source:e,target:r.target[i]=new Array(4)}):r.target[i]=LE(e));return n},UE.add=function(t){const e=+this._x.call(null,t),n=+this._y.call(null,t);return TE(this.cover(e,n),e,n,t)},UE.addAll=function(t){var e,n,r,i,o=t.length,a=new Array(o),s=new Array(o),u=1/0,l=1/0,c=-1/0,f=-1/0;for(n=0;n<o;++n)isNaN(r=+this._x.call(null,e=t[n]))||isNaN(i=+this._y.call(null,e))||(a[n]=r,s[n]=i,r<u&&(u=r),r>c&&(c=r),i<l&&(l=i),i>f&&(f=i));if(u>c||l>f)return this;for(this.cover(u,l).cover(c,f),n=0;n<o;++n)TE(this,a[n],s[n],t[n]);return this},UE.cover=function(t,e){if(isNaN(t=+t)||isNaN(e=+e))return this;var n=this._x0,r=this._y0,i=this._x1,o=this._y1;if(isNaN(n))i=(n=Math.floor(t))+1,o=(r=Math.floor(e))+1;else{for(var a,s,u=i-n||1,l=this._root;n>t||t>=i||r>e||e>=o;)switch(s=(e<r)<<1|t<n,(a=new Array(4))[s]=l,l=a,u*=2,s){case 0:i=n+u,o=r+u;break;case 1:n=i-u,o=r+u;break;case 2:i=n+u,r=o-u;break;case 3:n=i-u,r=o-u}this._root&&this._root.length&&(this._root=l)}return this._x0=n,this._y0=r,this._x1=i,this._y1=o,this},UE.data=function(){var t=[];return this.visit((function(e){if(!e.length)do{t.push(e.data)}while(e=e.next)})),t},UE.extent=function(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]},UE.find=function(t,e,n){var r,i,o,a,s,u,l,c=this._x0,f=this._y0,h=this._x1,d=this._y1,p=[],g=this._root;for(g&&p.push(new BE(g,c,f,h,d)),null==n?n=1/0:(c=t-n,f=e-n,h=t+n,d=e+n,n*=n);u=p.pop();)if(!(!(g=u.node)||(i=u.x0)>h||(o=u.y0)>d||(a=u.x1)<c||(s=u.y1)<f))if(g.length){var m=(i+a)/2,y=(o+s)/2;p.push(new BE(g[3],m,y,a,s),new BE(g[2],i,y,m,s),new BE(g[1],m,o,a,y),new BE(g[0],i,o,m,y)),(l=(e>=y)<<1|t>=m)&&(u=p[p.length-1],p[p.length-1]=p[p.length-1-l],p[p.length-1-l]=u)}else{var v=t-+this._x.call(null,g.data),_=e-+this._y.call(null,g.data),x=v*v+_*_;if(x<n){var b=Math.sqrt(n=x);c=t-b,f=e-b,h=t+b,d=e+b,r=g.data}}return r},UE.remove=function(t){if(isNaN(o=+this._x.call(null,t))||isNaN(a=+this._y.call(null,t)))return this;var e,n,r,i,o,a,s,u,l,c,f,h,d=this._root,p=this._x0,g=this._y0,m=this._x1,y=this._y1;if(!d)return this;if(d.length)for(;;){if((l=o>=(s=(p+m)/2))?p=s:m=s,(c=a>=(u=(g+y)/2))?g=u:y=u,e=d,!(d=d[f=c<<1|l]))return this;if(!d.length)break;(e[f+1&3]||e[f+2&3]||e[f+3&3])&&(n=e,h=f)}for(;d.data!==t;)if(r=d,!(d=d.next))return this;return(i=d.next)&&delete d.next,r?(i?r.next=i:delete r.next,this):e?(i?e[f]=i:delete e[f],(d=e[0]||e[1]||e[2]||e[3])&&d===(e[3]||e[2]||e[1]||e[0])&&!d.length&&(n?n[h]=d:this._root=d),this):(this._root=i,this)},UE.removeAll=function(t){for(var e=0,n=t.length;e<n;++e)this.remove(t[e]);return this},UE.root=function(){return this._root},UE.size=function(){var t=0;return this.visit((function(e){if(!e.length)do{++t}while(e=e.next)})),t},UE.visit=function(t){var e,n,r,i,o,a,s=[],u=this._root;for(u&&s.push(new BE(u,this._x0,this._y0,this._x1,this._y1));e=s.pop();)if(!t(u=e.node,r=e.x0,i=e.y0,o=e.x1,a=e.y1)&&u.length){var l=(r+o)/2,c=(i+a)/2;(n=u[3])&&s.push(new BE(n,l,c,o,a)),(n=u[2])&&s.push(new BE(n,r,c,l,a)),(n=u[1])&&s.push(new BE(n,l"
-  , ",i,o,c)),(n=u[0])&&s.push(new BE(n,r,i,l,c))}return this},UE.visitAfter=function(t){var e,n=[],r=[];for(this._root&&n.push(new BE(this._root,this._x0,this._y0,this._x1,this._y1));e=n.pop();){var i=e.node;if(i.length){var o,a=e.x0,s=e.y0,u=e.x1,l=e.y1,c=(a+u)/2,f=(s+l)/2;(o=i[0])&&n.push(new BE(o,a,s,c,f)),(o=i[1])&&n.push(new BE(o,c,s,u,f)),(o=i[2])&&n.push(new BE(o,a,f,c,l)),(o=i[3])&&n.push(new BE(o,c,f,u,l))}r.push(e)}for(;e=r.pop();)t(e.node,e.x0,e.y0,e.x1,e.y1);return this},UE.x=function(t){return arguments.length?(this._x=t,this):this._x},UE.y=function(t){return arguments.length?(this._y=t,this):this._y};var YE={value:()=>{}};function GE(){for(var t,e=0,n=arguments.length,r={};e<n;++e){if(!(t=arguments[e]+\"\")||t in r||/[\\s.]/.test(t))throw new Error(\"illegal type: \"+t);r[t]=[]}return new VE(r)}function VE(t){this._=t}function XE(t,e){for(var n,r=0,i=t.length;r<i;++r)if((n=t[r]).name===e)return n.value}function JE(t,e,n){for(var r=0,i=t.length;r<i;++r)if(t[r].name===e){t[r]=YE,t=t.slice(0,r).concat(t.slice(r+1));break}return null!=n&&t.push({name:e,value:n}),t}VE.prototype=GE.prototype={constructor:VE,on:function(t,e){var n,r,i=this._,o=(r=i,(t+\"\").trim().split(/^|\\s+/).map((function(t){var e=\"\",n=t.indexOf(\".\");if(n>=0&&(e=t.slice(n+1),t=t.slice(0,n)),t&&!r.hasOwnProperty(t))throw new Error(\"unknown type: \"+t);return{type:t,name:e}}))),a=-1,s=o.length;if(!(arguments.length<2)){if(null!=e&&\"function\"!=typeof e)throw new Error(\"invalid callback: \"+e);for(;++a<s;)if(n=(t=o[a]).type)i[n]=JE(i[n],t.name,e);else if(null==e)for(n in i)i[n]=JE(i[n],t.name,null);return this}for(;++a<s;)if((n=(t=o[a]).type)&&(n=XE(i[n],t.name)))return n},copy:function(){var t={},e=this._;for(var n in e)t[n]=e[n].slice();return new VE(t)},call:function(t,e){if((n=arguments.length-2)>0)for(var n,r,i=new Array(n),o=0;o<n;++o)i[o]=arguments[o+2];if(!this._.hasOwnProperty(t))throw new Error(\"unknown type: \"+t);for(o=0,n=(r=this._[t]).length;o<n;++o)r[o].value.apply(e,i)},apply:function(t,e,n){if(!this._.hasOwnProperty(t))throw new Error(\"unknown type: \"+t);for(var r=this._[t],i=0,o=r.length;i<o;++i)r[i].value.apply(e,n)}};var ZE,QE,KE=0,tD=0,eD=0,nD=1e3,rD=0,iD=0,oD=0,aD=\"object\"==typeof performance&&performance.now?performance:Date,sD=\"object\"==typeof window&&window.requestAnimationFrame?window.requestAnimationFrame.bind(window):function(t){setTimeout(t,17)};function uD(){return iD||(sD(lD),iD=aD.now()+oD)}function lD(){iD=0}function cD(){this._call=this._time=this._next=null}function fD(t,e,n){var r=new cD;return r.restart(t,e,n),r}function hD(){iD=(rD=aD.now())+oD,KE=tD=0;try{!function(){uD(),++KE;for(var t,e=ZE;e;)(t=iD-e._time)>=0&&e._call.call(void 0,t),e=e._next;--KE}()}finally{KE=0,function(){var t,e,n=ZE,r=1/0;for(;n;)n._call?(r>n._time&&(r=n._time),t=n,n=n._next):(e=n._next,n._next=null,n=t?t._next=e:ZE=e);QE=t,pD(r)}(),iD=0}}function dD(){var t=aD.now(),e=t-rD;e>nD&&(oD-=e,rD=t)}function pD(t){KE||(tD&&(tD=clearTimeout(tD)),t-iD>24?(t<1/0&&(tD=setTimeout(hD,t-aD.now()-oD)),eD&&(eD=clearInterval(eD))):(eD||(rD=aD.now(),eD=setInterval(dD,nD)),KE=1,sD(hD)))}cD.prototype=fD.prototype={constructor:cD,restart:function(t,e,n){if(\"function\"!=typeof t)throw new TypeError(\"callback is not a function\");n=(null==n?uD():+n)+(null==e?0:+e),this._next||QE===this||(QE?QE._next=this:ZE=this,QE=this),this._call=t,this._time=n,pD()},stop:function(){this._call&&(this._call=null,this._time=1/0,pD())}};const gD=1664525,mD=1013904223,yD=4294967296;function vD(t){return t.x}function _D(t){return t.y}var xD=10,bD=Math.PI*(3-Math.sqrt(5));function wD(t){var e,n=1,r=.001,i=1-Math.pow(r,1/300),o=0,a=.6,s=new Map,u=fD(f),l=GE(\"tick\",\"end\"),c=function(){let t=1;return()=>(t=(gD*t+mD)%yD)/yD}();function f(){h(),l.call(\"tick\",e),n<r&&(u.stop(),l.call(\"end\",e))}function h(r){var u,l,c=t.length;void 0===r&&(r=1);for(var f=0;f<r;++f)for(n+=(o-n)*i,s.forEach((function(t){t(n)})),u=0;u<c;++u)null==(l=t[u]).fx?l.x+=l.vx*=a:(l.x=l.fx,l.vx=0),null==l.fy?l.y+=l.vy*=a:(l.y=l.fy,l.vy=0);return e}function d(){for(var e,n=0,r=t.length;n<r;++n){if((e=t[n]).index=n,null!=e.fx&&(e.x"
-  , "=e.fx),null!=e.fy&&(e.y=e.fy),isNaN(e.x)||isNaN(e.y)){var i=xD*Math.sqrt(.5+n),o=n*bD;e.x=i*Math.cos(o),e.y=i*Math.sin(o)}(isNaN(e.vx)||isNaN(e.vy))&&(e.vx=e.vy=0)}}function p(e){return e.initialize&&e.initialize(t,c),e}return null==t&&(t=[]),d(),e={tick:h,restart:function(){return u.restart(f),e},stop:function(){return u.stop(),e},nodes:function(n){return arguments.length?(t=n,d(),s.forEach(p),e):t},alpha:function(t){return arguments.length?(n=+t,e):n},alphaMin:function(t){return arguments.length?(r=+t,e):r},alphaDecay:function(t){return arguments.length?(i=+t,e):+i},alphaTarget:function(t){return arguments.length?(o=+t,e):o},velocityDecay:function(t){return arguments.length?(a=1-t,e):1-a},randomSource:function(t){return arguments.length?(c=t,s.forEach(p),e):c},force:function(t,n){return arguments.length>1?(null==n?s.delete(t):s.set(t,p(n)),e):s.get(t)},find:function(e,n,r){var i,o,a,s,u,l=0,c=t.length;for(null==r?r=1/0:r*=r,l=0;l<c;++l)(a=(i=e-(s=t[l]).x)*i+(o=n-s.y)*o)<r&&(u=s,r=a);return u},on:function(t,n){return arguments.length>1?(l.on(t,n),e):l.on(t)}}}const kD={center:function(t,e){var n,r=1;function i(){var i,o,a=n.length,s=0,u=0;for(i=0;i<a;++i)s+=(o=n[i]).x,u+=o.y;for(s=(s/a-t)*r,u=(u/a-e)*r,i=0;i<a;++i)(o=n[i]).x-=s,o.y-=u}return null==t&&(t=0),null==e&&(e=0),i.initialize=function(t){n=t},i.x=function(e){return arguments.length?(t=+e,i):t},i.y=function(t){return arguments.length?(e=+t,i):e},i.strength=function(t){return arguments.length?(r=+t,i):r},i},collide:function(t){var e,n,r,i=1,o=1;function a(){for(var t,a,u,l,c,f,h,d=e.length,p=0;p<o;++p)for(a=OE(e,jE,IE).visitAfter(s),t=0;t<d;++t)u=e[t],f=n[u.index],h=f*f,l=u.x+u.vx,c=u.y+u.vy,a.visit(g);function g(t,e,n,o,a){var s=t.data,d=t.r,p=f+d;if(!s)return e>l+p||o<l-p||n>c+p||a<c-p;if(s.index>u.index){var g=l-s.x-s.vx,m=c-s.y-s.vy,y=g*g+m*m;y<p*p&&(0===g&&(y+=(g=PE(r))*g),0===m&&(y+=(m=PE(r))*m),y=(p-(y=Math.sqrt(y)))/y*i,u.vx+=(g*=y)*(p=(d*=d)/(h+d)),u.vy+=(m*=y)*p,s.vx-=g*(p=1-p),s.vy-=m*p)}}}function s(t){if(t.data)return t.r=n[t.data.index];for(var e=t.r=0;e<4;++e)t[e]&&t[e].r>t.r&&(t.r=t[e].r)}function u(){if(e){var r,i,o=e.length;for(n=new Array(o),r=0;r<o;++r)i=e[r],n[i.index]=+t(i,r,e)}}return\"function\"!=typeof t&&(t=qE(null==t?1:+t)),a.initialize=function(t,n){e=t,r=n,u()},a.iterations=function(t){return arguments.length?(o=+t,a):o},a.strength=function(t){return arguments.length?(i=+t,a):i},a.radius=function(e){return arguments.length?(t=\"function\"==typeof e?e:qE(+e),u(),a):t},a},nbody:function(){var t,e,n,r,i,o=qE(-30),a=1,s=1/0,u=.81;function l(n){var i,o=t.length,a=OE(t,vD,_D).visitAfter(f);for(r=n,i=0;i<o;++i)e=t[i],a.visit(h)}function c(){if(t){var e,n,r=t.length;for(i=new Array(r),e=0;e<r;++e)n=t[e],i[n.index]=+o(n,e,t)}}function f(t){var e,n,r,o,a,s=0,u=0;if(t.length){for(r=o=a=0;a<4;++a)(e=t[a])&&(n=Math.abs(e.value))&&(s+=e.value,u+=n,r+=n*e.x,o+=n*e.y);t.x=r/u,t.y=o/u}else{(e=t).x=e.data.x,e.y=e.data.y;do{s+=i[e.data.index]}while(e=e.next)}t.value=s}function h(t,o,l,c){if(!t.value)return!0;var f=t.x-e.x,h=t.y-e.y,d=c-o,p=f*f+h*h;if(d*d/u<p)return p<s&&(0===f&&(p+=(f=PE(n))*f),0===h&&(p+=(h=PE(n))*h),p<a&&(p=Math.sqrt(a*p)),e.vx+=f*t.value*r/p,e.vy+=h*t.value*r/p),!0;if(!(t.length||p>=s)){(t.data!==e||t.next)&&(0===f&&(p+=(f=PE(n))*f),0===h&&(p+=(h=PE(n))*h),p<a&&(p=Math.sqrt(a*p)));do{t.data!==e&&(d=i[t.data.index]*r/p,e.vx+=f*d,e.vy+=h*d)}while(t=t.next)}}return l.initialize=function(e,r){t=e,n=r,c()},l.strength=function(t){return arguments.length?(o=\"function\"==typeof t?t:qE(+t),c(),l):o},l.distanceMin=function(t){return arguments.length?(a=t*t,l):Math.sqrt(a)},l.distanceMax=function(t){return arguments.length?(s=t*t,l):Math.sqrt(s)},l.theta=function(t){return arguments.length?(u=t*t,l):Math.sqrt(u)},l},link:function(t){var e,n,r,i,o,a,s=WE,u=function(t){return 1/Math.min(i[t.source.index],i[t.target.index])},l=qE(30),c=1;function f(r){for(var i=0,s=t.length;i<c;++i)for(var u,l,f,h,d,p,g,m=0;m<s;++m)l=(u=t[m]).source,h=(f=u.target).x+f.vx-l.x-l.vx||PE(a),d=f.y+f.vy-l.y-l.vy||PE(a),h*=p=((p=Math.sqrt(h*h+d*d))-n[m])/p*r*e[m],d*=p,f.vx-=h*"
-  , "(g=o[m]),f.vy-=d*g,l.vx+=h*(g=1-g),l.vy+=d*g}function h(){if(r){var a,u,l=r.length,c=t.length,f=new Map(r.map(((t,e)=>[s(t,e,r),t])));for(a=0,i=new Array(l);a<c;++a)(u=t[a]).index=a,\"object\"!=typeof u.source&&(u.source=HE(f,u.source)),\"object\"!=typeof u.target&&(u.target=HE(f,u.target)),i[u.source.index]=(i[u.source.index]||0)+1,i[u.target.index]=(i[u.target.index]||0)+1;for(a=0,o=new Array(c);a<c;++a)u=t[a],o[a]=i[u.source.index]/(i[u.source.index]+i[u.target.index]);e=new Array(c),d(),n=new Array(c),p()}}function d(){if(r)for(var n=0,i=t.length;n<i;++n)e[n]=+u(t[n],n,t)}function p(){if(r)for(var e=0,i=t.length;e<i;++e)n[e]=+l(t[e],e,t)}return null==t&&(t=[]),f.initialize=function(t,e){r=t,a=e,h()},f.links=function(e){return arguments.length?(t=e,h(),f):t},f.id=function(t){return arguments.length?(s=t,f):s},f.iterations=function(t){return arguments.length?(c=+t,f):c},f.strength=function(t){return arguments.length?(u=\"function\"==typeof t?t:qE(+t),d(),f):u},f.distance=function(t){return arguments.length?(l=\"function\"==typeof t?t:qE(+t),p(),f):l},f},x:function(t){var e,n,r,i=qE(.1);function o(t){for(var i,o=0,a=e.length;o<a;++o)(i=e[o]).vx+=(r[o]-i.x)*n[o]*t}function a(){if(e){var o,a=e.length;for(n=new Array(a),r=new Array(a),o=0;o<a;++o)n[o]=isNaN(r[o]=+t(e[o],o,e))?0:+i(e[o],o,e)}}return\"function\"!=typeof t&&(t=qE(null==t?0:+t)),o.initialize=function(t){e=t,a()},o.strength=function(t){return arguments.length?(i=\"function\"==typeof t?t:qE(+t),a(),o):i},o.x=function(e){return arguments.length?(t=\"function\"==typeof e?e:qE(+e),a(),o):t},o},y:function(t){var e,n,r,i=qE(.1);function o(t){for(var i,o=0,a=e.length;o<a;++o)(i=e[o]).vy+=(r[o]-i.y)*n[o]*t}function a(){if(e){var o,a=e.length;for(n=new Array(a),r=new Array(a),o=0;o<a;++o)n[o]=isNaN(r[o]=+t(e[o],o,e))?0:+i(e[o],o,e)}}return\"function\"!=typeof t&&(t=qE(null==t?0:+t)),o.initialize=function(t){e=t,a()},o.strength=function(t){return arguments.length?(i=\"function\"==typeof t?t:qE(+t),a(),o):i},o.y=function(e){return arguments.length?(t=\"function\"==typeof e?e:qE(+e),a(),o):t},o}},AD=\"forces\",MD=[\"alpha\",\"alphaMin\",\"alphaTarget\",\"velocityDecay\",\"forces\"],ED=[\"static\",\"iterations\"],DD=[\"x\",\"y\",\"vx\",\"vy\"];function CD(t){Ja.call(this,null,t)}function FD(t,e,n,r){var i,o,a,s,u=X(e.forces);for(i=0,o=MD.length;i<o;++i)(a=MD[i])!==AD&&e.modified(a)&&t[a](e[a]);for(i=0,o=u.length;i<o;++i)s=AD+i,(a=n||e.modified(AD,i)?$D(u[i]):r&&SD(u[i],r)?t.force(s):null)&&t.force(s,a);for(o=t.numForces||0;i<o;++i)t.force(AD+i,null);return t.numForces=u.length,t}function SD(t,e){var n,i;for(n in t)if(Z(i=t[n])&&e.modified(r(i)))return 1;return 0}function $D(t){var e,n;for(n in lt(kD,t.force)||s(\"Unrecognized force: \"+t.force),e=kD[t.force](),t)Z(e[n])&&TD(e[n],t[n],t);return e}function TD(t,e,n){t(Z(e)?t=>e(t,n):e)}CD.Definition={type:\"Force\",metadata:{modifies:!0},params:[{name:\"static\",type:\"boolean\",default:!1},{name:\"restart\",type:\"boolean\",default:!1},{name:\"iterations\",type:\"number\",default:300},{name:\"alpha\",type:\"number\",default:1},{name:\"alphaMin\",type:\"number\",default:.001},{name:\"alphaTarget\",type:\"number\",default:0},{name:\"velocityDecay\",type:\"number\",default:.4},{name:\"forces\",type:\"param\",array:!0,params:[{key:{force:\"center\"},params:[{name:\"x\",type:\"number\",default:0},{name:\"y\",type:\"number\",default:0}]},{key:{force:\"collide\"},params:[{name:\"radius\",type:\"number\",expr:!0},{name:\"strength\",type:\"number\",default:.7},{name:\"iterations\",type:\"number\",default:1}]},{key:{force:\"nbody\"},params:[{name:\"strength\",type:\"number\",default:-30,expr:!0},{name:\"theta\",type:\"number\",default:.9},{name:\"distanceMin\",type:\"number\",default:1},{name:\"distanceMax\",type:\"number\"}]},{key:{force:\"link\"},params:[{name:\"links\",type:\"data\"},{name:\"id\",type:\"field\"},{name:\"distance\",type:\"number\",default:30,expr:!0},{name:\"strength\",type:\"number\",expr:!0},{name:\"iterations\",type:\"number\",default:1}]},{key:{force:\"x\"},params:[{name:\"strength\",type:\"number\",default:.1},{name:\"x\",type:\"field\"}]},{key:{force:\"y\"},params:[{name:\"strength\",type:\"number\",default:.1},{name:\"y\",type:\"field\"}]}]},{name:\"as\",type:\"string\",arr"
-  , "ay:!0,modify:!1,default:DD}]},dt(CD,Ja,{transform(t,e){var n,r,i=this.value,o=e.changed(e.ADD_REM),a=t.modified(MD),s=t.iterations||300;if(i?(o&&(e.modifies(\"index\"),i.nodes(e.source)),(a||e.changed(e.MOD))&&FD(i,t,0,e)):(this.value=i=function(t,e){const n=wD(t),r=n.stop,i=n.restart;let o=!1;return n.stopped=()=>o,n.restart=()=>(o=!1,i()),n.stop=()=>(o=!0,r()),FD(n,e,!0).on(\"end\",(()=>o=!0))}(e.source,t),i.on(\"tick\",(n=e.dataflow,r=this,()=>n.touch(r).run())),t.static||(o=!0,i.tick()),e.modifies(\"index\")),a||o||t.modified(ED)||e.changed()&&t.restart)if(i.alpha(Math.max(i.alpha(),t.alpha||1)).alphaDecay(1-Math.pow(i.alphaMin(),1/s)),t.static)for(i.stop();--s>=0;)i.tick();else if(i.stopped()&&i.restart(),!o)return e.StopPropagation;return this.finish(t,e)},finish(t,e){const n=e.dataflow;for(let t,e=this._argops,s=0,u=e.length;s<u;++s)if(t=e[s],t.name===AD&&\"link\"===t.op._argval.force)for(var r,i=t.op._argops,o=0,a=i.length;o<a;++o)if(\"links\"===i[o].name&&(r=i[o].op.source)){n.pulse(r,n.changeset().reflow());break}return e.reflow(t.modified()).modifies(DD)}});var BD=Object.freeze({__proto__:null,force:CD});function ND(t,e){return t.parent===e.parent?1:2}function zD(t,e){return t+e.x}function OD(t,e){return Math.max(t,e.y)}function RD(t){var e=0,n=t.children,r=n&&n.length;if(r)for(;--r>=0;)e+=n[r].value;else e=1;t.value=e}function LD(t,e){t instanceof Map?(t=[void 0,t],void 0===e&&(e=qD)):void 0===e&&(e=UD);for(var n,r,i,o,a,s=new ID(t),u=[s];n=u.pop();)if((i=e(n.data))&&(a=(i=Array.from(i)).length))for(n.children=i,o=a-1;o>=0;--o)u.push(r=i[o]=new ID(i[o])),r.parent=n,r.depth=n.depth+1;return s.eachBefore(jD)}function UD(t){return t.children}function qD(t){return Array.isArray(t)?t[1]:null}function PD(t){void 0!==t.data.value&&(t.value=t.data.value),t.data=t.data.data}function jD(t){var e=0;do{t.height=e}while((t=t.parent)&&t.height<++e)}function ID(t){this.data=t,this.depth=this.height=0,this.parent=null}function WD(t){return null==t?null:HD(t)}function HD(t){if(\"function\"!=typeof t)throw new Error;return t}function YD(){return 0}function GD(t){return function(){return t}}ID.prototype=LD.prototype={constructor:ID,count:function(){return this.eachAfter(RD)},each:function(t,e){let n=-1;for(const r of this)t.call(e,r,++n,this);return this},eachAfter:function(t,e){for(var n,r,i,o=this,a=[o],s=[],u=-1;o=a.pop();)if(s.push(o),n=o.children)for(r=0,i=n.length;r<i;++r)a.push(n[r]);for(;o=s.pop();)t.call(e,o,++u,this);return this},eachBefore:function(t,e){for(var n,r,i=this,o=[i],a=-1;i=o.pop();)if(t.call(e,i,++a,this),n=i.children)for(r=n.length-1;r>=0;--r)o.push(n[r]);return this},find:function(t,e){let n=-1;for(const r of this)if(t.call(e,r,++n,this))return r},sum:function(t){return this.eachAfter((function(e){for(var n=+t(e.data)||0,r=e.children,i=r&&r.length;--i>=0;)n+=r[i].value;e.value=n}))},sort:function(t){return this.eachBefore((function(e){e.children&&e.children.sort(t)}))},path:function(t){for(var e=this,n=function(t,e){if(t===e)return t;var n=t.ancestors(),r=e.ancestors(),i=null;t=n.pop(),e=r.pop();for(;t===e;)i=t,t=n.pop(),e=r.pop();return i}(e,t),r=[e];e!==n;)e=e.parent,r.push(e);for(var i=r.length;t!==n;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,e=[t];t=t.parent;)e.push(t);return e},descendants:function(){return Array.from(this)},leaves:function(){var t=[];return this.eachBefore((function(e){e.children||t.push(e)})),t},links:function(){var t=this,e=[];return t.each((function(n){n!==t&&e.push({source:n.parent,target:n})})),e},copy:function(){return LD(this).eachBefore(PD)},[Symbol.iterator]:function*(){var t,e,n,r,i=this,o=[i];do{for(t=o.reverse(),o=[];i=t.pop();)if(yield i,e=i.children)for(n=0,r=e.length;n<r;++n)o.push(e[n])}while(o.length)}};const VD=1664525,XD=1013904223,JD=4294967296;function ZD(t,e){var n,r;if(tC(e,t))return[e];for(n=0;n<t.length;++n)if(QD(e,t[n])&&tC(nC(t[n],e),t))return[t[n],e];for(n=0;n<t.length-1;++n)for(r=n+1;r<t.length;++r)if(QD(nC(t[n],t[r]),e)&&QD(nC(t[n],e),t[r])&&QD(nC(t[r],e),t[n])&&tC(rC(t[n],t[r],e),t))return[t[n],t[r],e];throw new Error}function QD(t,e"
-  , "){var n=t.r-e.r,r=e.x-t.x,i=e.y-t.y;return n<0||n*n<r*r+i*i}function KD(t,e){var n=t.r-e.r+1e-9*Math.max(t.r,e.r,1),r=e.x-t.x,i=e.y-t.y;return n>0&&n*n>r*r+i*i}function tC(t,e){for(var n=0;n<e.length;++n)if(!KD(t,e[n]))return!1;return!0}function eC(t){switch(t.length){case 1:return function(t){return{x:t.x,y:t.y,r:t.r}}(t[0]);case 2:return nC(t[0],t[1]);case 3:return rC(t[0],t[1],t[2])}}function nC(t,e){var n=t.x,r=t.y,i=t.r,o=e.x,a=e.y,s=e.r,u=o-n,l=a-r,c=s-i,f=Math.sqrt(u*u+l*l);return{x:(n+o+u/f*c)/2,y:(r+a+l/f*c)/2,r:(f+i+s)/2}}function rC(t,e,n){var r=t.x,i=t.y,o=t.r,a=e.x,s=e.y,u=e.r,l=n.x,c=n.y,f=n.r,h=r-a,d=r-l,p=i-s,g=i-c,m=u-o,y=f-o,v=r*r+i*i-o*o,_=v-a*a-s*s+u*u,x=v-l*l-c*c+f*f,b=d*p-h*g,w=(p*x-g*_)/(2*b)-r,k=(g*m-p*y)/b,A=(d*_-h*x)/(2*b)-i,M=(h*y-d*m)/b,E=k*k+M*M-1,D=2*(o+w*k+A*M),C=w*w+A*A-o*o,F=-(Math.abs(E)>1e-6?(D+Math.sqrt(D*D-4*E*C))/(2*E):C/D);return{x:r+w+k*F,y:i+A+M*F,r:F}}function iC(t,e,n){var r,i,o,a,s=t.x-e.x,u=t.y-e.y,l=s*s+u*u;l?(i=e.r+n.r,i*=i,a=t.r+n.r,i>(a*=a)?(r=(l+a-i)/(2*l),o=Math.sqrt(Math.max(0,a/l-r*r)),n.x=t.x-r*s-o*u,n.y=t.y-r*u+o*s):(r=(l+i-a)/(2*l),o=Math.sqrt(Math.max(0,i/l-r*r)),n.x=e.x+r*s-o*u,n.y=e.y+r*u+o*s)):(n.x=e.x+n.r,n.y=e.y)}function oC(t,e){var n=t.r+e.r-1e-6,r=e.x-t.x,i=e.y-t.y;return n>0&&n*n>r*r+i*i}function aC(t){var e=t._,n=t.next._,r=e.r+n.r,i=(e.x*n.r+n.x*e.r)/r,o=(e.y*n.r+n.y*e.r)/r;return i*i+o*o}function sC(t){this._=t,this.next=null,this.previous=null}function uC(t,e){if(!(o=(t=function(t){return\"object\"==typeof t&&\"length\"in t?t:Array.from(t)}(t)).length))return 0;var n,r,i,o,a,s,u,l,c,f,h;if((n=t[0]).x=0,n.y=0,!(o>1))return n.r;if(r=t[1],n.x=-r.r,r.x=n.r,r.y=0,!(o>2))return n.r+r.r;iC(r,n,i=t[2]),n=new sC(n),r=new sC(r),i=new sC(i),n.next=i.previous=r,r.next=n.previous=i,i.next=r.previous=n;t:for(u=3;u<o;++u){iC(n._,r._,i=t[u]),i=new sC(i),l=r.next,c=n.previous,f=r._.r,h=n._.r;do{if(f<=h){if(oC(l._,i._)){r=l,n.next=r,r.previous=n,--u;continue t}f+=l._.r,l=l.next}else{if(oC(c._,i._)){(n=c).next=r,r.previous=n,--u;continue t}h+=c._.r,c=c.previous}}while(l!==c.next);for(i.previous=n,i.next=r,n.next=r.previous=r=i,a=aC(n);(i=i.next)!==r;)(s=aC(i))<a&&(n=i,a=s);r=n.next}for(n=[r._],i=r;(i=i.next)!==r;)n.push(i._);for(i=function(t,e){for(var n,r,i=0,o=(t=function(t,e){let n,r,i=t.length;for(;i;)r=e()*i--|0,n=t[i],t[i]=t[r],t[r]=n;return t}(Array.from(t),e)).length,a=[];i<o;)n=t[i],r&&KD(r,n)?++i:(r=eC(a=ZD(a,n)),i=0);return r}(n,e),u=0;u<o;++u)(n=t[u]).x-=i.x,n.y-=i.y;return i.r}function lC(t){return Math.sqrt(t.value)}function cC(t){return function(e){e.children||(e.r=Math.max(0,+t(e)||0))}}function fC(t,e,n){return function(r){if(i=r.children){var i,o,a,s=i.length,u=t(r)*e||0;if(u)for(o=0;o<s;++o)i[o].r+=u;if(a=uC(i,n),u)for(o=0;o<s;++o)i[o].r-=u;r.r=a+u}}}function hC(t){return function(e){var n=e.parent;e.r*=t,n&&(e.x=n.x+t*e.x,e.y=n.y+t*e.y)}}function dC(t){t.x0=Math.round(t.x0),t.y0=Math.round(t.y0),t.x1=Math.round(t.x1),t.y1=Math.round(t.y1)}function pC(t,e,n,r,i){for(var o,a=t.children,s=-1,u=a.length,l=t.value&&(r-e)/t.value;++s<u;)(o=a[s]).y0=n,o.y1=i,o.x0=e,o.x1=e+=o.value*l}var gC={depth:-1},mC={},yC={};function vC(t){return t.id}function _C(t){return t.parentId}function xC(){var t,e=vC,n=_C;function r(r){var i,o,a,s,u,l,c,f,h=Array.from(r),d=e,p=n,g=new Map;if(null!=t){const e=h.map(((e,n)=>function(t){t=`${t}`;let e=t.length;wC(t,e-1)&&!wC(t,e-2)&&(t=t.slice(0,-1));return\"/\"===t[0]?t:`/${t}`}(t(e,n,r)))),n=e.map(bC),i=new Set(e).add(\"\");for(const t of n)i.has(t)||(i.add(t),e.push(t),n.push(bC(t)),h.push(yC));d=(t,n)=>e[n],p=(t,e)=>n[e]}for(a=0,i=h.length;a<i;++a)o=h[a],l=h[a]=new ID(o),null!=(c=d(o,a,r))&&(c+=\"\")&&(f=l.id=c,g.set(f,g.has(f)?mC:l)),null!=(c=p(o,a,r))&&(c+=\"\")&&(l.parent=c);for(a=0;a<i;++a)if(c=(l=h[a]).parent){if(!(u=g.get(c)))throw new Error(\"missing: \"+c);if(u===mC)throw new Error(\"ambiguous: \"+c);u.children?u.children.push(l):u.children=[l],l.parent=u}else{if(s)throw new Error(\"multiple roots\");s=l}if(!s)throw new Error(\"no root\");if(null!=t){for(;s.data===yC&&1===s.children.length;)s=s.children[0],--i;for(let t=h.length-1;t>=0&&(l="
-  , "h[t]).data===yC;--t)l.data=null}if(s.parent=gC,s.eachBefore((function(t){t.depth=t.parent.depth+1,--i})).eachBefore(jD),s.parent=null,i>0)throw new Error(\"cycle\");return s}return r.id=function(t){return arguments.length?(e=WD(t),r):e},r.parentId=function(t){return arguments.length?(n=WD(t),r):n},r.path=function(e){return arguments.length?(t=WD(e),r):t},r}function bC(t){let e=t.length;if(e<2)return\"\";for(;--e>1&&!wC(t,e););return t.slice(0,e)}function wC(t,e){if(\"/\"===t[e]){let n=0;for(;e>0&&\"\\\\\"===t[--e];)++n;if(0==(1&n))return!0}return!1}function kC(t,e){return t.parent===e.parent?1:2}function AC(t){var e=t.children;return e?e[0]:t.t}function MC(t){var e=t.children;return e?e[e.length-1]:t.t}function EC(t,e,n){var r=n/(e.i-t.i);e.c-=r,e.s+=n,t.c+=r,e.z+=n,e.m+=n}function DC(t,e,n){return t.a.parent===e.parent?t.a:n}function CC(t,e){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=e}function FC(t,e,n,r,i){for(var o,a=t.children,s=-1,u=a.length,l=t.value&&(i-n)/t.value;++s<u;)(o=a[s]).x0=e,o.x1=r,o.y0=n,o.y1=n+=o.value*l}CC.prototype=Object.create(ID.prototype);var SC=(1+Math.sqrt(5))/2;function $C(t,e,n,r,i,o){for(var a,s,u,l,c,f,h,d,p,g,m,y=[],v=e.children,_=0,x=0,b=v.length,w=e.value;_<b;){u=i-n,l=o-r;do{c=v[x++].value}while(!c&&x<b);for(f=h=c,m=c*c*(g=Math.max(l/u,u/l)/(w*t)),p=Math.max(h/m,m/f);x<b;++x){if(c+=s=v[x].value,s<f&&(f=s),s>h&&(h=s),m=c*c*g,(d=Math.max(h/m,m/f))>p){c-=s;break}p=d}y.push(a={value:c,dice:u<l,children:v.slice(_,x)}),a.dice?pC(a,n,r,i,w?r+=l*c/w:o):FC(a,n,r,w?n+=u*c/w:i,o),w-=c,_=x}return y}var TC=function t(e){function n(t,n,r,i,o){$C(e,t,n,r,i,o)}return n.ratio=function(e){return t((e=+e)>1?e:1)},n}(SC);var BC=function t(e){function n(t,n,r,i,o){if((a=t._squarify)&&a.ratio===e)for(var a,s,u,l,c,f=-1,h=a.length,d=t.value;++f<h;){for(u=(s=a[f]).children,l=s.value=0,c=u.length;l<c;++l)s.value+=u[l].value;s.dice?pC(s,n,r,i,d?r+=(o-r)*s.value/d:o):FC(s,n,r,d?n+=(i-n)*s.value/d:i,o),d-=s.value}else t._squarify=a=$C(e,t,n,r,i,o),a.ratio=e}return n.ratio=function(e){return t((e=+e)>1?e:1)},n}(SC);function NC(t,e,n){const r={};return t.each((t=>{const i=t.data;n(i)&&(r[e(i)]=t)})),t.lookup=r,t}function zC(t){Ja.call(this,null,t)}zC.Definition={type:\"Nest\",metadata:{treesource:!0,changes:!0},params:[{name:\"keys\",type:\"field\",array:!0},{name:\"generate\",type:\"boolean\"}]};const OC=t=>t.values;function RC(){const t=[],e={entries:t=>r(n(t,0),0),key:n=>(t.push(n),e)};function n(e,r){if(r>=t.length)return e;const i=e.length,o=t[r++],a={},s={};let u,l,c,f=-1;for(;++f<i;)u=o(l=e[f])+\"\",(c=a[u])?c.push(l):a[u]=[l];for(u in a)s[u]=n(a[u],r);return s}function r(e,n){if(++n>t.length)return e;const i=[];for(const t in e)i.push({key:t,values:r(e[t],n)});return i}return e}function LC(t){Ja.call(this,null,t)}dt(zC,Ja,{transform(t,e){e.source||s(\"Nest transform requires an upstream data source.\");var n=t.generate,r=t.modified(),i=e.clone(),o=this.value;return(!o||r||e.changed())&&(o&&o.each((t=>{t.children&&ma(t.data)&&i.rem.push(t.data)})),this.value=o=LD({values:X(t.keys).reduce(((t,e)=>(t.key(e),t)),RC()).entries(i.source)},OC),n&&o.each((t=>{t.children&&(t=_a(t.data),i.add.push(t),i.source.push(t))})),NC(o,ya,ya)),i.source.root=o,i}});const UC=(t,e)=>t.parent===e.parent?1:2;dt(LC,Ja,{transform(t,e){e.source&&e.source.root||s(this.constructor.name+\" transform requires a backing tree data source.\");const n=this.layout(t.method),r=this.fields,i=e.source.root,o=t.as||r;t.field?i.sum(t.field):i.count(),t.sort&&i.sort(ka(t.sort,(t=>t.data))),function(t,e,n){for(let r,i=0,o=e.length;i<o;++i)r=e[i],r in n&&t[r](n[r])}(n,this.params,t),n.separation&&n.separation(!1!==t.separation?UC:d);try{this.value=n(i)}catch(t){s(t)}return i.each((t=>function(t,e,n){const r=t.data,i=e.length-1;for(let o=0;o<i;++o)r[n[o]]=t[e[o]];r[n[i]]=t.children?t.children.length:0}(t,r,o))),e.reflow(t.modified()).modifies(o).modifies(\"leaf\")}});const qC=[\"x\",\"y\",\"r\",\"depth\",\"children\"];function PC(t){LC.call(this,t)}PC.Definition={type:\"Pack\",metadata:{tree:!0,modifies:!0},para"
-  , "ms:[{name:\"field\",type:\"field\"},{name:\"sort\",type:\"compare\"},{name:\"padding\",type:\"number\",default:0},{name:\"radius\",type:\"field\",default:null},{name:\"size\",type:\"number\",array:!0,length:2},{name:\"as\",type:\"string\",array:!0,length:qC.length,default:qC}]},dt(PC,LC,{layout:function(){var t=null,e=1,n=1,r=YD;function i(i){const o=function(){let t=1;return()=>(t=(VD*t+XD)%JD)/JD}();return i.x=e/2,i.y=n/2,t?i.eachBefore(cC(t)).eachAfter(fC(r,.5,o)).eachBefore(hC(1)):i.eachBefore(cC(lC)).eachAfter(fC(YD,1,o)).eachAfter(fC(r,i.r/Math.min(e,n),o)).eachBefore(hC(Math.min(e,n)/(2*i.r))),i}return i.radius=function(e){return arguments.length?(t=WD(e),i):t},i.size=function(t){return arguments.length?(e=+t[0],n=+t[1],i):[e,n]},i.padding=function(t){return arguments.length?(r=\"function\"==typeof t?t:GD(+t),i):r},i},params:[\"radius\",\"size\",\"padding\"],fields:qC});const jC=[\"x0\",\"y0\",\"x1\",\"y1\",\"depth\",\"children\"];function IC(t){LC.call(this,t)}function WC(t){Ja.call(this,null,t)}IC.Definition={type:\"Partition\",metadata:{tree:!0,modifies:!0},params:[{name:\"field\",type:\"field\"},{name:\"sort\",type:\"compare\"},{name:\"padding\",type:\"number\",default:0},{name:\"round\",type:\"boolean\",default:!1},{name:\"size\",type:\"number\",array:!0,length:2},{name:\"as\",type:\"string\",array:!0,length:jC.length,default:jC}]},dt(IC,LC,{layout:function(){var t=1,e=1,n=0,r=!1;function i(i){var o=i.height+1;return i.x0=i.y0=n,i.x1=t,i.y1=e/o,i.eachBefore(function(t,e){return function(r){r.children&&pC(r,r.x0,t*(r.depth+1)/e,r.x1,t*(r.depth+2)/e);var i=r.x0,o=r.y0,a=r.x1-n,s=r.y1-n;a<i&&(i=a=(i+a)/2),s<o&&(o=s=(o+s)/2),r.x0=i,r.y0=o,r.x1=a,r.y1=s}}(e,o)),r&&i.eachBefore(dC),i}return i.round=function(t){return arguments.length?(r=!!t,i):r},i.size=function(n){return arguments.length?(t=+n[0],e=+n[1],i):[t,e]},i.padding=function(t){return arguments.length?(n=+t,i):n},i},params:[\"size\",\"round\",\"padding\"],fields:jC}),WC.Definition={type:\"Stratify\",metadata:{treesource:!0},params:[{name:\"key\",type:\"field\",required:!0},{name:\"parentKey\",type:\"field\",required:!0}]},dt(WC,Ja,{transform(t,e){e.source||s(\"Stratify transform requires an upstream data source.\");let n=this.value;const r=t.modified(),i=e.fork(e.ALL).materialize(e.SOURCE),o=!n||r||e.changed(e.ADD_REM)||e.modified(t.key.fields)||e.modified(t.parentKey.fields);return i.source=i.source.slice(),o&&(n=i.source.length?NC(xC().id(t.key).parentId(t.parentKey)(i.source),t.key,p):NC(xC()([{}]),t.key,t.key)),i.source.root=this.value=n,i}});const HC={tidy:function(){var t=kC,e=1,n=1,r=null;function i(i){var u=function(t){for(var e,n,r,i,o,a=new CC(t,0),s=[a];e=s.pop();)if(r=e._.children)for(e.children=new Array(o=r.length),i=o-1;i>=0;--i)s.push(n=e.children[i]=new CC(r[i],i)),n.parent=e;return(a.parent=new CC(null,0)).children=[a],a}(i);if(u.eachAfter(o),u.parent.m=-u.z,u.eachBefore(a),r)i.eachBefore(s);else{var l=i,c=i,f=i;i.eachBefore((function(t){t.x<l.x&&(l=t),t.x>c.x&&(c=t),t.depth>f.depth&&(f=t)}));var h=l===c?1:t(l,c)/2,d=h-l.x,p=e/(c.x+h+d),g=n/(f.depth||1);i.eachBefore((function(t){t.x=(t.x+d)*p,t.y=t.depth*g}))}return i}function o(e){var n=e.children,r=e.parent.children,i=e.i?r[e.i-1]:null;if(n){!function(t){for(var e,n=0,r=0,i=t.children,o=i.length;--o>=0;)(e=i[o]).z+=n,e.m+=n,n+=e.s+(r+=e.c)}(e);var o=(n[0].z+n[n.length-1].z)/2;i?(e.z=i.z+t(e._,i._),e.m=e.z-o):e.z=o}else i&&(e.z=i.z+t(e._,i._));e.parent.A=function(e,n,r){if(n){for(var i,o=e,a=e,s=n,u=o.parent.children[0],l=o.m,c=a.m,f=s.m,h=u.m;s=MC(s),o=AC(o),s&&o;)u=AC(u),(a=MC(a)).a=e,(i=s.z+f-o.z-l+t(s._,o._))>0&&(EC(DC(s,e,r),e,i),l+=i,c+=i),f+=s.m,l+=o.m,h+=u.m,c+=a.m;s&&!MC(a)&&(a.t=s,a.m+=f-c),o&&!AC(u)&&(u.t=o,u.m+=l-h,r=e)}return r}(e,i,e.parent.A||r[0])}function a(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function s(t){t.x*=e,t.y=t.depth*n}return i.separation=function(e){return arguments.length?(t=e,i):t},i.size=function(t){return arguments.length?(r=!1,e=+t[0],n=+t[1],i):r?null:[e,n]},i.nodeSize=function(t){return arguments.length?(r=!0,e=+t[0],n=+t[1],i):r?[e,n]:null},i},cluster:function(){var t=ND,e=1,n=1,r=!1;function i(i){var o,a=0;i.eachAfter((function(e){var "
-  , "n=e.children;n?(e.x=function(t){return t.reduce(zD,0)/t.length}(n),e.y=function(t){return 1+t.reduce(OD,0)}(n)):(e.x=o?a+=t(e,o):0,e.y=0,o=e)}));var s=function(t){for(var e;e=t.children;)t=e[0];return t}(i),u=function(t){for(var e;e=t.children;)t=e[e.length-1];return t}(i),l=s.x-t(s,u)/2,c=u.x+t(u,s)/2;return i.eachAfter(r?function(t){t.x=(t.x-i.x)*e,t.y=(i.y-t.y)*n}:function(t){t.x=(t.x-l)/(c-l)*e,t.y=(1-(i.y?t.y/i.y:1))*n})}return i.separation=function(e){return arguments.length?(t=e,i):t},i.size=function(t){return arguments.length?(r=!1,e=+t[0],n=+t[1],i):r?null:[e,n]},i.nodeSize=function(t){return arguments.length?(r=!0,e=+t[0],n=+t[1],i):r?[e,n]:null},i}},YC=[\"x\",\"y\",\"depth\",\"children\"];function GC(t){LC.call(this,t)}function VC(t){Ja.call(this,[],t)}GC.Definition={type:\"Tree\",metadata:{tree:!0,modifies:!0},params:[{name:\"field\",type:\"field\"},{name:\"sort\",type:\"compare\"},{name:\"method\",type:\"enum\",default:\"tidy\",values:[\"tidy\",\"cluster\"]},{name:\"size\",type:\"number\",array:!0,length:2},{name:\"nodeSize\",type:\"number\",array:!0,length:2},{name:\"separation\",type:\"boolean\",default:!0},{name:\"as\",type:\"string\",array:!0,length:YC.length,default:YC}]},dt(GC,LC,{layout(t){const e=t||\"tidy\";if(lt(HC,e))return HC[e]();s(\"Unrecognized Tree layout method: \"+e)},params:[\"size\",\"nodeSize\"],fields:YC}),VC.Definition={type:\"TreeLinks\",metadata:{tree:!0,generates:!0,changes:!0},params:[]},dt(VC,Ja,{transform(t,e){const n=this.value,r=e.source&&e.source.root,i=e.fork(e.NO_SOURCE),o={};return r||s(\"TreeLinks transform requires a tree data source.\"),e.changed(e.ADD_REM)?(i.rem=n,e.visit(e.SOURCE,(t=>o[ya(t)]=1)),r.each((t=>{const e=t.data,n=t.parent&&t.parent.data;n&&o[ya(e)]&&o[ya(n)]&&i.add.push(_a({source:n,target:e}))})),this.value=i.add):e.changed(e.MOD)&&(e.visit(e.MOD,(t=>o[ya(t)]=1)),n.forEach((t=>{(o[ya(t.source)]||o[ya(t.target)])&&i.mod.push(t)}))),i}});const XC={binary:function(t,e,n,r,i){var o,a,s=t.children,u=s.length,l=new Array(u+1);for(l[0]=a=o=0;o<u;++o)l[o+1]=a+=s[o].value;!function t(e,n,r,i,o,a,u){if(e>=n-1){var c=s[e];return c.x0=i,c.y0=o,c.x1=a,void(c.y1=u)}var f=l[e],h=r/2+f,d=e+1,p=n-1;for(;d<p;){var g=d+p>>>1;l[g]<h?d=g+1:p=g}h-l[d-1]<l[d]-h&&e+1<d&&--d;var m=l[d]-f,y=r-m;if(a-i>u-o){var v=r?(i*y+a*m)/r:a;t(e,d,m,i,o,v,u),t(d,n,y,v,o,a,u)}else{var _=r?(o*y+u*m)/r:u;t(e,d,m,i,o,a,_),t(d,n,y,i,_,a,u)}}(0,u,t.value,e,n,r,i)},dice:pC,slice:FC,slicedice:function(t,e,n,r,i){(1&t.depth?FC:pC)(t,e,n,r,i)},squarify:TC,resquarify:BC},JC=[\"x0\",\"y0\",\"x1\",\"y1\",\"depth\",\"children\"];function ZC(t){LC.call(this,t)}ZC.Definition={type:\"Treemap\",metadata:{tree:!0,modifies:!0},params:[{name:\"field\",type:\"field\"},{name:\"sort\",type:\"compare\"},{name:\"method\",type:\"enum\",default:\"squarify\",values:[\"squarify\",\"resquarify\",\"binary\",\"dice\",\"slice\",\"slicedice\"]},{name:\"padding\",type:\"number\",default:0},{name:\"paddingInner\",type:\"number\",default:0},{name:\"paddingOuter\",type:\"number\",default:0},{name:\"paddingTop\",type:\"number\",default:0},{name:\"paddingRight\",type:\"number\",default:0},{name:\"paddingBottom\",type:\"number\",default:0},{name:\"paddingLeft\",type:\"number\",default:0},{name:\"ratio\",type:\"number\",default:1.618033988749895},{name:\"round\",type:\"boolean\",default:!1},{name:\"size\",type:\"number\",array:!0,length:2},{name:\"as\",type:\"string\",array:!0,length:JC.length,default:JC}]},dt(ZC,LC,{layout(){const t=function(){var t=TC,e=!1,n=1,r=1,i=[0],o=YD,a=YD,s=YD,u=YD,l=YD;function c(t){return t.x0=t.y0=0,t.x1=n,t.y1=r,t.eachBefore(f),i=[0],e&&t.eachBefore(dC),t}function f(e){var n=i[e.depth],r=e.x0+n,c=e.y0+n,f=e.x1-n,h=e.y1-n;f<r&&(r=f=(r+f)/2),h<c&&(c=h=(c+h)/2),e.x0=r,e.y0=c,e.x1=f,e.y1=h,e.children&&(n=i[e.depth+1]=o(e)/2,r+=l(e)-n,c+=a(e)-n,(f-=s(e)-n)<r&&(r=f=(r+f)/2),(h-=u(e)-n)<c&&(c=h=(c+h)/2),t(e,r,c,f,h))}return c.round=function(t){return arguments.length?(e=!!t,c):e},c.size=function(t){return arguments.length?(n=+t[0],r=+t[1],c):[n,r]},c.tile=function(e){return arguments.length?(t=HD(e),c):t},c.padding=function(t){return arguments.length?c.paddingInner(t).paddingOuter(t):c.paddingInner()},c.paddingInner=function(t){return arguments.lengt"
-  , "h?(o=\"function\"==typeof t?t:GD(+t),c):o},c.paddingOuter=function(t){return arguments.length?c.paddingTop(t).paddingRight(t).paddingBottom(t).paddingLeft(t):c.paddingTop()},c.paddingTop=function(t){return arguments.length?(a=\"function\"==typeof t?t:GD(+t),c):a},c.paddingRight=function(t){return arguments.length?(s=\"function\"==typeof t?t:GD(+t),c):s},c.paddingBottom=function(t){return arguments.length?(u=\"function\"==typeof t?t:GD(+t),c):u},c.paddingLeft=function(t){return arguments.length?(l=\"function\"==typeof t?t:GD(+t),c):l},c}();return t.ratio=e=>{const n=t.tile();n.ratio&&t.tile(n.ratio(e))},t.method=e=>{lt(XC,e)?t.tile(XC[e]):s(\"Unrecognized Treemap layout method: \"+e)},t},params:[\"method\",\"ratio\",\"size\",\"round\",\"padding\",\"paddingInner\",\"paddingOuter\",\"paddingTop\",\"paddingRight\",\"paddingBottom\",\"paddingLeft\"],fields:JC});var QC=Object.freeze({__proto__:null,nest:zC,pack:PC,partition:IC,stratify:WC,tree:GC,treelinks:VC,treemap:ZC});const KC=4278190080;function tF(t,e,n){return new Uint32Array(t.getImageData(0,0,e,n).data.buffer)}function eF(t,e,n){if(!e.length)return;const r=e[0].mark.marktype;\"group\"===r?e.forEach((e=>{e.items.forEach((e=>eF(t,e.items,n)))})):Yy[r].draw(t,{items:n?e.map(nF):e})}function nF(t){const e=ba(t,{});return e.stroke&&0!==e.strokeOpacity||e.fill&&0!==e.fillOpacity?{...e,strokeOpacity:1,stroke:\"#000\",fillOpacity:0}:e}const rF=5,iF=31,oF=32,aF=new Uint32Array(oF+1),sF=new Uint32Array(oF+1);sF[0]=0,aF[0]=~sF[0];for(let t=1;t<=oF;++t)sF[t]=sF[t-1]<<1|1,aF[t]=~sF[t];function uF(t,e,n){const r=Math.max(1,Math.sqrt(t*e/1e6)),i=~~((t+2*n+r)/r),o=~~((e+2*n+r)/r),a=t=>~~((t+n)/r);return a.invert=t=>t*r-n,a.bitmap=()=>function(t,e){const n=new Uint32Array(~~((t*e+oF)/oF));function r(t,e){n[t]|=e}function i(t,e){n[t]&=e}return{array:n,get:(e,r)=>{const i=r*t+e;return n[i>>>rF]&1<<(i&iF)},set:(e,n)=>{const i=n*t+e;r(i>>>rF,1<<(i&iF))},clear:(e,n)=>{const r=n*t+e;i(r>>>rF,~(1<<(r&iF)))},getRange:(e,r,i,o)=>{let a,s,u,l,c=o;for(;c>=r;--c)if(a=c*t+e,s=c*t+i,u=a>>>rF,l=s>>>rF,u===l){if(n[u]&aF[a&iF]&sF[1+(s&iF)])return!0}else{if(n[u]&aF[a&iF])return!0;if(n[l]&sF[1+(s&iF)])return!0;for(let t=u+1;t<l;++t)if(n[t])return!0}return!1},setRange:(e,n,i,o)=>{let a,s,u,l,c;for(;n<=o;++n)if(a=n*t+e,s=n*t+i,u=a>>>rF,l=s>>>rF,u===l)r(u,aF[a&iF]&sF[1+(s&iF)]);else for(r(u,aF[a&iF]),r(l,sF[1+(s&iF)]),c=u+1;c<l;++c)r(c,4294967295)},clearRange:(e,n,r,o)=>{let a,s,u,l,c;for(;n<=o;++n)if(a=n*t+e,s=n*t+r,u=a>>>rF,l=s>>>rF,u===l)i(u,sF[a&iF]|aF[1+(s&iF)]);else for(i(u,sF[a&iF]),i(l,aF[1+(s&iF)]),c=u+1;c<l;++c)i(c,0)},outOfBounds:(n,r,i,o)=>n<0||r<0||o>=e||i>=t}}(i,o),a.ratio=r,a.padding=n,a.width=t,a.height=e,a}function lF(t,e,n,r,i,o){let a=n/2;return t-a<0||t+a>i||e-(a=r/2)<0||e+a>o}function cF(t,e,n,r,i,o,a,s){const u=i*o/(2*r),l=t(e-u),c=t(e+u),f=t(n-(o/=2)),h=t(n+o);return a.outOfBounds(l,f,c,h)||a.getRange(l,f,c,h)||s&&s.getRange(l,f,c,h)}const fF=[-1,-1,1,1],hF=[-1,1,-1,1];const dF=[\"right\",\"center\",\"left\"],pF=[\"bottom\",\"middle\",\"top\"];function gF(t,e,n,r,i,o,a,s,u,l,c,f){return!(i.outOfBounds(t,n,e,r)||(f&&o||i).getRange(t,n,e,r))}const mF={\"top-left\":0,top:1,\"top-right\":2,left:4,middle:5,right:6,\"bottom-left\":8,bottom:9,\"bottom-right\":10},yF={naive:function(t,e,n,r){const i=t.width,o=t.height;return function(t){const e=t.datum.datum.items[r].items,n=e.length,a=t.datum.fontSize,s=Ey.width(t.datum,t.datum.text);let u,l,c,f,h,d,p,g=0;for(let r=0;r<n;++r)u=e[r].x,c=e[r].y,l=void 0===e[r].x2?u:e[r].x2,f=void 0===e[r].y2?c:e[r].y2,h=(u+l)/2,d=(c+f)/2,p=Math.abs(l-u+f-c),p>=g&&(g=p,t.x=h,t.y=d);return h=s/2,d=a/2,u=t.x-h,l=t.x+h,c=t.y-d,f=t.y+d,t.align=\"center\",u<0&&l<=i?t.align=\"left\":0<=u&&i<l&&(t.align=\"right\"),t.baseline=\"middle\",c<0&&f<=o?t.baseline=\"top\":0<=c&&o<f&&(t.baseline=\"bottom\"),!0}},\"reduced-search\":function(t,e,n,r){const i=t.width,o=t.height,a=e[0],s=e[1];function u(e,n,r,u,l){const c=t.invert(e),f=t.invert(n);let h,d=r,p=o;if(!lF(c,f,u,l,i,o)&&!cF(t,c,f,l,u,d,a,s)&&!cF(t,c,f,l,u,l,a,null)){for(;p-d>=1;)h=(d+p)/2,cF(t,c,f,l,u,h,a,s)?p=h:d=h;if(d>r)return[c,f,d,!0]}}return function(e){const s=e.datum.datum.items[r"
-  , "].items,l=s.length,c=e.datum.fontSize,f=Ey.width(e.datum,e.datum.text);let h,d,p,g,m,y,v,_,x,b,w,k,A,M,E,D,C,F=n?c:0,S=!1,$=!1,T=0;for(let r=0;r<l;++r){for(h=s[r].x,p=s[r].y,d=void 0===s[r].x2?h:s[r].x2,g=void 0===s[r].y2?p:s[r].y2,h>d&&(C=h,h=d,d=C),p>g&&(C=p,p=g,g=C),x=t(h),w=t(d),b=~~((x+w)/2),k=t(p),M=t(g),A=~~((k+M)/2),v=b;v>=x;--v)for(_=A;_>=k;--_)D=u(v,_,F,f,c),D&&([e.x,e.y,F,S]=D);for(v=b;v<=w;++v)for(_=A;_<=M;++_)D=u(v,_,F,f,c),D&&([e.x,e.y,F,S]=D);S||n||(E=Math.abs(d-h+g-p),m=(h+d)/2,y=(p+g)/2,E>=T&&!lF(m,y,f,c,i,o)&&!cF(t,m,y,c,f,c,a,null)&&(T=E,e.x=m,e.y=y,$=!0))}return!(!S&&!$)&&(m=f/2,y=c/2,a.setRange(t(e.x-m),t(e.y-y),t(e.x+m),t(e.y+y)),e.align=\"center\",e.baseline=\"middle\",!0)}},floodfill:function(t,e,n,r){const i=t.width,o=t.height,a=e[0],s=e[1],u=t.bitmap();return function(e){const l=e.datum.datum.items[r].items,c=l.length,f=e.datum.fontSize,h=Ey.width(e.datum,e.datum.text),d=[];let p,g,m,y,v,_,x,b,w,k,A,M,E=n?f:0,D=!1,C=!1,F=0;for(let r=0;r<c;++r){for(p=l[r].x,m=l[r].y,g=void 0===l[r].x2?p:l[r].x2,y=void 0===l[r].y2?m:l[r].y2,d.push([t((p+g)/2),t((m+y)/2)]);d.length;)if([x,b]=d.pop(),!(a.get(x,b)||s.get(x,b)||u.get(x,b))){u.set(x,b);for(let t=0;t<4;++t)v=x+fF[t],_=b+hF[t],u.outOfBounds(v,_,v,_)||d.push([v,_]);if(v=t.invert(x),_=t.invert(b),w=E,k=o,!lF(v,_,h,f,i,o)&&!cF(t,v,_,f,h,w,a,s)&&!cF(t,v,_,f,h,f,a,null)){for(;k-w>=1;)A=(w+k)/2,cF(t,v,_,f,h,A,a,s)?k=A:w=A;w>E&&(e.x=v,e.y=_,E=w,D=!0)}}D||n||(M=Math.abs(g-p+y-m),v=(p+g)/2,_=(m+y)/2,M>=F&&!lF(v,_,h,f,i,o)&&!cF(t,v,_,f,h,f,a,null)&&(F=M,e.x=v,e.y=_,C=!0))}return!(!D&&!C)&&(v=h/2,_=f/2,a.setRange(t(e.x-v),t(e.y-_),t(e.x+v),t(e.y+_)),e.align=\"center\",e.baseline=\"middle\",!0)}}};function vF(t,e,n,r,i,o,a,s,u,l,c){if(!t.length)return t;const f=Math.max(r.length,i.length),h=function(t,e){const n=new Float64Array(e),r=t.length;for(let e=0;e<r;++e)n[e]=t[e]||0;for(let t=r;t<e;++t)n[t]=n[r-1];return n}(r,f),d=function(t,e){const n=new Int8Array(e),r=t.length;for(let e=0;e<r;++e)n[e]|=mF[t[e]];for(let t=r;t<e;++t)n[t]=n[r-1];return n}(i,f),p=(x=t[0].datum)&&x.mark&&x.mark.marktype,g=\"group\"===p&&t[0].datum.items[u].marktype,m=\"area\"===g,y=function(t,e,n,r){const i=t=>[t.x,t.x,t.x,t.y,t.y,t.y];return t?\"line\"===t||\"area\"===t?t=>i(t.datum):\"line\"===e?t=>{const e=t.datum.items[r].items;return i(e.length?e[\"start\"===n?0:e.length-1]:{x:NaN,y:NaN})}:t=>{const e=t.datum.bounds;return[e.x1,(e.x1+e.x2)/2,e.x2,e.y1,(e.y1+e.y2)/2,e.y2]}:i}(p,g,s,u),v=null===l||l===1/0,_=m&&\"naive\"===c;var x;let b=-1,w=-1;const k=t.map((t=>{const e=v?Ey.width(t,t.text):void 0;return b=Math.max(b,e),w=Math.max(w,t.fontSize),{datum:t,opacity:0,x:void 0,y:void 0,align:void 0,baseline:void 0,boundary:y(t),textWidth:e}}));l=null===l||l===1/0?Math.max(b,w)+Math.max(...r):l;const A=uF(e[0],e[1],l);let M;if(!_){n&&k.sort(((t,e)=>n(t.datum,e.datum)));let e=!1;for(let t=0;t<d.length&&!e;++t)e=5===d[t]||h[t]<0;const r=(p&&a||m)&&t.map((t=>t.datum));M=o.length||r?function(t,e,n,r,i){const o=t.width,a=t.height,s=r||i,u=$c(o,a).getContext(\"2d\"),l=$c(o,a).getContext(\"2d\"),c=s&&$c(o,a).getContext(\"2d\");n.forEach((t=>eF(u,t,!1))),eF(l,e,!1),s&&eF(c,e,!0);const f=tF(u,o,a),h=tF(l,o,a),d=s&&tF(c,o,a),p=t.bitmap(),g=s&&t.bitmap();let m,y,v,_,x,b,w,k;for(y=0;y<a;++y)for(m=0;m<o;++m)x=y*o+m,b=f[x]&KC,k=h[x]&KC,w=s&&d[x]&KC,(b||w||k)&&(v=t(m),_=t(y),i||!b&&!k||p.set(v,_),s&&(b||w)&&g.set(v,_));return[p,g]}(A,r||[],o,e,m):function(t,e){const n=t.bitmap();return(e||[]).forEach((e=>n.set(t(e.boundary[0]),t(e.boundary[3])))),[n,void 0]}(A,a&&k)}const E=m?yF[c](A,M,a,u):function(t,e,n,r){const i=t.width,o=t.height,a=e[0],s=e[1],u=r.length;return function(e){const l=e.boundary,c=e.datum.fontSize;if(l[2]<0||l[5]<0||l[0]>i||l[3]>o)return!1;let f,h,d,p,g,m,y,v,_,x,b,w,k,A,M,E=e.textWidth??0;for(let i=0;i<u;++i){if(f=(3&n[i])-1,h=(n[i]>>>2&3)-1,d=0===f&&0===h||r[i]<0,p=f&&h?Math.SQRT1_2:1,g=r[i]<0?-1:1,m=l[1+f]+r[i]*f*p,b=l[4+h]+g*c*h/2+r[i]*h*p,v=b-c/2,_=b+c/2,w=t(m),A=t(v),M=t(_),!E){if(!gF(w,w,A,M,a,s,0,0,0,0,0,d))continue;E=Ey.width(e.datum,e.datum.text)}if(x=m+g*E*f/2,m=x-E/2,y=x+E/2,w=t(m),k=t(y),gF(w,k,A,M,a,s,0,0,"
-  , "0,0,0,d))return e.x=f?f*g<0?y:m:x,e.y=h?h*g<0?_:v:b,e.align=dF[f*g+1],e.baseline=pF[h*g+1],a.setRange(w,A,k,M),!0}return!1}}(A,M,d,h);return k.forEach((t=>t.opacity=+E(t))),k}const _F=[\"x\",\"y\",\"opacity\",\"align\",\"baseline\"],xF=[\"top-left\",\"left\",\"bottom-left\",\"top\",\"bottom\",\"top-right\",\"right\",\"bottom-right\"];function bF(t){Ja.call(this,null,t)}bF.Definition={type:\"Label\",metadata:{modifies:!0},params:[{name:\"size\",type:\"number\",array:!0,length:2,required:!0},{name:\"sort\",type:\"compare\"},{name:\"anchor\",type:\"string\",array:!0,default:xF},{name:\"offset\",type:\"number\",array:!0,default:[1]},{name:\"padding\",type:\"number\",default:0,null:!0},{name:\"lineAnchor\",type:\"string\",values:[\"start\",\"end\"],default:\"end\"},{name:\"markIndex\",type:\"number\",default:0},{name:\"avoidBaseMark\",type:\"boolean\",default:!0},{name:\"avoidMarks\",type:\"data\",array:!0},{name:\"method\",type:\"string\",default:\"naive\"},{name:\"as\",type:\"string\",array:!0,length:_F.length,default:_F}]},dt(bF,Ja,{transform(t,e){const n=t.modified();if(!(n||e.changed(e.ADD_REM)||function(n){const r=t[n];return Z(r)&&e.modified(r.fields)}(\"sort\")))return;t.size&&2===t.size.length||s(\"Size parameter should be specified as a [width, height] array.\");const r=t.as||_F;return vF(e.materialize(e.SOURCE).source||[],t.size,t.sort,X(null==t.offset?1:t.offset),X(t.anchor||xF),t.avoidMarks||[],!1!==t.avoidBaseMark,t.lineAnchor||\"end\",t.markIndex||0,void 0===t.padding?0:t.padding,t.method||\"naive\").forEach((t=>{const e=t.datum;e[r[0]]=t.x,e[r[1]]=t.y,e[r[2]]=t.opacity,e[r[3]]=t.align,e[r[4]]=t.baseline})),e.reflow(n).modifies(r)}});var wF=Object.freeze({__proto__:null,label:bF});function kF(t,e){var n,r,i,o,a,s,u=[],l=function(t){return t(o)};if(null==e)u.push(t);else for(n={},r=0,i=t.length;r<i;++r)o=t[r],(s=n[a=e.map(l)])||(n[a]=s=[],s.dims=a,u.push(s)),s.push(o);return u}function AF(t){Ja.call(this,null,t)}AF.Definition={type:\"Loess\",metadata:{generates:!0},params:[{name:\"x\",type:\"field\",required:!0},{name:\"y\",type:\"field\",required:!0},{name:\"groupby\",type:\"field\",array:!0},{name:\"bandwidth\",type:\"number\",default:.3},{name:\"as\",type:\"string\",array:!0}]},dt(AF,Ja,{transform(t,e){const r=e.fork(e.NO_SOURCE|e.NO_FIELDS);if(!this.value||e.changed()||t.modified()){const i=kF(e.materialize(e.SOURCE).source,t.groupby),o=(t.groupby||[]).map(n),a=o.length,s=t.as||[n(t.x),n(t.y)],u=[];i.forEach((e=>{Us(e,t.x,t.y,t.bandwidth||.3).forEach((t=>{const n={};for(let t=0;t<a;++t)n[o[t]]=e.dims[t];n[s[0]]=t[0],n[s[1]]=t[1],u.push(_a(n))}))})),this.value&&(r.rem=this.value),this.value=r.add=r.source=u}return r}});const MF={constant:Ds,linear:Ts,log:Bs,exp:Ns,pow:zs,quad:Os,poly:Rs};function EF(t){Ja.call(this,null,t)}EF.Definition={type:\"Regression\",metadata:{generates:!0},params:[{name:\"x\",type:\"field\",required:!0},{name:\"y\",type:\"field\",required:!0},{name:\"groupby\",type:\"field\",array:!0},{name:\"method\",type:\"string\",default:\"linear\",values:Object.keys(MF)},{name:\"order\",type:\"number\",default:3},{name:\"extent\",type:\"number\",array:!0,length:2},{name:\"params\",type:\"boolean\",default:!1},{name:\"as\",type:\"string\",array:!0}]},dt(EF,Ja,{transform(t,e){const r=e.fork(e.NO_SOURCE|e.NO_FIELDS);if(!this.value||e.changed()||t.modified()){const i=kF(e.materialize(e.SOURCE).source,t.groupby),o=(t.groupby||[]).map(n),a=t.method||\"linear\",u=null==t.order?3:t.order,l=((t,e)=>\"poly\"===t?e:\"quad\"===t?2:1)(a,u),c=t.as||[n(t.x),n(t.y)],f=MF[a],h=[];let d=t.extent;lt(MF,a)||s(\"Invalid regression method: \"+a),null!=d&&\"log\"===a&&d[0]<=0&&(e.dataflow.warn(\"Ignoring extent with values <= 0 for log regression.\"),d=null),i.forEach((n=>{if(n.length<=l)return void e.dataflow.warn(\"Skipping regression with more parameters than data points.\");const r=f(n,t.x,t.y,u);if(t.params)return void h.push(_a({keys:n.dims,coef:r.coef,rSquared:r.rSquared}));const i=d||st(n,t.x),s=t=>{const e={};for(let t=0;t<o.length;++t)e[o[t]]=n.dims[t];e[c[0]]=t[0],e[c[1]]=t[1],h.push(_a(e))};\"linear\"===a||\"constant\"===a?i.forEach((t=>s([t,r.predict(t)]))):Is(r.predict,i,25,200).forEach(s)})),this.value&&(r.rem=this.value),this.value=r.add=r.source=h}return r}});var"
-  , " DF=Object.freeze({__proto__:null,loess:AF,regression:EF});const CF=134217729,FF=33306690738754706e-32;function SF(t,e,n,r,i){let o,a,s,u,l=e[0],c=r[0],f=0,h=0;c>l==c>-l?(o=l,l=e[++f]):(o=c,c=r[++h]);let d=0;if(f<t&&h<n)for(c>l==c>-l?(a=l+o,s=o-(a-l),l=e[++f]):(a=c+o,s=o-(a-c),c=r[++h]),o=a,0!==s&&(i[d++]=s);f<t&&h<n;)c>l==c>-l?(a=o+l,u=a-o,s=o-(a-u)+(l-u),l=e[++f]):(a=o+c,u=a-o,s=o-(a-u)+(c-u),c=r[++h]),o=a,0!==s&&(i[d++]=s);for(;f<t;)a=o+l,u=a-o,s=o-(a-u)+(l-u),l=e[++f],o=a,0!==s&&(i[d++]=s);for(;h<n;)a=o+c,u=a-o,s=o-(a-u)+(c-u),c=r[++h],o=a,0!==s&&(i[d++]=s);return 0===o&&0!==d||(i[d++]=o),d}function $F(t){return new Float64Array(t)}const TF=22204460492503146e-32,BF=11093356479670487e-47,NF=$F(4),zF=$F(8),OF=$F(12),RF=$F(16),LF=$F(4);function UF(t,e,n,r,i,o){const a=(e-o)*(n-i),s=(t-i)*(r-o),u=a-s;if(0===a||0===s||a>0!=s>0)return u;const l=Math.abs(a+s);return Math.abs(u)>=33306690738754716e-32*l?u:-function(t,e,n,r,i,o,a){let s,u,l,c,f,h,d,p,g,m,y,v,_,x,b,w,k,A;const M=t-i,E=n-i,D=e-o,C=r-o;x=M*C,h=CF*M,d=h-(h-M),p=M-d,h=CF*C,g=h-(h-C),m=C-g,b=p*m-(x-d*g-p*g-d*m),w=D*E,h=CF*D,d=h-(h-D),p=D-d,h=CF*E,g=h-(h-E),m=E-g,k=p*m-(w-d*g-p*g-d*m),y=b-k,f=b-y,NF[0]=b-(y+f)+(f-k),v=x+y,f=v-x,_=x-(v-f)+(y-f),y=_-w,f=_-y,NF[1]=_-(y+f)+(f-w),A=v+y,f=A-v,NF[2]=v-(A-f)+(y-f),NF[3]=A;let F=function(t,e){let n=e[0];for(let r=1;r<t;r++)n+=e[r];return n}(4,NF),S=TF*a;if(F>=S||-F>=S)return F;if(f=t-M,s=t-(M+f)+(f-i),f=n-E,l=n-(E+f)+(f-i),f=e-D,u=e-(D+f)+(f-o),f=r-C,c=r-(C+f)+(f-o),0===s&&0===u&&0===l&&0===c)return F;if(S=BF*a+FF*Math.abs(F),F+=M*c+C*s-(D*l+E*u),F>=S||-F>=S)return F;x=s*C,h=CF*s,d=h-(h-s),p=s-d,h=CF*C,g=h-(h-C),m=C-g,b=p*m-(x-d*g-p*g-d*m),w=u*E,h=CF*u,d=h-(h-u),p=u-d,h=CF*E,g=h-(h-E),m=E-g,k=p*m-(w-d*g-p*g-d*m),y=b-k,f=b-y,LF[0]=b-(y+f)+(f-k),v=x+y,f=v-x,_=x-(v-f)+(y-f),y=_-w,f=_-y,LF[1]=_-(y+f)+(f-w),A=v+y,f=A-v,LF[2]=v-(A-f)+(y-f),LF[3]=A;const $=SF(4,NF,4,LF,zF);x=M*c,h=CF*M,d=h-(h-M),p=M-d,h=CF*c,g=h-(h-c),m=c-g,b=p*m-(x-d*g-p*g-d*m),w=D*l,h=CF*D,d=h-(h-D),p=D-d,h=CF*l,g=h-(h-l),m=l-g,k=p*m-(w-d*g-p*g-d*m),y=b-k,f=b-y,LF[0]=b-(y+f)+(f-k),v=x+y,f=v-x,_=x-(v-f)+(y-f),y=_-w,f=_-y,LF[1]=_-(y+f)+(f-w),A=v+y,f=A-v,LF[2]=v-(A-f)+(y-f),LF[3]=A;const T=SF($,zF,4,LF,OF);x=s*c,h=CF*s,d=h-(h-s),p=s-d,h=CF*c,g=h-(h-c),m=c-g,b=p*m-(x-d*g-p*g-d*m),w=u*l,h=CF*u,d=h-(h-u),p=u-d,h=CF*l,g=h-(h-l),m=l-g,k=p*m-(w-d*g-p*g-d*m),y=b-k,f=b-y,LF[0]=b-(y+f)+(f-k),v=x+y,f=v-x,_=x-(v-f)+(y-f),y=_-w,f=_-y,LF[1]=_-(y+f)+(f-w),A=v+y,f=A-v,LF[2]=v-(A-f)+(y-f),LF[3]=A;const B=SF(T,OF,4,LF,RF);return RF[B-1]}(t,e,n,r,i,o,l)}const qF=Math.pow(2,-52),PF=new Uint32Array(512);class jF{static from(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:VF,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:XF;const r=t.length,i=new Float64Array(2*r);for(let o=0;o<r;o++){const r=t[o];i[2*o]=e(r),i[2*o+1]=n(r)}return new jF(i)}constructor(t){const e=t.length>>1;if(e>0&&\"number\"!=typeof t[0])throw new Error(\"Expected coords to contain numbers.\");this.coords=t;const n=Math.max(2*e-5,0);this._triangles=new Uint32Array(3*n),this._halfedges=new Int32Array(3*n),this._hashSize=Math.ceil(Math.sqrt(e)),this._hullPrev=new Uint32Array(e),this._hullNext=new Uint32Array(e),this._hullTri=new Uint32Array(e),this._hullHash=new Int32Array(this._hashSize).fill(-1),this._ids=new Uint32Array(e),this._dists=new Float64Array(e),this.update()}update(){const{coords:t,_hullPrev:e,_hullNext:n,_hullTri:r,_hullHash:i}=this,o=t.length>>1;let a=1/0,s=1/0,u=-1/0,l=-1/0;for(let e=0;e<o;e++){const n=t[2*e],r=t[2*e+1];n<a&&(a=n),r<s&&(s=r),n>u&&(u=n),r>l&&(l=r),this._ids[e]=e}const c=(a+u)/2,f=(s+l)/2;let h,d,p,g=1/0;for(let e=0;e<o;e++){const n=IF(c,f,t[2*e],t[2*e+1]);n<g&&(h=e,g=n)}const m=t[2*h],y=t[2*h+1];g=1/0;for(let e=0;e<o;e++){if(e===h)continue;const n=IF(m,y,t[2*e],t[2*e+1]);n<g&&n>0&&(d=e,g=n)}let v=t[2*d],_=t[2*d+1],x=1/0;for(let e=0;e<o;e++){if(e===h||e===d)continue;const n=HF(m,y,v,_,t[2*e],t[2*e+1]);n<x&&(p=e,x=n)}let b=t[2*p],w=t[2*p+1];if(x===1/0){for(let e=0;e<o;e++)this._dists[e]=t[2*e]-t[0]||t[2*e+1]-t[1];YF(this._ids,this._dists,0,o-1);const e=new Uint32Array(o"
-  , ");let n=0;for(let t=0,r=-1/0;t<o;t++){const i=this._ids[t];this._dists[i]>r&&(e[n++]=i,r=this._dists[i])}return this.hull=e.subarray(0,n),this.triangles=new Uint32Array(0),void(this.halfedges=new Uint32Array(0))}if(UF(m,y,v,_,b,w)<0){const t=d,e=v,n=_;d=p,v=b,_=w,p=t,b=e,w=n}const k=function(t,e,n,r,i,o){const a=n-t,s=r-e,u=i-t,l=o-e,c=a*a+s*s,f=u*u+l*l,h=.5/(a*l-s*u),d=t+(l*c-s*f)*h,p=e+(a*f-u*c)*h;return{x:d,y:p}}(m,y,v,_,b,w);this._cx=k.x,this._cy=k.y;for(let e=0;e<o;e++)this._dists[e]=IF(t[2*e],t[2*e+1],k.x,k.y);YF(this._ids,this._dists,0,o-1),this._hullStart=h;let A=3;n[h]=e[p]=d,n[d]=e[h]=p,n[p]=e[d]=h,r[h]=0,r[d]=1,r[p]=2,i.fill(-1),i[this._hashKey(m,y)]=h,i[this._hashKey(v,_)]=d,i[this._hashKey(b,w)]=p,this.trianglesLen=0,this._addTriangle(h,d,p,-1,-1,-1);for(let o,a,s=0;s<this._ids.length;s++){const u=this._ids[s],l=t[2*u],c=t[2*u+1];if(s>0&&Math.abs(l-o)<=qF&&Math.abs(c-a)<=qF)continue;if(o=l,a=c,u===h||u===d||u===p)continue;let f=0;for(let t=0,e=this._hashKey(l,c);t<this._hashSize&&(f=i[(e+t)%this._hashSize],-1===f||f===n[f]);t++);f=e[f];let g,m=f;for(;g=n[m],UF(l,c,t[2*m],t[2*m+1],t[2*g],t[2*g+1])>=0;)if(m=g,m===f){m=-1;break}if(-1===m)continue;let y=this._addTriangle(m,u,n[m],-1,-1,r[m]);r[u]=this._legalize(y+2),r[m]=y,A++;let v=n[m];for(;g=n[v],UF(l,c,t[2*v],t[2*v+1],t[2*g],t[2*g+1])<0;)y=this._addTriangle(v,u,g,r[u],-1,r[v]),r[u]=this._legalize(y+2),n[v]=v,A--,v=g;if(m===f)for(;g=e[m],UF(l,c,t[2*g],t[2*g+1],t[2*m],t[2*m+1])<0;)y=this._addTriangle(g,u,m,-1,r[m],r[g]),this._legalize(y+2),r[g]=y,n[m]=m,A--,m=g;this._hullStart=e[u]=m,n[m]=e[v]=u,n[u]=v,i[this._hashKey(l,c)]=u,i[this._hashKey(t[2*m],t[2*m+1])]=m}this.hull=new Uint32Array(A);for(let t=0,e=this._hullStart;t<A;t++)this.hull[t]=e,e=n[e];this.triangles=this._triangles.subarray(0,this.trianglesLen),this.halfedges=this._halfedges.subarray(0,this.trianglesLen)}_hashKey(t,e){return Math.floor(function(t,e){const n=t/(Math.abs(t)+Math.abs(e));return(e>0?3-n:1+n)/4}(t-this._cx,e-this._cy)*this._hashSize)%this._hashSize}_legalize(t){const{_triangles:e,_halfedges:n,coords:r}=this;let i=0,o=0;for(;;){const a=n[t],s=t-t%3;if(o=s+(t+2)%3,-1===a){if(0===i)break;t=PF[--i];continue}const u=a-a%3,l=s+(t+1)%3,c=u+(a+2)%3,f=e[o],h=e[t],d=e[l],p=e[c];if(WF(r[2*f],r[2*f+1],r[2*h],r[2*h+1],r[2*d],r[2*d+1],r[2*p],r[2*p+1])){e[t]=p,e[a]=f;const r=n[c];if(-1===r){let e=this._hullStart;do{if(this._hullTri[e]===c){this._hullTri[e]=t;break}e=this._hullPrev[e]}while(e!==this._hullStart)}this._link(t,r),this._link(a,n[o]),this._link(o,c);const s=u+(a+1)%3;i<PF.length&&(PF[i++]=s)}else{if(0===i)break;t=PF[--i]}}return o}_link(t,e){this._halfedges[t]=e,-1!==e&&(this._halfedges[e]=t)}_addTriangle(t,e,n,r,i,o){const a=this.trianglesLen;return this._triangles[a]=t,this._triangles[a+1]=e,this._triangles[a+2]=n,this._link(a,r),this._link(a+1,i),this._link(a+2,o),this.trianglesLen+=3,a}}function IF(t,e,n,r){const i=t-n,o=e-r;return i*i+o*o}function WF(t,e,n,r,i,o,a,s){const u=t-a,l=e-s,c=n-a,f=r-s,h=i-a,d=o-s,p=c*c+f*f,g=h*h+d*d;return u*(f*g-p*d)-l*(c*g-p*h)+(u*u+l*l)*(c*d-f*h)<0}function HF(t,e,n,r,i,o){const a=n-t,s=r-e,u=i-t,l=o-e,c=a*a+s*s,f=u*u+l*l,h=.5/(a*l-s*u),d=(l*c-s*f)*h,p=(a*f-u*c)*h;return d*d+p*p}function YF(t,e,n,r){if(r-n<=20)for(let i=n+1;i<=r;i++){const r=t[i],o=e[r];let a=i-1;for(;a>=n&&e[t[a]]>o;)t[a+1]=t[a--];t[a+1]=r}else{let i=n+1,o=r;GF(t,n+r>>1,i),e[t[n]]>e[t[r]]&&GF(t,n,r),e[t[i]]>e[t[r]]&&GF(t,i,r),e[t[n]]>e[t[i]]&&GF(t,n,i);const a=t[i],s=e[a];for(;;){do{i++}while(e[t[i]]<s);do{o--}while(e[t[o]]>s);if(o<i)break;GF(t,i,o)}t[n+1]=t[o],t[o]=a,r-i+1>=o-n?(YF(t,e,i,r),YF(t,e,n,o-1)):(YF(t,e,n,o-1),YF(t,e,i,r))}}function GF(t,e,n){const r=t[e];t[e]=t[n],t[n]=r}function VF(t){return t[0]}function XF(t){return t[1]}const JF=1e-6;class ZF{constructor(){this._x0=this._y0=this._x1=this._y1=null,this._=\"\"}moveTo(t,e){this._+=`M${this._x0=this._x1=+t},${this._y0=this._y1=+e}`}closePath(){null!==this._x1&&(this._x1=this._x0,this._y1=this._y0,this._+=\"Z\")}lineTo(t,e){this._+=`L${this._x1=+t},${this._y1=+e}`}arc(t,e,n){const r=(t=+t)+(n=+n),i=e=+e;if(n<0)throw new Error(\""
-  , "negative radius\");null===this._x1?this._+=`M${r},${i}`:(Math.abs(this._x1-r)>JF||Math.abs(this._y1-i)>JF)&&(this._+=\"L\"+r+\",\"+i),n&&(this._+=`A${n},${n},0,1,1,${t-n},${e}A${n},${n},0,1,1,${this._x1=r},${this._y1=i}`)}rect(t,e,n,r){this._+=`M${this._x0=this._x1=+t},${this._y0=this._y1=+e}h${+n}v${+r}h${-n}Z`}value(){return this._||null}}class QF{constructor(){this._=[]}moveTo(t,e){this._.push([t,e])}closePath(){this._.push(this._[0].slice())}lineTo(t,e){this._.push([t,e])}value(){return this._.length?this._:null}}let KF=class{constructor(t){let[e,n,r,i]=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[0,0,960,500];if(!((r=+r)>=(e=+e)&&(i=+i)>=(n=+n)))throw new Error(\"invalid bounds\");this.delaunay=t,this._circumcenters=new Float64Array(2*t.points.length),this.vectors=new Float64Array(2*t.points.length),this.xmax=r,this.xmin=e,this.ymax=i,this.ymin=n,this._init()}update(){return this.delaunay.update(),this._init(),this}_init(){const{delaunay:{points:t,hull:e,triangles:n},vectors:r}=this;let i,o;const a=this.circumcenters=this._circumcenters.subarray(0,n.length/3*2);for(let r,s,u=0,l=0,c=n.length;u<c;u+=3,l+=2){const c=2*n[u],f=2*n[u+1],h=2*n[u+2],d=t[c],p=t[c+1],g=t[f],m=t[f+1],y=t[h],v=t[h+1],_=g-d,x=m-p,b=y-d,w=v-p,k=2*(_*w-x*b);if(Math.abs(k)<1e-9){if(void 0===i){i=o=0;for(const n of e)i+=t[2*n],o+=t[2*n+1];i/=e.length,o/=e.length}const n=1e9*Math.sign((i-d)*w-(o-p)*b);r=(d+y)/2-n*w,s=(p+v)/2+n*b}else{const t=1/k,e=_*_+x*x,n=b*b+w*w;r=d+(w*e-x*n)*t,s=p+(_*n-b*e)*t}a[l]=r,a[l+1]=s}let s,u,l,c=e[e.length-1],f=4*c,h=t[2*c],d=t[2*c+1];r.fill(0);for(let n=0;n<e.length;++n)c=e[n],s=f,u=h,l=d,f=4*c,h=t[2*c],d=t[2*c+1],r[s+2]=r[f]=l-d,r[s+3]=r[f+1]=h-u}render(t){const e=null==t?t=new ZF:void 0,{delaunay:{halfedges:n,inedges:r,hull:i},circumcenters:o,vectors:a}=this;if(i.length<=1)return null;for(let e=0,r=n.length;e<r;++e){const r=n[e];if(r<e)continue;const i=2*Math.floor(e/3),a=2*Math.floor(r/3),s=o[i],u=o[i+1],l=o[a],c=o[a+1];this._renderSegment(s,u,l,c,t)}let s,u=i[i.length-1];for(let e=0;e<i.length;++e){s=u,u=i[e];const n=2*Math.floor(r[u]/3),l=o[n],c=o[n+1],f=4*s,h=this._project(l,c,a[f+2],a[f+3]);h&&this._renderSegment(l,c,h[0],h[1],t)}return e&&e.value()}renderBounds(t){const e=null==t?t=new ZF:void 0;return t.rect(this.xmin,this.ymin,this.xmax-this.xmin,this.ymax-this.ymin),e&&e.value()}renderCell(t,e){const n=null==e?e=new ZF:void 0,r=this._clip(t);if(null===r||!r.length)return;e.moveTo(r[0],r[1]);let i=r.length;for(;r[0]===r[i-2]&&r[1]===r[i-1]&&i>1;)i-=2;for(let t=2;t<i;t+=2)r[t]===r[t-2]&&r[t+1]===r[t-1]||e.lineTo(r[t],r[t+1]);return e.closePath(),n&&n.value()}*cellPolygons(){const{delaunay:{points:t}}=this;for(let e=0,n=t.length/2;e<n;++e){const t=this.cellPolygon(e);t&&(t.index=e,yield t)}}cellPolygon(t){const e=new QF;return this.renderCell(t,e),e.value()}_renderSegment(t,e,n,r,i){let o;const a=this._regioncode(t,e),s=this._regioncode(n,r);0===a&&0===s?(i.moveTo(t,e),i.lineTo(n,r)):(o=this._clipSegment(t,e,n,r,a,s))&&(i.moveTo(o[0],o[1]),i.lineTo(o[2],o[3]))}contains(t,e,n){return(e=+e)==e&&(n=+n)==n&&this.delaunay._step(t,e,n)===t}*neighbors(t){const e=this._clip(t);if(e)for(const n of this.delaunay.neighbors(t)){const t=this._clip(n);if(t)t:for(let r=0,i=e.length;r<i;r+=2)for(let o=0,a=t.length;o<a;o+=2)if(e[r]===t[o]&&e[r+1]===t[o+1]&&e[(r+2)%i]===t[(o+a-2)%a]&&e[(r+3)%i]===t[(o+a-1)%a]){yield n;break t}}}_cell(t){const{circumcenters:e,delaunay:{inedges:n,halfedges:r,triangles:i}}=this,o=n[t];if(-1===o)return null;const a=[];let s=o;do{const n=Math.floor(s/3);if(a.push(e[2*n],e[2*n+1]),s=s%3==2?s-2:s+1,i[s]!==t)break;s=r[s]}while(s!==o&&-1!==s);return a}_clip(t){if(0===t&&1===this.delaunay.hull.length)return[this.xmax,this.ymin,this.xmax,this.ymax,this.xmin,this.ymax,this.xmin,this.ymin];const e=this._cell(t);if(null===e)return null;const{vectors:n}=this,r=4*t;return this._simplify(n[r]||n[r+1]?this._clipInfinite(t,e,n[r],n[r+1],n[r+2],n[r+3]):this._clipFinite(t,e))}_clipFinite(t,e){const n=e.length;let r,i,o,a,s=null,u=e[n-2],l=e[n-1],c=this._regioncode(u,l),f=0;for(let h=0;h<n;h+=2)if(r=u,i=l,u=e[h],l"
-  , "=e[h+1],o=c,c=this._regioncode(u,l),0===o&&0===c)a=f,f=0,s?s.push(u,l):s=[u,l];else{let e,n,h,d,p;if(0===o){if(null===(e=this._clipSegment(r,i,u,l,o,c)))continue;[n,h,d,p]=e}else{if(null===(e=this._clipSegment(u,l,r,i,c,o)))continue;[d,p,n,h]=e,a=f,f=this._edgecode(n,h),a&&f&&this._edge(t,a,f,s,s.length),s?s.push(n,h):s=[n,h]}a=f,f=this._edgecode(d,p),a&&f&&this._edge(t,a,f,s,s.length),s?s.push(d,p):s=[d,p]}if(s)a=f,f=this._edgecode(s[0],s[1]),a&&f&&this._edge(t,a,f,s,s.length);else if(this.contains(t,(this.xmin+this.xmax)/2,(this.ymin+this.ymax)/2))return[this.xmax,this.ymin,this.xmax,this.ymax,this.xmin,this.ymax,this.xmin,this.ymin];return s}_clipSegment(t,e,n,r,i,o){const a=i<o;for(a&&([t,e,n,r,i,o]=[n,r,t,e,o,i]);;){if(0===i&&0===o)return a?[n,r,t,e]:[t,e,n,r];if(i&o)return null;let s,u,l=i||o;8&l?(s=t+(n-t)*(this.ymax-e)/(r-e),u=this.ymax):4&l?(s=t+(n-t)*(this.ymin-e)/(r-e),u=this.ymin):2&l?(u=e+(r-e)*(this.xmax-t)/(n-t),s=this.xmax):(u=e+(r-e)*(this.xmin-t)/(n-t),s=this.xmin),i?(t=s,e=u,i=this._regioncode(t,e)):(n=s,r=u,o=this._regioncode(n,r))}}_clipInfinite(t,e,n,r,i,o){let a,s=Array.from(e);if((a=this._project(s[0],s[1],n,r))&&s.unshift(a[0],a[1]),(a=this._project(s[s.length-2],s[s.length-1],i,o))&&s.push(a[0],a[1]),s=this._clipFinite(t,s))for(let e,n=0,r=s.length,i=this._edgecode(s[r-2],s[r-1]);n<r;n+=2)e=i,i=this._edgecode(s[n],s[n+1]),e&&i&&(n=this._edge(t,e,i,s,n),r=s.length);else this.contains(t,(this.xmin+this.xmax)/2,(this.ymin+this.ymax)/2)&&(s=[this.xmin,this.ymin,this.xmax,this.ymin,this.xmax,this.ymax,this.xmin,this.ymax]);return s}_edge(t,e,n,r,i){for(;e!==n;){let n,o;switch(e){case 5:e=4;continue;case 4:e=6,n=this.xmax,o=this.ymin;break;case 6:e=2;continue;case 2:e=10,n=this.xmax,o=this.ymax;break;case 10:e=8;continue;case 8:e=9,n=this.xmin,o=this.ymax;break;case 9:e=1;continue;case 1:e=5,n=this.xmin,o=this.ymin}r[i]===n&&r[i+1]===o||!this.contains(t,n,o)||(r.splice(i,0,n,o),i+=2)}return i}_project(t,e,n,r){let i,o,a,s=1/0;if(r<0){if(e<=this.ymin)return null;(i=(this.ymin-e)/r)<s&&(a=this.ymin,o=t+(s=i)*n)}else if(r>0){if(e>=this.ymax)return null;(i=(this.ymax-e)/r)<s&&(a=this.ymax,o=t+(s=i)*n)}if(n>0){if(t>=this.xmax)return null;(i=(this.xmax-t)/n)<s&&(o=this.xmax,a=e+(s=i)*r)}else if(n<0){if(t<=this.xmin)return null;(i=(this.xmin-t)/n)<s&&(o=this.xmin,a=e+(s=i)*r)}return[o,a]}_edgecode(t,e){return(t===this.xmin?1:t===this.xmax?2:0)|(e===this.ymin?4:e===this.ymax?8:0)}_regioncode(t,e){return(t<this.xmin?1:t>this.xmax?2:0)|(e<this.ymin?4:e>this.ymax?8:0)}_simplify(t){if(t&&t.length>4){for(let e=0;e<t.length;e+=2){const n=(e+2)%t.length,r=(e+4)%t.length;(t[e]===t[n]&&t[n]===t[r]||t[e+1]===t[n+1]&&t[n+1]===t[r+1])&&(t.splice(n,2),e-=2)}t.length||(t=null)}return t}};const tS=2*Math.PI,eS=Math.pow;function nS(t){return t[0]}function rS(t){return t[1]}function iS(t,e,n){return[t+Math.sin(t+e)*n,e+Math.cos(t-e)*n]}class oS{static from(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:nS,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:rS,r=arguments.length>3?arguments[3]:void 0;return new oS(\"length\"in t?function(t,e,n,r){const i=t.length,o=new Float64Array(2*i);for(let a=0;a<i;++a){const i=t[a];o[2*a]=e.call(r,i,a,t),o[2*a+1]=n.call(r,i,a,t)}return o}(t,e,n,r):Float64Array.from(function*(t,e,n,r){let i=0;for(const o of t)yield e.call(r,o,i,t),yield n.call(r,o,i,t),++i}(t,e,n,r)))}constructor(t){this._delaunator=new jF(t),this.inedges=new Int32Array(t.length/2),this._hullIndex=new Int32Array(t.length/2),this.points=this._delaunator.coords,this._init()}update(){return this._delaunator.update(),this._init(),this}_init(){const t=this._delaunator,e=this.points;if(t.hull&&t.hull.length>2&&function(t){const{triangles:e,coords:n}=t;for(let t=0;t<e.length;t+=3){const r=2*e[t],i=2*e[t+1],o=2*e[t+2];if((n[o]-n[r])*(n[i+1]-n[r+1])-(n[i]-n[r])*(n[o+1]-n[r+1])>1e-10)return!1}return!0}(t)){this.collinear=Int32Array.from({length:e.length/2},((t,e)=>e)).sort(((t,n)=>e[2*t]-e[2*n]||e[2*t+1]-e[2*n+1]));const t=this.collinear[0],n=this.collinear[this.collinear.length-1],r=[e[2*t],e[2*t+1],e[2*n],e[2*n"
-  , "+1]],i=1e-8*Math.hypot(r[3]-r[1],r[2]-r[0]);for(let t=0,n=e.length/2;t<n;++t){const n=iS(e[2*t],e[2*t+1],i);e[2*t]=n[0],e[2*t+1]=n[1]}this._delaunator=new jF(e)}else delete this.collinear;const n=this.halfedges=this._delaunator.halfedges,r=this.hull=this._delaunator.hull,i=this.triangles=this._delaunator.triangles,o=this.inedges.fill(-1),a=this._hullIndex.fill(-1);for(let t=0,e=n.length;t<e;++t){const e=i[t%3==2?t-2:t+1];-1!==n[t]&&-1!==o[e]||(o[e]=t)}for(let t=0,e=r.length;t<e;++t)a[r[t]]=t;r.length<=2&&r.length>0&&(this.triangles=new Int32Array(3).fill(-1),this.halfedges=new Int32Array(3).fill(-1),this.triangles[0]=r[0],o[r[0]]=1,2===r.length&&(o[r[1]]=0,this.triangles[1]=r[1],this.triangles[2]=r[1]))}voronoi(t){return new KF(this,t)}*neighbors(t){const{inedges:e,hull:n,_hullIndex:r,halfedges:i,triangles:o,collinear:a}=this;if(a){const e=a.indexOf(t);return e>0&&(yield a[e-1]),void(e<a.length-1&&(yield a[e+1]))}const s=e[t];if(-1===s)return;let u=s,l=-1;do{if(yield l=o[u],u=u%3==2?u-2:u+1,o[u]!==t)return;if(u=i[u],-1===u){const e=n[(r[t]+1)%n.length];return void(e!==l&&(yield e))}}while(u!==s)}find(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;if((t=+t)!=t||(e=+e)!=e)return-1;const r=n;let i;for(;(i=this._step(n,t,e))>=0&&i!==n&&i!==r;)n=i;return i}_step(t,e,n){const{inedges:r,hull:i,_hullIndex:o,halfedges:a,triangles:s,points:u}=this;if(-1===r[t]||!u.length)return(t+1)%(u.length>>1);let l=t,c=eS(e-u[2*t],2)+eS(n-u[2*t+1],2);const f=r[t];let h=f;do{let r=s[h];const f=eS(e-u[2*r],2)+eS(n-u[2*r+1],2);if(f<c&&(c=f,l=r),h=h%3==2?h-2:h+1,s[h]!==t)break;if(h=a[h],-1===h){if(h=i[(o[t]+1)%i.length],h!==r&&eS(e-u[2*h],2)+eS(n-u[2*h+1],2)<c)return h;break}}while(h!==f);return l}render(t){const e=null==t?t=new ZF:void 0,{points:n,halfedges:r,triangles:i}=this;for(let e=0,o=r.length;e<o;++e){const o=r[e];if(o<e)continue;const a=2*i[e],s=2*i[o];t.moveTo(n[a],n[a+1]),t.lineTo(n[s],n[s+1])}return this.renderHull(t),e&&e.value()}renderPoints(t,e){void 0!==e||t&&\"function\"==typeof t.moveTo||(e=t,t=null),e=null==e?2:+e;const n=null==t?t=new ZF:void 0,{points:r}=this;for(let n=0,i=r.length;n<i;n+=2){const i=r[n],o=r[n+1];t.moveTo(i+e,o),t.arc(i,o,e,0,tS)}return n&&n.value()}renderHull(t){const e=null==t?t=new ZF:void 0,{hull:n,points:r}=this,i=2*n[0],o=n.length;t.moveTo(r[i],r[i+1]);for(let e=1;e<o;++e){const i=2*n[e];t.lineTo(r[i],r[i+1])}return t.closePath(),e&&e.value()}hullPolygon(){const t=new QF;return this.renderHull(t),t.value()}renderTriangle(t,e){const n=null==e?e=new ZF:void 0,{points:r,triangles:i}=this,o=2*i[t*=3],a=2*i[t+1],s=2*i[t+2];return e.moveTo(r[o],r[o+1]),e.lineTo(r[a],r[a+1]),e.lineTo(r[s],r[s+1]),e.closePath(),n&&n.value()}*trianglePolygons(){const{triangles:t}=this;for(let e=0,n=t.length/3;e<n;++e)yield this.trianglePolygon(e)}trianglePolygon(t){const e=new QF;return this.renderTriangle(t,e),e.value()}}function aS(t){Ja.call(this,null,t)}aS.Definition={type:\"Voronoi\",metadata:{modifies:!0},params:[{name:\"x\",type:\"field\",required:!0},{name:\"y\",type:\"field\",required:!0},{name:\"size\",type:\"number\",array:!0,length:2},{name:\"extent\",type:\"array\",array:!0,length:2,default:[[-1e5,-1e5],[1e5,1e5]],content:{type:\"number\",array:!0,length:2}},{name:\"as\",type:\"string\",default:\"path\"}]};const sS=[-1e5,-1e5,1e5,1e5];function uS(t){const e=t[0][0],n=t[0][1];let r=t.length-1;for(;t[r][0]===e&&t[r][1]===n;--r);return\"M\"+t.slice(0,r+1).join(\"L\")+\"Z\"}dt(aS,Ja,{transform(t,e){const n=t.as||\"path\",r=e.source;if(!r||!r.length)return e;let i=t.size;i=i?[0,0,i[0],i[1]]:(i=t.extent)?[i[0][0],i[0][1],i[1][0],i[1][1]]:sS;const o=this.value=oS.from(r,t.x,t.y).voronoi(i);for(let t=0,e=r.length;t<e;++t){const e=o.cellPolygon(t);r[t][n]=e&&(2!==(a=e).length||a[0][0]!==a[1][0]||a[0][1]!==a[1][1])?uS(e):null}var a;return e.reflow(t.modified()).modifies(n)}});var lS=Object.freeze({__proto__:null,voronoi:aS}),cS=Math.PI/180,fS=64,hS=2048;function dS(){var t,e,n,r,i,o,a,s=[256,256],u=vS,l=[],c=Math.random,f={};function h(t,e,n){for(var r,i,o,a=e.x,l=e.y,f=Math.hypot(s[0],s[1]),h=u(s),d=c()<.5?1:-1,p=-d;(r=h(p+=d))&&(i=~~r[0],o"
-  , "=~~r[1],!(Math.min(Math.abs(i),Math.abs(o))>=f));)if(e.x=a+i,e.y=l+o,!(e.x+e.x0<0||e.y+e.y0<0||e.x+e.x1>s[0]||e.y+e.y1>s[1])&&(!n||!gS(e,t,s[0]))&&(!n||yS(e,n))){for(var g,m=e.sprite,y=e.width>>5,v=s[0]>>5,_=e.x-(y<<4),x=127&_,b=32-x,w=e.y1-e.y0,k=(e.y+e.y0)*v+(_>>5),A=0;A<w;A++){g=0;for(var M=0;M<=y;M++)t[k+M]|=g<<b|(M<y?(g=m[A*y+M])>>>x:0);k+=v}return e.sprite=null,!0}return!1}return f.layout=function(){for(var u=function(t){t.width=t.height=1;var e=Math.sqrt(t.getContext(\"2d\").getImageData(0,0,1,1).data.length>>2);t.width=(fS<<5)/e,t.height=hS/e;var n=t.getContext(\"2d\");return n.fillStyle=n.strokeStyle=\"red\",n.textAlign=\"center\",{context:n,ratio:e}}($c()),f=function(t){var e=[],n=-1;for(;++n<t;)e[n]=0;return e}((s[0]>>5)*s[1]),d=null,p=l.length,g=-1,m=[],y=l.map((s=>({text:t(s),font:e(s),style:r(s),weight:i(s),rotate:o(s),size:~~(n(s)+1e-14),padding:a(s),xoff:0,yoff:0,x1:0,y1:0,x0:0,y0:0,hasText:!1,sprite:null,datum:s}))).sort(((t,e)=>e.size-t.size));++g<p;){var v=y[g];v.x=s[0]*(c()+.5)>>1,v.y=s[1]*(c()+.5)>>1,pS(u,v,y,g),v.hasText&&h(f,v,d)&&(m.push(v),d?mS(d,v):d=[{x:v.x+v.x0,y:v.y+v.y0},{x:v.x+v.x1,y:v.y+v.y1}],v.x-=s[0]>>1,v.y-=s[1]>>1)}return m},f.words=function(t){return arguments.length?(l=t,f):l},f.size=function(t){return arguments.length?(s=[+t[0],+t[1]],f):s},f.font=function(t){return arguments.length?(e=_S(t),f):e},f.fontStyle=function(t){return arguments.length?(r=_S(t),f):r},f.fontWeight=function(t){return arguments.length?(i=_S(t),f):i},f.rotate=function(t){return arguments.length?(o=_S(t),f):o},f.text=function(e){return arguments.length?(t=_S(e),f):t},f.spiral=function(t){return arguments.length?(u=xS[t]||t,f):u},f.fontSize=function(t){return arguments.length?(n=_S(t),f):n},f.padding=function(t){return arguments.length?(a=_S(t),f):a},f.random=function(t){return arguments.length?(c=t,f):c},f}function pS(t,e,n,r){if(!e.sprite){var i=t.context,o=t.ratio;i.clearRect(0,0,(fS<<5)/o,hS/o);var a,s,u,l,c,f=0,h=0,d=0,p=n.length;for(--r;++r<p;){if(e=n[r],i.save(),i.font=e.style+\" \"+e.weight+\" \"+~~((e.size+1)/o)+\"px \"+e.font,a=i.measureText(e.text+\"m\").width*o,u=e.size<<1,e.rotate){var g=Math.sin(e.rotate*cS),m=Math.cos(e.rotate*cS),y=a*m,v=a*g,_=u*m,x=u*g;a=Math.max(Math.abs(y+x),Math.abs(y-x))+31>>5<<5,u=~~Math.max(Math.abs(v+_),Math.abs(v-_))}else a=a+31>>5<<5;if(u>d&&(d=u),f+a>=fS<<5&&(f=0,h+=d,d=0),h+u>=hS)break;i.translate((f+(a>>1))/o,(h+(u>>1))/o),e.rotate&&i.rotate(e.rotate*cS),i.fillText(e.text,0,0),e.padding&&(i.lineWidth=2*e.padding,i.strokeText(e.text,0,0)),i.restore(),e.width=a,e.height=u,e.xoff=f,e.yoff=h,e.x1=a>>1,e.y1=u>>1,e.x0=-e.x1,e.y0=-e.y1,e.hasText=!0,f+=a}for(var b=i.getImageData(0,0,(fS<<5)/o,hS/o).data,w=[];--r>=0;)if((e=n[r]).hasText){for(s=(a=e.width)>>5,u=e.y1-e.y0,l=0;l<u*s;l++)w[l]=0;if(null==(f=e.xoff))return;h=e.yoff;var k=0,A=-1;for(c=0;c<u;c++){for(l=0;l<a;l++){var M=s*c+(l>>5),E=b[(h+c)*(fS<<5)+(f+l)<<2]?1<<31-l%32:0;w[M]|=E,k|=E}k?A=c:(e.y0++,u--,c--,h++)}e.y1=e.y0+A,e.sprite=w.slice(0,(e.y1-e.y0)*s)}}}function gS(t,e,n){n>>=5;for(var r,i=t.sprite,o=t.width>>5,a=t.x-(o<<4),s=127&a,u=32-s,l=t.y1-t.y0,c=(t.y+t.y0)*n+(a>>5),f=0;f<l;f++){r=0;for(var h=0;h<=o;h++)if((r<<u|(h<o?(r=i[f*o+h])>>>s:0))&e[c+h])return!0;c+=n}return!1}function mS(t,e){var n=t[0],r=t[1];e.x+e.x0<n.x&&(n.x=e.x+e.x0),e.y+e.y0<n.y&&(n.y=e.y+e.y0),e.x+e.x1>r.x&&(r.x=e.x+e.x1),e.y+e.y1>r.y&&(r.y=e.y+e.y1)}function yS(t,e){return t.x+t.x1>e[0].x&&t.x+t.x0<e[1].x&&t.y+t.y1>e[0].y&&t.y+t.y0<e[1].y}function vS(t){var e=t[0]/t[1];return function(t){return[e*(t*=.1)*Math.cos(t),t*Math.sin(t)]}}function _S(t){return\"function\"==typeof t?t:function(){return t}}var xS={archimedean:vS,rectangular:function(t){var e=4*t[0]/t[1],n=0,r=0;return function(t){var i=t<0?-1:1;switch(Math.sqrt(1+4*i*t)-i&3){case 0:n+=e;break;case 1:r+=4;break;case 2:n-=e;break;default:r-=4}return[n,r]}}};const bS=[\"x\",\"y\",\"font\",\"fontSize\",\"fontStyle\",\"fontWeight\",\"angle\"],wS=[\"text\",\"font\",\"rotate\",\"fontSize\",\"fontStyle\",\"fontWeight\"];function kS(t){Ja.call(this,dS(),t)}kS.Definition={type:\"Wordcloud\",metadata:{modifies:!0},params:[{name:\"size\",type:"
-  , "\"number\",array:!0,length:2},{name:\"font\",type:\"string\",expr:!0,default:\"sans-serif\"},{name:\"fontStyle\",type:\"string\",expr:!0,default:\"normal\"},{name:\"fontWeight\",type:\"string\",expr:!0,default:\"normal\"},{name:\"fontSize\",type:\"number\",expr:!0,default:14},{name:\"fontSizeRange\",type:\"number\",array:\"nullable\",default:[10,50]},{name:\"rotate\",type:\"number\",expr:!0,default:0},{name:\"text\",type:\"field\"},{name:\"spiral\",type:\"string\",values:[\"archimedean\",\"rectangular\"]},{name:\"padding\",type:\"number\",expr:!0},{name:\"as\",type:\"string\",array:!0,length:7,default:bS}]},dt(kS,Ja,{transform(e,n){!e.size||e.size[0]&&e.size[1]||s(\"Wordcloud size dimensions must be non-zero.\");const r=e.modified();if(!(r||n.changed(n.ADD_REM)||wS.some((function(t){const r=e[t];return Z(r)&&n.modified(r.fields)}))))return;const i=n.materialize(n.SOURCE).source,o=this.value,a=e.as||bS;let u,l=e.fontSize||14;if(Z(l)?u=e.fontSizeRange:l=it(l),u){const t=l,e=sp(\"sqrt\")().domain(st(i,t)).range(u);l=n=>e(t(n))}i.forEach((t=>{t[a[0]]=NaN,t[a[1]]=NaN,t[a[3]]=0}));const c=o.words(i).text(e.text).size(e.size||[500,500]).padding(e.padding||1).spiral(e.spiral||\"archimedean\").rotate(e.rotate||0).font(e.font||\"sans-serif\").fontStyle(e.fontStyle||\"normal\").fontWeight(e.fontWeight||\"normal\").fontSize(l).random(t.random).layout(),f=o.size(),h=f[0]>>1,d=f[1]>>1,p=c.length;for(let t,e,n=0;n<p;++n)t=c[n],e=t.datum,e[a[0]]=t.x+h,e[a[1]]=t.y+d,e[a[2]]=t.font,e[a[3]]=t.size,e[a[4]]=t.style,e[a[5]]=t.weight,e[a[6]]=t.rotate;return n.reflow(r).modifies(a)}});var AS=Object.freeze({__proto__:null,wordcloud:kS});const MS=t=>new Uint8Array(t),ES=t=>new Uint16Array(t),DS=t=>new Uint32Array(t);function CS(t,e,n){const r=(e<257?MS:e<65537?ES:DS)(t);return n&&r.set(n),r}function FS(t,e,n){const r=1<<e;return{one:r,zero:~r,range:n.slice(),bisect:t.bisect,index:t.index,size:t.size,onAdd(t,e){const n=this,i=n.bisect(n.range,t.value),o=t.index,a=i[0],s=i[1],u=o.length;let l;for(l=0;l<a;++l)e[o[l]]|=r;for(l=s;l<u;++l)e[o[l]]|=r;return n}}}function SS(){let t=DS(0),e=[],n=0;return{insert:function(r,i,o){if(!i.length)return[];const a=n,s=i.length,u=DS(s);let l,c,f,h=Array(s);for(f=0;f<s;++f)h[f]=r(i[f]),u[f]=f;if(h=function(t,e){return t.sort.call(e,((e,n)=>{const r=t[e],i=t[n];return r<i?-1:r>i?1:0})),function(t,e){return Array.from(e,(e=>t[e]))}(t,e)}(h,u),a)l=e,c=t,e=Array(a+s),t=DS(a+s),function(t,e,n,r,i,o,a,s,u){let l,c=0,f=0;for(l=0;c<r&&f<a;++l)e[c]<i[f]?(s[l]=e[c],u[l]=n[c++]):(s[l]=i[f],u[l]=o[f++]+t);for(;c<r;++c,++l)s[l]=e[c],u[l]=n[c];for(;f<a;++f,++l)s[l]=i[f],u[l]=o[f]+t}(o,l,c,a,h,u,s,e,t);else{if(o>0)for(f=0;f<s;++f)u[f]+=o;e=h,t=u}return n=a+s,{index:u,value:h}},remove:function(r,i){const o=n;let a,s,u;for(s=0;!i[t[s]]&&s<o;++s);for(u=s;s<o;++s)i[a=t[s]]||(t[u]=a,e[u]=e[s],++u);n=o-r},bisect:function(t,r){let i;return r?i=r.length:(r=e,i=n),[ae(r,t[0],0,i),oe(r,t[1],0,i)]},reindex:function(e){for(let r=0,i=n;r<i;++r)t[r]=e[t[r]]},index:()=>t,size:()=>n}}function $S(t){Ja.call(this,function(){let t=8,e=[],n=DS(0),r=CS(0,t),i=CS(0,t);return{data:()=>e,seen:()=>n=function(t,e,n){return t.length>=e?t:((n=n||new t.constructor(e)).set(t),n)}(n,e.length),add(t){for(let n,r=0,i=e.length,o=t.length;r<o;++r)n=t[r],n._index=i++,e.push(n)},remove(t,n){const o=e.length,a=Array(o-t),s=e;let u,l,c;for(l=0;!n[l]&&l<o;++l)a[l]=e[l],s[l]=l;for(c=l;l<o;++l)u=e[l],n[l]?s[l]=-1:(s[l]=c,r[c]=r[l],i[c]=i[l],a[c]=u,u._index=c++),r[l]=0;return e=a,s},size:()=>e.length,curr:()=>r,prev:()=>i,reset:t=>i[t]=r[t],all:()=>t<257?255:t<65537?65535:4294967295,set(t,e){r[t]|=e},clear(t,e){r[t]&=~e},resize(e,n){(e>r.length||n>t)&&(t=Math.max(n,t),r=CS(e,t,r),i=CS(e,t))}}}(),t),this._indices=null,this._dims=null}function TS(t){Ja.call(this,null,t)}$S.Definition={type:\"CrossFilter\",metadata:{},params:[{name:\"fields\",type:\"field\",array:!0,required:!0},{name:\"query\",type:\"array\",array:!0,required:!0,content:{type:\"number\",array:!0,length:2}}]},dt($S,Ja,{transform(t,e){return this._dims?t.modified(\"fields\")||t.fields.some((t=>e.modified(t.fields)))?this.reinit(t,e):this.eval(t,e):this.init(t,e)},init(t,e){const n=t.fiel"
-  , "ds,r=t.query,i=this._indices={},o=this._dims=[],a=r.length;let s,u,l=0;for(;l<a;++l)s=n[l].fname,u=i[s]||(i[s]=SS()),o.push(FS(u,l,r[l]));return this.eval(t,e)},reinit(t,e){const n=e.materialize().fork(),r=t.fields,i=t.query,o=this._indices,a=this._dims,s=this.value,u=s.curr(),l=s.prev(),c=s.all(),f=n.rem=n.add,h=n.mod,d=i.length,p={};let g,m,y,v,_,x,b,w,k;if(l.set(u),e.rem.length&&(_=this.remove(t,e,n)),e.add.length&&s.add(e.add),e.mod.length)for(x={},v=e.mod,b=0,w=v.length;b<w;++b)x[v[b]._index]=1;for(b=0;b<d;++b)k=r[b],(!a[b]||t.modified(\"fields\",b)||e.modified(k.fields))&&(y=k.fname,(g=p[y])||(o[y]=m=SS(),p[y]=g=m.insert(k,e.source,0)),a[b]=FS(m,b,i[b]).onAdd(g,u));for(b=0,w=s.data().length;b<w;++b)_[b]||(l[b]!==u[b]?f.push(b):x[b]&&u[b]!==c&&h.push(b));return s.mask=(1<<d)-1,n},eval(t,e){const n=e.materialize().fork(),r=this._dims.length;let i=0;return e.rem.length&&(this.remove(t,e,n),i|=(1<<r)-1),t.modified(\"query\")&&!t.modified(\"fields\")&&(i|=this.update(t,e,n)),e.add.length&&(this.insert(t,e,n),i|=(1<<r)-1),e.mod.length&&(this.modify(e,n),i|=(1<<r)-1),this.value.mask=i,n},insert(t,e,n){const r=e.add,i=this.value,o=this._dims,a=this._indices,s=t.fields,u={},l=n.add,c=i.size()+r.length,f=o.length;let h,d,p,g=i.size();i.resize(c,f),i.add(r);const m=i.curr(),y=i.prev(),v=i.all();for(h=0;h<f;++h)d=s[h].fname,p=u[d]||(u[d]=a[d].insert(s[h],r,g)),o[h].onAdd(p,m);for(;g<c;++g)y[g]=v,m[g]!==v&&l.push(g)},modify(t,e){const n=e.mod,r=this.value,i=r.curr(),o=r.all(),a=t.mod;let s,u,l;for(s=0,u=a.length;s<u;++s)l=a[s]._index,i[l]!==o&&n.push(l)},remove(t,e,n){const r=this._indices,i=this.value,o=i.curr(),a=i.prev(),s=i.all(),u={},l=n.rem,c=e.rem;let f,h,d,p;for(f=0,h=c.length;f<h;++f)d=c[f]._index,u[d]=1,a[d]=p=o[d],o[d]=s,p!==s&&l.push(d);for(d in r)r[d].remove(h,u);return this.reindex(e,h,u),u},reindex(t,e,n){const r=this._indices,i=this.value;t.runAfter((()=>{const t=i.remove(e,n);for(const e in r)r[e].reindex(t)}))},update(t,e,n){const r=this._dims,i=t.query,o=e.stamp,a=r.length;let s,u,l=0;for(n.filters=0,u=0;u<a;++u)t.modified(\"query\",u)&&(s=u,++l);if(1===l)l=r[s].one,this.incrementOne(r[s],i[s],n.add,n.rem);else for(u=0,l=0;u<a;++u)t.modified(\"query\",u)&&(l|=r[u].one,this.incrementAll(r[u],i[u],o,n.add),n.rem=n.add);return l},incrementAll(t,e,n,r){const i=this.value,o=i.seen(),a=i.curr(),s=i.prev(),u=t.index(),l=t.bisect(t.range),c=t.bisect(e),f=c[0],h=c[1],d=l[0],p=l[1],g=t.one;let m,y,v;if(f<d)for(m=f,y=Math.min(d,h);m<y;++m)v=u[m],o[v]!==n&&(s[v]=a[v],o[v]=n,r.push(v)),a[v]^=g;else if(f>d)for(m=d,y=Math.min(f,p);m<y;++m)v=u[m],o[v]!==n&&(s[v]=a[v],o[v]=n,r.push(v)),a[v]^=g;if(h>p)for(m=Math.max(f,p),y=h;m<y;++m)v=u[m],o[v]!==n&&(s[v]=a[v],o[v]=n,r.push(v)),a[v]^=g;else if(h<p)for(m=Math.max(d,h),y=p;m<y;++m)v=u[m],o[v]!==n&&(s[v]=a[v],o[v]=n,r.push(v)),a[v]^=g;t.range=e.slice()},incrementOne(t,e,n,r){const i=this.value.curr(),o=t.index(),a=t.bisect(t.range),s=t.bisect(e),u=s[0],l=s[1],c=a[0],f=a[1],h=t.one;let d,p,g;if(u<c)for(d=u,p=Math.min(c,l);d<p;++d)g=o[d],i[g]^=h,n.push(g);else if(u>c)for(d=c,p=Math.min(u,f);d<p;++d)g=o[d],i[g]^=h,r.push(g);if(l>f)for(d=Math.max(u,f),p=l;d<p;++d)g=o[d],i[g]^=h,n.push(g);else if(l<f)for(d=Math.max(c,l),p=f;d<p;++d)g=o[d],i[g]^=h,r.push(g);t.range=e.slice()}}),TS.Definition={type:\"ResolveFilter\",metadata:{},params:[{name:\"ignore\",type:\"number\",required:!0,description:\"A bit mask indicating which filters to ignore.\"},{name:\"filter\",type:\"object\",required:!0,description:\"Per-tuple filter bitmaps from a CrossFilter transform.\"}]},dt(TS,Ja,{transform(t,e){const n=~(t.ignore||0),r=t.filter,i=r.mask;if(0==(i&n))return e.StopPropagation;const o=e.fork(e.ALL),a=r.data(),s=r.curr(),u=r.prev(),l=t=>s[t]&n?null:a[t];return o.filter(o.MOD,l),i&i-1?(o.filter(o.ADD,(t=>{const e=s[t]&n;return!e&&e^u[t]&n?a[t]:null})),o.filter(o.REM,(t=>{const e=s[t]&n;return e&&!(e^e^u[t]&n)?a[t]:null}))):(o.filter(o.ADD,l),o.filter(o.REM,(t=>(s[t]&n)===i?a[t]:null))),o.filter(o.SOURCE,(t=>l(t._index)))}});var BS=Object.freeze({__proto__:null,crossfilter:$S,resolvefilter:TS});const NS=\"Literal\",zS=\"Property\","
-  , "OS=\"ArrayExpression\",RS=\"BinaryExpression\",LS=\"CallExpression\",US=\"ConditionalExpression\",qS=\"LogicalExpression\",PS=\"MemberExpression\",jS=\"ObjectExpression\",IS=\"UnaryExpression\";function WS(t){this.type=t}var HS,YS,GS,VS,XS;WS.prototype.visit=function(t){let e,n,r;if(t(this))return 1;for(e=function(t){switch(t.type){case OS:return t.elements;case RS:case qS:return[t.left,t.right];case LS:return[t.callee].concat(t.arguments);case US:return[t.test,t.consequent,t.alternate];case PS:return[t.object,t.property];case jS:return t.properties;case zS:return[t.key,t.value];case IS:return[t.argument];default:return[]}}(this),n=0,r=e.length;n<r;++n)if(e[n].visit(t))return 1};var JS=1,ZS=2,QS=3,KS=4,t$=5,e$=6,n$=7,r$=8;(HS={})[JS]=\"Boolean\",HS[ZS]=\"<end>\",HS[QS]=\"Identifier\",HS[KS]=\"Keyword\",HS[t$]=\"Null\",HS[e$]=\"Numeric\",HS[n$]=\"Punctuator\",HS[r$]=\"String\",HS[9]=\"RegularExpression\";var i$=\"ArrayExpression\",o$=\"BinaryExpression\",a$=\"CallExpression\",s$=\"ConditionalExpression\",u$=\"Identifier\",l$=\"Literal\",c$=\"LogicalExpression\",f$=\"MemberExpression\",h$=\"ObjectExpression\",d$=\"Property\",p$=\"UnaryExpression\",g$=\"Unexpected token %0\",m$=\"Unexpected number\",y$=\"Unexpected string\",v$=\"Unexpected identifier\",_$=\"Unexpected reserved word\",x$=\"Unexpected end of input\",b$=\"Invalid regular expression\",w$=\"Invalid regular expression: missing /\",k$=\"Octal literals are not allowed in strict mode.\",A$=\"Duplicate data property in object literal not allowed in strict mode\",M$=\"ILLEGAL\",E$=\"Disabled.\",D$=new RegExp(\"[\\\\xAA\\\\xB5\\\\xBA\\\\xC0-\\\\xD6\\\\xD8-\\\\xF6\\\\xF8-\\\\u02C1\\\\u02C6-\\\\u02D1\\\\u02E0-\\\\u02E4\\\\u02EC\\\\u02EE\\\\u0370-\\\\u0374\\\\u0376\\\\u0377\\\\u037A-\\\\u037D\\\\u037F\\\\u0386\\\\u0388-\\\\u038A\\\\u038C\\\\u038E-\\\\u03A1\\\\u03A3-\\\\u03F5\\\\u03F7-\\\\u0481\\\\u048A-\\\\u052F\\\\u0531-\\\\u0556\\\\u0559\\\\u0561-\\\\u0587\\\\u05D0-\\\\u05EA\\\\u05F0-\\\\u05F2\\\\u0620-\\\\u064A\\\\u066E\\\\u066F\\\\u0671-\\\\u06D3\\\\u06D5\\\\u06E5\\\\u06E6\\\\u06EE\\\\u06EF\\\\u06FA-\\\\u06FC\\\\u06FF\\\\u0710\\\\u0712-\\\\u072F\\\\u074D-\\\\u07A5\\\\u07B1\\\\u07CA-\\\\u07EA\\\\u07F4\\\\u07F5\\\\u07FA\\\\u0800-\\\\u0815\\\\u081A\\\\u0824\\\\u0828\\\\u0840-\\\\u0858\\\\u08A0-\\\\u08B2\\\\u0904-\\\\u0939\\\\u093D\\\\u0950\\\\u0958-\\\\u0961\\\\u0971-\\\\u0980\\\\u0985-\\\\u098C\\\\u098F\\\\u0990\\\\u0993-\\\\u09A8\\\\u09AA-\\\\u09B0\\\\u09B2\\\\u09B6-\\\\u09B9\\\\u09BD\\\\u09CE\\\\u09DC\\\\u09DD\\\\u09DF-\\\\u09E1\\\\u09F0\\\\u09F1\\\\u0A05-\\\\u0A0A\\\\u0A0F\\\\u0A10\\\\u0A13-\\\\u0A28\\\\u0A2A-\\\\u0A30\\\\u0A32\\\\u0A33\\\\u0A35\\\\u0A36\\\\u0A38\\\\u0A39\\\\u0A59-\\\\u0A5C\\\\u0A5E\\\\u0A72-\\\\u0A74\\\\u0A85-\\\\u0A8D\\\\u0A8F-\\\\u0A91\\\\u0A93-\\\\u0AA8\\\\u0AAA-\\\\u0AB0\\\\u0AB2\\\\u0AB3\\\\u0AB5-\\\\u0AB9\\\\u0ABD\\\\u0AD0\\\\u0AE0\\\\u0AE1\\\\u0B05-\\\\u0B0C\\\\u0B0F\\\\u0B10\\\\u0B13-\\\\u0B28\\\\u0B2A-\\\\u0B30\\\\u0B32\\\\u0B33\\\\u0B35-\\\\u0B39\\\\u0B3D\\\\u0B5C\\\\u0B5D\\\\u0B5F-\\\\u0B61\\\\u0B71\\\\u0B83\\\\u0B85-\\\\u0B8A\\\\u0B8E-\\\\u0B90\\\\u0B92-\\\\u0B95\\\\u0B99\\\\u0B9A\\\\u0B9C\\\\u0B9E\\\\u0B9F\\\\u0BA3\\\\u0BA4\\\\u0BA8-\\\\u0BAA\\\\u0BAE-\\\\u0BB9\\\\u0BD0\\\\u0C05-\\\\u0C0C\\\\u0C0E-\\\\u0C10\\\\u0C12-\\\\u0C28\\\\u0C2A-\\\\u0C39\\\\u0C3D\\\\u0C58\\\\u0C59\\\\u0C60\\\\u0C61\\\\u0C85-\\\\u0C8C\\\\u0C8E-\\\\u0C90\\\\u0C92-\\\\u0CA8\\\\u0CAA-\\\\u0CB3\\\\u0CB5-\\\\u0CB9\\\\u0CBD\\\\u0CDE\\\\u0CE0\\\\u0CE1\\\\u0CF1\\\\u0CF2\\\\u0D05-\\\\u0D0C\\\\u0D0E-\\\\u0D10\\\\u0D12-\\\\u0D3A\\\\u0D3D\\\\u0D4E\\\\u0D60\\\\u0D61\\\\u0D7A-\\\\u0D7F\\\\u0D85-\\\\u0D96\\\\u0D9A-\\\\u0DB1\\\\u0DB3-\\\\u0DBB\\\\u0DBD\\\\u0DC0-\\\\u0DC6\\\\u0E01-\\\\u0E30\\\\u0E32\\\\u0E33\\\\u0E40-\\\\u0E46\\\\u0E81\\\\u0E82\\\\u0E84\\\\u0E87\\\\u0E88\\\\u0E8A\\\\u0E8D\\\\u0E94-\\\\u0E97\\\\u0E99-\\\\u0E9F\\\\u0EA1-\\\\u0EA3\\\\u0EA5\\\\u0EA7\\\\u0EAA\\\\u0EAB\\\\u0EAD-\\\\u0EB0\\\\u0EB2\\\\u0EB3\\\\u0EBD\\\\u0EC0-\\\\u0EC4\\\\u0EC6\\\\u0EDC-\\\\u0EDF\\\\u0F00\\\\u0F40-\\\\u0F47\\\\u0F49-\\\\u0F6C\\\\u0F88-\\\\u0F8C\\\\u1000-\\\\u102A\\\\u103F\\\\u1050-\\\\u1055\\\\u105A-\\\\u105D\\\\u1061\\\\u1065\\\\u1066\\\\u106E-\\\\u1070\\\\u1075-\\\\u1081\\\\u108E\\\\u10A0-\\\\u10C5\\\\u10C7\\\\u10CD\\\\u10D0-\\\\u10FA\\\\u10FC-\\\\u1248\\\\u124A-\\\\u124D\\\\u1250-\\\\u1256\\\\u1258\\\\u125A-\\\\u125D\\\\u1260-\\\\u1288\\\\u128A-\\\\u128D\\\\u1290-\\\\u12B0\\\\u12B2-\\\\u12B5\\\\u12B8-\\\\u12BE\\\\u12C0\\\\u12C2-\\\\u12C5\\\\u12C8-\\\\u12D6\\\\u12D8-\\\\u1310\\\\u1312-\\\\u1315\\\\u1318-\\\\u135A\\\\u1380-\\\\u138F\\\\u13A0-\\\\u13F4\\\\u1401-\\\\u166C\\\\u166F-\\\\u167F\\\\u1681-\\\\u169A\\\\u16A0-\\\\u16EA\\\\u16EE-\\\\u16F8\\\\u1700-\\\\u170C\\\\u170E-\\\\u1711\\\\u1720-\\\\u1731\\\\u1740-\\\\u1751\\\\u1760-\\\\u176C\\\\u176E-\\\\u1770\\\\u1780-\\\\u17B3\\\\u17D7\\\\u17DC\\\\u1820-\\\\u1877\\\\u1880-\\\\u18A8\\\\u18AA\\\\u18B0-\\\\u18F5\\\\u19"
-  , "00-\\\\u191E\\\\u1950-\\\\u196D\\\\u1970-\\\\u1974\\\\u1980-\\\\u19AB\\\\u19C1-\\\\u19C7\\\\u1A00-\\\\u1A16\\\\u1A20-\\\\u1A54\\\\u1AA7\\\\u1B05-\\\\u1B33\\\\u1B45-\\\\u1B4B\\\\u1B83-\\\\u1BA0\\\\u1BAE\\\\u1BAF\\\\u1BBA-\\\\u1BE5\\\\u1C00-\\\\u1C23\\\\u1C4D-\\\\u1C4F\\\\u1C5A-\\\\u1C7D\\\\u1CE9-\\\\u1CEC\\\\u1CEE-\\\\u1CF1\\\\u1CF5\\\\u1CF6\\\\u1D00-\\\\u1DBF\\\\u1E00-\\\\u1F15\\\\u1F18-\\\\u1F1D\\\\u1F20-\\\\u1F45\\\\u1F48-\\\\u1F4D\\\\u1F50-\\\\u1F57\\\\u1F59\\\\u1F5B\\\\u1F5D\\\\u1F5F-\\\\u1F7D\\\\u1F80-\\\\u1FB4\\\\u1FB6-\\\\u1FBC\\\\u1FBE\\\\u1FC2-\\\\u1FC4\\\\u1FC6-\\\\u1FCC\\\\u1FD0-\\\\u1FD3\\\\u1FD6-\\\\u1FDB\\\\u1FE0-\\\\u1FEC\\\\u1FF2-\\\\u1FF4\\\\u1FF6-\\\\u1FFC\\\\u2071\\\\u207F\\\\u2090-\\\\u209C\\\\u2102\\\\u2107\\\\u210A-\\\\u2113\\\\u2115\\\\u2119-\\\\u211D\\\\u2124\\\\u2126\\\\u2128\\\\u212A-\\\\u212D\\\\u212F-\\\\u2139\\\\u213C-\\\\u213F\\\\u2145-\\\\u2149\\\\u214E\\\\u2160-\\\\u2188\\\\u2C00-\\\\u2C2E\\\\u2C30-\\\\u2C5E\\\\u2C60-\\\\u2CE4\\\\u2CEB-\\\\u2CEE\\\\u2CF2\\\\u2CF3\\\\u2D00-\\\\u2D25\\\\u2D27\\\\u2D2D\\\\u2D30-\\\\u2D67\\\\u2D6F\\\\u2D80-\\\\u2D96\\\\u2DA0-\\\\u2DA6\\\\u2DA8-\\\\u2DAE\\\\u2DB0-\\\\u2DB6\\\\u2DB8-\\\\u2DBE\\\\u2DC0-\\\\u2DC6\\\\u2DC8-\\\\u2DCE\\\\u2DD0-\\\\u2DD6\\\\u2DD8-\\\\u2DDE\\\\u2E2F\\\\u3005-\\\\u3007\\\\u3021-\\\\u3029\\\\u3031-\\\\u3035\\\\u3038-\\\\u303C\\\\u3041-\\\\u3096\\\\u309D-\\\\u309F\\\\u30A1-\\\\u30FA\\\\u30FC-\\\\u30FF\\\\u3105-\\\\u312D\\\\u3131-\\\\u318E\\\\u31A0-\\\\u31BA\\\\u31F0-\\\\u31FF\\\\u3400-\\\\u4DB5\\\\u4E00-\\\\u9FCC\\\\uA000-\\\\uA48C\\\\uA4D0-\\\\uA4FD\\\\uA500-\\\\uA60C\\\\uA610-\\\\uA61F\\\\uA62A\\\\uA62B\\\\uA640-\\\\uA66E\\\\uA67F-\\\\uA69D\\\\uA6A0-\\\\uA6EF\\\\uA717-\\\\uA71F\\\\uA722-\\\\uA788\\\\uA78B-\\\\uA78E\\\\uA790-\\\\uA7AD\\\\uA7B0\\\\uA7B1\\\\uA7F7-\\\\uA801\\\\uA803-\\\\uA805\\\\uA807-\\\\uA80A\\\\uA80C-\\\\uA822\\\\uA840-\\\\uA873\\\\uA882-\\\\uA8B3\\\\uA8F2-\\\\uA8F7\\\\uA8FB\\\\uA90A-\\\\uA925\\\\uA930-\\\\uA946\\\\uA960-\\\\uA97C\\\\uA984-\\\\uA9B2\\\\uA9CF\\\\uA9E0-\\\\uA9E4\\\\uA9E6-\\\\uA9EF\\\\uA9FA-\\\\uA9FE\\\\uAA00-\\\\uAA28\\\\uAA40-\\\\uAA42\\\\uAA44-\\\\uAA4B\\\\uAA60-\\\\uAA76\\\\uAA7A\\\\uAA7E-\\\\uAAAF\\\\uAAB1\\\\uAAB5\\\\uAAB6\\\\uAAB9-\\\\uAABD\\\\uAAC0\\\\uAAC2\\\\uAADB-\\\\uAADD\\\\uAAE0-\\\\uAAEA\\\\uAAF2-\\\\uAAF4\\\\uAB01-\\\\uAB06\\\\uAB09-\\\\uAB0E\\\\uAB11-\\\\uAB16\\\\uAB20-\\\\uAB26\\\\uAB28-\\\\uAB2E\\\\uAB30-\\\\uAB5A\\\\uAB5C-\\\\uAB5F\\\\uAB64\\\\uAB65\\\\uABC0-\\\\uABE2\\\\uAC00-\\\\uD7A3\\\\uD7B0-\\\\uD7C6\\\\uD7CB-\\\\uD7FB\\\\uF900-\\\\uFA6D\\\\uFA70-\\\\uFAD9\\\\uFB00-\\\\uFB06\\\\uFB13-\\\\uFB17\\\\uFB1D\\\\uFB1F-\\\\uFB28\\\\uFB2A-\\\\uFB36\\\\uFB38-\\\\uFB3C\\\\uFB3E\\\\uFB40\\\\uFB41\\\\uFB43\\\\uFB44\\\\uFB46-\\\\uFBB1\\\\uFBD3-\\\\uFD3D\\\\uFD50-\\\\uFD8F\\\\uFD92-\\\\uFDC7\\\\uFDF0-\\\\uFDFB\\\\uFE70-\\\\uFE74\\\\uFE76-\\\\uFEFC\\\\uFF21-\\\\uFF3A\\\\uFF41-\\\\uFF5A\\\\uFF66-\\\\uFFBE\\\\uFFC2-\\\\uFFC7\\\\uFFCA-\\\\uFFCF\\\\uFFD2-\\\\uFFD7\\\\uFFDA-\\\\uFFDC]\"),C$=new RegExp(\"[\\\\xAA\\\\xB5\\\\xBA\\\\xC0-\\\\xD6\\\\xD8-\\\\xF6\\\\xF8-\\\\u02C1\\\\u02C6-\\\\u02D1\\\\u02E0-\\\\u02E4\\\\u02EC\\\\u02EE\\\\u0300-\\\\u0374\\\\u0376\\\\u0377\\\\u037A-\\\\u037D\\\\u037F\\\\u0386\\\\u0388-\\\\u038A\\\\u038C\\\\u038E-\\\\u03A1\\\\u03A3-\\\\u03F5\\\\u03F7-\\\\u0481\\\\u0483-\\\\u0487\\\\u048A-\\\\u052F\\\\u0531-\\\\u0556\\\\u0559\\\\u0561-\\\\u0587\\\\u0591-\\\\u05BD\\\\u05BF\\\\u05C1\\\\u05C2\\\\u05C4\\\\u05C5\\\\u05C7\\\\u05D0-\\\\u05EA\\\\u05F0-\\\\u05F2\\\\u0610-\\\\u061A\\\\u0620-\\\\u0669\\\\u066E-\\\\u06D3\\\\u06D5-\\\\u06DC\\\\u06DF-\\\\u06E8\\\\u06EA-\\\\u06FC\\\\u06FF\\\\u0710-\\\\u074A\\\\u074D-\\\\u07B1\\\\u07C0-\\\\u07F5\\\\u07FA\\\\u0800-\\\\u082D\\\\u0840-\\\\u085B\\\\u08A0-\\\\u08B2\\\\u08E4-\\\\u0963\\\\u0966-\\\\u096F\\\\u0971-\\\\u0983\\\\u0985-\\\\u098C\\\\u098F\\\\u0990\\\\u0993-\\\\u09A8\\\\u09AA-\\\\u09B0\\\\u09B2\\\\u09B6-\\\\u09B9\\\\u09BC-\\\\u09C4\\\\u09C7\\\\u09C8\\\\u09CB-\\\\u09CE\\\\u09D7\\\\u09DC\\\\u09DD\\\\u09DF-\\\\u09E3\\\\u09E6-\\\\u09F1\\\\u0A01-\\\\u0A03\\\\u0A05-\\\\u0A0A\\\\u0A0F\\\\u0A10\\\\u0A13-\\\\u0A28\\\\u0A2A-\\\\u0A30\\\\u0A32\\\\u0A33\\\\u0A35\\\\u0A36\\\\u0A38\\\\u0A39\\\\u0A3C\\\\u0A3E-\\\\u0A42\\\\u0A47\\\\u0A48\\\\u0A4B-\\\\u0A4D\\\\u0A51\\\\u0A59-\\\\u0A5C\\\\u0A5E\\\\u0A66-\\\\u0A75\\\\u0A81-\\\\u0A83\\\\u0A85-\\\\u0A8D\\\\u0A8F-\\\\u0A91\\\\u0A93-\\\\u0AA8\\\\u0AAA-\\\\u0AB0\\\\u0AB2\\\\u0AB3\\\\u0AB5-\\\\u0AB9\\\\u0ABC-\\\\u0AC5\\\\u0AC7-\\\\u0AC9\\\\u0ACB-\\\\u0ACD\\\\u0AD0\\\\u0AE0-\\\\u0AE3\\\\u0AE6-\\\\u0AEF\\\\u0B01-\\\\u0B03\\\\u0B05-\\\\u0B0C\\\\u0B0F\\\\u0B10\\\\u0B13-\\\\u0B28\\\\u0B2A-\\\\u0B30\\\\u0B32\\\\u0B33\\\\u0B35-\\\\u0B39\\\\u0B3C-\\\\u0B44\\\\u0B47\\\\u0B48\\\\u0B4B-\\\\u0B4D\\\\u0B56\\\\u0B57\\\\u0B5C\\\\u0B5D\\\\u0B5F-\\\\u0B63\\\\u0B66-\\\\u0B6F\\\\u0B71\\\\u0B82\\\\u0B83\\\\u0B85-\\\\u0B8A\\\\u0B8E-\\\\u0B90\\\\u0B92-\\\\u0B95\\\\u0B99\\\\u0B9A\\\\u0B9C\\\\u0B9E\\\\u0B9F\\\\u0BA3\\\\u0BA4\\\\u0BA8-\\\\u0BAA\\\\u0BAE-\\\\u0BB9\\\\u0BBE-\\\\u0BC2\\\\u0BC6-\\\\u0BC8\\\\u0BCA-\\\\u0BCD\\\\u0BD0\\\\u0BD7\\\\u0BE6-\\\\u0BEF\\\\u0C00-\\\\u0C03\\\\u0C05-\\\\u0C0C\\\\u0C0E-\\\\u0C10\\\\u0C12-\\\\u0C28\\\\u0C2A-\\\\u0C39\\\\u0C3D-\\\\u0C44\\\\u0C46-\\\\u0C48\\\\u0C4A-\\\\u0C4D\\\\u0C55\\\\u0C56\\\\u0C"
-  , "58\\\\u0C59\\\\u0C60-\\\\u0C63\\\\u0C66-\\\\u0C6F\\\\u0C81-\\\\u0C83\\\\u0C85-\\\\u0C8C\\\\u0C8E-\\\\u0C90\\\\u0C92-\\\\u0CA8\\\\u0CAA-\\\\u0CB3\\\\u0CB5-\\\\u0CB9\\\\u0CBC-\\\\u0CC4\\\\u0CC6-\\\\u0CC8\\\\u0CCA-\\\\u0CCD\\\\u0CD5\\\\u0CD6\\\\u0CDE\\\\u0CE0-\\\\u0CE3\\\\u0CE6-\\\\u0CEF\\\\u0CF1\\\\u0CF2\\\\u0D01-\\\\u0D03\\\\u0D05-\\\\u0D0C\\\\u0D0E-\\\\u0D10\\\\u0D12-\\\\u0D3A\\\\u0D3D-\\\\u0D44\\\\u0D46-\\\\u0D48\\\\u0D4A-\\\\u0D4E\\\\u0D57\\\\u0D60-\\\\u0D63\\\\u0D66-\\\\u0D6F\\\\u0D7A-\\\\u0D7F\\\\u0D82\\\\u0D83\\\\u0D85-\\\\u0D96\\\\u0D9A-\\\\u0DB1\\\\u0DB3-\\\\u0DBB\\\\u0DBD\\\\u0DC0-\\\\u0DC6\\\\u0DCA\\\\u0DCF-\\\\u0DD4\\\\u0DD6\\\\u0DD8-\\\\u0DDF\\\\u0DE6-\\\\u0DEF\\\\u0DF2\\\\u0DF3\\\\u0E01-\\\\u0E3A\\\\u0E40-\\\\u0E4E\\\\u0E50-\\\\u0E59\\\\u0E81\\\\u0E82\\\\u0E84\\\\u0E87\\\\u0E88\\\\u0E8A\\\\u0E8D\\\\u0E94-\\\\u0E97\\\\u0E99-\\\\u0E9F\\\\u0EA1-\\\\u0EA3\\\\u0EA5\\\\u0EA7\\\\u0EAA\\\\u0EAB\\\\u0EAD-\\\\u0EB9\\\\u0EBB-\\\\u0EBD\\\\u0EC0-\\\\u0EC4\\\\u0EC6\\\\u0EC8-\\\\u0ECD\\\\u0ED0-\\\\u0ED9\\\\u0EDC-\\\\u0EDF\\\\u0F00\\\\u0F18\\\\u0F19\\\\u0F20-\\\\u0F29\\\\u0F35\\\\u0F37\\\\u0F39\\\\u0F3E-\\\\u0F47\\\\u0F49-\\\\u0F6C\\\\u0F71-\\\\u0F84\\\\u0F86-\\\\u0F97\\\\u0F99-\\\\u0FBC\\\\u0FC6\\\\u1000-\\\\u1049\\\\u1050-\\\\u109D\\\\u10A0-\\\\u10C5\\\\u10C7\\\\u10CD\\\\u10D0-\\\\u10FA\\\\u10FC-\\\\u1248\\\\u124A-\\\\u124D\\\\u1250-\\\\u1256\\\\u1258\\\\u125A-\\\\u125D\\\\u1260-\\\\u1288\\\\u128A-\\\\u128D\\\\u1290-\\\\u12B0\\\\u12B2-\\\\u12B5\\\\u12B8-\\\\u12BE\\\\u12C0\\\\u12C2-\\\\u12C5\\\\u12C8-\\\\u12D6\\\\u12D8-\\\\u1310\\\\u1312-\\\\u1315\\\\u1318-\\\\u135A\\\\u135D-\\\\u135F\\\\u1380-\\\\u138F\\\\u13A0-\\\\u13F4\\\\u1401-\\\\u166C\\\\u166F-\\\\u167F\\\\u1681-\\\\u169A\\\\u16A0-\\\\u16EA\\\\u16EE-\\\\u16F8\\\\u1700-\\\\u170C\\\\u170E-\\\\u1714\\\\u1720-\\\\u1734\\\\u1740-\\\\u1753\\\\u1760-\\\\u176C\\\\u176E-\\\\u1770\\\\u1772\\\\u1773\\\\u1780-\\\\u17D3\\\\u17D7\\\\u17DC\\\\u17DD\\\\u17E0-\\\\u17E9\\\\u180B-\\\\u180D\\\\u1810-\\\\u1819\\\\u1820-\\\\u1877\\\\u1880-\\\\u18AA\\\\u18B0-\\\\u18F5\\\\u1900-\\\\u191E\\\\u1920-\\\\u192B\\\\u1930-\\\\u193B\\\\u1946-\\\\u196D\\\\u1970-\\\\u1974\\\\u1980-\\\\u19AB\\\\u19B0-\\\\u19C9\\\\u19D0-\\\\u19D9\\\\u1A00-\\\\u1A1B\\\\u1A20-\\\\u1A5E\\\\u1A60-\\\\u1A7C\\\\u1A7F-\\\\u1A89\\\\u1A90-\\\\u1A99\\\\u1AA7\\\\u1AB0-\\\\u1ABD\\\\u1B00-\\\\u1B4B\\\\u1B50-\\\\u1B59\\\\u1B6B-\\\\u1B73\\\\u1B80-\\\\u1BF3\\\\u1C00-\\\\u1C37\\\\u1C40-\\\\u1C49\\\\u1C4D-\\\\u1C7D\\\\u1CD0-\\\\u1CD2\\\\u1CD4-\\\\u1CF6\\\\u1CF8\\\\u1CF9\\\\u1D00-\\\\u1DF5\\\\u1DFC-\\\\u1F15\\\\u1F18-\\\\u1F1D\\\\u1F20-\\\\u1F45\\\\u1F48-\\\\u1F4D\\\\u1F50-\\\\u1F57\\\\u1F59\\\\u1F5B\\\\u1F5D\\\\u1F5F-\\\\u1F7D\\\\u1F80-\\\\u1FB4\\\\u1FB6-\\\\u1FBC\\\\u1FBE\\\\u1FC2-\\\\u1FC4\\\\u1FC6-\\\\u1FCC\\\\u1FD0-\\\\u1FD3\\\\u1FD6-\\\\u1FDB\\\\u1FE0-\\\\u1FEC\\\\u1FF2-\\\\u1FF4\\\\u1FF6-\\\\u1FFC\\\\u200C\\\\u200D\\\\u203F\\\\u2040\\\\u2054\\\\u2071\\\\u207F\\\\u2090-\\\\u209C\\\\u20D0-\\\\u20DC\\\\u20E1\\\\u20E5-\\\\u20F0\\\\u2102\\\\u2107\\\\u210A-\\\\u2113\\\\u2115\\\\u2119-\\\\u211D\\\\u2124\\\\u2126\\\\u2128\\\\u212A-\\\\u212D\\\\u212F-\\\\u2139\\\\u213C-\\\\u213F\\\\u2145-\\\\u2149\\\\u214E\\\\u2160-\\\\u2188\\\\u2C00-\\\\u2C2E\\\\u2C30-\\\\u2C5E\\\\u2C60-\\\\u2CE4\\\\u2CEB-\\\\u2CF3\\\\u2D00-\\\\u2D25\\\\u2D27\\\\u2D2D\\\\u2D30-\\\\u2D67\\\\u2D6F\\\\u2D7F-\\\\u2D96\\\\u2DA0-\\\\u2DA6\\\\u2DA8-\\\\u2DAE\\\\u2DB0-\\\\u2DB6\\\\u2DB8-\\\\u2DBE\\\\u2DC0-\\\\u2DC6\\\\u2DC8-\\\\u2DCE\\\\u2DD0-\\\\u2DD6\\\\u2DD8-\\\\u2DDE\\\\u2DE0-\\\\u2DFF\\\\u2E2F\\\\u3005-\\\\u3007\\\\u3021-\\\\u302F\\\\u3031-\\\\u3035\\\\u3038-\\\\u303C\\\\u3041-\\\\u3096\\\\u3099\\\\u309A\\\\u309D-\\\\u309F\\\\u30A1-\\\\u30FA\\\\u30FC-\\\\u30FF\\\\u3105-\\\\u312D\\\\u3131-\\\\u318E\\\\u31A0-\\\\u31BA\\\\u31F0-\\\\u31FF\\\\u3400-\\\\u4DB5\\\\u4E00-\\\\u9FCC\\\\uA000-\\\\uA48C\\\\uA4D0-\\\\uA4FD\\\\uA500-\\\\uA60C\\\\uA610-\\\\uA62B\\\\uA640-\\\\uA66F\\\\uA674-\\\\uA67D\\\\uA67F-\\\\uA69D\\\\uA69F-\\\\uA6F1\\\\uA717-\\\\uA71F\\\\uA722-\\\\uA788\\\\uA78B-\\\\uA78E\\\\uA790-\\\\uA7AD\\\\uA7B0\\\\uA7B1\\\\uA7F7-\\\\uA827\\\\uA840-\\\\uA873\\\\uA880-\\\\uA8C4\\\\uA8D0-\\\\uA8D9\\\\uA8E0-\\\\uA8F7\\\\uA8FB\\\\uA900-\\\\uA92D\\\\uA930-\\\\uA953\\\\uA960-\\\\uA97C\\\\uA980-\\\\uA9C0\\\\uA9CF-\\\\uA9D9\\\\uA9E0-\\\\uA9FE\\\\uAA00-\\\\uAA36\\\\uAA40-\\\\uAA4D\\\\uAA50-\\\\uAA59\\\\uAA60-\\\\uAA76\\\\uAA7A-\\\\uAAC2\\\\uAADB-\\\\uAADD\\\\uAAE0-\\\\uAAEF\\\\uAAF2-\\\\uAAF6\\\\uAB01-\\\\uAB06\\\\uAB09-\\\\uAB0E\\\\uAB11-\\\\uAB16\\\\uAB20-\\\\uAB26\\\\uAB28-\\\\uAB2E\\\\uAB30-\\\\uAB5A\\\\uAB5C-\\\\uAB5F\\\\uAB64\\\\uAB65\\\\uABC0-\\\\uABEA\\\\uABEC\\\\uABED\\\\uABF0-\\\\uABF9\\\\uAC00-\\\\uD7A3\\\\uD7B0-\\\\uD7C6\\\\uD7CB-\\\\uD7FB\\\\uF900-\\\\uFA6D\\\\uFA70-\\\\uFAD9\\\\uFB00-\\\\uFB06\\\\uFB13-\\\\uFB17\\\\uFB1D-\\\\uFB28\\\\uFB2A-\\\\uFB36\\\\uFB38-\\\\uFB3C\\\\uFB3E\\\\uFB40\\\\uFB41\\\\uFB43\\\\uFB44\\\\uFB46-\\\\uFBB1\\\\uFBD3-\\\\uFD3D\\\\uFD50-\\\\uFD8F\\\\uFD92-\\\\uFDC7\\\\uFDF0-\\\\uFDFB\\\\uFE00-\\\\uFE0F\\\\uFE20-\\\\uFE2D\\\\uFE33\\\\uFE34\\\\uFE4D-\\\\uFE4F\\\\uFE70-\\\\uFE74\\\\uFE76-\\\\uFEFC\\\\uFF10-\\\\uFF19\\\\uFF21-\\\\uFF3A\\\\uFF3F\\\\uFF41-\\\\uFF5A\\\\uFF66-\\\\uFFBE\\\\uFFC2-\\\\uFFC7\\\\uFFCA-\\\\uFFCF\\\\uFFD2-\\\\uFFD7\\\\uF"
-  , "FDA-\\\\uFFDC]\");function F$(t,e){if(!t)throw new Error(\"ASSERT: \"+e)}function S$(t){return t>=48&&t<=57}function $$(t){return\"0123456789abcdefABCDEF\".includes(t)}function T$(t){return\"01234567\".includes(t)}function B$(t){return 32===t||9===t||11===t||12===t||160===t||t>=5760&&[5760,6158,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8239,8287,12288,65279].includes(t)}function N$(t){return 10===t||13===t||8232===t||8233===t}function z$(t){return 36===t||95===t||t>=65&&t<=90||t>=97&&t<=122||92===t||t>=128&&D$.test(String.fromCharCode(t))}function O$(t){return 36===t||95===t||t>=65&&t<=90||t>=97&&t<=122||t>=48&&t<=57||92===t||t>=128&&C$.test(String.fromCharCode(t))}const R$={if:1,in:1,do:1,var:1,for:1,new:1,try:1,let:1,this:1,else:1,case:1,void:1,with:1,enum:1,while:1,break:1,catch:1,throw:1,const:1,yield:1,class:1,super:1,return:1,typeof:1,delete:1,switch:1,export:1,import:1,public:1,static:1,default:1,finally:1,extends:1,package:1,private:1,function:1,continue:1,debugger:1,interface:1,protected:1,instanceof:1,implements:1};function L$(){for(;GS<VS;){const t=YS.charCodeAt(GS);if(!B$(t)&&!N$(t))break;++GS}}function U$(t){var e,n,r,i=0;for(n=\"u\"===t?4:2,e=0;e<n;++e)GS<VS&&$$(YS[GS])?(r=YS[GS++],i=16*i+\"0123456789abcdef\".indexOf(r.toLowerCase())):eT({},g$,M$);return String.fromCharCode(i)}function q$(){var t,e,n,r;for(e=0,\"}\"===(t=YS[GS])&&eT({},g$,M$);GS<VS&&$$(t=YS[GS++]);)e=16*e+\"0123456789abcdef\".indexOf(t.toLowerCase());return(e>1114111||\"}\"!==t)&&eT({},g$,M$),e<=65535?String.fromCharCode(e):(n=55296+(e-65536>>10),r=56320+(e-65536&1023),String.fromCharCode(n,r))}function P$(){var t,e;for(t=YS.charCodeAt(GS++),e=String.fromCharCode(t),92===t&&(117!==YS.charCodeAt(GS)&&eT({},g$,M$),++GS,(t=U$(\"u\"))&&\"\\\\\"!==t&&z$(t.charCodeAt(0))||eT({},g$,M$),e=t);GS<VS&&O$(t=YS.charCodeAt(GS));)++GS,e+=String.fromCharCode(t),92===t&&(e=e.substr(0,e.length-1),117!==YS.charCodeAt(GS)&&eT({},g$,M$),++GS,(t=U$(\"u\"))&&\"\\\\\"!==t&&O$(t.charCodeAt(0))||eT({},g$,M$),e+=t);return e}function j$(){var t,e;return t=GS,e=92===YS.charCodeAt(GS)?P$():function(){var t,e;for(t=GS++;GS<VS;){if(92===(e=YS.charCodeAt(GS)))return GS=t,P$();if(!O$(e))break;++GS}return YS.slice(t,GS)}(),{type:1===e.length?QS:R$.hasOwnProperty(e)?KS:\"null\"===e?t$:\"true\"===e||\"false\"===e?JS:QS,value:e,start:t,end:GS}}function I$(){var t,e,n,r,i=GS,o=YS.charCodeAt(GS),a=YS[GS];switch(o){case 46:case 40:case 41:case 59:case 44:case 123:case 125:case 91:case 93:case 58:case 63:case 126:return++GS,{type:n$,value:String.fromCharCode(o),start:i,end:GS};default:if(61===(t=YS.charCodeAt(GS+1)))switch(o){case 43:case 45:case 47:case 60:case 62:case 94:case 124:case 37:case 38:case 42:return GS+=2,{type:n$,value:String.fromCharCode(o)+String.fromCharCode(t),start:i,end:GS};case 33:case 61:return GS+=2,61===YS.charCodeAt(GS)&&++GS,{type:n$,value:YS.slice(i,GS),start:i,end:GS}}}return\">>>=\"===(r=YS.substr(GS,4))?{type:n$,value:r,start:i,end:GS+=4}:\">>>\"===(n=r.substr(0,3))||\"<<=\"===n||\">>=\"===n?{type:n$,value:n,start:i,end:GS+=3}:a===(e=n.substr(0,2))[1]&&\"+-<>&|\".includes(a)||\"=>\"===e?{type:n$,value:e,start:i,end:GS+=2}:(\"//\"===e&&eT({},g$,M$),\"<>=!+-*%&|^/\".includes(a)?(++GS,{type:n$,value:a,start:i,end:GS}):void eT({},g$,M$))}function W$(){var t,e,n;if(F$(S$((n=YS[GS]).charCodeAt(0))||\".\"===n,\"Numeric literal must start with a decimal digit or a decimal point\"),e=GS,t=\"\",\".\"!==n){if(t=YS[GS++],n=YS[GS],\"0\"===t){if(\"x\"===n||\"X\"===n)return++GS,function(t){let e=\"\";for(;GS<VS&&$$(YS[GS]);)e+=YS[GS++];return 0===e.length&&eT({},g$,M$),z$(YS.charCodeAt(GS))&&eT({},g$,M$),{type:e$,value:parseInt(\"0x\"+e,16),start:t,end:GS}}(e);if(T$(n))return function(t){let e=\"0\"+YS[GS++];for(;GS<VS&&T$(YS[GS]);)e+=YS[GS++];return(z$(YS.charCodeAt(GS))||S$(YS.charCodeAt(GS)))&&eT({},g$,M$),{type:e$,value:parseInt(e,8),octal:!0,start:t,end:GS}}(e);n&&S$(n.charCodeAt(0))&&eT({},g$,M$)}for(;S$(YS.charCodeAt(GS));)t+=YS[GS++];n=YS[GS]}if(\".\"===n){for(t+=YS[GS++];S$(YS.charCodeAt(GS));)t+=YS[GS++];n=YS[GS]}if(\"e\"===n||\"E\"===n)if(t+=YS[GS++],\"+\"!==(n=YS[GS])&&\"-\"!==n||(t+=YS[GS++]),S$(YS.charCodeAt(GS)))for(;S$("
-  , "YS.charCodeAt(GS));)t+=YS[GS++];else eT({},g$,M$);return z$(YS.charCodeAt(GS))&&eT({},g$,M$),{type:e$,value:parseFloat(t),start:e,end:GS}}function H$(){var t,e,n,r;return XS=null,L$(),t=GS,e=function(){var t,e,n,r;for(F$(\"/\"===(t=YS[GS]),\"Regular expression literal must start with a slash\"),e=YS[GS++],n=!1,r=!1;GS<VS;)if(e+=t=YS[GS++],\"\\\\\"===t)N$((t=YS[GS++]).charCodeAt(0))&&eT({},w$),e+=t;else if(N$(t.charCodeAt(0)))eT({},w$);else if(n)\"]\"===t&&(n=!1);else{if(\"/\"===t){r=!0;break}\"[\"===t&&(n=!0)}return r||eT({},w$),{value:e.substr(1,e.length-2),literal:e}}(),n=function(){var t,e,n;for(e=\"\",n=\"\";GS<VS&&O$((t=YS[GS]).charCodeAt(0));)++GS,\"\\\\\"===t&&GS<VS?eT({},g$,M$):(n+=t,e+=t);return n.search(/[^gimuy]/g)>=0&&eT({},b$,n),{value:n,literal:e}}(),r=function(t,e){let n=t;e.includes(\"u\")&&(n=n.replace(/\\\\u\\{([0-9a-fA-F]+)\\}/g,((t,e)=>{if(parseInt(e,16)<=1114111)return\"x\";eT({},b$)})).replace(/[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]/g,\"x\"));try{new RegExp(n)}catch(t){eT({},b$)}try{return new RegExp(t,e)}catch(t){return null}}(e.value,n.value),{literal:e.literal+n.literal,value:r,regex:{pattern:e.value,flags:n.value},start:t,end:GS}}function Y$(){if(L$(),GS>=VS)return{type:ZS,start:GS,end:GS};const t=YS.charCodeAt(GS);return z$(t)?j$():40===t||41===t||59===t?I$():39===t||34===t?function(){var t,e,n,r,i=\"\",o=!1;for(F$(\"'\"===(t=YS[GS])||'\"'===t,\"String literal must starts with a quote\"),e=GS,++GS;GS<VS;){if((n=YS[GS++])===t){t=\"\";break}if(\"\\\\\"===n)if((n=YS[GS++])&&N$(n.charCodeAt(0)))\"\\r\"===n&&\"\\n\"===YS[GS]&&++GS;else switch(n){case\"u\":case\"x\":\"{\"===YS[GS]?(++GS,i+=q$()):i+=U$(n);break;case\"n\":i+=\"\\n\";break;case\"r\":i+=\"\\r\";break;case\"t\":i+=\"\\t\";break;case\"b\":i+=\"\\b\";break;case\"f\":i+=\"\\f\";break;case\"v\":i+=\"\\v\";break;default:T$(n)?(0!==(r=\"01234567\".indexOf(n))&&(o=!0),GS<VS&&T$(YS[GS])&&(o=!0,r=8*r+\"01234567\".indexOf(YS[GS++]),\"0123\".includes(n)&&GS<VS&&T$(YS[GS])&&(r=8*r+\"01234567\".indexOf(YS[GS++]))),i+=String.fromCharCode(r)):i+=n}else{if(N$(n.charCodeAt(0)))break;i+=n}}return\"\"!==t&&eT({},g$,M$),{type:r$,value:i,octal:o,start:e,end:GS}}():46===t?S$(YS.charCodeAt(GS+1))?W$():I$():S$(t)?W$():I$()}function G$(){const t=XS;return GS=t.end,XS=Y$(),GS=t.end,t}function V$(){const t=GS;XS=Y$(),GS=t}function X$(t,e,n){const r=new WS(\"||\"===t||\"&&\"===t?c$:o$);return r.operator=t,r.left=e,r.right=n,r}function J$(t,e){const n=new WS(a$);return n.callee=t,n.arguments=e,n}function Z$(t){const e=new WS(u$);return e.name=t,e}function Q$(t){const e=new WS(l$);return e.value=t.value,e.raw=YS.slice(t.start,t.end),t.regex&&(\"//\"===e.raw&&(e.raw=\"/(?:)/\"),e.regex=t.regex),e}function K$(t,e,n){const r=new WS(f$);return r.computed=\"[\"===t,r.object=e,r.property=n,r.computed||(n.member=!0),r}function tT(t,e,n){const r=new WS(d$);return r.key=e,r.value=n,r.kind=t,r}function eT(t,e){var n,r=Array.prototype.slice.call(arguments,2),i=e.replace(/%(\\d)/g,((t,e)=>(F$(e<r.length,\"Message reference must be in range\"),r[e])));throw(n=new Error(i)).index=GS,n.description=i,n}function nT(t){t.type===ZS&&eT(t,x$),t.type===e$&&eT(t,m$),t.type===r$&&eT(t,y$),t.type===QS&&eT(t,v$),t.type===KS&&eT(t,_$),eT(t,g$,t.value)}function rT(t){const e=G$();e.type===n$&&e.value===t||nT(e)}function iT(t){return XS.type===n$&&XS.value===t}function oT(t){return XS.type===KS&&XS.value===t}function aT(){const t=[];for(GS=XS.start,rT(\"[\");!iT(\"]\");)iT(\",\")?(G$(),t.push(null)):(t.push(vT()),iT(\"]\")||rT(\",\"));return G$(),function(t){const e=new WS(i$);return e.elements=t,e}(t)}function sT(){GS=XS.start;const t=G$();return t.type===r$||t.type===e$?(t.octal&&eT(t,k$),Q$(t)):Z$(t.value)}function uT(){var t,e,n;return GS=XS.start,(t=XS).type===QS?(n=sT(),rT(\":\"),tT(\"init\",n,vT())):t.type!==ZS&&t.type!==n$?(e=sT(),rT(\":\"),tT(\"init\",e,vT())):void nT(t)}function lT(){var t,e,n=[],r={},i=String;for(GS=XS.start,rT(\"{\");!iT(\"}\");)e=\"$\"+((t=uT()).key.type===u$?t.key.name:i(t.key.value)),Object.prototype.hasOwnProperty.call(r,e)?eT({},A$):r[e]=!0,n.push(t),iT(\"}\")||rT(\",\");return rT(\"}\"),function(t){const e=new WS(h$);return e.properties=t,e}(n)}const cT={if:1};function fT(){var t,e,n;if(iT(\"(\"))return func"
-  , "tion(){rT(\"(\");const t=_T();return rT(\")\"),t}();if(iT(\"[\"))return aT();if(iT(\"{\"))return lT();if(t=XS.type,GS=XS.start,t===QS||cT[XS.value])n=Z$(G$().value);else if(t===r$||t===e$)XS.octal&&eT(XS,k$),n=Q$(G$());else{if(t===KS)throw new Error(E$);t===JS?((e=G$()).value=\"true\"===e.value,n=Q$(e)):t===t$?((e=G$()).value=null,n=Q$(e)):iT(\"/\")||iT(\"/=\")?(n=Q$(H$()),V$()):nT(G$())}return n}function hT(){const t=[];if(rT(\"(\"),!iT(\")\"))for(;GS<VS&&(t.push(vT()),!iT(\")\"));)rT(\",\");return rT(\")\"),t}function dT(){GS=XS.start;const t=G$();return function(t){return t.type===QS||t.type===KS||t.type===JS||t.type===t$}(t)||nT(t),Z$(t.value)}function pT(){rT(\"[\");const t=_T();return rT(\"]\"),t}function gT(){const t=function(){var t;for(t=fT();;)if(iT(\".\"))rT(\".\"),t=K$(\".\",t,dT());else if(iT(\"(\"))t=J$(t,hT());else{if(!iT(\"[\"))break;t=K$(\"[\",t,pT())}return t}();if(XS.type===n$&&(iT(\"++\")||iT(\"--\")))throw new Error(E$);return t}function mT(){var t,e;if(XS.type!==n$&&XS.type!==KS)e=gT();else{if(iT(\"++\")||iT(\"--\"))throw new Error(E$);if(iT(\"+\")||iT(\"-\")||iT(\"~\")||iT(\"!\"))t=G$(),e=mT(),e=function(t,e){const n=new WS(p$);return n.operator=t,n.argument=e,n.prefix=!0,n}(t.value,e);else{if(oT(\"delete\")||oT(\"void\")||oT(\"typeof\"))throw new Error(E$);e=gT()}}return e}function yT(t){let e=0;if(t.type!==n$&&t.type!==KS)return 0;switch(t.value){case\"||\":e=1;break;case\"&&\":e=2;break;case\"|\":e=3;break;case\"^\":e=4;break;case\"&\":e=5;break;case\"==\":case\"!=\":case\"===\":case\"!==\":e=6;break;case\"<\":case\">\":case\"<=\":case\">=\":case\"instanceof\":case\"in\":e=7;break;case\"<<\":case\">>\":case\">>>\":e=8;break;case\"+\":case\"-\":e=9;break;case\"*\":case\"/\":case\"%\":e=11}return e}function vT(){var t,e;return t=function(){var t,e,n,r,i,o,a,s,u,l;if(t=XS,u=mT(),0===(i=yT(r=XS)))return u;for(r.prec=i,G$(),e=[t,XS],o=[u,r,a=mT()];(i=yT(XS))>0;){for(;o.length>2&&i<=o[o.length-2].prec;)a=o.pop(),s=o.pop().value,u=o.pop(),e.pop(),n=X$(s,u,a),o.push(n);(r=G$()).prec=i,o.push(r),e.push(XS),n=mT(),o.push(n)}for(n=o[l=o.length-1],e.pop();l>1;)e.pop(),n=X$(o[l-1].value,o[l-2],n),l-=2;return n}(),iT(\"?\")&&(G$(),e=vT(),rT(\":\"),t=function(t,e,n){const r=new WS(s$);return r.test=t,r.consequent=e,r.alternate=n,r}(t,e,vT())),t}function _T(){const t=vT();if(iT(\",\"))throw new Error(E$);return t}function xT(t){GS=0,VS=(YS=t).length,XS=null,V$();const e=_T();if(XS.type!==ZS)throw new Error(\"Unexpect token after expression.\");return e}var bT={NaN:\"NaN\",E:\"Math.E\",LN2:\"Math.LN2\",LN10:\"Math.LN10\",LOG2E:\"Math.LOG2E\",LOG10E:\"Math.LOG10E\",PI:\"Math.PI\",SQRT1_2:\"Math.SQRT1_2\",SQRT2:\"Math.SQRT2\",MIN_VALUE:\"Number.MIN_VALUE\",MAX_VALUE:\"Number.MAX_VALUE\"};function wT(t){function e(e,n,r){return i=>function(e,n,r,i){let o=t(n[0]);return r&&(o=r+\"(\"+o+\")\",0===r.lastIndexOf(\"new \",0)&&(o=\"(\"+o+\")\")),o+\".\"+e+(i<0?\"\":0===i?\"()\":\"(\"+n.slice(1).map(t).join(\",\")+\")\")}(e,i,n,r)}const n=\"new Date\",r=\"String\",i=\"RegExp\";return{isNaN:\"Number.isNaN\",isFinite:\"Number.isFinite\",abs:\"Math.abs\",acos:\"Math.acos\",asin:\"Math.asin\",atan:\"Math.atan\",atan2:\"Math.atan2\",ceil:\"Math.ceil\",cos:\"Math.cos\",exp:\"Math.exp\",floor:\"Math.floor\",hypot:\"Math.hypot\",log:\"Math.log\",max:\"Math.max\",min:\"Math.min\",pow:\"Math.pow\",random:\"Math.random\",round:\"Math.round\",sin:\"Math.sin\",sqrt:\"Math.sqrt\",tan:\"Math.tan\",clamp:function(e){e.length<3&&s(\"Missing arguments to clamp function.\"),e.length>3&&s(\"Too many arguments to clamp function.\");const n=e.map(t);return\"Math.max(\"+n[1]+\", Math.min(\"+n[2]+\",\"+n[0]+\"))\"},now:\"Date.now\",utc:\"Date.UTC\",datetime:n,date:e(\"getDate\",n,0),day:e(\"getDay\",n,0),year:e(\"getFullYear\",n,0),month:e(\"getMonth\",n,0),hours:e(\"getHours\",n,0),minutes:e(\"getMinutes\",n,0),seconds:e(\"getSeconds\",n,0),milliseconds:e(\"getMilliseconds\",n,0),time:e(\"getTime\",n,0),timezoneoffset:e(\"getTimezoneOffset\",n,0),utcdate:e(\"getUTCDate\",n,0),utcday:e(\"getUTCDay\",n,0),utcyear:e(\"getUTCFullYear\",n,0),utcmonth:e(\"getUTCMonth\",n,0),utchours:e(\"getUTCHours\",n,0),utcminutes:e(\"getUTCMinutes\",n,0),utcseconds:e(\"getUTCSeconds\",n,0),utcmilliseconds:e(\"getUTCMilliseconds\",n,0),length:e(\"length\",null,-1),parseFloat:\"parseFloat\",parseInt:\"parseInt\",upper:e(\"toUpp"
-  , "erCase\",r,0),lower:e(\"toLowerCase\",r,0),substring:e(\"substring\",r),split:e(\"split\",r),trim:e(\"trim\",r,0),btoa:\"btoa\",atob:\"atob\",regexp:i,test:e(\"test\",i),if:function(e){e.length<3&&s(\"Missing arguments to if function.\"),e.length>3&&s(\"Too many arguments to if function.\");const n=e.map(t);return\"(\"+n[0]+\"?\"+n[1]+\":\"+n[2]+\")\"}}}function kT(t){const e=(t=t||{}).allowed?Bt(t.allowed):{},n=t.forbidden?Bt(t.forbidden):{},r=t.constants||bT,i=(t.functions||wT)(h),o=t.globalvar,a=t.fieldvar,u=Z(o)?o:t=>`${o}[\"${t}\"]`;let l={},c={},f=0;function h(t){if(xt(t))return t;const e=d[t.type];return null==e&&s(\"Unsupported type: \"+t.type),e(t)}const d={Literal:t=>t.raw,Identifier:t=>{const i=t.name;return f>0?i:lt(n,i)?s(\"Illegal identifier: \"+i):lt(r,i)?r[i]:lt(e,i)?i:(l[i]=1,u(i))},MemberExpression:t=>{const e=!t.computed,n=h(t.object);e&&(f+=1);const r=h(t.property);return n===a&&(c[function(t){const e=t&&t.length-1;return e&&('\"'===t[0]&&'\"'===t[e]||\"'\"===t[0]&&\"'\"===t[e])?t.slice(1,-1):t}(r)]=1),e&&(f-=1),n+(e?\".\"+r:\"[\"+r+\"]\")},CallExpression:t=>{\"Identifier\"!==t.callee.type&&s(\"Illegal callee type: \"+t.callee.type);const e=t.callee.name,n=t.arguments,r=lt(i,e)&&i[e];return r||s(\"Unrecognized function: \"+e),Z(r)?r(n):r+\"(\"+n.map(h).join(\",\")+\")\"},ArrayExpression:t=>\"[\"+t.elements.map(h).join(\",\")+\"]\",BinaryExpression:t=>\"(\"+h(t.left)+\" \"+t.operator+\" \"+h(t.right)+\")\",UnaryExpression:t=>\"(\"+t.operator+h(t.argument)+\")\",ConditionalExpression:t=>\"(\"+h(t.test)+\"?\"+h(t.consequent)+\":\"+h(t.alternate)+\")\",LogicalExpression:t=>\"(\"+h(t.left)+t.operator+h(t.right)+\")\",ObjectExpression:t=>{for(const e of t.properties){const t=e.key.name;m.has(t)&&s(\"Illegal property: \"+t)}return\"{\"+t.properties.map(h).join(\",\")+\"}\"},Property:t=>{f+=1;const e=h(t.key);return f-=1,e+\":\"+h(t.value)}};function p(t){const e={code:h(t),globals:Object.keys(l),fields:Object.keys(c)};return l={},c={},e}return p.functions=i,p.constants=r,p}const AT=Symbol(\"vega_selection_getter\");function MT(t){return t.getter&&t.getter[AT]||(t.getter=l(t.field),t.getter[AT]=!0),t.getter}const ET=\"intersect\",DT=\"union\",CT=\"_vgsid_\",FT=l(CT),ST=\"E\",$T=\"R\",TT=\"R-E\",BT=\"R-LE\",NT=\"R-RE\",zT=\"E-LT\",OT=\"E-LTE\",RT=\"E-GT\",LT=\"E-GTE\",UT=\"E-VALID\",qT=\"E-ONE\",PT=\"index:unit\";function jT(t,e){for(var n,r,i=e.fields,o=e.values,a=i.length,s=0;s<a;++s)if(mt(n=MT(r=i[s])(t))&&(n=$(n)),mt(o[s])&&(o[s]=$(o[s])),A(o[s])&&mt(o[s][0])&&(o[s]=o[s].map($)),r.type===ST){if(A(o[s])?!o[s].includes(n):n!==o[s])return!1}else if(r.type===$T){if(!pt(n,o[s]))return!1}else if(r.type===NT){if(!pt(n,o[s],!0,!1))return!1}else if(r.type===TT){if(!pt(n,o[s],!1,!1))return!1}else if(r.type===BT){if(!pt(n,o[s],!1,!0))return!1}else if(r.type===zT){if(n>=o[s])return!1}else if(r.type===OT){if(n>o[s])return!1}else if(r.type===RT){if(n<=o[s])return!1}else if(r.type===LT){if(n<o[s])return!1}else if(r.type===UT){if(null===n||isNaN(n))return!1}else if(r.type===qT&&-1===o[s].indexOf(n))return!1;return!0}const IT=ee(FT),WT=IT.left,HT=IT.right;var YT={[`${CT}_union`]:function(){const t=new le;for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];for(const e of n)for(const n of e)t.add(n);return t},[`${CT}_intersect`]:function(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),r=1;r<e;r++)n[r-1]=arguments[r];t=new le(t),n=n.map(Te);t:for(const e of t)for(const r of n)if(!r.has(e)){t.delete(e);continue t}return t},E_union:function(t,e){if(!t.length)return e;for(var n=0,r=e.length;n<r;++n)t.includes(e[n])||t.push(e[n]);return t},E_intersect:function(t,e){return t.length?t.filter((t=>e.includes(t))):e},R_union:function(t,e){var n=$(e[0]),r=$(e[1]);return n>r&&(n=e[1],r=e[0]),t.length?(t[0]>n&&(t[0]=n),t[1]<r&&(t[1]=r),t):[n,r]},R_intersect:function(t,e){var n=$(e[0]),r=$(e[1]);return n>r&&(n=e[1],r=e[0]),t.length?r<t[0]||t[1]<n?[]:(t[0]<n&&(t[0]=n),t[1]>r&&(t[1]=r),t):[n,r]}};function GT(t,e,n,r){e[0].type!==NS&&s(\"First argument to selection functions must be a string literal.\");const i=e[0].value,o=\"unit\",a=\"@\"+o,u=\":\"+i;(e.length>=2&&S(e).value)!==ET||lt(r,a)||(r[a]=n.getData(i).indataRef(n,o)),lt(r,u)||(r[u]=n.getData"
-  , "(i).tuplesRef())}function VT(t){const e=this.context.data[t];return e?e.values.value:[]}const XT=t=>function(e,n){const r=this.context.dataflow.locale();return null===e?\"null\":r[t](n)(e)},JT=XT(\"format\"),ZT=XT(\"timeFormat\"),QT=XT(\"utcFormat\"),KT=XT(\"timeParse\"),tB=XT(\"utcParse\"),eB=new Date(2e3,0,1);function nB(t,e,n){return Number.isInteger(t)&&Number.isInteger(e)?(eB.setYear(2e3),eB.setMonth(t),eB.setDate(e),ZT.call(this,eB,n)):\"\"}const rB=\"%\",iB=\"$\";function oB(t,e,n,r){e[0].type!==NS&&s(\"First argument to data functions must be a string literal.\");const i=e[0].value,o=\":\"+i;if(!lt(o,r))try{r[o]=n.getData(i).tuplesRef()}catch(t){}}function aB(t,e,n,r){if(e[0].type===NS)sB(n,r,e[0].value);else for(t in n.scales)sB(n,r,t)}function sB(t,e,n){const r=rB+n;if(!lt(e,r))try{e[r]=t.scaleRef(n)}catch(t){}}function uB(t,e){if(xt(t)){const n=e.scales[t];return n&&ap(n.value)?n.value:void 0}if(Z(t))return ap(t)?t:void 0}function lB(t,e,n){e.__bandwidth=t=>t&&t.bandwidth?t.bandwidth():0,n._bandwidth=aB,n._range=aB,n._scale=aB;const r=e=>\"_[\"+(e.type===NS?Ct(rB+e.value):Ct(rB)+\"+\"+t(e))+\"]\";return{_bandwidth:t=>`this.__bandwidth(${r(t[0])})`,_range:t=>`${r(t[0])}.range()`,_scale:e=>`${r(e[0])}(${t(e[1])})`}}function cB(t,e){return function(n,r,i){if(n){const e=uB(n,(i||this).context);return e&&e.path[t](r)}return e(r)}}const fB=cB(\"area\",(function(t){return Bw=new se,gw(t,Nw),2*Bw})),hB=cB(\"bounds\",(function(t){var e,n,r,i,o,a,s;if(Aw=kw=-(bw=ww=1/0),Sw=[],gw(t,uk),n=Sw.length){for(Sw.sort(yk),e=1,o=[r=Sw[0]];e<n;++e)vk(r,(i=Sw[e])[0])||vk(r,i[1])?(mk(r[0],i[1])>mk(r[0],r[1])&&(r[1]=i[1]),mk(i[0],r[1])>mk(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(a=-1/0,e=0,r=o[n=o.length-1];e<=n;r=i,++e)i=o[e],(s=mk(r[1],i[0]))>a&&(a=s,bw=i[0],kw=r[1])}return Sw=$w=null,bw===1/0||ww===1/0?[[NaN,NaN],[NaN,NaN]]:[[bw,ww],[kw,Aw]]})),dB=cB(\"centroid\",(function(t){Yw=Gw=Vw=Xw=Jw=Zw=Qw=Kw=0,tk=new se,ek=new se,nk=new se,gw(t,_k);var e=+tk,n=+ek,r=+nk,i=tw(e,n,r);return i<Pb&&(e=Zw,n=Qw,r=Kw,Gw<qb&&(e=Vw,n=Xw,r=Jw),(i=tw(e,n,r))<Pb)?[NaN,NaN]:[Jb(n,e)*Yb,uw(r/i)*Yb]}));function pB(t,e,n){try{t[e].apply(t,[\"EXPRESSION\"].concat([].slice.call(n)))}catch(e){t.warn(e)}return n[n.length-1]}function gB(t){const e=t/255;return e<=.03928?e/12.92:Math.pow((e+.055)/1.055,2.4)}function mB(t){const e=af(t);return.2126*gB(e.r)+.7152*gB(e.g)+.0722*gB(e.b)}function yB(t,e){return t===e||t!=t&&e!=e||(A(t)?!(!A(e)||t.length!==e.length)&&function(t,e){for(let n=0,r=t.length;n<r;++n)if(!yB(t[n],e[n]))return!1;return!0}(t,e):!(!M(t)||!M(e))&&vB(t,e))}function vB(t,e){for(const n in t)if(!yB(t[n],e[n]))return!1;return!0}function _B(t){return e=>vB(t,e)}const xB={};function bB(t){return A(t)||ArrayBuffer.isView(t)?t:null}function wB(t){return bB(t)||(xt(t)?t:null)}const kB=t=>t.data;function AB(t,e){const n=VT.call(e,t);return n.root&&n.root.lookup||{}}const MB=()=>\"undefined\"!=typeof window&&window||null;function EB(t,e,n){if(!t)return[];const[r,i]=t,o=(new Xg).set(r[0],r[1],i[0],i[1]);return N_(n||this.context.dataflow.scenegraph().root,o,function(t){let e=null;if(t){const n=X(t.marktype),r=X(t.markname);e=t=>(!n.length||n.some((e=>t.marktype===e)))&&(!r.length||r.some((e=>t.name===e)))}return e}(e))}const DB={random:()=>t.random(),cumulativeNormal:hs,cumulativeLogNormal:vs,cumulativeUniform:As,densityNormal:fs,densityLogNormal:ys,densityUniform:ks,quantileNormal:ds,quantileLogNormal:_s,quantileUniform:Ms,sampleNormal:cs,sampleLogNormal:ms,sampleUniform:ws,isArray:A,isBoolean:gt,isDate:mt,isDefined:t=>void 0!==t,isNumber:vt,isObject:M,isRegExp:_t,isString:xt,isTuple:ma,isValid:t=>null!=t&&t==t,toBoolean:Ft,toDate:t=>$t(t),toNumber:$,toString:Tt,indexof:function(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),r=1;r<e;r++)n[r-1]=arguments[r];return wB(t).indexOf(...n)},join:function(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),r=1;r<e;r++)n[r-1]=arguments[r];return bB(t).join(...n)},lastindexof:function(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),r=1;r<e;r++)n[r-1]=arguments[r];return wB(t).lastIndexOf(...n)},replace:function(t,e,n){return Z(n)&&s(\"Functi"
-  , "on argument passed to replace.\"),xt(e)||_t(e)||s(\"Please pass a string or RegExp argument to replace.\"),String(t).replace(e,n)},reverse:function(t){return bB(t).slice().reverse()},sort:function(t){return bB(t).slice().sort(tt)},slice:function(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),r=1;r<e;r++)n[r-1]=arguments[r];return wB(t).slice(...n)},flush:ht,lerp:wt,merge:function(){const t=[].slice.call(arguments);return t.unshift({}),at(...t)},pad:Et,peek:S,pluck:function(t,e){const n=xB[e]||(xB[e]=l(e));return A(t)?t.map(n):n(t)},span:Dt,inrange:pt,truncate:Nt,rgb:af,lab:Sf,hcl:Of,hsl:gf,luminance:mB,contrast:function(t,e){const n=mB(t),r=mB(e);return(Math.max(n,r)+.05)/(Math.min(n,r)+.05)},sequence:Se,format:JT,utcFormat:QT,utcParse:tB,utcOffset:Tr,utcSequence:zr,timeFormat:ZT,timeParse:KT,timeOffset:$r,timeSequence:Nr,timeUnitSpecifier:rr,monthFormat:function(t){return nB.call(this,t,1,\"%B\")},monthAbbrevFormat:function(t){return nB.call(this,t,1,\"%b\")},dayFormat:function(t){return nB.call(this,0,2+t,\"%A\")},dayAbbrevFormat:function(t){return nB.call(this,0,2+t,\"%a\")},quarter:G,utcquarter:V,week:sr,utcweek:dr,dayofyear:ar,utcdayofyear:hr,warn:function(){return pB(this.context.dataflow,\"warn\",arguments)},info:function(){return pB(this.context.dataflow,\"info\",arguments)},debug:function(){return pB(this.context.dataflow,\"debug\",arguments)},extent:t=>st(t),inScope:function(t){const e=this.context.group;let n=!1;if(e)for(;t;){if(t===e){n=!0;break}t=t.mark.group}return n},intersect:EB,clampRange:J,pinchDistance:function(t){const e=t.touches,n=e[0].clientX-e[1].clientX,r=e[0].clientY-e[1].clientY;return Math.hypot(n,r)},pinchAngle:function(t){const e=t.touches;return Math.atan2(e[0].clientY-e[1].clientY,e[0].clientX-e[1].clientX)},screen:function(){const t=MB();return t?t.screen:{}},containerSize:function(){const t=this.context.dataflow,e=t.container&&t.container();return e?[e.clientWidth,e.clientHeight]:[void 0,void 0]},windowSize:function(){const t=MB();return t?[t.innerWidth,t.innerHeight]:[void 0,void 0]},bandspace:function(t,e,n){return $d(t||0,e||0,n||0)},setdata:function(t,e){const n=this.context.dataflow,r=this.context.data[t].input;return n.pulse(r,n.changeset().remove(p).insert(e)),1},pathShape:function(t){let e=null;return function(n){return n?vg(n,e=e||sg(t)):t}},panLinear:L,panLog:U,panPow:q,panSymlog:P,zoomLinear:I,zoomLog:W,zoomPow:H,zoomSymlog:Y,encode:function(t,e,n){if(t){const n=this.context.dataflow,r=t.mark.source;n.pulse(r,n.changeset().encode(t,e))}return void 0!==n?n:t},modify:function(t,e,n,r,i,o){const a=this.context.dataflow,s=this.context.data[t],u=s.input,l=a.stamp();let c,f,h=s.changes;if(!1===a._trigger||!(u.value.length||e||r))return 0;if((!h||h.stamp<l)&&(s.changes=h=a.changeset(),h.stamp=l,a.runAfter((()=>{s.modified=!0,a.pulse(u,h).run()}),!0,1)),n&&(c=!0===n?p:A(n)||ma(n)?n:_B(n),h.remove(c)),e&&h.insert(e),r&&(c=_B(r),u.value.some(c)?h.remove(c):h.insert(r)),i)for(f in o)h.modify(i,f,o[f]);return 1},lassoAppend:function(t,e,n){let r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:5;const i=(t=X(t))[t.length-1];return void 0===i||Math.hypot(i[0]-e,i[1]-n)>r?[...t,[e,n]]:t},lassoPath:function(t){return X(t).reduce(((e,n,r)=>{let[i,o]=n;return e+(0==r?`M ${i},${o} `:r===t.length-1?\" Z\":`L ${i},${o} `)}),\"\")},intersectLasso:function(t,e,n){const{x:r,y:i,mark:o}=n,a=(new Xg).set(Number.MAX_SAFE_INTEGER,Number.MAX_SAFE_INTEGER,Number.MIN_SAFE_INTEGER,Number.MIN_SAFE_INTEGER);for(const[t,n]of e)t<a.x1&&(a.x1=t),t>a.x2&&(a.x2=t),n<a.y1&&(a.y1=n),n>a.y2&&(a.y2=n);return a.translate(r,i),EB([[a.x1,a.y1],[a.x2,a.y2]],t,o).filter((t=>function(t,e,n){let r=0;for(let i=0,o=n.length-1;i<n.length;o=i++){const[a,s]=n[o],[u,l]=n[i];l>e!=s>e&&t<(a-u)*(e-l)/(s-l)+u&&r++}return 1&r}(t.x,t.y,e)))}},CB=[\"view\",\"item\",\"group\",\"xy\",\"x\",\"y\"],FB=\"this.\",SB={},$B={forbidden:[\"_\"],allowed:[\"datum\",\"event\",\"item\"],fieldvar:\"datum\",globalvar:t=>`_[${Ct(iB+t)}]`,functions:function(t){const e=wT(t);CB.forEach((t=>e[t]=\"event.vega.\"+t));for(const t in DB)e[t]=FB+t;return at(e,lB(t,DB,SB)),e},constants:bT,visitors"
-  , ":SB},TB=kT($B);function BB(t,e,n){return 1===arguments.length?DB[t]:(DB[t]=e,n&&(SB[t]=n),TB&&(TB.functions[t]=FB+t),this)}function NB(t,e){const n={};let r;try{r=xT(t=xt(t)?t:Ct(t)+\"\")}catch(e){s(\"Expression parse error: \"+t)}r.visit((t=>{if(t.type!==LS)return;const r=t.callee.name,i=$B.visitors[r];i&&i(r,t.arguments,e,n)}));const i=TB(r);return i.globals.forEach((t=>{const r=iB+t;!lt(n,r)&&e.getSignal(t)&&(n[r]=e.signalRef(t))})),{$expr:at({code:i.code},e.options.ast?{ast:r}:null),$fields:i.fields,$params:n}}BB(\"bandwidth\",(function(t,e){const n=uB(t,(e||this).context);return n&&n.bandwidth?n.bandwidth():0}),aB),BB(\"copy\",(function(t,e){const n=uB(t,(e||this).context);return n?n.copy():void 0}),aB),BB(\"domain\",(function(t,e){const n=uB(t,(e||this).context);return n?n.domain():[]}),aB),BB(\"range\",(function(t,e){const n=uB(t,(e||this).context);return n&&n.range?n.range():[]}),aB),BB(\"invert\",(function(t,e,n){const r=uB(t,(n||this).context);return r?A(e)?(r.invertRange||r.invert)(e):(r.invert||r.invertExtent)(e):void 0}),aB),BB(\"scale\",(function(t,e,n){const r=uB(t,(n||this).context);return r?r(e):void 0}),aB),BB(\"gradient\",(function(t,e,n,r,i){t=uB(t,(i||this).context);const o=Kp(e,n);let a=t.domain(),s=a[0],u=S(a),l=f;return u-s?l=xp(t,s,u):t=(t.interpolator?sp(\"sequential\")().interpolator(t.interpolator()):sp(\"linear\")().interpolate(t.interpolate()).range(t.range())).domain([s=0,u=1]),t.ticks&&(a=t.ticks(+r||15),s!==a[0]&&a.unshift(s),u!==S(a)&&a.push(u)),a.forEach((e=>o.stop(l(e),t(e)))),o}),aB),BB(\"geoArea\",fB,aB),BB(\"geoBounds\",hB,aB),BB(\"geoCentroid\",dB,aB),BB(\"geoShape\",(function(t,e,n){const r=uB(t,(n||this).context);return function(t){return r?r.path.context(t)(e):\"\"}}),aB),BB(\"geoScale\",(function(t,e){const n=uB(t,(e||this).context);return n&&n.scale()}),aB),BB(\"indata\",(function(t,e,n){const r=this.context.data[t][\"index:\"+e],i=r?r.value.get(n):void 0;return i?i.count:i}),(function(t,e,n,r){e[0].type!==NS&&s(\"First argument to indata must be a string literal.\"),e[1].type!==NS&&s(\"Second argument to indata must be a string literal.\");const i=e[0].value,o=e[1].value,a=\"@\"+o;lt(a,r)||(r[a]=n.getData(i).indataRef(n,o))})),BB(\"data\",VT,oB),BB(\"treePath\",(function(t,e,n){const r=AB(t,this),i=r[e],o=r[n];return i&&o?i.path(o).map(kB):void 0}),oB),BB(\"treeAncestors\",(function(t,e){const n=AB(t,this)[e];return n?n.ancestors().map(kB):void 0}),oB),BB(\"vlSelectionTest\",(function(t,e,n){for(var r,i,o,a,s,u=this.context.data[t],l=u?u.values.value:[],c=u?u[PT]&&u[PT].value:void 0,f=n===ET,h=l.length,d=0;d<h;++d)if(r=l[d],c&&f){if(-1===(o=(i=i||{})[a=r.unit]||0))continue;if(s=jT(e,r),i[a]=s?-1:++o,s&&1===c.size)return!0;if(!s&&o===c.get(a).count)return!1}else if(f^(s=jT(e,r)))return s;return h&&f}),GT),BB(\"vlSelectionIdTest\",(function(t,e,n){const r=this.context.data[t],i=r?r.values.value:[],o=r?r[PT]&&r[PT].value:void 0,a=n===ET,s=FT(e),u=WT(i,s);if(u===i.length)return!1;if(FT(i[u])!==s)return!1;if(o&&a){if(1===o.size)return!0;if(HT(i,s)-u<o.size)return!1}return!0}),GT),BB(\"vlSelectionResolve\",(function(t,e,n,r){for(var i,o,a,s,u,l,c,f,h,d,p,g,m=this.context.data[t],y=m?m.values.value:[],v={},_={},x={},b=y.length,w=0;w<b;++w)if(s=(i=y[w]).unit,o=i.fields,a=i.values,o&&a){for(p=0,g=o.length;p<g;++p)u=o[p],f=(c=v[u.field]||(v[u.field]={}))[s]||(c[s]=[]),x[u.field]=h=u.type.charAt(0),d=YT[`${h}_union`],c[s]=d(f,X(a[p]));n&&(f=_[s]||(_[s]=[])).push(X(a).reduce(((t,e,n)=>(t[o[n].field]=e,t)),{}))}else u=CT,l=FT(i),(f=(c=v[u]||(v[u]={}))[s]||(c[s]=[])).push(l),n&&(f=_[s]||(_[s]=[])).push({[CT]:l});if(e=e||DT,v[CT]?v[CT]=YT[`${CT}_${e}`](...Object.values(v[CT])):Object.keys(v).forEach((t=>{v[t]=Object.keys(v[t]).map((e=>v[t][e])).reduce(((n,r)=>void 0===n?r:YT[`${x[t]}_${e}`](n,r)))})),y=Object.keys(_),n&&y.length){v[r?\"vlPoint\":\"vlMulti\"]=e===DT?{or:y.reduce(((t,e)=>(t.push(..._[e]),t)),[])}:{and:y.map((t=>({or:_[t]})))}}return v}),GT),BB(\"vlSelectionTuples\",(function(t,e){return A(t)||s(\"First argument to selectionTuples must be an array.\"),M(e)||s(\"Second argument to selectionTuples must be an object.\"),t.map((t=>at(e.fields?{v"
-  , "alues:e.fields.map((e=>MT(e)(t.datum)))}:{[CT]:FT(t.datum)},e)))}));const zB=Bt([\"rule\"]),OB=Bt([\"group\",\"image\",\"rect\"]);function RB(t){return(t+\"\").toLowerCase()}function LB(t,e,n){n.endsWith(\";\")||(n=\"return(\"+n+\");\");const r=Function(...e.concat(n));return t&&t.functions?r.bind(t.functions):r}var UB={operator:(t,e)=>LB(t,[\"_\"],e.code),parameter:(t,e)=>LB(t,[\"datum\",\"_\"],e.code),event:(t,e)=>LB(t,[\"event\"],e.code),handler:(t,e)=>LB(t,[\"_\",\"event\"],`var datum=event.item&&event.item.datum;return ${e.code};`),encode:(t,e)=>{const{marktype:n,channels:r}=e;let i=\"var o=item,datum=o.datum,m=0,$;\";for(const t in r){const e=\"o[\"+Ct(t)+\"]\";i+=`$=${r[t].code};if(${e}!==$)${e}=$,m=1;`}return i+=function(t,e){let n=\"\";return zB[e]||(t.x2&&(t.x?(OB[e]&&(n+=\"if(o.x>o.x2)$=o.x,o.x=o.x2,o.x2=$;\"),n+=\"o.width=o.x2-o.x;\"):n+=\"o.x=o.x2-(o.width||0);\"),t.xc&&(n+=\"o.x=o.xc-(o.width||0)/2;\"),t.y2&&(t.y?(OB[e]&&(n+=\"if(o.y>o.y2)$=o.y,o.y=o.y2,o.y2=$;\"),n+=\"o.height=o.y2-o.y;\"):n+=\"o.y=o.y2-(o.height||0);\"),t.yc&&(n+=\"o.y=o.yc-(o.height||0)/2;\")),n}(r,n),i+=\"return m;\",LB(t,[\"item\",\"_\"],i)},codegen:{get(t){const e=`[${t.map(Ct).join(\"][\")}]`,n=Function(\"_\",`return _${e};`);return n.path=e,n},comparator(t,e){let n;const r=Function(\"a\",\"b\",\"var u, v; return \"+t.map(((t,r)=>{const i=e[r];let o,a;return t.path?(o=`a${t.path}`,a=`b${t.path}`):((n=n||{})[\"f\"+r]=t,o=`this.f${r}(a)`,a=`this.f${r}(b)`),function(t,e,n,r){return`((u = ${t}) < (v = ${e}) || u == null) && v != null ? ${n}\\n  : (u > v || v == null) && u != null ? ${r}\\n  : ((v = v instanceof Date ? +v : v), (u = u instanceof Date ? +u : u)) !== u && v === v ? ${n}\\n  : v !== v && u === u ? ${r} : `}(o,a,-i,i)})).join(\"\")+\"0;\");return n?r.bind(n):r}}};function qB(t,e,n){if(!t||!M(t))return t;for(let r,i=0,o=PB.length;i<o;++i)if(r=PB[i],lt(t,r.key))return r.parse(t,e,n);return t}var PB=[{key:\"$ref\",parse:function(t,e){return e.get(t.$ref)||s(\"Operator not defined: \"+t.$ref)}},{key:\"$key\",parse:function(t,e){const n=\"k:\"+t.$key+\"_\"+!!t.$flat;return e.fn[n]||(e.fn[n]=bt(t.$key,t.$flat,e.expr.codegen))}},{key:\"$expr\",parse:function(t,n,r){t.$params&&n.parseParameters(t.$params,r);const i=\"e:\"+t.$expr.code;return n.fn[i]||(n.fn[i]=e(n.parameterExpression(t.$expr),t.$fields))}},{key:\"$field\",parse:function(t,e){if(!t.$field)return null;const n=\"f:\"+t.$field+\"_\"+t.$name;return e.fn[n]||(e.fn[n]=l(t.$field,t.$name,e.expr.codegen))}},{key:\"$encode\",parse:function(t,n){const r=t.$encode,i={};for(const t in r){const o=r[t];i[t]=e(n.encodeExpression(o.$expr),o.$fields),i[t].output=o.$output}return i}},{key:\"$compare\",parse:function(t,e){const n=\"c:\"+t.$compare+\"_\"+t.$order,r=X(t.$compare).map((t=>t&&t.$tupleid?ya:t));return e.fn[n]||(e.fn[n]=K(r,t.$order,e.expr.codegen))}},{key:\"$context\",parse:function(t,e){return e}},{key:\"$subflow\",parse:function(t,e){const n=t.$subflow;return function(t,r,i){const o=e.fork().parse(n),a=o.get(n.operators[0].id),s=o.signals.parent;return s&&s.set(i),a.detachSubflow=()=>e.detach(o),a}}},{key:\"$tupleid\",parse:function(){return ya}}];const jB={skip:!0};function IB(t,e,n,r){return new WB(t,e,n,r)}function WB(t,e,n,r){this.dataflow=t,this.transforms=e,this.events=t.events.bind(t),this.expr=r||UB,this.signals={},this.scales={},this.nodes={},this.data={},this.fn={},n&&(this.functions=Object.create(n),this.functions.context=this)}function HB(t){this.dataflow=t.dataflow,this.transforms=t.transforms,this.events=t.events,this.expr=t.expr,this.signals=Object.create(t.signals),this.scales=Object.create(t.scales),this.nodes=Object.create(t.nodes),this.data=Object.create(t.data),this.fn=Object.create(t.fn),t.functions&&(this.functions=Object.create(t.functions),this.functions.context=this)}function YB(t,e){t&&(null==e?t.removeAttribute(\"aria-label\"):t.setAttribute(\"aria-label\",e))}WB.prototype=HB.prototype={fork(){const t=new HB(this);return(this.subcontext||(this.subcontext=[])).push(t),t},detach(t){this.subcontext=this.subcontext.filter((e=>e!==t));const e=Object.keys(t.nodes);for(const n of e)t.nodes[n]._targets=null;for(const n of e)t.nodes[n].detach();t.nodes=null},get(t){ret"
-  , "urn this.nodes[t]},set(t,e){return this.nodes[t]=e},add(t,e){const n=this,r=n.dataflow,i=t.value;if(n.set(t.id,e),function(t){return\"collect\"===RB(t)}(t.type)&&i&&(i.$ingest?r.ingest(e,i.$ingest,i.$format):i.$request?r.preload(e,i.$request,i.$format):r.pulse(e,r.changeset().insert(i))),t.root&&(n.root=e),t.parent){let i=n.get(t.parent.$ref);i?(r.connect(i,[e]),e.targets().add(i)):(n.unresolved=n.unresolved||[]).push((()=>{i=n.get(t.parent.$ref),r.connect(i,[e]),e.targets().add(i)}))}if(t.signal&&(n.signals[t.signal]=e),t.scale&&(n.scales[t.scale]=e),t.data)for(const r in t.data){const i=n.data[r]||(n.data[r]={});t.data[r].forEach((t=>i[t]=e))}},resolve(){return(this.unresolved||[]).forEach((t=>t())),delete this.unresolved,this},operator(t,e){this.add(t,this.dataflow.add(t.value,e))},transform(t,e){this.add(t,this.dataflow.add(this.transforms[RB(e)]))},stream(t,e){this.set(t.id,e)},update(t,e,n,r,i){this.dataflow.on(e,n,r,i,t.options)},operatorExpression(t){return this.expr.operator(this,t)},parameterExpression(t){return this.expr.parameter(this,t)},eventExpression(t){return this.expr.event(this,t)},handlerExpression(t){return this.expr.handler(this,t)},encodeExpression(t){return this.expr.encode(this,t)},parse:function(t){const e=this,n=t.operators||[];return t.background&&(e.background=t.background),t.eventConfig&&(e.eventConfig=t.eventConfig),t.locale&&(e.locale=t.locale),n.forEach((t=>e.parseOperator(t))),n.forEach((t=>e.parseOperatorParameters(t))),(t.streams||[]).forEach((t=>e.parseStream(t))),(t.updates||[]).forEach((t=>e.parseUpdate(t))),e.resolve()},parseOperator:function(t){const e=this;!function(t){return\"operator\"===RB(t)}(t.type)&&t.type?e.transform(t,t.type):e.operator(t,t.update?e.operatorExpression(t.update):null)},parseOperatorParameters:function(t){const e=this;if(t.params){const n=e.get(t.id);n||s(\"Invalid operator id: \"+t.id),e.dataflow.connect(n,n.parameters(e.parseParameters(t.params),t.react,t.initonly))}},parseParameters:function(t,e){e=e||{};const n=this;for(const r in t){const i=t[r];e[r]=A(i)?i.map((t=>qB(t,n,e))):qB(i,n,e)}return e},parseStream:function(t){var e,n=this,r=null!=t.filter?n.eventExpression(t.filter):void 0,i=null!=t.stream?n.get(t.stream):void 0;t.source?i=n.events(t.source,t.type,r):t.merge&&(i=(e=t.merge.map((t=>n.get(t))))[0].merge.apply(e[0],e.slice(1))),t.between&&(e=t.between.map((t=>n.get(t))),i=i.between(e[0],e[1])),t.filter&&(i=i.filter(r)),null!=t.throttle&&(i=i.throttle(+t.throttle)),null!=t.debounce&&(i=i.debounce(+t.debounce)),null==i&&s(\"Invalid stream definition: \"+JSON.stringify(t)),t.consume&&i.consume(!0),n.stream(t,i)},parseUpdate:function(t){var e,n=this,r=M(r=t.source)?r.$ref:r,i=n.get(r),o=t.update,a=void 0;i||s(\"Source not defined: \"+t.source),e=t.target&&t.target.$expr?n.eventExpression(t.target.$expr):n.get(t.target),o&&o.$expr&&(o.$params&&(a=n.parseParameters(o.$params)),o=n.handlerExpression(o.$expr)),n.update(t,i,e,o,a)},getState:function(t){var e=this,n={};if(t.signals){var r=n.signals={};Object.keys(e.signals).forEach((n=>{const i=e.signals[n];t.signals(n,i)&&(r[n]=i.value)}))}if(t.data){var i=n.data={};Object.keys(e.data).forEach((n=>{const r=e.data[n];t.data(n,r)&&(i[n]=r.input.value)}))}return e.subcontext&&!1!==t.recurse&&(n.subcontext=e.subcontext.map((e=>e.getState(t)))),n},setState:function(t){var e=this,n=e.dataflow,r=t.data,i=t.signals;Object.keys(i||{}).forEach((t=>{n.update(e.signals[t],i[t],jB)})),Object.keys(r||{}).forEach((t=>{n.pulse(e.data[t].input,n.changeset().remove(p).insert(r[t]))})),(t.subcontext||[]).forEach(((t,n)=>{const r=e.subcontext[n];r&&r.setState(t)}))}};const GB=\"default\";function VB(t,e){const n=t.globalCursor()?\"undefined\"!=typeof document&&document.body:t.container();if(n)return null==e?n.style.removeProperty(\"cursor\"):n.style.cursor=e}function XB(t,e){var n=t._runtime.data;return lt(n,e)||s(\"Unrecognized data set: \"+e),n[e]}function JB(t,e){Aa(e)||s(\"Second argument to changes must be a changeset.\");const n=XB(this,t);return n.modified=!0,this.pulse(n.input,e)}function ZB(t){var e=t.padding();return Math.max(0,t._v"
-  , "iewWidth+e.left+e.right)}function QB(t){var e=t.padding();return Math.max(0,t._viewHeight+e.top+e.bottom)}function KB(t){var e=t.padding(),n=t._origin;return[e.left+n[0],e.top+n[1]]}function tN(t,e,n){var r,i,o=t._renderer,a=o&&o.canvas();return a&&(i=KB(t),(r=sv(e.changedTouches?e.changedTouches[0]:e,a))[0]-=i[0],r[1]-=i[1]),e.dataflow=t,e.item=n,e.vega=function(t,e,n){const r=e?\"group\"===e.mark.marktype?e:e.mark.group:null;function i(t){var n,i=r;if(t)for(n=e;n;n=n.mark.group)if(n.mark.name===t){i=n;break}return i&&i.mark&&i.mark.interactive?i:{}}function o(t){if(!t)return n;xt(t)&&(t=i(t));const e=n.slice();for(;t;)e[0]-=t.x||0,e[1]-=t.y||0,t=t.mark&&t.mark.group;return e}return{view:it(t),item:it(e||{}),group:i,xy:o,x:t=>o(t)[0],y:t=>o(t)[1]}}(t,n,r),e}const eN=\"view\",nN={trap:!1};function rN(t,e,n,r){t._eventListeners.push({type:n,sources:X(e),handler:r})}function iN(t,e,n){const r=t._eventConfig&&t._eventConfig[e];return!(!1===r||M(r)&&!r[n])||(t.warn(`Blocked ${e} ${n} event listener.`),!1)}function oN(t){return t.item}function aN(t){return t.item.mark.source}function sN(t){return function(e,n){return n.vega.view().changeset().encode(n.item,t)}}function uN(t,e,n){const r=document.createElement(t);for(const t in e)r.setAttribute(t,e[t]);return null!=n&&(r.textContent=n),r}const lN=\"vega-bind\",cN=\"vega-bind-name\",fN=\"vega-bind-radio\";function hN(t,e,n,r){const i=n.event||\"input\",o=()=>t.update(e.value);r.signal(n.signal,e.value),e.addEventListener(i,o),rN(r,e,i,o),t.set=t=>{e.value=t,e.dispatchEvent(function(t){return\"undefined\"!=typeof Event?new Event(t):{type:t}}(i))}}function dN(t,e,n,r){const i=r.signal(n.signal),o=uN(\"div\",{class:lN}),a=\"radio\"===n.input?o:o.appendChild(uN(\"label\"));a.appendChild(uN(\"span\",{class:cN},n.name||n.signal)),e.appendChild(o);let s=pN;switch(n.input){case\"checkbox\":s=gN;break;case\"select\":s=mN;break;case\"radio\":s=yN;break;case\"range\":s=vN}s(t,a,n,i)}function pN(t,e,n,r){const i=uN(\"input\");for(const t in n)\"signal\"!==t&&\"element\"!==t&&i.setAttribute(\"input\"===t?\"type\":t,n[t]);i.setAttribute(\"name\",n.signal),i.value=r,e.appendChild(i),i.addEventListener(\"input\",(()=>t.update(i.value))),t.elements=[i],t.set=t=>i.value=t}function gN(t,e,n,r){const i={type:\"checkbox\",name:n.signal};r&&(i.checked=!0);const o=uN(\"input\",i);e.appendChild(o),o.addEventListener(\"change\",(()=>t.update(o.checked))),t.elements=[o],t.set=t=>o.checked=!!t||null}function mN(t,e,n,r){const i=uN(\"select\",{name:n.signal}),o=n.labels||[];n.options.forEach(((t,e)=>{const n={value:t};_N(t,r)&&(n.selected=!0),i.appendChild(uN(\"option\",n,(o[e]||t)+\"\"))})),e.appendChild(i),i.addEventListener(\"change\",(()=>{t.update(n.options[i.selectedIndex])})),t.elements=[i],t.set=t=>{for(let e=0,r=n.options.length;e<r;++e)if(_N(n.options[e],t))return void(i.selectedIndex=e)}}function yN(t,e,n,r){const i=uN(\"span\",{class:fN}),o=n.labels||[];e.appendChild(i),t.elements=n.options.map(((e,a)=>{const s={type:\"radio\",name:n.signal,value:e};_N(e,r)&&(s.checked=!0);const u=uN(\"input\",s);u.addEventListener(\"change\",(()=>t.update(e)));const l=uN(\"label\",{},(o[a]||e)+\"\");return l.prepend(u),i.appendChild(l),u})),t.set=e=>{const n=t.elements,r=n.length;for(let t=0;t<r;++t)_N(n[t].value,e)&&(n[t].checked=!0)}}function vN(t,e,n,r){r=void 0!==r?r:(+n.max+ +n.min)/2;const i=null!=n.max?n.max:Math.max(100,+r)||100,o=n.min||Math.min(0,i,+r)||0,a=n.step||be(o,i,100),s=uN(\"input\",{type:\"range\",name:n.signal,min:o,max:i,step:a});s.value=r;const u=uN(\"span\",{},+r);e.appendChild(s),e.appendChild(u);const l=()=>{u.textContent=s.value,t.update(+s.value)};s.addEventListener(\"input\",l),s.addEventListener(\"change\",l),t.elements=[s],t.set=t=>{s.value=t,u.textContent=t}}function _N(t,e){return t===e||t+\"\"==e+\"\"}function xN(t,e,n,r,i,o){return(e=e||new r(t.loader())).initialize(n,ZB(t),QB(t),KB(t),i,o).background(t.background())}function bN(t,e){return e?function(){try{e.apply(this,arguments)}catch(e){t.error(e)}}:null}function wN(t,e,n){if(\"string\"==typeof e){if(\"undefined\"==typeof document)return t.error(\"DOM document instance not found.\"),null;if(!(e=document.queryS"
-  , "elector(e)))return t.error(\"Signal bind element not found: \"+e),null}if(e&&n)try{e.textContent=\"\"}catch(n){e=null,t.error(n)}return e}const kN=t=>+t||0;function AN(t){return M(t)?{top:kN(t.top),bottom:kN(t.bottom),left:kN(t.left),right:kN(t.right)}:(t=>({top:t,bottom:t,left:t,right:t}))(kN(t))}async function MN(t,e,n,r){const i=B_(e),o=i&&i.headless;return o||s(\"Unrecognized renderer type: \"+e),await t.runAsync(),xN(t,null,null,o,n,r).renderAsync(t._scenegraph.root)}var EN=\"width\",DN=\"height\",CN=\"padding\",FN={skip:!0};function SN(t,e){var n=t.autosize(),r=t.padding();return e-(n&&n.contains===CN?r.left+r.right:0)}function $N(t,e){var n=t.autosize(),r=t.padding();return e-(n&&n.contains===CN?r.top+r.bottom:0)}function TN(t,e){return e.modified&&A(e.input.value)&&!t.startsWith(\"_:vega:_\")}function BN(t,e){return!(\"parent\"===t||e instanceof Za.proxy)}function NN(t,e,n,r){const i=t.element();i&&i.setAttribute(\"title\",function(t){return null==t?\"\":A(t)?zN(t):M(t)&&!mt(t)?(e=t,Object.keys(e).map((t=>{const n=e[t];return t+\": \"+(A(n)?zN(n):ON(n))})).join(\"\\n\")):t+\"\";var e}(r))}function zN(t){return\"[\"+t.map(ON).join(\", \")+\"]\"}function ON(t){return A(t)?\"[…]\":M(t)&&!mt(t)?\"{…}\":t}function RN(t,e){const n=this;if(e=e||{},Va.call(n),e.loader&&n.loader(e.loader),e.logger&&n.logger(e.logger),null!=e.logLevel&&n.logLevel(e.logLevel),e.locale||t.locale){const r=at({},t.locale,e.locale);n.locale(Ro(r.number,r.time))}n._el=null,n._elBind=null,n._renderType=e.renderer||$_.Canvas,n._scenegraph=new tv;const r=n._scenegraph.root;n._renderer=null,n._tooltip=e.tooltip||NN,n._redraw=!0,n._handler=(new $v).scene(r),n._globalCursor=!1,n._preventDefault=!1,n._timers=[],n._eventListeners=[],n._resizeListeners=[],n._eventConfig=function(t){const e=at({defaults:{}},t),n=(t,e)=>{e.forEach((e=>{A(t[e])&&(t[e]=Bt(t[e]))}))};return n(e.defaults,[\"prevent\",\"allow\"]),n(e,[\"view\",\"window\",\"selector\"]),e}(t.eventConfig),n.globalCursor(n._eventConfig.globalCursor);const i=function(t,e,n){return IB(t,Za,DB,n).parse(e)}(n,t,e.expr);n._runtime=i,n._signals=i.signals,n._bind=(t.bindings||[]).map((t=>({state:null,param:at({},t)}))),i.root&&i.root.set(r),r.source=i.data.root.input,n.pulse(i.data.root.input,n.changeset().insert(r.items)),n._width=n.width(),n._height=n.height(),n._viewWidth=SN(n,n._width),n._viewHeight=$N(n,n._height),n._origin=[0,0],n._resize=0,n._autosize=1,function(t){var e=t._signals,n=e[EN],r=e[DN],i=e[CN];function o(){t._autosize=t._resize=1}t._resizeWidth=t.add(null,(e=>{t._width=e.size,t._viewWidth=SN(t,e.size),o()}),{size:n}),t._resizeHeight=t.add(null,(e=>{t._height=e.size,t._viewHeight=$N(t,e.size),o()}),{size:r});const a=t.add(null,o,{pad:i});t._resizeWidth.rank=n.rank+1,t._resizeHeight.rank=r.rank+1,a.rank=i.rank+1}(n),function(t){t.add(null,(e=>(t._background=e.bg,t._resize=1,e.bg)),{bg:t._signals.background})}(n),function(t){const e=t._signals.cursor||(t._signals.cursor=t.add({user:GB,item:null}));t.on(t.events(\"view\",\"pointermove\"),e,((t,n)=>{const r=e.value,i=r?xt(r)?r:r.user:GB,o=n.item&&n.item.cursor||null;return r&&i===r.user&&o==r.item?r:{user:i,item:o}})),t.add(null,(function(e){let n=e.cursor,r=this.value;return xt(n)||(r=n.item,n=n.user),VB(t,n&&n!==GB?n:r||n),r}),{cursor:e})}(n),n.description(t.description),e.hover&&n.hover(),e.container&&n.initialize(e.container,e.bind),e.watchPixelRatio&&n._watchPixelRatio()}function LN(t,e){return lt(t._signals,e)?t._signals[e]:s(\"Unrecognized signal name: \"+Ct(e))}function UN(t,e){const n=(t._targets||[]).filter((t=>t._update&&t._update.handler===e));return n.length?n[0]:null}function qN(t,e,n,r){let i=UN(n,r);return i||(i=bN(t,(()=>r(e,n.value))),i.handler=r,t.on(n,null,i)),t}function PN(t,e,n){const r=UN(e,n);return r&&e._targets.remove(r),t}dt(RN,Va,{async evaluate(t,e,n){if(await Va.prototype.evaluate.call(this,t,e),this._redraw||this._resize)try{this._renderer&&(this._resize&&(this._resize=0,function(t){var e=KB(t),n=ZB(t),r=QB(t);t._renderer.background(t.background()),t._renderer.resize(n,r,e),t._handler.origin(e),t._resizeListeners.forEach((e=>{try{e(n,r)}catch(e){t.error(e)}}))}("
-  , "this)),await this._renderer.renderAsync(this._scenegraph.root)),this._redraw=!1}catch(t){this.error(t)}return n&&da(this,n),this},dirty(t){this._redraw=!0,this._renderer&&this._renderer.dirty(t)},description(t){if(arguments.length){const e=null!=t?t+\"\":null;return e!==this._desc&&YB(this._el,this._desc=e),this}return this._desc},container(){return this._el},scenegraph(){return this._scenegraph},origin(){return this._origin.slice()},signal(t,e,n){const r=LN(this,t);return 1===arguments.length?r.value:this.update(r,e,n)},width(t){return arguments.length?this.signal(\"width\",t):this.signal(\"width\")},height(t){return arguments.length?this.signal(\"height\",t):this.signal(\"height\")},padding(t){return arguments.length?this.signal(\"padding\",AN(t)):AN(this.signal(\"padding\"))},autosize(t){return arguments.length?this.signal(\"autosize\",t):this.signal(\"autosize\")},background(t){return arguments.length?this.signal(\"background\",t):this.signal(\"background\")},renderer(t){return arguments.length?(B_(t)||s(\"Unrecognized renderer type: \"+t),t!==this._renderType&&(this._renderType=t,this._resetRenderer()),this):this._renderType},tooltip(t){return arguments.length?(t!==this._tooltip&&(this._tooltip=t,this._resetRenderer()),this):this._tooltip},loader(t){return arguments.length?(t!==this._loader&&(Va.prototype.loader.call(this,t),this._resetRenderer()),this):this._loader},resize(){return this._autosize=1,this.touch(LN(this,\"autosize\"))},_resetRenderer(){this._renderer&&(this._renderer=null,this.initialize(this._el,this._elBind))},_resizeView:function(t,e,n,r,i,o){this.runAfter((a=>{let s=0;a._autosize=0,a.width()!==n&&(s=1,a.signal(EN,n,FN),a._resizeWidth.skip(!0)),a.height()!==r&&(s=1,a.signal(DN,r,FN),a._resizeHeight.skip(!0)),a._viewWidth!==t&&(a._resize=1,a._viewWidth=t),a._viewHeight!==e&&(a._resize=1,a._viewHeight=e),a._origin[0]===i[0]&&a._origin[1]===i[1]||(a._resize=1,a._origin=i),s&&a.run(\"enter\"),o&&a.runAfter((t=>t.resize()))}),!1,1)},addEventListener(t,e,n){let r=e;return n&&!1===n.trap||(r=bN(this,e),r.raw=e),this._handler.on(t,r),this},removeEventListener(t,e){for(var n,r,i=this._handler.handlers(t),o=i.length;--o>=0;)if(r=i[o].type,n=i[o].handler,t===r&&(e===n||e===n.raw)){this._handler.off(r,n);break}return this},addResizeListener(t){const e=this._resizeListeners;return e.includes(t)||e.push(t),this},removeResizeListener(t){var e=this._resizeListeners,n=e.indexOf(t);return n>=0&&e.splice(n,1),this},addSignalListener(t,e){return qN(this,t,LN(this,t),e)},removeSignalListener(t,e){return PN(this,LN(this,t),e)},addDataListener(t,e){return qN(this,t,XB(this,t).values,e)},removeDataListener(t,e){return PN(this,XB(this,t).values,e)},globalCursor(t){if(arguments.length){if(this._globalCursor!==!!t){const e=VB(this,null);this._globalCursor=!!t,e&&VB(this,e)}return this}return this._globalCursor},preventDefault(t){return arguments.length?(this._preventDefault=t,this):this._preventDefault},timer:function(t,e){this._timers.push(function(t,e,n){var r=new cD,i=e;return null==e?(r.restart(t,e,n),r):(r._restart=r.restart,r.restart=function(t,e,n){e=+e,n=null==n?uD():+n,r._restart((function o(a){a+=i,r._restart(o,i+=e,n),t(a)}),e,n)},r.restart(t,e,n),r)}((function(e){t({timestamp:Date.now(),elapsed:e})}),e))},events:function(t,e,n){var r,i=this,o=new Ba(n),a=function(n,r){i.runAsync(null,(()=>{t===eN&&function(t,e){var n=t._eventConfig.defaults,r=n.prevent,i=n.allow;return!1!==r&&!0!==i&&(!0===r||!1===i||(r?r[e]:i?!i[e]:t.preventDefault()))}(i,e)&&n.preventDefault(),o.receive(tN(i,n,r))}))};if(\"timer\"===t)iN(i,\"timer\",e)&&i.timer(a,e);else if(t===eN)iN(i,\"view\",e)&&i.addEventListener(e,a,nN);else if(\"window\"===t?iN(i,\"window\",e)&&\"undefined\"!=typeof window&&(r=[window]):\"undefined\"!=typeof document&&iN(i,\"selector\",e)&&(r=Array.from(document.querySelectorAll(t))),r){for(var s=0,u=r.length;s<u;++s)r[s].addEventListener(e,a);rN(i,r,e,a)}else i.warn(\"Can not resolve event source: \"+t);return o},finalize:function(){var t,e,n,r,i,o=this._tooltip,a=this._timers,s=this._handler.handlers(),u=this._eventListeners;for(t=a.length;--t>=0;)a[t].stop();for(t"
-  , "=u.length;--t>=0;)for(e=(n=u[t]).sources.length;--e>=0;)n.sources[e].removeEventListener(n.type,n.handler);for(o&&o.call(this,this._handler,null,null,null),t=s.length;--t>=0;)i=s[t].type,r=s[t].handler,this._handler.off(i,r);return this},hover:function(t,e){return e=[e||\"update\",(t=[t||\"hover\"])[0]],this.on(this.events(\"view\",\"pointerover\",oN),aN,sN(t)),this.on(this.events(\"view\",\"pointerout\",oN),aN,sN(e)),this},data:function(t,e){return arguments.length<2?XB(this,t).values.value:JB.call(this,t,Ma().remove(p).insert(e))},change:JB,insert:function(t,e){return JB.call(this,t,Ma().insert(e))},remove:function(t,e){return JB.call(this,t,Ma().remove(e))},scale:function(t){var e=this._runtime.scales;return lt(e,t)||s(\"Unrecognized scale or projection: \"+t),e[t].value},initialize:function(t,e){const n=this,r=n._renderType,i=n._eventConfig.bind,o=B_(r);t=n._el=t?wN(n,t,!0):null,function(t){const e=t.container();e&&(e.setAttribute(\"role\",\"graphics-document\"),e.setAttribute(\"aria-roleDescription\",\"visualization\"),YB(e,t.description()))}(n),o||n.error(\"Unrecognized renderer type: \"+r);const a=o.handler||$v,s=t?o.renderer:o.headless;return n._renderer=s?xN(n,n._renderer,t,s):null,n._handler=function(t,e,n,r){const i=new r(t.loader(),bN(t,t.tooltip())).scene(t.scenegraph().root).initialize(n,KB(t),t);return e&&e.handlers().forEach((t=>{i.on(t.type,t.handler)})),i}(n,n._handler,t,a),n._redraw=!0,t&&\"none\"!==i&&(e=e?n._elBind=wN(n,e,!0):t.appendChild(uN(\"form\",{class:\"vega-bindings\"})),n._bind.forEach((t=>{t.param.element&&\"container\"!==i&&(t.element=wN(n,t.param.element,!!t.param.input))})),n._bind.forEach((t=>{!function(t,e,n){if(!e)return;const r=n.param;let i=n.state;i||(i=n.state={elements:null,active:!1,set:null,update:e=>{e!=t.signal(r.signal)&&t.runAsync(null,(()=>{i.source=!0,t.signal(r.signal,e)}))}},r.debounce&&(i.update=ot(r.debounce,i.update))),(null==r.input&&r.element?hN:dN)(i,e,r,t),i.active||(t.on(t._signals[r.signal],null,(()=>{i.source?i.source=!1:i.set(t.signal(r.signal))})),i.active=!0)}(n,t.element||e,t)}))),n},toImageURL:async function(t,e){t!==$_.Canvas&&t!==$_.SVG&&t!==$_.PNG&&s(\"Unrecognized image type: \"+t);const n=await MN(this,t,e);return t===$_.SVG?function(t,e){const n=new Blob([t],{type:e});return window.URL.createObjectURL(n)}(n.svg(),\"image/svg+xml\"):n.canvas().toDataURL(\"image/png\")},toCanvas:async function(t,e){return(await MN(this,$_.Canvas,t,e)).canvas()},toSVG:async function(t){return(await MN(this,$_.SVG,t)).svg()},getState:function(t){return this._runtime.getState(t||{data:TN,signals:BN,recurse:!0})},setState:function(t){return this.runAsync(null,(e=>{e._trigger=!1,e._runtime.setState(t)}),(t=>{t._trigger=!0})),this},_watchPixelRatio:function(){if(\"canvas\"===this.renderer()&&this._renderer._canvas){let t=null;const e=()=>{null!=t&&t();const n=matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);n.addEventListener(\"change\",e),t=()=>{n.removeEventListener(\"change\",e)},this._renderer._canvas.getContext(\"2d\").pixelRatio=window.devicePixelRatio||1,this._redraw=!0,this._resize=1,this.resize().runAsync()};e()}}});const jN=\"view\",IN=\"[\",WN=\"]\",HN=\"{\",YN=\"}\",GN=\":\",VN=\",\",XN=\"@\",JN=\">\",ZN=/[[\\]{}]/,QN={\"*\":1,arc:1,area:1,group:1,image:1,line:1,path:1,rect:1,rule:1,shape:1,symbol:1,text:1,trail:1};let KN,tz;function ez(t,e,n){return KN=e||jN,tz=n||QN,rz(t.trim()).map(iz)}function nz(t,e,n,r,i){const o=t.length;let a,s=0;for(;e<o;++e){if(a=t[e],!s&&a===n)return e;i&&i.includes(a)?--s:r&&r.includes(a)&&++s}return e}function rz(t){const e=[],n=t.length;let r=0,i=0;for(;i<n;)i=nz(t,i,VN,IN+HN,WN+YN),e.push(t.substring(r,i).trim()),r=++i;if(0===e.length)throw\"Empty event selector: \"+t;return e}function iz(t){return\"[\"===t[0]?function(t){const e=t.length;let n,r=1;if(r=nz(t,r,WN,IN,WN),r===e)throw\"Empty between selector: \"+t;if(n=rz(t.substring(1,r)),2!==n.length)throw\"Between selector must have two elements: \"+t;if(t=t.slice(r+1).trim(),t[0]!==JN)throw\"Expected '>' after between selector: \"+t;n=n.map(iz);const i=iz(t.slice(1).trim());if(i.between)return{between:n,stream:i};i.between=n;return i}(t):functio"
-  , "n(t){const e={source:KN},n=[];let r,i,o=[0,0],a=0,s=0,u=t.length,l=0;if(t[u-1]===YN){if(l=t.lastIndexOf(HN),!(l>=0))throw\"Unmatched right brace: \"+t;try{o=function(t){const e=t.split(VN);if(!t.length||e.length>2)throw t;return e.map((e=>{const n=+e;if(n!=n)throw t;return n}))}(t.substring(l+1,u-1))}catch(e){throw\"Invalid throttle specification: \"+t}u=(t=t.slice(0,l).trim()).length,l=0}if(!u)throw t;t[0]===XN&&(a=++l);r=nz(t,l,GN),r<u&&(n.push(t.substring(s,r).trim()),s=l=++r);if(l=nz(t,l,IN),l===u)n.push(t.substring(s,u).trim());else if(n.push(t.substring(s,l).trim()),i=[],s=++l,s===u)throw\"Unmatched left bracket: \"+t;for(;l<u;){if(l=nz(t,l,WN),l===u)throw\"Unmatched left bracket: \"+t;if(i.push(t.substring(s,l).trim()),l<u-1&&t[++l]!==IN)throw\"Expected left bracket: \"+t;s=++l}if(!(u=n.length)||ZN.test(n[u-1]))throw\"Invalid event selector: \"+t;u>1?(e.type=n[1],a?e.markname=n[0].slice(1):!function(t){return tz[t]}(n[0])?e.source=n[0]:e.marktype=n[0]):e.type=n[0];\"!\"===e.type.slice(-1)&&(e.consume=!0,e.type=e.type.slice(0,-1));null!=i&&(e.filter=i);o[0]&&(e.throttle=o[0]);o[1]&&(e.debounce=o[1]);return e}(t)}function oz(t){return M(t)?t:{type:t||\"pad\"}}const az=t=>+t||0,sz=t=>({top:t,bottom:t,left:t,right:t});function uz(t){return M(t)?t.signal?t:{top:az(t.top),bottom:az(t.bottom),left:az(t.left),right:az(t.right)}:sz(az(t))}const lz=t=>M(t)&&!A(t)?at({},t):{value:t};function cz(t,e,n,r){if(null!=n){return M(n)&&!A(n)||A(n)&&n.length&&M(n[0])?t.update[e]=n:t[r||\"enter\"][e]={value:n},1}return 0}function fz(t,e,n){for(const n in e)cz(t,n,e[n]);for(const e in n)cz(t,e,n[e],\"update\")}function hz(t,e,n){for(const r in e)n&&lt(n,r)||(t[r]=at(t[r]||{},e[r]));return t}function dz(t,e){return e&&(e.enter&&e.enter[t]||e.update&&e.update[t])}const pz=\"mark\",gz=\"frame\",mz=\"scope\",yz=\"axis\",vz=\"axis-domain\",_z=\"axis-grid\",xz=\"axis-label\",bz=\"axis-tick\",wz=\"axis-title\",kz=\"legend\",Az=\"legend-band\",Mz=\"legend-entry\",Ez=\"legend-gradient\",Dz=\"legend-label\",Cz=\"legend-symbol\",Fz=\"legend-title\",Sz=\"title\",$z=\"title-text\",Tz=\"title-subtitle\";function Bz(t,e,n){t[e]=n&&n.signal?{signal:n.signal}:{value:n}}const Nz=t=>xt(t)?Ct(t):t.signal?`(${t.signal})`:Lz(t);function zz(t){if(null!=t.gradient)return function(t){const e=[t.start,t.stop,t.count].map((t=>null==t?null:Ct(t)));for(;e.length&&null==S(e);)e.pop();return e.unshift(Nz(t.gradient)),`gradient(${e.join(\",\")})`}(t);let e=t.signal?`(${t.signal})`:t.color?function(t){return t.c?Oz(\"hcl\",t.h,t.c,t.l):t.h||t.s?Oz(\"hsl\",t.h,t.s,t.l):t.l||t.a?Oz(\"lab\",t.l,t.a,t.b):t.r||t.g||t.b?Oz(\"rgb\",t.r,t.g,t.b):null}(t.color):null!=t.field?Lz(t.field):void 0!==t.value?Ct(t.value):void 0;return null!=t.scale&&(e=function(t,e){const n=Nz(t.scale);null!=t.range?e=`lerp(_range(${n}), ${+t.range})`:(void 0!==e&&(e=`_scale(${n}, ${e})`),t.band&&(e=(e?e+\"+\":\"\")+`_bandwidth(${n})`+(1==+t.band?\"\":\"*\"+Rz(t.band)),t.extra&&(e=`(datum.extra ? _scale(${n}, datum.extra.value) : ${e})`)),null==e&&(e=\"0\"));return e}(t,e)),void 0===e&&(e=null),null!=t.exponent&&(e=`pow(${e},${Rz(t.exponent)})`),null!=t.mult&&(e+=`*${Rz(t.mult)}`),null!=t.offset&&(e+=`+${Rz(t.offset)}`),t.round&&(e=`round(${e})`),e}const Oz=(t,e,n,r)=>`(${t}(${[e,n,r].map(zz).join(\",\")})+'')`;function Rz(t){return M(t)?\"(\"+zz(t)+\")\":t}function Lz(t){return Uz(M(t)?t:{datum:t})}function Uz(t){let e,n,r;if(t.signal)e=\"datum\",r=t.signal;else if(t.group||t.parent){for(n=Math.max(1,t.level||1),e=\"item\";n-- >0;)e+=\".mark.group\";t.parent?(r=t.parent,e+=\".datum\"):r=t.group}else t.datum?(e=\"datum\",r=t.datum):s(\"Invalid field reference: \"+Ct(t));return t.signal||(r=xt(r)?u(r).map(Ct).join(\"][\"):Uz(r)),e+\"[\"+r+\"]\"}function qz(t,e,n,r,i,o){const a={};(o=o||{}).encoders={$encode:a},t=function(t,e,n,r,i){const o={},a={};let s,u,l,c;for(u in u=\"lineBreak\",\"text\"!==e||null==i[u]||dz(u,t)||Bz(o,u,i[u]),(\"legend\"==n||String(n).startsWith(\"axis\"))&&(n=null),c=n===gz?i.group:n===pz?at({},i.mark,i[e]):null,c)l=dz(u,t)||(\"fill\"===u||\"stroke\"===u)&&(dz(\"fill\",t)||dz(\"stroke\",t)),l||Bz(o,u,c[u]);for(u in X(r).forEach((e=>{const n=i.style&&i.style[e];for(const e in n)dz(e,t)||Bz(o,e,"
-  , "n[e])})),t=at({},t),o)c=o[u],c.signal?(s=s||{})[u]=c:a[u]=c;return t.enter=at(a,t.enter),s&&(t.update=at(s,t.update)),t}(t,e,n,r,i.config);for(const n in t)a[n]=Pz(t[n],e,o,i);return o}function Pz(t,e,n,r){const i={},o={};for(const e in t)null!=t[e]&&(i[e]=jz((a=t[e],A(a)?function(t){let e=\"\";return t.forEach((t=>{const n=zz(t);e+=t.test?`(${t.test})?${n}:`:n})),\":\"===S(e)&&(e+=\"null\"),e}(a):zz(a)),r,n,o));var a;return{$expr:{marktype:e,channels:i},$fields:Object.keys(o),$output:Object.keys(t)}}function jz(t,e,n,r){const i=NB(t,e);return i.$fields.forEach((t=>r[t]=1)),at(n,i.$params),i.$expr}const Iz=\"outer\",Wz=[\"value\",\"update\",\"init\",\"react\",\"bind\"];function Hz(t,e){s(t+' for \"outer\" push: '+Ct(e))}function Yz(t,e){const n=t.name;if(t.push===Iz)e.signals[n]||Hz(\"No prior signal definition\",n),Wz.forEach((e=>{void 0!==t[e]&&Hz(\"Invalid property \",e)}));else{const r=e.addSignal(n,t.value);!1===t.react&&(r.react=!1),t.bind&&e.addBinding(n,t.bind)}}function Gz(t,e,n,r){this.id=-1,this.type=t,this.value=e,this.params=n,r&&(this.parent=r)}function Vz(t,e,n,r){return new Gz(t,e,n,r)}function Xz(t,e){return Vz(\"operator\",t,e)}function Jz(t){const e={$ref:t.id};return t.id<0&&(t.refs=t.refs||[]).push(e),e}function Zz(t,e){return e?{$field:t,$name:e}:{$field:t}}const Qz=Zz(\"key\");function Kz(t,e){return{$compare:t,$order:e}}const tO=\"descending\";function eO(t,e){return(t&&t.signal?\"$\"+t.signal:t||\"\")+(t&&e?\"_\":\"\")+(e&&e.signal?\"$\"+e.signal:e||\"\")}const nO=\"scope\",rO=\"view\";function iO(t){return t&&t.signal}function oO(t){if(iO(t))return!0;if(M(t))for(const e in t)if(oO(t[e]))return!0;return!1}function aO(t,e){return null!=t?t:e}function sO(t){return t&&t.signal||t}const uO=\"timer\";function lO(t,e){return(t.merge?cO:t.stream?fO:t.type?hO:s(\"Invalid stream specification: \"+Ct(t)))(t,e)}function cO(t,e){const n=dO({merge:t.merge.map((t=>lO(t,e)))},t,e);return e.addStream(n).id}function fO(t,e){const n=dO({stream:lO(t.stream,e)},t,e);return e.addStream(n).id}function hO(t,e){let n;t.type===uO?(n=e.event(uO,t.throttle),t={between:t.between,filter:t.filter}):n=e.event(function(t){return t===nO?rO:t||rO}(t.source),t.type);const r=dO({stream:n},t,e);return 1===Object.keys(r).length?n:e.addStream(r).id}function dO(t,e,n){let r=e.between;return r&&(2!==r.length&&s('Stream \"between\" parameter must have 2 entries: '+Ct(e)),t.between=[lO(r[0],n),lO(r[1],n)]),r=e.filter?[].concat(e.filter):[],(e.marktype||e.markname||e.markrole)&&r.push(function(t,e,n){const r=\"event.item\";return r+(t&&\"*\"!==t?\"&&\"+r+\".mark.marktype==='\"+t+\"'\":\"\")+(n?\"&&\"+r+\".mark.role==='\"+n+\"'\":\"\")+(e?\"&&\"+r+\".mark.name==='\"+e+\"'\":\"\")}(e.marktype,e.markname,e.markrole)),e.source===nO&&r.push(\"inScope(event.item)\"),r.length&&(t.filter=NB(\"(\"+r.join(\")&&(\")+\")\",n).$expr),null!=(r=e.throttle)&&(t.throttle=+r),null!=(r=e.debounce)&&(t.debounce=+r),e.consume&&(t.consume=!0),t}const pO={code:\"_.$value\",ast:{type:\"Identifier\",value:\"value\"}};function gO(t,e,n){const r=t.encode,i={target:n};let o=t.events,a=t.update,u=[];o||s(\"Signal update missing events specification.\"),xt(o)&&(o=ez(o,e.isSubscope()?nO:rO)),o=X(o).filter((t=>t.signal||t.scale?(u.push(t),0):1)),u.length>1&&(u=[mO(u)]),o.length&&u.push(o.length>1?{merge:o}:o[0]),null!=r&&(a&&s(\"Signal encode and update are mutually exclusive.\"),a=\"encode(item(),\"+Ct(r)+\")\"),i.update=xt(a)?NB(a,e):null!=a.expr?NB(a.expr,e):null!=a.value?a.value:null!=a.signal?{$expr:pO,$params:{$value:e.signalRef(a.signal)}}:s(\"Invalid signal update specification.\"),t.force&&(i.options={force:!0}),u.forEach((t=>e.addUpdate(at(function(t,e){return{source:t.signal?e.signalRef(t.signal):t.scale?e.scaleRef(t.scale):lO(t,e)}}(t,e),i))))}function mO(t){return{signal:\"[\"+t.map((t=>t.scale?'scale(\"'+t.scale+'\")':t.signal))+\"]\"}}const yO=t=>(e,n,r)=>Vz(t,n,e||void 0,r),vO=yO(\"aggregate\"),_O=yO(\"axisticks\"),xO=yO(\"bound\"),bO=yO(\"collect\"),wO=yO(\"compare\"),kO=yO(\"datajoin\"),AO=yO(\"encode\"),MO=yO(\"expression\"),EO=yO(\"facet\"),DO=yO(\"field\"),CO=yO(\"key\"),FO=yO(\"legendentries\"),SO=yO(\"load\"),$O=yO(\"mark\"),TO=yO(\"multiextent\"),BO=yO(\"multivalues\"),NO=yO(\"overlap\"),"
-  , "zO=yO(\"params\"),OO=yO(\"prefacet\"),RO=yO(\"projection\"),LO=yO(\"proxy\"),UO=yO(\"relay\"),qO=yO(\"render\"),PO=yO(\"scale\"),jO=yO(\"sieve\"),IO=yO(\"sortitems\"),WO=yO(\"viewlayout\"),HO=yO(\"values\");let YO=0;const GO={min:\"min\",max:\"max\",count:\"sum\"};function VO(t,e){const n=e.getScale(t.name).params;let r;for(r in n.domain=QO(t.domain,t,e),null!=t.range&&(n.range=aR(t,e,n)),null!=t.interpolate&&function(t,e){e.interpolate=XO(t.type||t),null!=t.gamma&&(e.interpolateGamma=XO(t.gamma))}(t.interpolate,n),null!=t.nice&&(n.nice=function(t,e){return t.signal?e.signalRef(t.signal):M(t)?{interval:XO(t.interval),step:XO(t.step)}:XO(t)}(t.nice,e)),null!=t.bins&&(n.bins=function(t,e){return t.signal||A(t)?JO(t,e):e.objectProperty(t)}(t.bins,e)),t)lt(n,r)||\"name\"===r||(n[r]=XO(t[r],e))}function XO(t,e){return M(t)?t.signal?e.signalRef(t.signal):s(\"Unsupported object: \"+Ct(t)):t}function JO(t,e){return t.signal?e.signalRef(t.signal):t.map((t=>XO(t,e)))}function ZO(t){s(\"Can not find data set: \"+Ct(t))}function QO(t,e,n){if(t)return t.signal?n.signalRef(t.signal):(A(t)?KO:t.fields?eR:tR)(t,e,n);null==e.domainMin&&null==e.domainMax||s(\"No scale domain defined for domainMin/domainMax to override.\")}function KO(t,e,n){return t.map((t=>XO(t,n)))}function tR(t,e,n){const r=n.getData(t.data);return r||ZO(t.data),fp(e.type)?r.valuesRef(n,t.field,rR(t.sort,!1)):gp(e.type)?r.domainRef(n,t.field):r.extentRef(n,t.field)}function eR(t,e,n){const r=t.data,i=t.fields.reduce(((t,e)=>(e=xt(e)?{data:r,field:e}:A(e)||e.signal?function(t,e){const n=\"_:vega:_\"+YO++,r=bO({});if(A(t))r.value={$ingest:t};else if(t.signal){const i=\"setdata(\"+Ct(n)+\",\"+t.signal+\")\";r.params.input=e.signalRef(i)}return e.addDataPipeline(n,[r,jO({})]),{data:n,field:\"data\"}}(e,n):e,t.push(e),t)),[]);return(fp(e.type)?nR:gp(e.type)?iR:oR)(t,n,i)}function nR(t,e,n){const r=rR(t.sort,!0);let i,o;const a=n.map((t=>{const n=e.getData(t.data);return n||ZO(t.data),n.countsRef(e,t.field,r)})),s={groupby:Qz,pulse:a};r&&(i=r.op||\"count\",o=r.field?eO(i,r.field):\"count\",s.ops=[GO[i]],s.fields=[e.fieldRef(o)],s.as=[o]),i=e.add(vO(s));const u=e.add(bO({pulse:Jz(i)}));return o=e.add(HO({field:Qz,sort:e.sortRef(r),pulse:Jz(u)})),Jz(o)}function rR(t,e){return t&&(t.field||t.op?t.field||\"count\"===t.op?e&&t.field&&t.op&&!GO[t.op]&&s(\"Multiple domain scales can not be sorted using \"+t.op):s(\"No field provided for sort aggregate op: \"+t.op):M(t)?t.field=\"key\":t={field:\"key\"}),t}function iR(t,e,n){const r=n.map((t=>{const n=e.getData(t.data);return n||ZO(t.data),n.domainRef(e,t.field)}));return Jz(e.add(BO({values:r})))}function oR(t,e,n){const r=n.map((t=>{const n=e.getData(t.data);return n||ZO(t.data),n.extentRef(e,t.field)}));return Jz(e.add(TO({extents:r})))}function aR(t,e,n){const r=e.config.range;let i=t.range;if(i.signal)return e.signalRef(i.signal);if(xt(i)){if(r&&lt(r,i))return aR(t=at({},t,{range:r[i]}),e,n);\"width\"===i?i=[0,{signal:\"width\"}]:\"height\"===i?i=fp(t.type)?[0,{signal:\"height\"}]:[{signal:\"height\"},0]:s(\"Unrecognized scale range value: \"+Ct(i))}else{if(i.scheme)return n.scheme=A(i.scheme)?JO(i.scheme,e):XO(i.scheme,e),i.extent&&(n.schemeExtent=JO(i.extent,e)),void(i.count&&(n.schemeCount=XO(i.count,e)));if(i.step)return void(n.rangeStep=XO(i.step,e));if(fp(t.type)&&!A(i))return QO(i,t,e);A(i)||s(\"Unsupported range type: \"+Ct(i))}return i.map((t=>(A(t)?JO:XO)(t,e)))}function sR(t,e,n){return A(t)?t.map((t=>sR(t,e,n))):M(t)?t.signal?n.signalRef(t.signal):\"fit\"===e?t:s(\"Unsupported parameter object: \"+Ct(t)):t}const uR=\"top\",lR=\"left\",cR=\"right\",fR=\"bottom\",hR=\"center\",dR=\"vertical\",pR=\"start\",gR=\"end\",mR=\"index\",yR=\"label\",vR=\"offset\",_R=\"perc\",xR=\"perc2\",bR=\"value\",wR=\"guide-label\",kR=\"guide-title\",AR=\"group-title\",MR=\"group-subtitle\",ER=\"symbol\",DR=\"gradient\",CR=\"discrete\",FR=\"size\",SR=[FR,\"shape\",\"fill\",\"stroke\",\"strokeWidth\",\"strokeDash\",\"opacity\"],$R={name:1,style:1,interactive:1},TR={value:0},BR={value:1},NR=\"group\",zR=\"rect\",OR=\"rule\",RR=\"symbol\",LR=\"text\";function UR(t){return t.type=NR,t.interactive=t.interactive||!1,t}function qR(t,e){const n=(n,r)=>aO(t[n],aO(e[n],r));return n.isVerti"
-  , "cal=n=>dR===aO(t.direction,e.direction||(n?e.symbolDirection:e.gradientDirection)),n.gradientLength=()=>aO(t.gradientLength,e.gradientLength||e.gradientWidth),n.gradientThickness=()=>aO(t.gradientThickness,e.gradientThickness||e.gradientHeight),n.entryColumns=()=>aO(t.columns,aO(e.columns,+n.isVertical(!0))),n}function PR(t,e){const n=e&&(e.update&&e.update[t]||e.enter&&e.enter[t]);return n&&n.signal?n:n?n.value:null}function jR(t,e,n){return`item.anchor === '${pR}' ? ${t} : item.anchor === '${gR}' ? ${e} : ${n}`}const IR=jR(Ct(lR),Ct(cR),Ct(hR));function WR(t,e){return e?t?M(t)?Object.assign({},t,{offset:WR(t.offset,e)}):{value:t,offset:e}:e:t}function HR(t,e){return e?(t.name=e.name,t.style=e.style||t.style,t.interactive=!!e.interactive,t.encode=hz(t.encode,e,$R)):t.interactive=!1,t}function YR(t,e,n,r){const i=qR(t,n),o=i.isVertical(),a=i.gradientThickness(),s=i.gradientLength();let u,l,c,f,h;o?(l=[0,1],c=[0,0],f=a,h=s):(l=[0,0],c=[1,0],f=s,h=a);const d={enter:u={opacity:TR,x:TR,y:TR,width:lz(f),height:lz(h)},update:at({},u,{opacity:BR,fill:{gradient:e,start:l,stop:c}}),exit:{opacity:TR}};return fz(d,{stroke:i(\"gradientStrokeColor\"),strokeWidth:i(\"gradientStrokeWidth\")},{opacity:i(\"gradientOpacity\")}),HR({type:zR,role:Ez,encode:d},r)}function GR(t,e,n,r,i){const o=qR(t,n),a=o.isVertical(),s=o.gradientThickness(),u=o.gradientLength();let l,c,f,h,d=\"\";a?(l=\"y\",f=\"y2\",c=\"x\",h=\"width\",d=\"1-\"):(l=\"x\",f=\"x2\",c=\"y\",h=\"height\");const p={opacity:TR,fill:{scale:e,field:bR}};p[l]={signal:d+\"datum.\"+_R,mult:u},p[c]=TR,p[f]={signal:d+\"datum.\"+xR,mult:u},p[h]=lz(s);const g={enter:p,update:at({},p,{opacity:BR}),exit:{opacity:TR}};return fz(g,{stroke:o(\"gradientStrokeColor\"),strokeWidth:o(\"gradientStrokeWidth\")},{opacity:o(\"gradientOpacity\")}),HR({type:zR,role:Az,key:bR,from:i,encode:g},r)}const VR=`datum.${_R}<=0?\"${lR}\":datum.${_R}>=1?\"${cR}\":\"${hR}\"`,XR=`datum.${_R}<=0?\"${fR}\":datum.${_R}>=1?\"${uR}\":\"middle\"`;function JR(t,e,n,r){const i=qR(t,e),o=i.isVertical(),a=lz(i.gradientThickness()),s=i.gradientLength();let u,l,c,f,h=i(\"labelOverlap\"),d=\"\";const p={enter:u={opacity:TR},update:l={opacity:BR,text:{field:yR}},exit:{opacity:TR}};return fz(p,{fill:i(\"labelColor\"),fillOpacity:i(\"labelOpacity\"),font:i(\"labelFont\"),fontSize:i(\"labelFontSize\"),fontStyle:i(\"labelFontStyle\"),fontWeight:i(\"labelFontWeight\"),limit:aO(t.labelLimit,e.gradientLabelLimit)}),o?(u.align={value:\"left\"},u.baseline=l.baseline={signal:XR},c=\"y\",f=\"x\",d=\"1-\"):(u.align=l.align={signal:VR},u.baseline={value:\"top\"},c=\"x\",f=\"y\"),u[c]=l[c]={signal:d+\"datum.\"+_R,mult:s},u[f]=l[f]=a,a.offset=aO(t.labelOffset,e.gradientLabelOffset)||0,h=h?{separation:i(\"labelSeparation\"),method:h,order:\"datum.\"+mR}:void 0,HR({type:LR,role:Dz,style:wR,key:bR,from:r,encode:p,overlap:h},n)}function ZR(t,e,n,r,i){const o=qR(t,e),a=n.entries,s=!(!a||!a.interactive),u=a?a.name:void 0,l=o(\"clipHeight\"),c=o(\"symbolOffset\"),f={data:\"value\"},h=`(${i}) ? datum.${vR} : datum.${FR}`,d=l?lz(l):{field:FR},p=`datum.${mR}`,g=`max(1, ${i})`;let m,y,v,_,x;d.mult=.5,m={enter:y={opacity:TR,x:{signal:h,mult:.5,offset:c},y:d},update:v={opacity:BR,x:y.x,y:y.y},exit:{opacity:TR}};let b=null,w=null;t.fill||(b=e.symbolBaseFillColor,w=e.symbolBaseStrokeColor),fz(m,{fill:o(\"symbolFillColor\",b),shape:o(\"symbolType\"),size:o(\"symbolSize\"),stroke:o(\"symbolStrokeColor\",w),strokeDash:o(\"symbolDash\"),strokeDashOffset:o(\"symbolDashOffset\"),strokeWidth:o(\"symbolStrokeWidth\")},{opacity:o(\"symbolOpacity\")}),SR.forEach((e=>{t[e]&&(v[e]=y[e]={scale:t[e],field:bR})}));const k=HR({type:RR,role:Cz,key:bR,from:f,clip:!!l||void 0,encode:m},n.symbols),A=lz(c);A.offset=o(\"labelOffset\"),m={enter:y={opacity:TR,x:{signal:h,offset:A},y:d},update:v={opacity:BR,text:{field:yR},x:y.x,y:y.y},exit:{opacity:TR}},fz(m,{align:o(\"labelAlign\"),baseline:o(\"labelBaseline\"),fill:o(\"labelColor\"),fillOpacity:o(\"labelOpacity\"),font:o(\"labelFont\"),fontSize:o(\"labelFontSize\"),fontStyle:o(\"labelFontStyle\"),fontWeight:o(\"labelFontWeight\"),limit:o(\"labelLimit\")});const M=HR({type:LR,role:Dz,style:wR,key:bR,from:f,encode:m},n.labels);return m={enter:{noBound:{va"
-  , "lue:!l},width:TR,height:l?lz(l):TR,opacity:TR},exit:{opacity:TR},update:v={opacity:BR,row:{signal:null},column:{signal:null}}},o.isVertical(!0)?(_=`ceil(item.mark.items.length / ${g})`,v.row.signal=`${p}%${_}`,v.column.signal=`floor(${p} / ${_})`,x={field:[\"row\",p]}):(v.row.signal=`floor(${p} / ${g})`,v.column.signal=`${p} % ${g}`,x={field:p}),v.column.signal=`(${i})?${v.column.signal}:${p}`,UR({role:mz,from:r={facet:{data:r,name:\"value\",groupby:mR}},encode:hz(m,a,$R),marks:[k,M],name:u,interactive:s,sort:x})}const QR='item.orient === \"left\"',KR='item.orient === \"right\"',tL=`(${QR} || ${KR})`,eL=`datum.vgrad && ${tL}`,nL=jR('\"top\"','\"bottom\"','\"middle\"'),rL=`datum.vgrad && ${KR} ? (${jR('\"right\"','\"left\"','\"center\"')}) : (${tL} && !(datum.vgrad && ${QR})) ? \"left\" : ${IR}`,iL=`item._anchor || (${tL} ? \"middle\" : \"start\")`,oL=`${eL} ? (${QR} ? -90 : 90) : 0`,aL=`${tL} ? (datum.vgrad ? (${KR} ? \"bottom\" : \"top\") : ${nL}) : \"top\"`;function sL(t,e){let n;return M(t)&&(t.signal?n=t.signal:t.path?n=\"pathShape(\"+uL(t.path)+\")\":t.sphere&&(n=\"geoShape(\"+uL(t.sphere)+', {type: \"Sphere\"})')),n?e.signalRef(n):!!t}function uL(t){return M(t)&&t.signal?t.signal:Ct(t)}function lL(t){const e=t.role||\"\";return e.startsWith(\"axis\")||e.startsWith(\"legend\")||e.startsWith(\"title\")?e:t.type===NR?mz:e||pz}function cL(t){return{marktype:t.type,name:t.name||void 0,role:t.role||lL(t),zindex:+t.zindex||void 0,aria:t.aria,description:t.description}}function fL(t,e){return t&&t.signal?e.signalRef(t.signal):!1!==t}function hL(t,e){const n=Qa(t.type);n||s(\"Unrecognized transform type: \"+Ct(t.type));const r=Vz(n.type.toLowerCase(),null,dL(n,t,e));return t.signal&&e.addSignal(t.signal,e.proxy(r)),r.metadata=n.metadata||{},r}function dL(t,e,n){const r={},i=t.params.length;for(let o=0;o<i;++o){const i=t.params[o];r[i.name]=pL(i,e,n)}return r}function pL(t,e,n){const r=t.type,i=e[t.name];return\"index\"===r?function(t,e,n){xt(e.from)||s('Lookup \"from\" parameter must be a string literal.');return n.getData(e.from).lookupRef(n,e.key)}(0,e,n):void 0!==i?\"param\"===r?function(t,e,n){const r=e[t.name];return t.array?(A(r)||s(\"Expected an array of sub-parameters. Instead: \"+Ct(r)),r.map((e=>mL(t,e,n)))):mL(t,r,n)}(t,e,n):\"projection\"===r?n.projectionRef(e[t.name]):t.array&&!iO(i)?i.map((e=>gL(t,e,n))):gL(t,i,n):void(t.required&&s(\"Missing required \"+Ct(e.type)+\" parameter: \"+Ct(t.name)))}function gL(t,e,n){const r=t.type;if(iO(e))return xL(r)?s(\"Expression references can not be signals.\"):bL(r)?n.fieldRef(e):wL(r)?n.compareRef(e):n.signalRef(e.signal);{const i=t.expr||bL(r);return i&&yL(e)?n.exprRef(e.expr,e.as):i&&vL(e)?Zz(e.field,e.as):xL(r)?NB(e,n):_L(r)?Jz(n.getData(e).values):bL(r)?Zz(e):wL(r)?n.compareRef(e):e}}function mL(t,e,n){const r=t.params.length;let i;for(let n=0;n<r;++n){i=t.params[n];for(const t in i.key)if(i.key[t]!==e[t]){i=null;break}if(i)break}i||s(\"Unsupported parameter: \"+Ct(e));const o=at(dL(i,e,n),i.key);return Jz(n.add(zO(o)))}const yL=t=>t&&t.expr,vL=t=>t&&t.field,_L=t=>\"data\"===t,xL=t=>\"expr\"===t,bL=t=>\"field\"===t,wL=t=>\"compare\"===t;function kL(t,e){return t.$ref?t:t.data&&t.data.$ref?t.data:Jz(e.getData(t.data).output)}function AL(t,e,n,r,i){this.scope=t,this.input=e,this.output=n,this.values=r,this.aggregate=i,this.index={}}function ML(t){return xt(t)?t:null}function EL(t,e,n){const r=eO(n.op,n.field);let i;if(e.ops){for(let t=0,n=e.as.length;t<n;++t)if(e.as[t]===r)return}else e.ops=[\"count\"],e.fields=[null],e.as=[\"count\"];n.op&&(e.ops.push((i=n.op.signal)?t.signalRef(i):n.op),e.fields.push(t.fieldRef(n.field)),e.as.push(r))}function DL(t,e,n,r,i,o,a){const s=e[n]||(e[n]={}),u=function(t){return M(t)?(t.order===tO?\"-\":\"+\")+eO(t.op,t.field):\"\"}(o);let l,c,f=ML(i);if(null!=f&&(t=e.scope,f+=u?\"|\"+u:\"\",l=s[f]),!l){const n=o?{field:Qz,pulse:e.countsRef(t,i,o)}:{field:t.fieldRef(i),pulse:Jz(e.output)};u&&(n.sort=t.sortRef(o)),c=t.add(Vz(r,void 0,n)),a&&(e.index[i]=c),l=Jz(c),null!=f&&(s[f]=l)}return l}function CL(t,e,n){const r=t.remove,i=t.insert,o=t.toggle,a=t.modify,s=t.values,u=e.add(Xz()),l=NB(\"if(\"+t.trigger+',modify(\"'+n+'\",'+[i,r,o,a,"
-  , "s].map((t=>null==t?\"null\":t)).join(\",\")+\"),0)\",e);u.update=l.$expr,u.params=l.$params}function FL(t,e){const n=lL(t),r=t.type===NR,i=t.from&&t.from.facet,o=t.overlap;let a,u,l,c,f,h,d,p=t.layout||n===mz||n===gz;const g=n===pz||p||i,m=function(t,e,n){let r,i,o,a,u;return t?(r=t.facet)&&(e||s(\"Only group marks can be faceted.\"),null!=r.field?a=u=kL(r,n):(t.data?u=Jz(n.getData(t.data).aggregate):(o=hL(at({type:\"aggregate\",groupby:X(r.groupby)},r.aggregate),n),o.params.key=n.keyRef(r.groupby),o.params.pulse=kL(r,n),a=u=Jz(n.add(o))),i=n.keyRef(r.groupby,!0))):a=Jz(n.add(bO(null,[{}]))),a||(a=kL(t,n)),{key:i,pulse:a,parent:u}}(t.from,r,e);u=e.add(kO({key:m.key||(t.key?Zz(t.key):void 0),pulse:m.pulse,clean:!r}));const y=Jz(u);u=l=e.add(bO({pulse:y})),u=e.add($O({markdef:cL(t),interactive:fL(t.interactive,e),clip:sL(t.clip,e),context:{$context:!0},groups:e.lookup(),parent:e.signals.parent?e.signalRef(\"parent\"):null,index:e.markpath(),pulse:Jz(u)}));const v=Jz(u);u=c=e.add(AO(qz(t.encode,t.type,n,t.style,e,{mod:!1,pulse:v}))),u.params.parent=e.encode(),t.transform&&t.transform.forEach((t=>{const n=hL(t,e),r=n.metadata;(r.generates||r.changes)&&s(\"Mark transforms should not generate new data.\"),r.nomod||(c.params.mod=!0),n.params.pulse=Jz(u),e.add(u=n)})),t.sort&&(u=e.add(IO({sort:e.compareRef(t.sort),pulse:Jz(u)})));const _=Jz(u);(i||p)&&(p=e.add(WO({layout:e.objectProperty(t.layout),legends:e.legends,mark:v,pulse:_})),h=Jz(p));const x=e.add(xO({mark:v,pulse:h||_}));d=Jz(x),r&&(g&&(a=e.operators,a.pop(),p&&a.pop()),e.pushState(_,h||d,y),i?function(t,e,n){const r=t.from.facet,i=r.name,o=kL(r,e);let a;r.name||s(\"Facet must have a name: \"+Ct(r)),r.data||s(\"Facet must reference a data set: \"+Ct(r)),r.field?a=e.add(OO({field:e.fieldRef(r.field),pulse:o})):r.groupby?a=e.add(EO({key:e.keyRef(r.groupby),group:Jz(e.proxy(n.parent)),pulse:o})):s(\"Facet must specify groupby or field: \"+Ct(r));const u=e.fork(),l=u.add(bO()),c=u.add(jO({pulse:Jz(l)}));u.addData(i,new AL(u,l,l,c)),u.addSignal(\"parent\",null),a.params.subflow={$subflow:u.parse(t).toRuntime()}}(t,e,m):g?function(t,e,n){const r=e.add(OO({pulse:n.pulse})),i=e.fork();i.add(jO()),i.addSignal(\"parent\",null),r.params.subflow={$subflow:i.parse(t).toRuntime()}}(t,e,m):e.parse(t),e.popState(),g&&(p&&a.push(p),a.push(x))),o&&(d=function(t,e,n){const r=t.method,i=t.bound,o=t.separation,a={separation:iO(o)?n.signalRef(o.signal):o,method:iO(r)?n.signalRef(r.signal):r,pulse:e};t.order&&(a.sort=n.compareRef({field:t.order}));if(i){const t=i.tolerance;a.boundTolerance=iO(t)?n.signalRef(t.signal):+t,a.boundScale=n.scaleRef(i.scale),a.boundOrient=i.orient}return Jz(n.add(NO(a)))}(o,d,e));const b=e.add(qO({pulse:d})),w=e.add(jO({pulse:Jz(b)},void 0,e.parent()));null!=t.name&&(f=t.name,e.addData(f,new AL(e,l,b,w)),t.on&&t.on.forEach((t=>{(t.insert||t.remove||t.toggle)&&s(\"Marks only support modify triggers.\"),CL(t,e,f)})))}function SL(t,e){const n=e.config.legend,r=t.encode||{},i=qR(t,n),o=r.legend||{},a=o.name||void 0,u=o.interactive,l=o.style,c={};let f,h,d,p=0;SR.forEach((e=>t[e]?(c[e]=t[e],p=p||t[e]):0)),p||s(\"Missing valid scale for legend.\");const g=function(t,e){let n=t.type||ER;t.type||1!==function(t){return SR.reduce(((e,n)=>e+(t[n]?1:0)),0)}(t)||!t.fill&&!t.stroke||(n=cp(e)?DR:hp(e)?CR:ER);return n!==DR?n:hp(e)?CR:DR}(t,e.scaleType(p)),m={title:null!=t.title,scales:c,type:g,vgrad:\"symbol\"!==g&&i.isVertical()},y=Jz(e.add(bO(null,[m]))),v=Jz(e.add(FO(h={type:g,scale:e.scaleRef(p),count:e.objectProperty(i(\"tickCount\")),limit:e.property(i(\"symbolLimit\")),values:e.objectProperty(t.values),minstep:e.property(t.tickMinStep),formatType:e.property(t.formatType),formatSpecifier:e.property(t.format)})));return g===DR?(d=[YR(t,p,n,r.gradient),JR(t,n,r.labels,v)],h.count=h.count||e.signalRef(`max(2,2*floor((${sO(i.gradientLength())})/100))`)):g===CR?d=[GR(t,p,n,r.gradient,v),JR(t,n,r.labels,v)]:(f=function(t,e){const n=qR(t,e);return{align:n(\"gridAlign\"),columns:n.entryColumns(),center:{row:!0,column:!1},padding:{row:n(\"rowPadding\"),column:n(\"columnPadding\")}}}(t,n),d=[ZR(t,n,r,v,sO(f.columns))],h.siz"
-  , "e=function(t,e,n){const r=sO(TL(\"size\",t,n)),i=sO(TL(\"strokeWidth\",t,n)),o=sO(function(t,e,n){return PR(\"fontSize\",t)||function(t,e,n){const r=e.config.style[n];return r&&r[t]}(\"fontSize\",e,n)}(n[1].encode,e,wR));return NB(`max(ceil(sqrt(${r})+${i}),${o})`,e)}(t,e,d[0].marks)),d=[UR({role:Mz,from:y,encode:{enter:{x:{value:0},y:{value:0}}},marks:d,layout:f,interactive:u})],m.title&&d.push(function(t,e,n,r){const i=qR(t,e),o={enter:{opacity:TR},update:{opacity:BR,x:{field:{group:\"padding\"}},y:{field:{group:\"padding\"}}},exit:{opacity:TR}};return fz(o,{orient:i(\"titleOrient\"),_anchor:i(\"titleAnchor\"),anchor:{signal:iL},angle:{signal:oL},align:{signal:rL},baseline:{signal:aL},text:t.title,fill:i(\"titleColor\"),fillOpacity:i(\"titleOpacity\"),font:i(\"titleFont\"),fontSize:i(\"titleFontSize\"),fontStyle:i(\"titleFontStyle\"),fontWeight:i(\"titleFontWeight\"),limit:i(\"titleLimit\"),lineHeight:i(\"titleLineHeight\")},{align:i(\"titleAlign\"),baseline:i(\"titleBaseline\")}),HR({type:LR,role:Fz,style:kR,from:r,encode:o},n)}(t,n,r.title,y)),FL(UR({role:kz,from:y,encode:hz($L(i,t,n),o,$R),marks:d,aria:i(\"aria\"),description:i(\"description\"),zindex:i(\"zindex\"),name:a,interactive:u,style:l}),e)}function $L(t,e,n){const r={enter:{},update:{}};return fz(r,{orient:t(\"orient\"),offset:t(\"offset\"),padding:t(\"padding\"),titlePadding:t(\"titlePadding\"),cornerRadius:t(\"cornerRadius\"),fill:t(\"fillColor\"),stroke:t(\"strokeColor\"),strokeWidth:n.strokeWidth,strokeDash:n.strokeDash,x:t(\"legendX\"),y:t(\"legendY\"),format:e.format,formatType:e.formatType}),r}function TL(t,e,n){return e[t]?`scale(\"${e[t]}\",datum)`:PR(t,n[0].encode)}AL.fromEntries=function(t,e){const n=e.length,r=e[n-1],i=e[n-2];let o=e[0],a=null,s=1;for(o&&\"load\"===o.type&&(o=e[1]),t.add(e[0]);s<n;++s)e[s].params.pulse=Jz(e[s-1]),t.add(e[s]),\"aggregate\"===e[s].type&&(a=e[s]);return new AL(t,o,i,r,a)},AL.prototype={countsRef(t,e,n){const r=this,i=r.counts||(r.counts={}),o=ML(e);let a,s,u;return null!=o&&(t=r.scope,a=i[o]),a?n&&n.field&&EL(t,a.agg.params,n):(u={groupby:t.fieldRef(e,\"key\"),pulse:Jz(r.output)},n&&n.field&&EL(t,u,n),s=t.add(vO(u)),a=t.add(bO({pulse:Jz(s)})),a={agg:s,ref:Jz(a)},null!=o&&(i[o]=a)),a.ref},tuplesRef(){return Jz(this.values)},extentRef(t,e){return DL(t,this,\"extent\",\"extent\",e,!1)},domainRef(t,e){return DL(t,this,\"domain\",\"values\",e,!1)},valuesRef(t,e,n){return DL(t,this,\"vals\",\"values\",e,n||!0)},lookupRef(t,e){return DL(t,this,\"lookup\",\"tupleindex\",e,!1)},indataRef(t,e){return DL(t,this,\"indata\",\"tupleindex\",e,!0,!0)}};const BL=`item.orient===\"${lR}\"?-90:item.orient===\"${cR}\"?90:0`;function NL(t,e){const n=qR(t=xt(t)?{text:t}:t,e.config.title),r=t.encode||{},i=r.group||{},o=i.name||void 0,a=i.interactive,s=i.style,u=[],l=Jz(e.add(bO(null,[{}])));return u.push(function(t,e,n,r){const i={value:0},o=t.text,a={enter:{opacity:i},update:{opacity:{value:1}},exit:{opacity:i}};return fz(a,{text:o,align:{signal:\"item.mark.group.align\"},angle:{signal:\"item.mark.group.angle\"},limit:{signal:\"item.mark.group.limit\"},baseline:\"top\",dx:e(\"dx\"),dy:e(\"dy\"),fill:e(\"color\"),font:e(\"font\"),fontSize:e(\"fontSize\"),fontStyle:e(\"fontStyle\"),fontWeight:e(\"fontWeight\"),lineHeight:e(\"lineHeight\")},{align:e(\"align\"),angle:e(\"angle\"),baseline:e(\"baseline\")}),HR({type:LR,role:$z,style:AR,from:r,encode:a},n)}(t,n,function(t){const e=t.encode;return e&&e.title||at({name:t.name,interactive:t.interactive,style:t.style},e)}(t),l)),t.subtitle&&u.push(function(t,e,n,r){const i={value:0},o=t.subtitle,a={enter:{opacity:i},update:{opacity:{value:1}},exit:{opacity:i}};return fz(a,{text:o,align:{signal:\"item.mark.group.align\"},angle:{signal:\"item.mark.group.angle\"},limit:{signal:\"item.mark.group.limit\"},baseline:\"top\",dx:e(\"dx\"),dy:e(\"dy\"),fill:e(\"subtitleColor\"),font:e(\"subtitleFont\"),fontSize:e(\"subtitleFontSize\"),fontStyle:e(\"subtitleFontStyle\"),fontWeight:e(\"subtitleFontWeight\"),lineHeight:e(\"subtitleLineHeight\")},{align:e(\"align\"),angle:e(\"angle\"),baseline:e(\"baseline\")}),HR({type:LR,role:Tz,style:MR,from:r,encode:a},n)}(t,n,r.subtitle,l)),FL(UR({role:Sz,from:l,encode:zL(n,i),marks:u,aria:n(\"aria\"),description:n(\"descrip"
-  , "tion\"),zindex:n(\"zindex\"),name:o,interactive:a,style:s}),e)}function zL(t,e){const n={enter:{},update:{}};return fz(n,{orient:t(\"orient\"),anchor:t(\"anchor\"),align:{signal:IR},angle:{signal:BL},limit:t(\"limit\"),frame:t(\"frame\"),offset:t(\"offset\")||0,padding:t(\"subtitlePadding\")}),hz(n,e,$R)}function OL(t,e){const n=[];t.transform&&t.transform.forEach((t=>{n.push(hL(t,e))})),t.on&&t.on.forEach((n=>{CL(n,e,t.name)})),e.addDataPipeline(t.name,function(t,e,n){const r=[];let i,o,a,s,u,l=null,c=!1,f=!1;t.values?iO(t.values)||oO(t.format)?(r.push(LL(e,t)),r.push(l=RL())):r.push(l=RL({$ingest:t.values,$format:t.format})):t.url?oO(t.url)||oO(t.format)?(r.push(LL(e,t)),r.push(l=RL())):r.push(l=RL({$request:t.url,$format:t.format})):t.source&&(l=i=X(t.source).map((t=>Jz(e.getData(t).output))),r.push(null));for(o=0,a=n.length;o<a;++o)s=n[o],u=s.metadata,l||u.source||r.push(l=RL()),r.push(s),u.generates&&(f=!0),u.modifies&&!f&&(c=!0),u.source?l=s:u.changes&&(l=null);i&&(a=i.length-1,r[0]=UO({derive:c,pulse:a?i:i[0]}),(c||a)&&r.splice(1,0,RL()));l||r.push(RL());return r.push(jO({})),r}(t,e,n))}function RL(t){const e=bO({},t);return e.metadata={source:!0},e}function LL(t,e){return SO({url:e.url?t.property(e.url):void 0,async:e.async?t.property(e.async):void 0,values:e.values?t.property(e.values):void 0,format:t.objectProperty(e.format)})}const UL=t=>t===fR||t===uR,qL=(t,e,n)=>iO(t)?GL(t.signal,e,n):t===lR||t===uR?e:n,PL=(t,e,n)=>iO(t)?HL(t.signal,e,n):UL(t)?e:n,jL=(t,e,n)=>iO(t)?YL(t.signal,e,n):UL(t)?n:e,IL=(t,e,n)=>iO(t)?VL(t.signal,e,n):t===uR?{value:e}:{value:n},WL=(t,e,n)=>iO(t)?XL(t.signal,e,n):t===cR?{value:e}:{value:n},HL=(t,e,n)=>JL(`${t} === '${uR}' || ${t} === '${fR}'`,e,n),YL=(t,e,n)=>JL(`${t} !== '${uR}' && ${t} !== '${fR}'`,e,n),GL=(t,e,n)=>QL(`${t} === '${lR}' || ${t} === '${uR}'`,e,n),VL=(t,e,n)=>QL(`${t} === '${uR}'`,e,n),XL=(t,e,n)=>QL(`${t} === '${cR}'`,e,n),JL=(t,e,n)=>(e=null!=e?lz(e):e,n=null!=n?lz(n):n,ZL(e)&&ZL(n)?{signal:`${t} ? (${e=e?e.signal||Ct(e.value):null}) : (${n=n?n.signal||Ct(n.value):null})`}:[at({test:t},e)].concat(n||[])),ZL=t=>null==t||1===Object.keys(t).length,QL=(t,e,n)=>({signal:`${t} ? (${tU(e)}) : (${tU(n)})`}),KL=(t,e,n,r,i)=>({signal:(null!=r?`${t} === '${lR}' ? (${tU(r)}) : `:\"\")+(null!=n?`${t} === '${fR}' ? (${tU(n)}) : `:\"\")+(null!=i?`${t} === '${cR}' ? (${tU(i)}) : `:\"\")+(null!=e?`${t} === '${uR}' ? (${tU(e)}) : `:\"\")+\"(null)\"}),tU=t=>iO(t)?t.signal:null==t?null:Ct(t),eU=(t,e)=>0===e?0:iO(t)?{signal:`(${t.signal}) * ${e}`}:{value:t*e},nU=(t,e)=>{const n=t.signal;return n&&n.endsWith(\"(null)\")?{signal:n.slice(0,-6)+e.signal}:t};function rU(t,e,n,r){let i;if(e&&lt(e,t))return e[t];if(lt(n,t))return n[t];if(t.startsWith(\"title\")){switch(t){case\"titleColor\":i=\"fill\";break;case\"titleFont\":case\"titleFontSize\":case\"titleFontWeight\":i=t[5].toLowerCase()+t.slice(6)}return r[kR][i]}if(t.startsWith(\"label\")){switch(t){case\"labelColor\":i=\"fill\";break;case\"labelFont\":case\"labelFontSize\":i=t[5].toLowerCase()+t.slice(6)}return r[wR][i]}return null}function iU(t){const e={};for(const n of t)if(n)for(const t in n)e[t]=1;return Object.keys(e)}function oU(t,e){return{scale:t.scale,range:e}}function aU(t,e,n,r,i){const o=qR(t,e),a=t.orient,s=t.gridScale,u=qL(a,1,-1),l=function(t,e){if(1===e);else if(M(t)){let n=t=at({},t);for(;null!=n.mult;){if(!M(n.mult))return n.mult=iO(e)?{signal:`(${n.mult}) * (${e.signal})`}:n.mult*e,t;n=n.mult=at({},n.mult)}n.mult=e}else t=iO(e)?{signal:`(${e.signal}) * (${t||0})`}:e*(t||0);return t}(t.offset,u);let c,f,h;const d={enter:c={opacity:TR},update:h={opacity:BR},exit:f={opacity:TR}};fz(d,{stroke:o(\"gridColor\"),strokeCap:o(\"gridCap\"),strokeDash:o(\"gridDash\"),strokeDashOffset:o(\"gridDashOffset\"),strokeOpacity:o(\"gridOpacity\"),strokeWidth:o(\"gridWidth\")});const p={scale:t.scale,field:bR,band:i.band,extra:i.extra,offset:i.offset,round:o(\"tickRound\")},g=PL(a,{signal:\"height\"},{signal:\"width\"}),m=s?{scale:s,range:0,mult:u,offset:l}:{value:0,offset:l},y=s?{scale:s,range:1,mult:u,offset:l}:at(g,{mult:u,offset:l});return c.x=h.x=PL(a,p,m),c.y=h.y=jL(a,p,m),c.x2=h.x2=jL(a,y),c.y2=h.y"
-  , "2=PL(a,y),f.x=PL(a,p),f.y=jL(a,p),HR({type:OR,role:_z,key:bR,from:r,encode:d},n)}function sU(t,e,n,r,i){return{signal:'flush(range(\"'+t+'\"), scale(\"'+t+'\", datum.value), '+e+\",\"+n+\",\"+r+\",\"+i+\")\"}}function uU(t,e,n,r){const i=qR(t,e),o=t.orient,a=qL(o,-1,1);let s,u;const l={enter:s={opacity:TR,anchor:lz(i(\"titleAnchor\",null)),align:{signal:IR}},update:u=at({},s,{opacity:BR,text:lz(t.title)}),exit:{opacity:TR}},c={signal:`lerp(range(\"${t.scale}\"), ${jR(0,1,.5)})`};return u.x=PL(o,c),u.y=jL(o,c),s.angle=PL(o,TR,eU(a,90)),s.baseline=PL(o,IL(o,fR,uR),{value:fR}),u.angle=s.angle,u.baseline=s.baseline,fz(l,{fill:i(\"titleColor\"),fillOpacity:i(\"titleOpacity\"),font:i(\"titleFont\"),fontSize:i(\"titleFontSize\"),fontStyle:i(\"titleFontStyle\"),fontWeight:i(\"titleFontWeight\"),limit:i(\"titleLimit\"),lineHeight:i(\"titleLineHeight\")},{align:i(\"titleAlign\"),angle:i(\"titleAngle\"),baseline:i(\"titleBaseline\")}),function(t,e,n,r){const i=(t,e)=>null!=t?(n.update[e]=nU(lz(t),n.update[e]),!1):!dz(e,r),o=i(t(\"titleX\"),\"x\"),a=i(t(\"titleY\"),\"y\");n.enter.auto=a===o?lz(a):PL(e,lz(a),lz(o))}(i,o,l,n),l.update.align=nU(l.update.align,s.align),l.update.angle=nU(l.update.angle,s.angle),l.update.baseline=nU(l.update.baseline,s.baseline),HR({type:LR,role:wz,style:kR,from:r,encode:l},n)}function lU(t,e){const n=function(t,e){var n,r,i,o=e.config,a=o.style,s=o.axis,u=\"band\"===e.scaleType(t.scale)&&o.axisBand,l=t.orient;if(iO(l)){const t=iU([o.axisX,o.axisY]),e=iU([o.axisTop,o.axisBottom,o.axisLeft,o.axisRight]);for(i of(n={},t))n[i]=PL(l,rU(i,o.axisX,s,a),rU(i,o.axisY,s,a));for(i of(r={},e))r[i]=KL(l.signal,rU(i,o.axisTop,s,a),rU(i,o.axisBottom,s,a),rU(i,o.axisLeft,s,a),rU(i,o.axisRight,s,a))}else n=l===uR||l===fR?o.axisX:o.axisY,r=o[\"axis\"+l[0].toUpperCase()+l.slice(1)];return n||r||u?at({},s,n,r,u):s}(t,e),r=t.encode||{},i=r.axis||{},o=i.name||void 0,a=i.interactive,s=i.style,u=qR(t,n),l=function(t){const e=t(\"tickBand\");let n,r,i=t(\"tickOffset\");return e?e.signal?(n={signal:`(${e.signal}) === 'extent' ? 1 : 0.5`},r={signal:`(${e.signal}) === 'extent'`},M(i)||(i={signal:`(${e.signal}) === 'extent' ? 0 : ${i}`})):\"extent\"===e?(n=1,r=!0,i=0):(n=.5,r=!1):(n=t(\"bandPosition\"),r=t(\"tickExtra\")),{extra:r,band:n,offset:i}}(u),c={scale:t.scale,ticks:!!u(\"ticks\"),labels:!!u(\"labels\"),grid:!!u(\"grid\"),domain:!!u(\"domain\"),title:null!=t.title},f=Jz(e.add(bO({},[c]))),h=Jz(e.add(_O({scale:e.scaleRef(t.scale),extra:e.property(l.extra),count:e.objectProperty(t.tickCount),values:e.objectProperty(t.values),minstep:e.property(t.tickMinStep),formatType:e.property(t.formatType),formatSpecifier:e.property(t.format)}))),d=[];let p;return c.grid&&d.push(aU(t,n,r.grid,h,l)),c.ticks&&(p=u(\"tickSize\"),d.push(function(t,e,n,r,i,o){const a=qR(t,e),s=t.orient,u=qL(s,-1,1);let l,c,f;const h={enter:l={opacity:TR},update:f={opacity:BR},exit:c={opacity:TR}};fz(h,{stroke:a(\"tickColor\"),strokeCap:a(\"tickCap\"),strokeDash:a(\"tickDash\"),strokeDashOffset:a(\"tickDashOffset\"),strokeOpacity:a(\"tickOpacity\"),strokeWidth:a(\"tickWidth\")});const d=lz(i);d.mult=u;const p={scale:t.scale,field:bR,band:o.band,extra:o.extra,offset:o.offset,round:a(\"tickRound\")};return f.y=l.y=PL(s,TR,p),f.y2=l.y2=PL(s,d),c.x=PL(s,p),f.x=l.x=jL(s,TR,p),f.x2=l.x2=jL(s,d),c.y=jL(s,p),HR({type:OR,role:bz,key:bR,from:r,encode:h},n)}(t,n,r.ticks,h,p,l))),c.labels&&(p=c.ticks?p:0,d.push(function(t,e,n,r,i,o){const a=qR(t,e),s=t.orient,u=t.scale,l=qL(s,-1,1),c=sO(a(\"labelFlush\")),f=sO(a(\"labelFlushOffset\")),h=a(\"labelAlign\"),d=a(\"labelBaseline\");let p,g=0===c||!!c;const m=lz(i);m.mult=l,m.offset=lz(a(\"labelPadding\")||0),m.offset.mult=l;const y={scale:u,field:bR,band:.5,offset:WR(o.offset,a(\"labelOffset\"))},v=PL(s,g?sU(u,c,'\"left\"','\"right\"','\"center\"'):{value:\"center\"},WL(s,\"left\",\"right\")),_=PL(s,IL(s,\"bottom\",\"top\"),g?sU(u,c,'\"top\"','\"bottom\"','\"middle\"'):{value:\"middle\"}),x=sU(u,c,`-(${f})`,f,0);g=g&&f;const b={opacity:TR,x:PL(s,y,m),y:jL(s,y,m)},w={enter:b,update:p={opacity:BR,text:{field:yR},x:b.x,y:b.y,align:v,baseline:_},exit:{opacity:TR,x:b.x,y:b.y}};fz(w,{dx:!h&&g?PL(s,x):null,dy:!d&&g?jL(s,x):null}),fz(w,{angle:a(\"labelAng"
-  , "le\"),fill:a(\"labelColor\"),fillOpacity:a(\"labelOpacity\"),font:a(\"labelFont\"),fontSize:a(\"labelFontSize\"),fontWeight:a(\"labelFontWeight\"),fontStyle:a(\"labelFontStyle\"),limit:a(\"labelLimit\"),lineHeight:a(\"labelLineHeight\")},{align:h,baseline:d});const k=a(\"labelBound\");let A=a(\"labelOverlap\");return A=A||k?{separation:a(\"labelSeparation\"),method:A,order:\"datum.index\",bound:k?{scale:u,orient:s,tolerance:k}:null}:void 0,p.align!==v&&(p.align=nU(p.align,v)),p.baseline!==_&&(p.baseline=nU(p.baseline,_)),HR({type:LR,role:xz,style:wR,key:bR,from:r,encode:w,overlap:A},n)}(t,n,r.labels,h,p,l))),c.domain&&d.push(function(t,e,n,r){const i=qR(t,e),o=t.orient;let a,s;const u={enter:a={opacity:TR},update:s={opacity:BR},exit:{opacity:TR}};fz(u,{stroke:i(\"domainColor\"),strokeCap:i(\"domainCap\"),strokeDash:i(\"domainDash\"),strokeDashOffset:i(\"domainDashOffset\"),strokeWidth:i(\"domainWidth\"),strokeOpacity:i(\"domainOpacity\")});const l=oU(t,0),c=oU(t,1);return a.x=s.x=PL(o,l,TR),a.x2=s.x2=PL(o,c),a.y=s.y=jL(o,l,TR),a.y2=s.y2=jL(o,c),HR({type:OR,role:vz,from:r,encode:u},n)}(t,n,r.domain,f)),c.title&&d.push(uU(t,n,r.title,f)),FL(UR({role:yz,from:f,encode:hz(cU(u,t),i,$R),marks:d,aria:u(\"aria\"),description:u(\"description\"),zindex:u(\"zindex\"),name:o,interactive:a,style:s}),e)}function cU(t,e){const n={enter:{},update:{}};return fz(n,{orient:t(\"orient\"),offset:t(\"offset\")||0,position:aO(e.position,0),titlePadding:t(\"titlePadding\"),minExtent:t(\"minExtent\"),maxExtent:t(\"maxExtent\"),range:{signal:`abs(span(range(\"${e.scale}\")))`},translate:t(\"translate\"),format:e.format,formatType:e.formatType}),n}function fU(t,e,n){const r=X(t.signals),i=X(t.scales);return n||r.forEach((t=>Yz(t,e))),X(t.projections).forEach((t=>function(t,e){const n=e.config.projection||{},r={};for(const n in t)\"name\"!==n&&(r[n]=sR(t[n],n,e));for(const t in n)null==r[t]&&(r[t]=sR(n[t],t,e));e.addProjection(t.name,r)}(t,e))),i.forEach((t=>function(t,e){const n=t.type||\"linear\";up(n)||s(\"Unrecognized scale type: \"+Ct(n)),e.addScale(t.name,{type:n,domain:void 0})}(t,e))),X(t.data).forEach((t=>OL(t,e))),i.forEach((t=>VO(t,e))),(n||r).forEach((t=>function(t,e){const n=e.getSignal(t.name);let r=t.update;t.init&&(r?s(\"Signals can not include both init and update expressions.\"):(r=t.init,n.initonly=!0)),r&&(r=NB(r,e),n.update=r.$expr,n.params=r.$params),t.on&&t.on.forEach((t=>gO(t,e,n.id)))}(t,e))),X(t.axes).forEach((t=>lU(t,e))),X(t.marks).forEach((t=>FL(t,e))),X(t.legends).forEach((t=>SL(t,e))),t.title&&NL(t.title,e),e.parseLambdas(),e}const hU=t=>hz({enter:{x:{value:0},y:{value:0}},update:{width:{signal:\"width\"},height:{signal:\"height\"}}},t);function dU(t,e){const n=e.config,r=Jz(e.root=e.add(Xz())),i=function(t,e){const n=n=>aO(t[n],e[n]),r=[pU(\"background\",n(\"background\")),pU(\"autosize\",oz(n(\"autosize\"))),pU(\"padding\",uz(n(\"padding\"))),pU(\"width\",n(\"width\")||0),pU(\"height\",n(\"height\")||0)],i=r.reduce(((t,e)=>(t[e.name]=e,t)),{}),o={};return X(t.signals).forEach((t=>{lt(i,t.name)?t=at(i[t.name],t):r.push(t),o[t.name]=t})),X(e.signals).forEach((t=>{lt(o,t.name)||lt(i,t.name)||r.push(t)})),r}(t,n);i.forEach((t=>Yz(t,e))),e.description=t.description||n.description,e.eventConfig=n.events,e.legends=e.objectProperty(n.legend&&n.legend.layout),e.locale=n.locale;const o=e.add(bO()),a=e.add(AO(qz(hU(t.encode),NR,gz,t.style,e,{pulse:Jz(o)}))),s=e.add(WO({layout:e.objectProperty(t.layout),legends:e.legends,autosize:e.signalRef(\"autosize\"),mark:r,pulse:Jz(a)}));e.operators.pop(),e.pushState(Jz(a),Jz(s),null),fU(t,e,i),e.operators.push(s);let u=e.add(xO({mark:r,pulse:Jz(s)}));return u=e.add(qO({pulse:Jz(u)})),u=e.add(jO({pulse:Jz(u)})),e.addData(\"root\",new AL(e,o,o,u)),e}function pU(t,e){return e&&e.signal?{name:t,update:e.signal}:{name:t,value:e}}function gU(t,e){this.config=t||{},this.options=e||{},this.bindings=[],this.field={},this.signals={},this.lambdas={},this.scales={},this.events={},this.data={},this.streams=[],this.updates=[],this.operators=[],this.eventConfig=null,this.locale=null,this._id=0,this._subid=0,this._nextsub=[0],this._parent=[],this._encode=[],this._lookup=[],this._markpath=[]}fun"
-  , "ction mU(t){this.config=t.config,this.options=t.options,this.legends=t.legends,this.field=Object.create(t.field),this.signals=Object.create(t.signals),this.lambdas=Object.create(t.lambdas),this.scales=Object.create(t.scales),this.events=Object.create(t.events),this.data=Object.create(t.data),this.streams=[],this.updates=[],this.operators=[],this._id=0,this._subid=++t._nextsub[0],this._nextsub=t._nextsub,this._parent=t._parent.slice(),this._encode=t._encode.slice(),this._lookup=t._lookup.slice(),this._markpath=t._markpath}function yU(t){return(A(t)?vU:_U)(t)}function vU(t){const e=t.length;let n=\"[\";for(let r=0;r<e;++r){const e=t[r];n+=(r>0?\",\":\"\")+(M(e)?e.signal||yU(e):Ct(e))}return n+\"]\"}function _U(t){let e,n,r=\"{\",i=0;for(e in t)n=t[e],r+=(++i>1?\",\":\"\")+Ct(e)+\":\"+(M(n)?n.signal||yU(n):Ct(n));return r+\"}\"}gU.prototype=mU.prototype={parse(t){return fU(t,this)},fork(){return new mU(this)},isSubscope(){return this._subid>0},toRuntime(){return this.finish(),{description:this.description,operators:this.operators,streams:this.streams,updates:this.updates,bindings:this.bindings,eventConfig:this.eventConfig,locale:this.locale}},id(){return(this._subid?this._subid+\":\":0)+this._id++},add(t){return this.operators.push(t),t.id=this.id(),t.refs&&(t.refs.forEach((e=>{e.$ref=t.id})),t.refs=null),t},proxy(t){const e=t instanceof Gz?Jz(t):t;return this.add(LO({value:e}))},addStream(t){return this.streams.push(t),t.id=this.id(),t},addUpdate(t){return this.updates.push(t),t},finish(){let t,e;for(t in this.root&&(this.root.root=!0),this.signals)this.signals[t].signal=t;for(t in this.scales)this.scales[t].scale=t;function n(t,e,n){let r,i;t&&(r=t.data||(t.data={}),i=r[e]||(r[e]=[]),i.push(n))}for(t in this.data){e=this.data[t],n(e.input,t,\"input\"),n(e.output,t,\"output\"),n(e.values,t,\"values\");for(const r in e.index)n(e.index[r],t,\"index:\"+r)}return this},pushState(t,e,n){this._encode.push(Jz(this.add(jO({pulse:t})))),this._parent.push(e),this._lookup.push(n?Jz(this.proxy(n)):null),this._markpath.push(-1)},popState(){this._encode.pop(),this._parent.pop(),this._lookup.pop(),this._markpath.pop()},parent(){return S(this._parent)},encode(){return S(this._encode)},lookup(){return S(this._lookup)},markpath(){const t=this._markpath;return++t[t.length-1]},fieldRef(t,e){if(xt(t))return Zz(t,e);t.signal||s(\"Unsupported field reference: \"+Ct(t));const n=t.signal;let r=this.field[n];if(!r){const t={name:this.signalRef(n)};e&&(t.as=e),this.field[n]=r=Jz(this.add(DO(t)))}return r},compareRef(t){let e=!1;const n=t=>iO(t)?(e=!0,this.signalRef(t.signal)):function(t){return t&&t.expr}(t)?(e=!0,this.exprRef(t.expr)):t,r=X(t.field).map(n),i=X(t.order).map(n);return e?Jz(this.add(wO({fields:r,orders:i}))):Kz(r,i)},keyRef(t,e){let n=!1;const r=this.signals;return t=X(t).map((t=>iO(t)?(n=!0,Jz(r[t.signal])):t)),n?Jz(this.add(CO({fields:t,flat:e}))):function(t,e){const n={$key:t};return e&&(n.$flat=!0),n}(t,e)},sortRef(t){if(!t)return t;const e=eO(t.op,t.field),n=t.order||\"ascending\";return n.signal?Jz(this.add(wO({fields:e,orders:this.signalRef(n.signal)}))):Kz(e,n)},event(t,e){const n=t+\":\"+e;if(!this.events[n]){const r=this.id();this.streams.push({id:r,source:t,type:e}),this.events[n]=r}return this.events[n]},hasOwnSignal(t){return lt(this.signals,t)},addSignal(t,e){this.hasOwnSignal(t)&&s(\"Duplicate signal name: \"+Ct(t));const n=e instanceof Gz?e:this.add(Xz(e));return this.signals[t]=n},getSignal(t){return this.signals[t]||s(\"Unrecognized signal name: \"+Ct(t)),this.signals[t]},signalRef(t){return this.signals[t]?Jz(this.signals[t]):(lt(this.lambdas,t)||(this.lambdas[t]=this.add(Xz(null))),Jz(this.lambdas[t]))},parseLambdas(){const t=Object.keys(this.lambdas);for(let e=0,n=t.length;e<n;++e){const n=t[e],r=NB(n,this),i=this.lambdas[n];i.params=r.$params,i.update=r.$expr}},property(t){return t&&t.signal?this.signalRef(t.signal):t},objectProperty(t){return t&&M(t)?this.signalRef(t.signal||yU(t)):t},exprRef(t,e){const n={expr:NB(t,this)};return e&&(n.expr.$name=e),Jz(this.add(MO(n)))},addBinding(t,e){this.bindings||s(\"Nested signals do not support binding: \"+Ct(t)"
-  , "),this.bindings.push(at({signal:t},e))},addScaleProj(t,e){lt(this.scales,t)&&s(\"Duplicate scale or projection name: \"+Ct(t)),this.scales[t]=this.add(e)},addScale(t,e){this.addScaleProj(t,PO(e))},addProjection(t,e){this.addScaleProj(t,RO(e))},getScale(t){return this.scales[t]||s(\"Unrecognized scale name: \"+Ct(t)),this.scales[t]},scaleRef(t){return Jz(this.getScale(t))},scaleType(t){return this.getScale(t).params.type},projectionRef(t){return this.scaleRef(t)},projectionType(t){return this.scaleType(t)},addData(t,e){return lt(this.data,t)&&s(\"Duplicate data set name: \"+Ct(t)),this.data[t]=e},getData(t){return this.data[t]||s(\"Undefined data set name: \"+Ct(t)),this.data[t]},addDataPipeline(t,e){return lt(this.data,t)&&s(\"Duplicate data set name: \"+Ct(t)),this.addData(t,AL.fromEntries(this,e))}},at(Za,yl,sb,Ub,$E,BD,wF,QC,DF,lS,AS,BS),t.Bounds=Xg,t.CanvasHandler=$v,t.CanvasRenderer=Lv,t.DATE=Yn,t.DAY=Gn,t.DAYOFYEAR=Vn,t.Dataflow=Va,t.Debug=w,t.DisallowedObjectProperties=m,t.Error=_,t.EventStream=Ba,t.Gradient=Kp,t.GroupItem=Zg,t.HOURS=Xn,t.Handler=uv,t.HybridHandler=D_,t.HybridRenderer=E_,t.Info=b,t.Item=Jg,t.MILLISECONDS=Qn,t.MINUTES=Jn,t.MONTH=Wn,t.Marks=Yy,t.MultiPulse=Ia,t.None=v,t.Operator=Sa,t.Parameters=Da,t.Pulse=Ua,t.QUARTER=In,t.RenderType=$_,t.Renderer=cv,t.ResourceLoader=Qg,t.SECONDS=Zn,t.SVGHandler=qv,t.SVGRenderer=f_,t.SVGStringRenderer=A_,t.Scenegraph=tv,t.TIME_UNITS=Kn,t.Transform=Ja,t.View=RN,t.WEEK=Hn,t.Warn=x,t.YEAR=jn,t.accessor=e,t.accessorFields=r,t.accessorName=n,t.array=X,t.ascending=tt,t.bandwidthNRD=rs,t.bin=is,t.bootstrapCI=os,t.boundClip=U_,t.boundContext=_m,t.boundItem=Gy,t.boundMark=Xy,t.boundStroke=em,t.changeset=Ma,t.clampRange=J,t.codegenExpression=kT,t.compare=K,t.constant=it,t.cumulativeLogNormal=vs,t.cumulativeNormal=hs,t.cumulativeUniform=As,t.dayofyear=ar,t.debounce=ot,t.defaultLocale=Lo,t.definition=Qa,t.densityLogNormal=ys,t.densityNormal=fs,t.densityUniform=ks,t.domChild=iv,t.domClear=ov,t.domCreate=nv,t.domFind=rv,t.dotbin=as,t.error=s,t.expressionFunction=BB,t.extend=at,t.extent=st,t.extentIndex=ut,t.falsy=g,t.fastmap=ft,t.field=l,t.flush=ht,t.font=Ly,t.fontFamily=Ry,t.fontSize=Ty,t.format=sa,t.formatLocale=So,t.formats=ua,t.hasOwnProperty=lt,t.id=c,t.identity=f,t.inferType=ta,t.inferTypes=ea,t.ingest=_a,t.inherits=dt,t.inrange=pt,t.interpolate=bp,t.interpolateColors=vp,t.interpolateRange=yp,t.intersect=N_,t.intersectBoxLine=Sm,t.intersectPath=Em,t.intersectPoint=Dm,t.intersectRule=Fm,t.isArray=A,t.isBoolean=gt,t.isDate=mt,t.isFunction=Z,t.isIterable=yt,t.isNumber=vt,t.isObject=M,t.isRegExp=_t,t.isString=xt,t.isTuple=ma,t.key=bt,t.lerp=wt,t.lineHeight=By,t.loader=fa,t.locale=Ro,t.logger=k,t.lruCache=kt,t.markup=r_,t.merge=At,t.mergeConfig=D,t.multiLineOffset=zy,t.one=d,t.pad=Et,t.panLinear=L,t.panLog=U,t.panPow=q,t.panSymlog=P,t.parse=function(t,e,n){return M(t)||s(\"Input Vega specification must be an object.\"),dU(t,new gU(e=D(function(){const t=\"sans-serif\",e=\"#4c78a8\",n=\"#000\",r=\"#888\",i=\"#ddd\";return{description:\"Vega visualization\",padding:0,autosize:\"pad\",background:null,events:{defaults:{allow:[\"wheel\"]}},group:null,mark:null,arc:{fill:e},area:{fill:e},image:null,line:{stroke:e,strokeWidth:2},path:{stroke:e},rect:{fill:e},rule:{stroke:n},shape:{stroke:e},symbol:{fill:e,size:64},text:{fill:n,font:t,fontSize:11},trail:{fill:e,size:2},style:{\"guide-label\":{fill:n,font:t,fontSize:10},\"guide-title\":{fill:n,font:t,fontSize:11,fontWeight:\"bold\"},\"group-title\":{fill:n,font:t,fontSize:13,fontWeight:\"bold\"},\"group-subtitle\":{fill:n,font:t,fontSize:12},point:{size:30,strokeWidth:2,shape:\"circle\"},circle:{size:30,strokeWidth:2},square:{size:30,strokeWidth:2,shape:\"square\"},cell:{fill:\"transparent\",stroke:i},view:{fill:\"transparent\"}},title:{orient:\"top\",anchor:\"middle\",offset:4,subtitlePadding:3},axis:{minExtent:0,maxExtent:200,bandPosition:.5,domain:!0,domainWidth:1,domainColor:r,grid:!1,gridWidth:1,gridColor:i,labels:!0,labelAngle:0,labelLimit:180,labelOffset:0,labelPadding:2,ticks:!0,tickColor:r,tickOffset:0,tickRound:!0,tickSize:5,tickWidth:1,titlePadding:4},axisBand:{tickOffset:-.5},"
-  , "projection:{type:\"mercator\"},legend:{orient:\"right\",padding:0,gridAlign:\"each\",columnPadding:10,rowPadding:2,symbolDirection:\"vertical\",gradientDirection:\"vertical\",gradientLength:200,gradientThickness:16,gradientStrokeColor:i,gradientStrokeWidth:0,gradientLabelOffset:2,labelAlign:\"left\",labelBaseline:\"middle\",labelLimit:160,labelOffset:4,labelOverlap:!0,symbolLimit:30,symbolType:\"circle\",symbolSize:100,symbolOffset:0,symbolStrokeWidth:1.5,symbolBaseFillColor:\"transparent\",symbolBaseStrokeColor:r,titleLimit:180,titleOrient:\"top\",titlePadding:5,layout:{offset:18,direction:\"horizontal\",left:{direction:\"vertical\"},right:{direction:\"vertical\"}}},range:{category:{scheme:\"tableau10\"},ordinal:{scheme:\"blues\"},heatmap:{scheme:\"yellowgreenblue\"},ramp:{scheme:\"blues\"},diverging:{scheme:\"blueorange\",extent:[1,0]},symbol:[\"circle\",\"square\",\"triangle-up\",\"cross\",\"diamond\",\"triangle-right\",\"triangle-down\",\"triangle-left\"]}}}(),e,t.config),n)).toRuntime()},t.parseExpression=xT,t.parseSelector=ez,t.path=Rl,t.pathCurves=eg,t.pathEqual=j_,t.pathParse=sg,t.pathRectangle=$g,t.pathRender=vg,t.pathSymbols=wg,t.pathTrail=Tg,t.peek=S,t.point=sv,t.projection=KM,t.quantileLogNormal=_s,t.quantileNormal=ds,t.quantileUniform=Ms,t.quantiles=es,t.quantizeInterpolator=_p,t.quarter=G,t.quartiles=ns,t.randomInteger=function(e,n){let r,i,o;null==n&&(n=e,e=0);const a={min(t){return arguments.length?(r=t||0,o=i-r,a):r},max(t){return arguments.length?(i=t||0,o=i-r,a):i},sample:()=>r+Math.floor(o*t.random()),pdf:t=>t===Math.floor(t)&&t>=r&&t<i?1/o:0,cdf(t){const e=Math.floor(t);return e<r?0:e>=i?1:(e-r+1)/o},icdf:t=>t>=0&&t<=1?r-1+Math.floor(t*o):NaN};return a.min(e).max(n)},t.randomKDE=gs,t.randomLCG=function(t){return function(){return(t=(1103515245*t+12345)%2147483647)/2147483647}},t.randomLogNormal=xs,t.randomMixture=bs,t.randomNormal=ps,t.randomUniform=Es,t.read=ca,t.regressionConstant=Ds,t.regressionExp=Ns,t.regressionLinear=Ts,t.regressionLoess=Us,t.regressionLog=Bs,t.regressionPoly=Rs,t.regressionPow=zs,t.regressionQuad=Os,t.renderModule=B_,t.repeat=Mt,t.resetDefaultLocale=function(){return Co(),Bo(),Lo()},t.resetSVGClipId=Gg,t.resetSVGDefIds=function(){Gg(),Vp=0},t.responseType=la,t.runtimeContext=IB,t.sampleCurve=Is,t.sampleLogNormal=ms,t.sampleNormal=cs,t.sampleUniform=ws,t.scale=sp,t.sceneEqual=P_,t.sceneFromJSON=Qy,t.scenePickVisit=Pm,t.sceneToJSON=Zy,t.sceneVisit=qm,t.sceneZOrder=Um,t.scheme=Mp,t.serializeXML=i_,t.setHybridRendererOptions=function(t){M_.svgMarkTypes=t.svgMarkTypes??[\"text\"],M_.svgOnTop=t.svgOnTop??!0,M_.debug=t.debug??!1},t.setRandom=function(e){t.random=e},t.span=Dt,t.splitAccessPath=u,t.stringValue=Ct,t.textMetrics=Ey,t.timeBin=Jr,t.timeFloor=wr,t.timeFormatLocale=zo,t.timeInterval=Cr,t.timeOffset=$r,t.timeSequence=Nr,t.timeUnitSpecifier=rr,t.timeUnits=er,t.toBoolean=Ft,t.toDate=$t,t.toNumber=$,t.toSet=Bt,t.toString=Tt,t.transform=Ka,t.transforms=Za,t.truncate=Nt,t.truthy=p,t.tupleid=ya,t.typeParsers=Zo,t.utcFloor=Mr,t.utcInterval=Fr,t.utcOffset=Tr,t.utcSequence=zr,t.utcdayofyear=hr,t.utcquarter=V,t.utcweek=dr,t.version=\"5.33.1\",t.visitArray=zt,t.week=sr,t.writeConfig=C,t.zero=h,t.zoomLinear=I,t.zoomLog=W,t.zoomPow=H,t.zoomSymlog=Y}));\n//# sourceMappingURL=vega.min.js.map\n"
-  ]
-
-vegaLiteJS :: Text
-vegaLiteJS = T.concat
-  [ "!function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?t(exports,require(\"vega\")):\"function\"==typeof define&&define.amd?define([\"exports\",\"vega\"],t):t((e=\"undefined\"!=typeof globalThis?globalThis:e||self).vegaLite={},e.vega)}(this,(function(e,t){\"use strict\";var n=\"5.23.0\";function i(e){return J(e,\"or\")}function r(e){return J(e,\"and\")}function o(e){return J(e,\"not\")}function a(e,t){if(o(e))a(e.not,t);else if(r(e))for(const n of e.and)a(n,t);else if(i(e))for(const n of e.or)a(n,t);else t(e)}function s(e,t){return o(e)?{not:s(e.not,t)}:r(e)?{and:e.and.map((e=>s(e,t)))}:i(e)?{or:e.or.map((e=>s(e,t)))}:t(e)}const l=structuredClone;function c(e){throw new Error(e)}function u(e,n){const i={};for(const r of n)t.hasOwnProperty(e,r)&&(i[r]=e[r]);return i}function f(e,t){const n={...e};for(const e of t)delete n[e];return n}function d(e){if(t.isNumber(e))return e;const n=t.isString(e)?e:Q(e);if(n.length<250)return n;let i=0;for(let e=0;e<n.length;e++){i=(i<<5)-i+n.charCodeAt(e),i|=0}return i}function m(e){return!1===e||null===e}function p(e,t){return e.includes(t)}function g(e,t){let n=0;for(const[i,r]of e.entries())if(t(r,i,n++))return!0;return!1}function h(e,t){let n=0;for(const[i,r]of e.entries())if(!t(r,i,n++))return!1;return!0}function y(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),i=1;i<t;i++)n[i-1]=arguments[i];for(const t of n)v(e,t??{});return e}function v(e,n){for(const i of D(n))t.writeConfig(e,i,n[i],!0)}function b(e,t){const n=[],i={};let r;for(const o of e)r=t(o),r in i||(i[r]=1,n.push(o));return n}function x(e,t){if(e.size!==t.size)return!1;for(const n of e)if(!t.has(n))return!1;return!0}function $(e,t){for(const n of e)if(t.has(n))return!0;return!1}function w(e){const n=new Set;for(const i of e){const e=t.splitAccessPath(i).map(((e,t)=>0===t?e:`[${e}]`)),r=e.map(((t,n)=>e.slice(0,n+1).join(\"\")));for(const e of r)n.add(e)}return n}function k(e,t){return void 0===e||void 0===t||$(w(e),w(t))}function S(e){return 0===D(e).length}Set.prototype.toJSON=function(){return`Set(${[...this].map((e=>Q(e))).join(\",\")})`};const D=Object.keys,F=Object.values,O=Object.entries;function z(e){return!0===e||!1===e}function C(e){const t=e.replace(/\\W/g,\"_\");return(e.match(/^\\d+/)?\"_\":\"\")+t}function _(e,t){return o(e)?`!(${_(e.not,t)})`:r(e)?`(${e.and.map((e=>_(e,t))).join(\") && (\")})`:i(e)?`(${e.or.map((e=>_(e,t))).join(\") || (\")})`:t(e)}function P(e,t){if(0===t.length)return!0;const n=t.shift();return n in e&&P(e[n],t)&&delete e[n],S(e)}function N(e){return e.charAt(0).toUpperCase()+e.substr(1)}function A(e){let n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:\"datum\";const i=t.splitAccessPath(e),r=[];for(let e=1;e<=i.length;e++){const o=`[${i.slice(0,e).map(t.stringValue).join(\"][\")}]`;r.push(`${n}${o}`)}return r.join(\" && \")}function T(e){return`${arguments.length>1&&void 0!==arguments[1]?arguments[1]:\"datum\"}[${t.stringValue(t.splitAccessPath(e).join(\".\"))}]`}function j(e){return`datum['${e.replaceAll(\"'\",\"\\\\'\")}']`}function E(e){return e.replace(/(\\[|\\]|\\.|'|\")/g,\"\\\\$1\")}function M(e){return`${t.splitAccessPath(e).map(E).join(\"\\\\.\")}`}function R(e,t,n){return e.replace(new RegExp(t.replace(/[-/\\\\^$*+?.()|[\\]{}]/g,\"\\\\$&\"),\"g\"),n)}function L(e){return`${t.splitAccessPath(e).join(\".\")}`}function q(e){return e?t.splitAccessPath(e).length:0}function U(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return t.find((e=>void 0!==e))}let W=42;function I(e){const t=++W;return e?String(e)+t:t}function B(e){return V(e)?e:`__${e}`}function V(e){return e.startsWith(\"__\")}function H(e){if(void 0!==e)return(e%360+360)%360}function G(e){return!!t.isNumber(e)||!isNaN(e)&&!isNaN(parseFloat(e))}const Y=Object.getPrototypeOf(structuredClone({}));function X(e,t){if(e===t)return!0;if(e&&t&&\"object\"==typeof e&&\"object\"==typeof t){if(e.constructor.name!==t.constructor.name)return!1;let n,i;if(Array.isArray(e)){if(n=e.length,n!=t.length)return!1;for(i=n;0!=i--;)if(!X(e[i],t[i]))return!1;return!0}if(e instanceof Map&&t instanceof Map){if(e.size!==t.size)return!1;for(const n of e.entries())if(!t.has(n[0]))r"
-  , "eturn!1;for(const n of e.entries())if(!X(n[1],t.get(n[0])))return!1;return!0}if(e instanceof Set&&t instanceof Set){if(e.size!==t.size)return!1;for(const n of e.entries())if(!t.has(n[0]))return!1;return!0}if(ArrayBuffer.isView(e)&&ArrayBuffer.isView(t)){if(n=e.length,n!=t.length)return!1;for(i=n;0!=i--;)if(e[i]!==t[i])return!1;return!0}if(e.constructor===RegExp)return e.source===t.source&&e.flags===t.flags;if(e.valueOf!==Object.prototype.valueOf&&e.valueOf!==Y.valueOf)return e.valueOf()===t.valueOf();if(e.toString!==Object.prototype.toString&&e.toString!==Y.toString)return e.toString()===t.toString();const r=Object.keys(e);if(n=r.length,n!==Object.keys(t).length)return!1;for(i=n;0!=i--;)if(!Object.prototype.hasOwnProperty.call(t,r[i]))return!1;for(i=n;0!=i--;){const n=r[i];if(!X(e[n],t[n]))return!1}return!0}return e!=e&&t!=t}function Q(e){const t=[];return function e(n){if(n&&n.toJSON&&\"function\"==typeof n.toJSON&&(n=n.toJSON()),void 0===n)return;if(\"number\"==typeof n)return isFinite(n)?\"\"+n:\"null\";if(\"object\"!=typeof n)return JSON.stringify(n);let i,r;if(Array.isArray(n)){for(r=\"[\",i=0;i<n.length;i++)i&&(r+=\",\"),r+=e(n[i])||\"null\";return r+\"]\"}if(null===n)return\"null\";if(t.includes(n))throw new TypeError(\"Converting circular structure to JSON\");const o=t.push(n)-1,a=Object.keys(n).sort();for(r=\"\",i=0;i<a.length;i++){const t=a[i],o=e(n[t]);o&&(r&&(r+=\",\"),r+=JSON.stringify(t)+\":\"+o)}return t.splice(o,1),`{${r}}`}(e)}function J(e,n){return t.isObject(e)&&t.hasOwnProperty(e,n)&&void 0!==e[n]}const K=\"row\",Z=\"column\",ee=\"facet\",te=\"x\",ne=\"y\",ie=\"x2\",re=\"y2\",oe=\"xOffset\",ae=\"yOffset\",se=\"radius\",le=\"radius2\",ce=\"theta\",ue=\"theta2\",fe=\"latitude\",de=\"longitude\",me=\"latitude2\",pe=\"longitude2\",ge=\"time\",he=\"color\",ye=\"fill\",ve=\"stroke\",be=\"shape\",xe=\"size\",$e=\"angle\",we=\"opacity\",ke=\"fillOpacity\",Se=\"strokeOpacity\",De=\"strokeWidth\",Fe=\"strokeDash\",Oe=\"text\",ze=\"order\",Ce=\"detail\",_e=\"key\",Pe=\"tooltip\",Ne=\"href\",Ae=\"url\",Te=\"description\",je={theta:1,theta2:1,radius:1,radius2:1};function Ee(e){return t.hasOwnProperty(je,e)}const Me={longitude:1,longitude2:1,latitude:1,latitude2:1};function Re(e){switch(e){case fe:return\"y\";case me:return\"y2\";case de:return\"x\";case pe:return\"x2\"}}function Le(e){return t.hasOwnProperty(Me,e)}const qe=D(Me),Ue={x:1,y:1,x2:1,y2:1,...je,...Me,xOffset:1,yOffset:1,color:1,fill:1,stroke:1,time:1,opacity:1,fillOpacity:1,strokeOpacity:1,strokeWidth:1,strokeDash:1,size:1,angle:1,shape:1,order:1,text:1,detail:1,key:1,tooltip:1,href:1,url:1,description:1};function We(e){return e===he||e===ye||e===ve}const Ie={row:1,column:1,facet:1},Be=D(Ie),Ve={...Ue,...Ie},He=D(Ve),{order:Ge,detail:Ye,tooltip:Xe,...Qe}=Ve,{row:Je,column:Ke,facet:Ze,...et}=Qe;function tt(e){return t.hasOwnProperty(Ve,e)}const nt=[ie,re,me,pe,ue,le];function it(e){return rt(e)!==e}function rt(e){switch(e){case ie:return te;case re:return ne;case me:return fe;case pe:return de;case ue:return ce;case le:return se}return e}function ot(e){if(Ee(e))switch(e){case ce:return\"startAngle\";case ue:return\"endAngle\";case se:return\"outerRadius\";case le:return\"innerRadius\"}return e}function at(e){switch(e){case te:return ie;case ne:return re;case fe:return me;case de:return pe;case ce:return ue;case se:return le}}function st(e){switch(e){case te:case ie:return\"width\";case ne:case re:return\"height\"}}function lt(e){switch(e){case te:return\"xOffset\";case ne:return\"yOffset\";case ie:return\"x2Offset\";case re:return\"y2Offset\";case ce:return\"thetaOffset\";case se:return\"radiusOffset\";case ue:return\"theta2Offset\";case le:return\"radius2Offset\"}}function ct(e){switch(e){case te:return\"xOffset\";case ne:return\"yOffset\"}}function ut(e){switch(e){case\"xOffset\":return\"x\";case\"yOffset\":return\"y\"}}const ft=D(Ue),{x:dt,y:mt,x2:pt,y2:gt,xOffset:ht,yOffset:yt,latitude:vt,longitude:bt,latitude2:xt,longitude2:$t,theta:wt,theta2:kt,radius:St,radius2:Dt,...Ft}=Ue,Ot=D(Ft),zt={x:1,y:1},Ct=D(zt);function _t(e){return t.hasOwnProperty(zt,e)}const Pt={theta:1,radius:1},Nt=D(Pt);function At(e){return\"width\"===e?te:ne}const Tt={xOffset:1,yOffset:1};function jt(e){return t.hasOwnProperty(T"
-  , "t,e)}const Et={time:1};function Mt(e){return e in Et}const{text:Rt,tooltip:Lt,href:qt,url:Ut,description:Wt,detail:It,key:Bt,order:Vt,...Ht}=Ft,Gt=D(Ht);const Yt={...zt,...Pt,...Tt,...Ht},Xt=D(Yt);function Qt(e){return t.hasOwnProperty(Yt,e)}function Jt(e,t){return function(e){switch(e){case he:case ye:case ve:case Te:case Ce:case _e:case Pe:case Ne:case ze:case we:case ke:case Se:case De:case ee:case K:case Z:return Kt;case te:case ne:case oe:case ae:case fe:case de:case ge:return en;case ie:case re:case me:case pe:return{area:\"always\",bar:\"always\",image:\"always\",rect:\"always\",rule:\"always\",circle:\"binned\",point:\"binned\",square:\"binned\",tick:\"binned\",line:\"binned\",trail:\"binned\"};case xe:return{point:\"always\",tick:\"always\",rule:\"always\",circle:\"always\",square:\"always\",bar:\"always\",text:\"always\",line:\"always\",trail:\"always\"};case Fe:return{line:\"always\",point:\"always\",tick:\"always\",rule:\"always\",circle:\"always\",square:\"always\",bar:\"always\",geoshape:\"always\"};case be:return{point:\"always\",geoshape:\"always\"};case Oe:return{text:\"always\"};case $e:return{point:\"always\",square:\"always\",text:\"always\"};case Ae:return{image:\"always\"};case ce:case se:return{text:\"always\",arc:\"always\"};case ue:case le:return{arc:\"always\"}}}(e)[t]}const Kt={arc:\"always\",area:\"always\",bar:\"always\",circle:\"always\",geoshape:\"always\",image:\"always\",line:\"always\",rule:\"always\",point:\"always\",rect:\"always\",square:\"always\",trail:\"always\",text:\"always\",tick:\"always\"},{geoshape:Zt,...en}=Kt;function tn(e){switch(e){case te:case ne:case ce:case se:case oe:case ae:case xe:case $e:case De:case we:case ke:case Se:case ge:case ie:case re:case ue:case le:return;case ee:case K:case Z:case be:case Fe:case Oe:case Pe:case Ne:case Ae:case Te:return\"discrete\";case he:case ye:case ve:return\"flexible\";case fe:case de:case me:case pe:case Ce:case _e:case ze:return}}const nn={argmax:1,argmin:1,average:1,count:1,distinct:1,exponential:1,exponentialb:1,product:1,max:1,mean:1,median:1,min:1,missing:1,q1:1,q3:1,ci0:1,ci1:1,stderr:1,stdev:1,stdevp:1,sum:1,valid:1,values:1,variance:1,variancep:1},rn={count:1,min:1,max:1};function on(e){return J(e,\"argmin\")}function an(e){return J(e,\"argmax\")}function sn(e){return t.isString(e)&&t.hasOwnProperty(nn,e)}const ln=new Set([\"count\",\"valid\",\"missing\",\"distinct\"]);function cn(e){return t.isString(e)&&ln.has(e)}const un=new Set([\"count\",\"sum\",\"distinct\",\"valid\",\"missing\"]),fn=new Set([\"mean\",\"average\",\"median\",\"q1\",\"q3\",\"min\",\"max\"]);function dn(e){return t.isBoolean(e)&&(e=Oa(e,void 0)),\"bin\"+D(e).map((t=>hn(e[t])?C(`_${t}_${O(e[t])}`):C(`_${t}_${e[t]}`))).join(\"\")}function mn(e){return!0===e||gn(e)&&!e.binned}function pn(e){return\"binned\"===e||gn(e)&&!0===e.binned}function gn(e){return t.isObject(e)}function hn(e){return J(e,\"param\")}function yn(e){switch(e){case K:case Z:case xe:case he:case ye:case ve:case De:case we:case ke:case Se:case be:return 6;case Fe:return 4;default:return 10}}function vn(e){return J(e,\"expr\")}function bn(e){let{level:t}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{level:0};const n=D(e||{}),i={};for(const r of n)i[r]=0===t?Cn(e[r]):bn(e[r],{level:t-1});return i}function xn(e){const{anchor:t,frame:n,offset:i,orient:r,angle:o,limit:a,color:s,subtitleColor:l,subtitleFont:c,subtitleFontSize:f,subtitleFontStyle:d,subtitleFontWeight:m,subtitleLineHeight:p,subtitlePadding:g,...h}=e,y={...t?{anchor:t}:{},...n?{frame:n}:{},...i?{offset:i}:{},...r?{orient:r}:{},...void 0!==o?{angle:o}:{},...void 0!==a?{limit:a}:{}},v={...l?{subtitleColor:l}:{},...c?{subtitleFont:c}:{},...f?{subtitleFontSize:f}:{},...d?{subtitleFontStyle:d}:{},...m?{subtitleFontWeight:m}:{},...p?{subtitleLineHeight:p}:{},...g?{subtitlePadding:g}:{}};return{titleMarkConfig:{...h,...s?{fill:s}:{}},subtitleMarkConfig:u(e,[\"align\",\"baseline\",\"dx\",\"dy\",\"limit\"]),nonMarkTitleProperties:y,subtitle:v}}function $n(e){return t.isString(e)||t.isArray(e)&&t.isString(e[0])}function wn(e){return J(e,\"signal\")}function kn(e){return J(e,\"step\")}function Sn(e){return!t.isArray(e)&&(J(e,\"field\")&&J(e,\"data\"))}const Dn=D({aria:1,description:1,ariaRole:1,ariaRo"
-  , "leDescription:1,blend:1,opacity:1,fill:1,fillOpacity:1,stroke:1,strokeCap:1,strokeWidth:1,strokeOpacity:1,strokeDash:1,strokeDashOffset:1,strokeJoin:1,strokeOffset:1,strokeMiterLimit:1,startAngle:1,endAngle:1,padAngle:1,innerRadius:1,outerRadius:1,size:1,shape:1,interpolate:1,tension:1,orient:1,align:1,baseline:1,text:1,dir:1,dx:1,dy:1,ellipsis:1,limit:1,radius:1,theta:1,angle:1,font:1,fontSize:1,fontWeight:1,fontStyle:1,lineBreak:1,lineHeight:1,cursor:1,href:1,tooltip:1,cornerRadius:1,cornerRadiusTopLeft:1,cornerRadiusTopRight:1,cornerRadiusBottomLeft:1,cornerRadiusBottomRight:1,aspect:1,width:1,height:1,url:1,smooth:1}),Fn={arc:1,area:1,group:1,image:1,line:1,path:1,rect:1,rule:1,shape:1,symbol:1,text:1,trail:1},On=[\"cornerRadius\",\"cornerRadiusTopLeft\",\"cornerRadiusTopRight\",\"cornerRadiusBottomLeft\",\"cornerRadiusBottomRight\"];function zn(e){const n=t.isArray(e.condition)?e.condition.map(_n):_n(e.condition);return{...Cn(e),condition:n}}function Cn(e){if(vn(e)){const{expr:t,...n}=e;return{signal:t,...n}}return e}function _n(e){if(vn(e)){const{expr:t,...n}=e;return{signal:t,...n}}return e}function Pn(e){if(vn(e)){const{expr:t,...n}=e;return{signal:t,...n}}return wn(e)?e:void 0!==e?{value:e}:void 0}function Nn(e){return wn(e)?e.signal:t.stringValue(e.value)}function An(e){return wn(e)?e.signal:null==e?null:t.stringValue(e)}function Tn(e,t,n){for(const i of n){const n=Mn(i,t.markDef,t.config);void 0!==n&&(e[i]=Pn(n))}return e}function jn(e){return[].concat(e.type,e.style??[])}function En(e,t,n){let i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};const{vgChannel:r,ignoreVgConfig:o}=i;return r&&J(t,r)?t[r]:void 0!==t[e]?t[e]:!o||r&&r!==e?Mn(e,t,n,i):void 0}function Mn(e,t,n){let{vgChannel:i}=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};const r=Rn(e,t,n.style);return U(i?r:void 0,r,i?n[t.type][i]:void 0,n[t.type][e],i?n.mark[i]:n.mark[e])}function Rn(e,t,n){return Ln(e,jn(t),n)}function Ln(e,n,i){let r;n=t.array(n);for(const t of n){const n=i[t];J(n,e)&&(r=n[e])}return r}function qn(e,n){return t.array(e).reduce(((e,t)=>(e.field.push(ma(t,n)),e.order.push(t.sort??\"ascending\"),e)),{field:[],order:[]})}function Un(e,t){const n=[...e];return t.forEach((e=>{for(const t of n)if(X(t,e))return;n.push(e)})),n}function Wn(e,n){return X(e,n)||!n?e:e?[...t.array(e),...t.array(n)].join(\", \"):n}function In(e,t){const n=e.value,i=t.value;if(null==n||null===i)return{explicit:e.explicit,value:null};if(($n(n)||wn(n))&&($n(i)||wn(i)))return{explicit:e.explicit,value:Wn(n,i)};if($n(n)||wn(n))return{explicit:e.explicit,value:n};if($n(i)||wn(i))return{explicit:e.explicit,value:i};if(!($n(n)||wn(n)||$n(i)||wn(i)))return{explicit:e.explicit,value:Un(n,i)};throw new Error(\"It should never reach here\")}function Bn(e){return`Invalid specification ${Q(e)}. Make sure the specification includes at least one of the following properties: \"mark\", \"layer\", \"facet\", \"hconcat\", \"vconcat\", \"concat\", or \"repeat\".`}const Vn='Autosize \"fit\" only works for single views and layered views.';function Hn(e){return`${\"width\"==e?\"Width\":\"Height\"} \"container\" only works for single views and layered views.`}function Gn(e){return`${\"width\"==e?\"Width\":\"Height\"} \"container\" only works well with autosize \"fit\" or \"fit-${\"width\"==e?\"x\":\"y\"}\".`}function Yn(e){return e?`Dropping \"fit-${e}\" because spec has discrete ${st(e)}.`:'Dropping \"fit\" because spec has discrete size.'}function Xn(e){return`Unknown field for ${e}. Cannot calculate view size.`}function Qn(e){return`Cannot project a selection on encoding channel \"${e}\", which has no field.`}function Jn(e,t){return`Cannot project a selection on encoding channel \"${e}\" as it uses an aggregate function (\"${t}\").`}function Kn(e){return`Selection not supported for ${e} yet.`}const Zn=\"The same selection must be used to override scale domains in a layered view.\";function ei(e){return`The \"columns\" property cannot be used when \"${e}\" has nested row/column.`}const ti=\"Animation involving facet, layer, or concat is currently unsupported.\";function ni(e,t,n){return`An ancestor parsed field \"${e}\" as ${n} but a ch"
-  , "ild wants to parse the field as ${t}.`}function ii(e){return`Config.customFormatTypes is not true, thus custom format type and format for channel ${e} are dropped.`}function ri(e){return`${e}Offset dropped because ${e} is continuous`}function oi(e){return`Invalid field type \"${e}\".`}function ai(e,t){const{fill:n,stroke:i}=t;return`Dropping color ${e} as the plot also has ${n&&i?\"fill and stroke\":n?\"fill\":\"stroke\"}.`}function si(e,t){return`Dropping ${Q(e)} from channel \"${t}\" since it does not contain any data field, datum, value, or signal.`}function li(e,t,n){return`${e} dropped as it is incompatible with \"${t}\".`}function ci(e){return`${e} encoding should be discrete (ordinal / nominal / binned).`}function ui(e){return`${e} encoding should be discrete (ordinal / nominal / binned) or use a discretizing scale (e.g. threshold).`}function fi(e,t){return`Using discrete channel \"${e}\" to encode \"${t}\" field can be misleading as it does not encode ${\"ordinal\"===t?\"order\":\"magnitude\"}.`}function di(e){return`Using unaggregated domain with raw field has no effect (${Q(e)}).`}function mi(e){return`Unaggregated domain not applicable for \"${e}\" since it produces values outside the origin domain of the source data.`}function pi(e){return`Unaggregated domain is currently unsupported for log scale (${Q(e)}).`}function gi(e,t,n){return`${n}-scale's \"${t}\" is dropped as it does not work with ${e} scale.`}function hi(e){return`The step for \"${e}\" is dropped because the ${\"width\"===e?\"x\":\"y\"} is continuous.`}const yi=\"Domains that should be unioned has conflicting sort properties. Sort will be set to true.\";function vi(e,t){return`Invalid ${e}: ${Q(t)}.`}function bi(e){return`1D error band does not support ${e}.`}function xi(e){return`Channel ${e} is required for \"binned\" bin.`}const $i=t.logger(t.Warn);let wi=$i;function ki(){wi.error(...arguments)}function Si(){wi.warn(...arguments)}function Di(e){if(e&&t.isObject(e))for(const t of Ai)if(J(e,t))return!0;return!1}const Fi=[\"january\",\"february\",\"march\",\"april\",\"may\",\"june\",\"july\",\"august\",\"september\",\"october\",\"november\",\"december\"],Oi=Fi.map((e=>e.substr(0,3))),zi=[\"sunday\",\"monday\",\"tuesday\",\"wednesday\",\"thursday\",\"friday\",\"saturday\"],Ci=zi.map((e=>e.substr(0,3)));function _i(e,n){const i=[];if(n&&void 0!==e.day&&D(e).length>1&&(Si(function(e){return`Dropping day from datetime ${Q(e)} as day cannot be combined with other units.`}(e)),delete(e=l(e)).day),void 0!==e.year?i.push(e.year):i.push(2012),void 0!==e.month){const r=n?function(e){if(G(e)&&(e=+e),t.isNumber(e))return e-1;{const t=e.toLowerCase(),n=Fi.indexOf(t);if(-1!==n)return n;const i=t.substr(0,3),r=Oi.indexOf(i);if(-1!==r)return r;throw new Error(vi(\"month\",e))}}(e.month):e.month;i.push(r)}else if(void 0!==e.quarter){const r=n?function(e){if(G(e)&&(e=+e),t.isNumber(e))return e>4&&Si(vi(\"quarter\",e)),e-1;throw new Error(vi(\"quarter\",e))}(e.quarter):e.quarter;i.push(t.isNumber(r)?3*r:`${r}*3`)}else i.push(0);if(void 0!==e.date)i.push(e.date);else if(void 0!==e.day){const r=n?function(e){if(G(e)&&(e=+e),t.isNumber(e))return e%7;{const t=e.toLowerCase(),n=zi.indexOf(t);if(-1!==n)return n;const i=t.substr(0,3),r=Ci.indexOf(i);if(-1!==r)return r;throw new Error(vi(\"day\",e))}}(e.day):e.day;i.push(t.isNumber(r)?r+1:`${r}+1`)}else i.push(1);for(const t of[\"hours\",\"minutes\",\"seconds\",\"milliseconds\"]){const n=e[t];i.push(void 0===n?0:n)}return i}function Pi(e){const t=_i(e,!0).join(\", \");return e.utc?`utc(${t})`:`datetime(${t})`}const Ni={year:1,quarter:1,month:1,week:1,day:1,dayofyear:1,date:1,hours:1,minutes:1,seconds:1,milliseconds:1},Ai=D(Ni);function Ti(e){return t.isObject(e)?e.binned:ji(e)}function ji(e){return e&&e.startsWith(\"binned\")}function Ei(e){return e.startsWith(\"utc\")}const Mi={\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"};function Ri(e){return Ai.filter((t=>qi(e,t)))}function Li(e){const t=Ri(e);return t[t.length-1]}function qi(e,t){const n=e.indexOf(t);return!(n<0)&&(!(n>0&&\"seconds\"===t&&\"i\"===e.charAt(n-1))&&(!(e.length>n+3&&\"day\"===t&&\"o\"===e.charAt(n+3))&&!(n>0&&\"year\"===t&&\"f\"===e.charAt(n-1))))}function"
-  , " Ui(e,t){let{end:n}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{end:!1};const i=A(t),r=Ei(e)?\"utc\":\"\";let o;const a={};for(const t of Ai)qi(e,t)&&(a[t]=\"quarter\"===(s=t)?`(${r}quarter(${i})-1)`:`${r}${s}(${i})`,o=t);var s;return n&&(a[o]+=\"+1\"),function(e){const t=_i(e,!1).join(\", \");return e.utc?`utc(${t})`:`datetime(${t})`}(a)}function Wi(e){if(!e)return;return`timeUnitSpecifier(${Q(Ri(e))}, ${Q(Mi)})`}function Ii(e){if(!e)return;let n;return t.isString(e)?n=ji(e)?{unit:e.substring(6),binned:!0}:{unit:e}:t.isObject(e)&&(n={...e,...e.unit?{unit:e.unit}:{}}),Ei(n.unit)&&(n.utc=!0,n.unit=n.unit.substring(3)),n}function Bi(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:e=>e;const n=Ii(e),i=Li(n.unit);if(i&&\"day\"!==i){const e={year:2001,month:1,date:1,hours:0,minutes:0,seconds:0,milliseconds:0},{step:r,part:o}=Hi(i,n.step);return`${t(Pi({...e,[o]:+e[o]+r}))} - ${t(Pi(e))}`}}const Vi={year:1,month:1,date:1,hours:1,minutes:1,seconds:1,milliseconds:1};function Hi(e){let n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;if(function(e){return t.hasOwnProperty(Vi,e)}(e))return{part:e,step:n};switch(e){case\"day\":case\"dayofyear\":return{part:\"date\",step:n};case\"quarter\":return{part:\"month\",step:3*n};case\"week\":return{part:\"date\",step:7*n}}}function Gi(e){return!!e?.field&&void 0!==e.equal}function Yi(e){return!!e?.field&&void 0!==e.lt}function Xi(e){return!!e?.field&&void 0!==e.lte}function Qi(e){return!!e?.field&&void 0!==e.gt}function Ji(e){return!!e?.field&&void 0!==e.gte}function Ki(e){if(e?.field){if(t.isArray(e.range)&&2===e.range.length)return!0;if(wn(e.range))return!0}return!1}function Zi(e){return!!e?.field&&(t.isArray(e.oneOf)||t.isArray(e.in))}function er(e){return Zi(e)||Gi(e)||Ki(e)||Yi(e)||Qi(e)||Xi(e)||Ji(e)}function tr(e,t){return _a(e,{timeUnit:t,wrapTime:!0})}function nr(e){let t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];const{field:n}=e,i=Ii(e.timeUnit),{unit:r,binned:o}=i||{},a=ma(e,{expr:\"datum\"}),s=r?`time(${o?a:Ui(r,n)})`:a;if(Gi(e))return`${s}===${tr(e.equal,r)}`;if(Yi(e)){return`${s}<${tr(e.lt,r)}`}if(Qi(e)){return`${s}>${tr(e.gt,r)}`}if(Xi(e)){return`${s}<=${tr(e.lte,r)}`}if(Ji(e)){return`${s}>=${tr(e.gte,r)}`}if(Zi(e))return`indexof([${function(e,t){return e.map((e=>tr(e,t)))}(e.oneOf,r).join(\",\")}], ${s}) !== -1`;if(function(e){return!!e?.field&&void 0!==e.valid}(e))return ir(s,e.valid);if(Ki(e)){const{range:n}=bn(e),i=wn(n)?{signal:`${n.signal}[0]`}:n[0],o=wn(n)?{signal:`${n.signal}[1]`}:n[1];if(null!==i&&null!==o&&t)return\"inrange(\"+s+\", [\"+tr(i,r)+\", \"+tr(o,r)+\"])\";const a=[];return null!==i&&a.push(`${s} >= ${tr(i,r)}`),null!==o&&a.push(`${s} <= ${tr(o,r)}`),a.length>0?a.join(\" && \"):\"true\"}throw new Error(`Invalid field predicate: ${Q(e)}`)}function ir(e){return!(arguments.length>1&&void 0!==arguments[1])||arguments[1]?`isValid(${e}) && isFinite(+${e})`:`!isValid(${e}) || !isFinite(+${e})`}function rr(e){return er(e)&&e.timeUnit?{...e,timeUnit:Ii(e.timeUnit)}:e}function or(e){return\"quantitative\"===e||\"temporal\"===e}function ar(e){return\"ordinal\"===e||\"nominal\"===e}const sr=\"quantitative\",lr=\"ordinal\",cr=\"temporal\",ur=\"nominal\",fr=\"geojson\";const dr={LINEAR:\"linear\",LOG:\"log\",POW:\"pow\",SQRT:\"sqrt\",SYMLOG:\"symlog\",IDENTITY:\"identity\",SEQUENTIAL:\"sequential\",TIME:\"time\",UTC:\"utc\",QUANTILE:\"quantile\",QUANTIZE:\"quantize\",THRESHOLD:\"threshold\",BIN_ORDINAL:\"bin-ordinal\",ORDINAL:\"ordinal\",POINT:\"point\",BAND:\"band\"},mr={linear:\"numeric\",log:\"numeric\",pow:\"numeric\",sqrt:\"numeric\",symlog:\"numeric\",identity:\"numeric\",sequential:\"numeric\",time:\"time\",utc:\"time\",ordinal:\"ordinal\",\"bin-ordinal\":\"bin-ordinal\",point:\"ordinal-position\",band:\"ordinal-position\",quantile:\"discretizing\",quantize:\"discretizing\",threshold:\"discretizing\"};function pr(e,t){const n=mr[e],i=mr[t];return n===i||\"ordinal-position\"===n&&\"time\"===i||\"ordinal-position\"===i&&\"time\"===n}const gr={linear:0,log:1,pow:1,sqrt:1,symlog:1,identity:1,sequential:1,time:0,utc:0,point:10,band:11,ordinal:0,\"bin-ordinal\":0,quantile:0,quantize:0,threshold:0};function hr(e){return gr[e]}const yr=new Set([\"line"
-  , "ar\",\"log\",\"pow\",\"sqrt\",\"symlog\"]),vr=new Set([...yr,\"time\",\"utc\"]);function br(e){return yr.has(e)}const xr=new Set([\"quantile\",\"quantize\",\"threshold\"]),$r=new Set([...vr,...xr,\"sequential\",\"identity\"]),wr=new Set([\"ordinal\",\"bin-ordinal\",\"point\",\"band\"]);function kr(e){return wr.has(e)}function Sr(e){return $r.has(e)}function Dr(e){return vr.has(e)}function Fr(e){return xr.has(e)}function Or(e){return J(e,\"param\")}const{type:zr,domain:Cr,range:_r,rangeMax:Pr,rangeMin:Nr,scheme:Ar,...Tr}={type:1,domain:1,domainMax:1,domainMin:1,domainMid:1,domainRaw:1,align:1,range:1,rangeMax:1,rangeMin:1,scheme:1,bins:1,reverse:1,round:1,clamp:1,nice:1,base:1,exponent:1,constant:1,interpolate:1,zero:1,padding:1,paddingInner:1,paddingOuter:1},jr=D(Tr);function Er(e,t){switch(t){case\"type\":case\"domain\":case\"reverse\":case\"range\":return!0;case\"scheme\":case\"interpolate\":return![\"point\",\"band\",\"identity\"].includes(e);case\"bins\":return![\"point\",\"band\",\"identity\",\"ordinal\"].includes(e);case\"round\":return Dr(e)||\"band\"===e||\"point\"===e;case\"padding\":case\"rangeMin\":case\"rangeMax\":return Dr(e)||[\"point\",\"band\"].includes(e);case\"paddingOuter\":case\"align\":return[\"point\",\"band\"].includes(e);case\"paddingInner\":return\"band\"===e;case\"domainMax\":case\"domainMid\":case\"domainMin\":case\"domainRaw\":case\"clamp\":return Dr(e);case\"nice\":return Dr(e)||\"quantize\"===e||\"threshold\"===e;case\"exponent\":return\"pow\"===e;case\"base\":return\"log\"===e;case\"constant\":return\"symlog\"===e;case\"zero\":return Sr(e)&&!p([\"log\",\"time\",\"utc\",\"threshold\",\"quantile\"],e)}}function Mr(e,t){switch(t){case\"interpolate\":case\"scheme\":case\"domainMid\":return We(e)?void 0:`Cannot use the scale property \"${t}\" with non-color channel.`;case\"align\":case\"type\":case\"bins\":case\"domain\":case\"domainMax\":case\"domainMin\":case\"domainRaw\":case\"range\":case\"base\":case\"exponent\":case\"constant\":case\"nice\":case\"padding\":case\"paddingInner\":case\"paddingOuter\":case\"rangeMax\":case\"rangeMin\":case\"reverse\":case\"round\":case\"clamp\":case\"zero\":return}}const Rr={arc:\"arc\",area:\"area\",bar:\"bar\",image:\"image\",line:\"line\",point:\"point\",rect:\"rect\",rule:\"rule\",text:\"text\",tick:\"tick\",trail:\"trail\",circle:\"circle\",square:\"square\",geoshape:\"geoshape\"},Lr=Rr.arc,qr=Rr.area,Ur=Rr.bar,Wr=Rr.image,Ir=Rr.line,Br=Rr.point,Vr=Rr.rect,Hr=Rr.rule,Gr=Rr.text,Yr=Rr.tick,Xr=Rr.trail,Qr=Rr.circle,Jr=Rr.square,Kr=Rr.geoshape;function Zr(e){return[\"line\",\"area\",\"trail\"].includes(e)}function eo(e){return[\"rect\",\"bar\",\"image\",\"arc\",\"tick\"].includes(e)}const to=new Set(D(Rr));function no(e){return J(e,\"type\")}const io=[\"stroke\",\"strokeWidth\",\"strokeDash\",\"strokeDashOffset\",\"strokeOpacity\",\"strokeJoin\",\"strokeMiterLimit\",\"fill\",\"fillOpacity\"],ro=D({color:1,filled:1,invalid:1,order:1,radius2:1,theta2:1,timeUnitBandSize:1,timeUnitBandPosition:1}),oo=[\"binSpacing\",\"continuousBandSize\",\"discreteBandSize\",\"minBandSize\"],ao={area:[\"line\",\"point\"],bar:oo,rect:oo,line:[\"point\"],tick:[\"bandSize\",\"thickness\",...oo]},so=D({mark:1,arc:1,area:1,bar:1,circle:1,image:1,line:1,point:1,rect:1,rule:1,square:1,text:1,tick:1,trail:1,geoshape:1});function lo(e){return J(e,\"band\")}const co={horizontal:[\"cornerRadiusTopRight\",\"cornerRadiusBottomRight\"],vertical:[\"cornerRadiusTopLeft\",\"cornerRadiusTopRight\"]},uo={binSpacing:0,continuousBandSize:5,minBandSize:.25,timeUnitBandPosition:.5},fo={...uo,binSpacing:1},mo={...uo,thickness:1};function po(e,t){let{isPath:n}=t;return void 0===e||\"break-paths-show-path-domains\"===e?n?\"break-paths-show-domains\":\"filter\":null===e?\"show\":e}function go(e){let{markDef:t,config:n,scaleChannel:i,scaleType:r,isCountAggregate:o}=e;if(!r||!Sr(r)||o)return\"always-valid\";const a=po(En(\"invalid\",t,n),{isPath:Zr(t.type)}),s=n.scale?.invalid?.[i];return void 0!==s?\"show\":a}function ho(e){let{scaleName:t,scale:n,mode:i}=e;const r=`domain('${t}')`;if(!n||!t)return;const o=`${r}[0]`,a=`peek(${r})`,s=n.domainHasZero();if(\"definitely\"===s)return{scale:t,value:0};if(\"maybe\"===s){return{signal:`scale('${t}', inrange(0, ${r}) ? 0 : ${\"zeroOrMin\"===i?o:a})`}}return{signal:`scale('${t}', ${\"zeroOrMin\"===i?o:a})`}}function yo(e){let{scaleChannel:t,channelDef:"
-  , "n,scale:i,scaleName:r,markDef:o,config:a}=e;const s=i?.get(\"type\"),l=wa(n),c=go({scaleChannel:t,markDef:o,config:a,scaleType:s,isCountAggregate:cn(l?.aggregate)});if(l&&\"show\"===c){const e=a.scale.invalid?.[t]??\"zero-or-min\";return{test:ir(ma(l,{expr:\"datum\"}),!1),...vo(e,i,r)}}}function vo(e,n,i){if(r=e,t.isObject(r)&&\"value\"in r){const{value:t}=e;return wn(t)?{signal:t.signal}:{value:t}}var r;return ho({scale:n,scaleName:i,mode:\"zeroOrMin\"})}function bo(e){const{channel:t,channelDef:n,markDef:i,scale:r,scaleName:o,config:a}=e,s=rt(t),l=wo(e),c=yo({scaleChannel:s,channelDef:n,scale:r,scaleName:o,markDef:i,config:a});return void 0!==c?[c,l]:l}function xo(e,t,n,i){const r={};if(t&&(r.scale=t),ta(e)){const{datum:t}=e;Di(t)?r.signal=Pi(t):wn(t)?r.signal=t.signal:vn(t)?r.signal=t.expr:r.value=t}else r.field=ma(e,n);if(i){const{offset:e,band:t}=i;e&&(r.offset=e),t&&(r.band=t)}return r}function $o(e){let{scaleName:t,fieldOrDatumDef:n,fieldOrDatumDef2:i,offset:r,startSuffix:o,endSuffix:a=\"end\",bandPosition:s=.5}=e;const l=!wn(s)&&0<s&&s<1?\"datum\":void 0,c=ma(n,{expr:l,suffix:o}),u=void 0!==i?ma(i,{expr:l}):ma(n,{suffix:a,expr:l}),f={};if(0===s||1===s){f.scale=t;const e=0===s?c:u;f.field=e}else{const e=wn(s)?`(1-${s.signal}) * ${c} + ${s.signal} * ${u}`:`${1-s} * ${c} + ${s} * ${u}`;f.signal=`scale(\"${t}\", ${e})`}return r&&(f.offset=r),f}function wo(e){let{channel:n,channelDef:i,channel2Def:r,markDef:o,config:a,scaleName:s,scale:l,stack:c,offset:u,defaultRef:f,bandPosition:d}=e;if(i){if(oa(i)){const e=l?.get(\"type\");if(aa(i)){d??=Ho({fieldDef:i,fieldDef2:r,markDef:o,config:a});const{bin:t,timeUnit:l,type:f}=i;if(mn(t)||d&&l&&f===cr)return c?.impute?xo(i,s,{binSuffix:\"mid\"},{offset:u}):d&&!kr(e)?$o({scaleName:s,fieldOrDatumDef:i,bandPosition:d,offset:u}):xo(i,s,Na(i,n)?{binSuffix:\"range\"}:{},{offset:u});if(pn(t)){if(Zo(r))return $o({scaleName:s,fieldOrDatumDef:i,fieldOrDatumDef2:r,bandPosition:d,offset:u});Si(xi(n===te?ie:re))}}return xo(i,s,kr(e)?{binSuffix:\"range\"}:{},{offset:u,band:\"band\"===e?d??i.bandPosition??.5:void 0})}if(sa(i)){const e=u?{offset:u}:{};return{...ko(n,i.value),...e}}}return t.isFunction(f)&&(f=f()),f?{...f,...u?{offset:u}:{}}:f}function ko(e,t){return p([\"x\",\"x2\"],e)&&\"width\"===t?{field:{group:\"width\"}}:p([\"y\",\"y2\"],e)&&\"height\"===t?{field:{group:\"height\"}}:Pn(t)}function So(e){return e&&\"number\"!==e&&\"time\"!==e}function Do(e,t,n){return`${e}(${t}${n?`, ${Q(n)}`:\"\"})`}const Fo=\" – \";function Oo(e){let{fieldOrDatumDef:n,format:i,formatType:r,expr:o,normalizeStack:a,config:s}=e;if(So(r))return Co({fieldOrDatumDef:n,format:i,formatType:r,expr:o,config:s});const l=zo(n,o,a),c=ea(n);if(void 0===i&&void 0===r&&s.customFormatTypes){if(\"quantitative\"===c){if(a&&s.normalizedNumberFormatType)return Co({fieldOrDatumDef:n,format:s.normalizedNumberFormat,formatType:s.normalizedNumberFormatType,expr:o,config:s});if(s.numberFormatType)return Co({fieldOrDatumDef:n,format:s.numberFormat,formatType:s.numberFormatType,expr:o,config:s})}if(\"temporal\"===c&&s.timeFormatType&&Zo(n)&&void 0===n.timeUnit)return Co({fieldOrDatumDef:n,format:s.timeFormat,formatType:s.timeFormatType,expr:o,config:s})}if(Ca(n)){const e=function(e){let{field:n,timeUnit:i,format:r,formatType:o,rawTimeFormat:a,isUTCScale:s}=e;return!i||r?!i&&o?`${o}(${n}, '${r}')`:(r=t.isString(r)?r:a,`${s?\"utc\":\"time\"}Format(${n}, '${r}')`):function(e,t,n){if(!e)return;const i=Wi(e);return`${n||Ei(e)?\"utc\":\"time\"}Format(${t}, ${i})`}(i,n,s)}({field:l,timeUnit:Zo(n)?Ii(n.timeUnit)?.unit:void 0,format:i,formatType:s.timeFormatType,rawTimeFormat:s.timeFormat,isUTCScale:la(n)&&n.scale?.type===dr.UTC});return e?{signal:e}:void 0}if(i=No({type:c,specifiedFormat:i,config:s,normalizeStack:a}),Zo(n)&&mn(n.bin)){return{signal:jo(l,ma(n,{expr:o,binSuffix:\"end\"}),i,r,s)}}return i||\"quantitative\"===ea(n)?{signal:`${Ao(l,i)}`}:{signal:`isValid(${l}) ? ${l} : \"\"+${l}`}}function zo(e,t,n){return Zo(e)?n?`${ma(e,{expr:t,suffix:\"end\"})}-${ma(e,{expr:t,suffix:\"start\"})}`:ma(e,{expr:t}):function(e){const{datum:t}=e;return Di(t)?Pi(t):`${Q(t)}`}(e)}function Co(e){let{fieldOrDatumDef:t,format"
-  , ":n,formatType:i,expr:r,normalizeStack:o,config:a,field:s}=e;if(s??=zo(t,r,o),\"datum.value\"!==s&&Zo(t)&&mn(t.bin)){return{signal:jo(s,ma(t,{expr:r,binSuffix:\"end\"}),n,i,a)}}return{signal:Do(i,s,n)}}function _o(e,n,i,r,o,a){if(!t.isString(r)||!So(r)){if(void 0===i&&void 0===r&&o.customFormatTypes&&\"quantitative\"===ea(e)){if(o.normalizedNumberFormatType&&ca(e)&&\"normalize\"===e.stack)return;if(o.numberFormatType)return}if(ca(e)&&\"normalize\"===e.stack&&o.normalizedNumberFormat)return No({type:\"quantitative\",config:o,normalizeStack:!0});if(Ca(e)){const t=Zo(e)?Ii(e.timeUnit)?.unit:void 0;if(void 0===t&&o.customFormatTypes&&o.timeFormatType)return;return function(e){let{specifiedFormat:t,timeUnit:n,config:i,omitTimeFormatConfig:r}=e;if(t)return t;if(n)return{signal:Wi(n)};return r?void 0:i.timeFormat}({specifiedFormat:i,timeUnit:t,config:o,omitTimeFormatConfig:a})}return No({type:n,specifiedFormat:i,config:o})}}function Po(e,t,n){return e&&(wn(e)||\"number\"===e||\"time\"===e)?e:Ca(t)&&\"time\"!==n&&\"utc\"!==n?Zo(t)&&Ii(t?.timeUnit)?.utc?\"utc\":\"time\":void 0}function No(e){let{type:n,specifiedFormat:i,config:r,normalizeStack:o}=e;return t.isString(i)?i:n===sr?o?r.normalizedNumberFormat:r.numberFormat:void 0}function Ao(e,t){return`format(${e}, \"${t||\"\"}\")`}function To(e,n,i,r){return So(i)?Do(i,e,n):Ao(e,(t.isString(n)?n:void 0)??r.numberFormat)}function jo(e,t,n,i,r){if(void 0===n&&void 0===i&&r.customFormatTypes&&r.numberFormatType)return jo(e,t,r.numberFormat,r.numberFormatType,r);const o=To(e,n,i,r),a=To(t,n,i,r);return`${ir(e,!1)} ? \"null\" : ${o} + \"${Fo}\" + ${a}`}const Eo=\"min\",Mo={x:1,y:1,color:1,fill:1,stroke:1,strokeWidth:1,size:1,shape:1,fillOpacity:1,strokeOpacity:1,opacity:1,text:1};function Ro(e){return t.hasOwnProperty(Mo,e)}function Lo(e){return e&&(\"count\"===e.op||J(e,\"field\"))}function qo(e){return e&&t.isArray(e)}function Uo(e){return J(e,\"row\")||J(e,\"column\")}function Wo(e){return J(e,\"header\")}function Io(e){return J(e,\"facet\")}function Bo(e){const{field:t,timeUnit:n,bin:i,aggregate:r}=e;return{...n?{timeUnit:n}:{},...i?{bin:i}:{},...r?{aggregate:r}:{},field:t}}function Vo(e){return J(e,\"sort\")}function Ho(e){let{fieldDef:t,fieldDef2:n,markDef:i,config:r}=e;if(oa(t)&&void 0!==t.bandPosition)return t.bandPosition;if(Zo(t)){const{timeUnit:e,bin:o}=t;if(e&&!n)return Mn(\"timeUnitBandPosition\",i,r);if(mn(o))return.5}}function Go(e){let{channel:t,fieldDef:n,fieldDef2:i,markDef:r,config:o,scaleType:a,useVlSizeChannel:s}=e;const l=st(t),c=En(s?\"size\":l,r,o,{vgChannel:l});if(void 0!==c)return c;if(Zo(n)){const{timeUnit:e,bin:t}=n;if(e&&!i)return{band:Mn(\"timeUnitBandSize\",r,o)};if(mn(t)&&!kr(a))return{band:1}}return eo(r.type)?a?kr(a)?o[r.type]?.discreteBandSize||{band:1}:o[r.type]?.continuousBandSize:o[r.type]?.discreteBandSize:void 0}function Yo(e,t,n,i){return!!(mn(e.bin)||e.timeUnit&&aa(e)&&\"temporal\"===e.type)&&void 0!==Ho({fieldDef:e,fieldDef2:t,markDef:n,config:i})}function Xo(e){return J(e,\"sort\")&&!J(e,\"field\")}function Qo(e){return J(e,\"condition\")}function Jo(e){const n=e?.condition;return!!n&&!t.isArray(n)&&Zo(n)}function Ko(e){const n=e?.condition;return!!n&&!t.isArray(n)&&oa(n)}function Zo(e){return J(e,\"field\")||\"count\"===e?.aggregate}function ea(e){return e?.type}function ta(e){return J(e,\"datum\")}function na(e){return aa(e)&&!pa(e)||ra(e)}function ia(e){return aa(e)&&\"quantitative\"===e.type&&!e.bin||ra(e)}function ra(e){return ta(e)&&t.isNumber(e.datum)}function oa(e){return Zo(e)||ta(e)}function aa(e){return e&&(J(e,\"field\")||\"count\"===e.aggregate)&&J(e,\"type\")}function sa(e){return J(e,\"value\")}function la(e){return J(e,\"scale\")||J(e,\"sort\")}function ca(e){return J(e,\"axis\")||J(e,\"stack\")||J(e,\"impute\")}function ua(e){return J(e,\"legend\")}function fa(e){return J(e,\"format\")||J(e,\"formatType\")}function da(e){return f(e,[\"legend\",\"axis\",\"header\",\"scale\"])}function ma(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=e.field;const i=t.prefix;let r=t.suffix,o=\"\";if(function(e){return\"count\"===e.aggregate}(e))n=B(\"count\");else{let i;if(!t.nofn)if(function(e){return J(e,\"op\")}(e))i=e.op;else{c"
-  , "onst{bin:a,aggregate:s,timeUnit:l}=e;mn(a)?(i=dn(a),r=(t.binSuffix??\"\")+(t.suffix??\"\")):s?an(s)?(o=`[\"${n}\"]`,n=`argmax_${s.argmax}`):on(s)?(o=`[\"${n}\"]`,n=`argmin_${s.argmin}`):i=String(s):l&&!Ti(l)&&(i=function(e){const{utc:t,...n}=Ii(e);return n.unit?(t?\"utc\":\"\")+D(n).map((e=>C(`${\"unit\"===e?\"\":`_${e}_`}${n[e]}`))).join(\"\"):(t?\"utc\":\"\")+\"timeunit\"+D(n).map((e=>C(`_${e}_${n[e]}`))).join(\"\")}(l),r=(![\"range\",\"mid\"].includes(t.binSuffix)&&t.binSuffix||\"\")+(t.suffix??\"\"))}i&&(n=n?`${i}_${n}`:i)}return r&&(n=`${n}_${r}`),i&&(n=`${i}_${n}`),t.forAs?L(n):t.expr?T(n,t.expr)+o:M(n)+o}function pa(e){switch(e.type){case\"nominal\":case\"ordinal\":case\"geojson\":return!0;case\"quantitative\":return Zo(e)&&!!e.bin;case\"temporal\":return!1}throw new Error(oi(e.type))}const ga=(e,t)=>{switch(t.fieldTitle){case\"plain\":return e.field;case\"functional\":return function(e){const{aggregate:t,bin:n,timeUnit:i,field:r}=e;if(an(t))return`${r} for argmax(${t.argmax})`;if(on(t))return`${r} for argmin(${t.argmin})`;const o=i&&!Ti(i)?Ii(i):void 0,a=t||o?.unit||o?.maxbins&&\"timeunit\"||mn(n)&&\"bin\";return a?`${a.toUpperCase()}(${r})`:r}(e);default:return function(e,t){const{field:n,bin:i,timeUnit:r,aggregate:o}=e;if(\"count\"===o)return t.countTitle;if(mn(i))return`${n} (binned)`;if(r&&!Ti(r)){const e=Ii(r)?.unit;if(e)return`${n} (${Ri(e).join(\"-\")})`}else if(o)return an(o)?`${n} for max ${o.argmax}`:on(o)?`${n} for min ${o.argmin}`:`${N(o)} of ${n}`;return n}(e,t)}};let ha=ga;function ya(e){ha=e}function va(e,t,n){let{allowDisabling:i,includeDefault:r=!0}=n;const o=ba(e)?.title;if(!Zo(e))return o??e.title;const a=e,s=r?xa(a,t):void 0;return i?U(o,a.title,s):o??a.title??s}function ba(e){return ca(e)&&e.axis?e.axis:ua(e)&&e.legend?e.legend:Wo(e)&&e.header?e.header:void 0}function xa(e,t){return ha(e,t)}function $a(e){if(fa(e)){const{format:t,formatType:n}=e;return{format:t,formatType:n}}{const t=ba(e)??{},{format:n,formatType:i}=t;return{format:n,formatType:i}}}function wa(e){return Zo(e)?e:Jo(e)?e.condition:void 0}function ka(e){return oa(e)?e:Ko(e)?e.condition:void 0}function Sa(e,n,i){let r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};if(t.isString(e)||t.isNumber(e)||t.isBoolean(e)){return Si(function(e,t,n){return`Channel ${e} is a ${t}. Converted to {value: ${Q(n)}}.`}(n,t.isString(e)?\"string\":t.isNumber(e)?\"number\":\"boolean\",e)),{value:e}}return oa(e)?Da(e,n,i,r):Ko(e)?{...e,condition:Da(e.condition,n,i,r)}:e}function Da(e,n,i,r){if(fa(e)){const{format:t,formatType:o,...a}=e;if(So(o)&&!i.customFormatTypes)return Si(ii(n)),Da(a,n,i,r)}else{const t=ca(e)?\"axis\":ua(e)?\"legend\":Wo(e)?\"header\":null;if(t&&e[t]){const{format:o,formatType:a,...s}=e[t];if(So(a)&&!i.customFormatTypes)return Si(ii(n)),Da({...e,[t]:s},n,i,r)}}return Zo(e)?Fa(e,n,r):function(e){let n=e.type;if(n)return e;const{datum:i}=e;return n=t.isNumber(i)?\"quantitative\":t.isString(i)?\"nominal\":Di(i)?\"temporal\":void 0,{...e,type:n}}(e)}function Fa(e,n){let{compositeMark:i=!1}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{aggregate:r,timeUnit:o,bin:a,field:s}=e,l={...e};if(i||!r||sn(r)||an(r)||on(r)||(Si(function(e){return`Invalid aggregation operator \"${e}\".`}(r)),delete l.aggregate),o&&(l.timeUnit=Ii(o)),s&&(l.field=`${s}`),mn(a)&&(l.bin=Oa(a,n)),pn(a)&&!_t(n)&&Si(function(e){return`Channel ${e} should not be used with \"binned\" bin.`}(n)),aa(l)){const{type:e}=l,t=function(e){if(e)switch(e=e.toLowerCase()){case\"q\":case sr:return\"quantitative\";case\"t\":case cr:return\"temporal\";case\"o\":case lr:return\"ordinal\";case\"n\":case ur:return\"nominal\";case fr:return\"geojson\"}}(e);e!==t&&(l.type=t),\"quantitative\"!==e&&cn(r)&&(Si(function(e,t){return`Invalid field type \"${e}\" for aggregate: \"${t}\", using \"quantitative\" instead.`}(e,r)),l.type=\"quantitative\")}else if(!it(n)){const e=function(e,n){switch(n){case\"latitude\":case\"longitude\":return\"quantitative\";case\"row\":case\"column\":case\"facet\":case\"shape\":case\"strokeDash\":return\"nominal\";case\"order\":return\"ordinal\"}if(Vo(e)&&t.isArray(e.sort))return\"ordinal\";const{aggregate:i,bin:r,timeUnit:o}=e;if(o)return\"temporal\";if(r||i&&!an(i)&"
-  , "&!on(i))return\"quantitative\";if(la(e)&&e.scale?.type)switch(mr[e.scale.type]){case\"numeric\":case\"discretizing\":return\"quantitative\";case\"time\":return\"temporal\"}return\"nominal\"}(l,n);l.type=e}if(aa(l)){const{compatible:e,warning:t}=function(e,t){const n=e.type;if(\"geojson\"===n&&\"shape\"!==t)return{compatible:!1,warning:`Channel ${t} should not be used with a geojson data.`};switch(t){case K:case Z:case ee:return pa(e)?za:{compatible:!1,warning:ci(t)};case te:case ne:case oe:case ae:case he:case ye:case ve:case Oe:case Ce:case _e:case Pe:case Ne:case Ae:case $e:case ce:case se:case Te:return za;case de:case pe:case fe:case me:return n!==sr?{compatible:!1,warning:`Channel ${t} should be used with a quantitative field only, not ${e.type} field.`}:za;case we:case ke:case Se:case De:case xe:case ue:case le:case ie:case re:case ge:return\"nominal\"!==n||e.sort?za:{compatible:!1,warning:`Channel ${t} should not be used with an unsorted discrete field.`};case be:case Fe:return pa(e)||la(i=e)&&Fr(i.scale?.type)?za:{compatible:!1,warning:ui(t)};case ze:return\"nominal\"!==e.type||\"sort\"in e?za:{compatible:!1,warning:\"Channel order is inappropriate for nominal field, which has no inherent order.\"}}var i}(l,n)||{};!1===e&&Si(t)}if(Vo(l)&&t.isString(l.sort)){const{sort:e}=l;if(Ro(e))return{...l,sort:{encoding:e}};const t=e.substring(1);if(\"-\"===e.charAt(0)&&Ro(t))return{...l,sort:{encoding:t,order:\"descending\"}}}if(Wo(l)){const{header:e}=l;if(e){const{orient:t,...n}=e;if(t)return{...l,header:{...n,labelOrient:e.labelOrient||t,titleOrient:e.titleOrient||t}}}}return l}function Oa(e,n){return t.isBoolean(e)?{maxbins:yn(n)}:\"binned\"===e?{binned:!0}:e.maxbins||e.step?e:{...e,maxbins:yn(n)}}const za={compatible:!0};function Ca(e){const{formatType:t}=$a(e);return\"time\"===t||!t&&((n=e)&&(\"temporal\"===n.type||Zo(n)&&!!n.timeUnit));var n}function _a(e,n){let{timeUnit:i,type:r,wrapTime:o,undefinedIfExprNotRequired:a}=n;const s=i&&Ii(i)?.unit;let l,c=s||\"temporal\"===r;return vn(e)?l=e.expr:wn(e)?l=e.signal:Di(e)?(c=!0,l=Pi(e)):(t.isString(e)||t.isNumber(e))&&c&&(l=`datetime(${Q(e)})`,function(e){return t.hasOwnProperty(Ni,e)}(s)&&(t.isNumber(e)&&e<1e4||t.isString(e)&&isNaN(Date.parse(e)))&&(l=Pi({[s]:e}))),l?o&&c?`time(${l})`:l:a?void 0:Q(e)}function Pa(e,t){const{type:n}=e;return t.map((t=>{const i=_a(t,{timeUnit:Zo(e)&&!Ti(e.timeUnit)?e.timeUnit:void 0,type:n,undefinedIfExprNotRequired:!0});return void 0!==i?{signal:i}:t}))}function Na(e,t){return mn(e.bin)?Qt(t)&&[\"ordinal\",\"nominal\"].includes(e.type):(console.warn(\"Only call this method for binned field defs.\"),!1)}const Aa={labelAlign:{part:\"labels\",vgProp:\"align\"},labelBaseline:{part:\"labels\",vgProp:\"baseline\"},labelColor:{part:\"labels\",vgProp:\"fill\"},labelFont:{part:\"labels\",vgProp:\"font\"},labelFontSize:{part:\"labels\",vgProp:\"fontSize\"},labelFontStyle:{part:\"labels\",vgProp:\"fontStyle\"},labelFontWeight:{part:\"labels\",vgProp:\"fontWeight\"},labelOpacity:{part:\"labels\",vgProp:\"opacity\"},labelOffset:null,labelPadding:null,gridColor:{part:\"grid\",vgProp:\"stroke\"},gridDash:{part:\"grid\",vgProp:\"strokeDash\"},gridDashOffset:{part:\"grid\",vgProp:\"strokeDashOffset\"},gridOpacity:{part:\"grid\",vgProp:\"opacity\"},gridWidth:{part:\"grid\",vgProp:\"strokeWidth\"},tickColor:{part:\"ticks\",vgProp:\"stroke\"},tickDash:{part:\"ticks\",vgProp:\"strokeDash\"},tickDashOffset:{part:\"ticks\",vgProp:\"strokeDashOffset\"},tickOpacity:{part:\"ticks\",vgProp:\"opacity\"},tickSize:null,tickWidth:{part:\"ticks\",vgProp:\"strokeWidth\"}};function Ta(e){return e?.condition}const ja=[\"domain\",\"grid\",\"labels\",\"ticks\",\"title\"],Ea={grid:\"grid\",gridCap:\"grid\",gridColor:\"grid\",gridDash:\"grid\",gridDashOffset:\"grid\",gridOpacity:\"grid\",gridScale:\"grid\",gridWidth:\"grid\",orient:\"main\",bandPosition:\"both\",aria:\"main\",description:\"main\",domain:\"main\",domainCap:\"main\",domainColor:\"main\",domainDash:\"main\",domainDashOffset:\"main\",domainOpacity:\"main\",domainWidth:\"main\",format:\"main\",formatType:\"main\",labelAlign:\"main\",labelAngle:\"main\",labelBaseline:\"main\",labelBound:\"main\",labelColor:\"main\",labelFlush:\"main\",labelFlushOffset:\"main\",labelFont:\"main\",labelFontSize:\"main\""
-  , ",labelFontStyle:\"main\",labelFontWeight:\"main\",labelLimit:\"main\",labelLineHeight:\"main\",labelOffset:\"main\",labelOpacity:\"main\",labelOverlap:\"main\",labelPadding:\"main\",labels:\"main\",labelSeparation:\"main\",maxExtent:\"main\",minExtent:\"main\",offset:\"both\",position:\"main\",tickCap:\"main\",tickColor:\"main\",tickDash:\"main\",tickDashOffset:\"main\",tickMinStep:\"both\",tickOffset:\"both\",tickOpacity:\"main\",tickRound:\"both\",ticks:\"main\",tickSize:\"main\",tickWidth:\"both\",title:\"main\",titleAlign:\"main\",titleAnchor:\"main\",titleAngle:\"main\",titleBaseline:\"main\",titleColor:\"main\",titleFont:\"main\",titleFontSize:\"main\",titleFontStyle:\"main\",titleFontWeight:\"main\",titleLimit:\"main\",titleLineHeight:\"main\",titleOpacity:\"main\",titlePadding:\"main\",titleX:\"main\",titleY:\"main\",encode:\"both\",scale:\"both\",tickBand:\"both\",tickCount:\"both\",tickExtra:\"both\",translate:\"both\",values:\"both\",zindex:\"both\"},Ma={orient:1,aria:1,bandPosition:1,description:1,domain:1,domainCap:1,domainColor:1,domainDash:1,domainDashOffset:1,domainOpacity:1,domainWidth:1,format:1,formatType:1,grid:1,gridCap:1,gridColor:1,gridDash:1,gridDashOffset:1,gridOpacity:1,gridWidth:1,labelAlign:1,labelAngle:1,labelBaseline:1,labelBound:1,labelColor:1,labelFlush:1,labelFlushOffset:1,labelFont:1,labelFontSize:1,labelFontStyle:1,labelFontWeight:1,labelLimit:1,labelLineHeight:1,labelOffset:1,labelOpacity:1,labelOverlap:1,labelPadding:1,labels:1,labelSeparation:1,maxExtent:1,minExtent:1,offset:1,position:1,tickBand:1,tickCap:1,tickColor:1,tickCount:1,tickDash:1,tickDashOffset:1,tickExtra:1,tickMinStep:1,tickOffset:1,tickOpacity:1,tickRound:1,ticks:1,tickSize:1,tickWidth:1,title:1,titleAlign:1,titleAnchor:1,titleAngle:1,titleBaseline:1,titleColor:1,titleFont:1,titleFontSize:1,titleFontStyle:1,titleFontWeight:1,titleLimit:1,titleLineHeight:1,titleOpacity:1,titlePadding:1,titleX:1,titleY:1,translate:1,values:1,zindex:1},Ra={...Ma,style:1,labelExpr:1,encoding:1};function La(e){return t.hasOwnProperty(Ra,e)}const qa=D({axis:1,axisBand:1,axisBottom:1,axisDiscrete:1,axisLeft:1,axisPoint:1,axisQuantitative:1,axisRight:1,axisTemporal:1,axisTop:1,axisX:1,axisXBand:1,axisXDiscrete:1,axisXPoint:1,axisXQuantitative:1,axisXTemporal:1,axisY:1,axisYBand:1,axisYDiscrete:1,axisYPoint:1,axisYQuantitative:1,axisYTemporal:1});function Ua(e){return J(e,\"mark\")}class Wa{constructor(e,t){this.name=e,this.run=t}hasMatchingType(e){return!!Ua(e)&&(no(t=e.mark)?t.type:t)===this.name;var t}}function Ia(e,n){const i=e&&e[n];return!!i&&(t.isArray(i)?g(i,(e=>!!e.field)):Zo(i)||Jo(i))}function Ba(e,n){const i=e&&e[n];return!!i&&(t.isArray(i)?g(i,(e=>!!e.field)):Zo(i)||ta(i)||Ko(i))}function Va(e,t){if(_t(t)){const n=e[t];if((Zo(n)||ta(n))&&(ar(n.type)||Zo(n)&&n.timeUnit)){return Ba(e,ct(t))}}return!1}function Ha(e){return g(He,(n=>{if(Ia(e,n)){const i=e[n];if(t.isArray(i))return g(i,(e=>!!e.aggregate));{const e=wa(i);return e&&!!e.aggregate}}return!1}))}function Ga(e,n){const i=[],r=[],o=[],a=[],s={};return Qa(e,((l,c)=>{if(Zo(l)){const{field:u,aggregate:f,bin:d,timeUnit:m,...p}=l;if(f||m||d){const e=ba(l),g=e?.title;let h=ma(l,{forAs:!0});const y={...g?[]:{title:va(l,n,{allowDisabling:!0})},...p,field:h};if(f){let e;if(an(f)?(e=\"argmax\",h=ma({op:\"argmax\",field:f.argmax},{forAs:!0}),y.field=`${h}.${u}`):on(f)?(e=\"argmin\",h=ma({op:\"argmin\",field:f.argmin},{forAs:!0}),y.field=`${h}.${u}`):\"boxplot\"!==f&&\"errorbar\"!==f&&\"errorband\"!==f&&(e=f),e){const t={op:e,as:h};u&&(t.field=u),a.push(t)}}else if(i.push(h),aa(l)&&mn(d)){if(r.push({bin:d,field:u,as:h}),i.push(ma(l,{binSuffix:\"end\"})),Na(l,c)&&i.push(ma(l,{binSuffix:\"range\"})),_t(c)){const e={field:`${h}_end`};s[`${c}2`]=e}y.bin=\"binned\",it(c)||(y.type=sr)}else if(m&&!Ti(m)){o.push({timeUnit:m,field:u,as:h});const e=aa(l)&&l.type!==cr&&\"time\";e&&(c===Oe||c===Pe?y.formatType=e:!function(e){return t.hasOwnProperty(Ft,e)}(c)?_t(c)&&(y.axis={formatType:e,...y.axis}):y.legend={formatType:e,...y.legend})}s[c]=y}else i.push(u),s[c]=e[c]}else s[c]=e[c]})),{bins:r,timeUnits:o,aggregate:a,groupby:i,encoding:s}}function Ya(e,t,n){const i=Jt(t,n);if(!i)return!1;if(\"binned\"===i){const n=e[t===ie?"
-  , "te:ne];return!!(Zo(n)&&Zo(e[t])&&pn(n.bin))}return!0}function Xa(e,t){const n={};for(const i of D(e)){const r=Sa(e[i],i,t,{compositeMark:!0});n[i]=r}return n}function Qa(e,n,i){if(e)for(const r of D(e)){const o=e[r];if(t.isArray(o))for(const e of o)n.call(i,e,r);else n.call(i,o,r)}}function Ja(e,n){return D(n).reduce(((i,r)=>{switch(r){case te:case ne:case Ne:case Te:case Ae:case ie:case re:case oe:case ae:case ce:case ue:case se:case le:case ge:case fe:case de:case me:case pe:case Oe:case be:case $e:case Pe:return i;case ze:if(\"line\"===e||\"trail\"===e)return i;case Ce:case _e:{const e=n[r];if(t.isArray(e)||Zo(e))for(const n of t.array(e))n.aggregate||i.push(ma(n,{}));return i}case xe:if(\"trail\"===e)return i;case he:case ye:case ve:case we:case ke:case Se:case Fe:case De:{const e=wa(n[r]);return e&&!e.aggregate&&i.push(ma(e,{})),i}}}),[])}function Ka(e,n,i){let r=!(arguments.length>3&&void 0!==arguments[3])||arguments[3];if(\"tooltip\"in i)return{tooltip:i.tooltip};return{tooltip:[...e.map((e=>{let{fieldPrefix:t,titlePrefix:i}=e;const o=r?` of ${Za(n)}`:\"\";return{field:t+n.field,type:n.type,title:wn(i)?{signal:`${i}\"${escape(o)}\"`}:i+o}})),...b(function(e){const n=[];for(const i of D(e))if(Ia(e,i)){const r=e[i],o=t.array(r);for(const e of o)Zo(e)?n.push(e):Jo(e)&&n.push(e.condition)}return n}(i).map(da),d)]}}function Za(e){const{title:t,field:n}=e;return U(t,n)}function es(e,n,i,r,o){const{scale:a,axis:s}=i;return l=>{let{partName:c,mark:u,positionPrefix:f,endPositionPrefix:d,extraEncoding:m={}}=l;const p=Za(i);return ts(e,c,o,{mark:u,encoding:{[n]:{field:`${f}_${i.field}`,type:i.type,...void 0!==p?{title:p}:{},...void 0!==a?{scale:a}:{},...void 0!==s?{axis:s}:{}},...t.isString(d)?{[`${n}2`]:{field:`${d}_${i.field}`}}:{},...r,...m}})}}function ts(e,n,i,r){const{clip:o,color:a,opacity:s}=e,l=e.type;return e[n]||void 0===e[n]&&i[n]?[{...r,mark:{...i[n],...o?{clip:o}:{},...a?{color:a}:{},...s?{opacity:s}:{},...no(r.mark)?r.mark:{type:r.mark},style:`${l}-${String(n)}`,...t.isBoolean(e[n])?{}:e[n]}}]:[]}function ns(e,t,n){const{encoding:i}=e,r=\"vertical\"===t?\"y\":\"x\",o=i[r],a=i[`${r}2`],s=i[`${r}Error`],l=i[`${r}Error2`];return{continuousAxisChannelDef:is(o,n),continuousAxisChannelDef2:is(a,n),continuousAxisChannelDefError:is(s,n),continuousAxisChannelDefError2:is(l,n),continuousAxis:r}}function is(e,t){if(e?.aggregate){const{aggregate:n,...i}=e;return n!==t&&Si(function(e,t){return`Continuous axis should not have customized aggregation function ${e}; ${t} already agregates the axis.`}(n,t)),i}return e}function rs(e,t){const{mark:n,encoding:i}=e,{x:r,y:o}=i;if(no(n)&&n.orient)return n.orient;if(na(r)){if(na(o)){const e=Zo(r)&&r.aggregate,n=Zo(o)&&o.aggregate;if(e||n!==t){if(n||e!==t){if(e===t&&n===t)throw new Error(\"Both x and y cannot have aggregate\");return Ca(o)&&!Ca(r)?\"horizontal\":\"vertical\"}return\"horizontal\"}return\"vertical\"}return\"horizontal\"}if(na(o))return\"vertical\";throw new Error(`Need a valid continuous axis for ${t}s`)}const os=\"boxplot\",as=new Wa(os,ls);function ss(e){return t.isNumber(e)?\"tukey\":e}function ls(e,n){let{config:i}=n;e={...e,encoding:Xa(e.encoding,i)};const{mark:r,encoding:o,params:a,projection:s,...l}=e,c=no(r)?r:{type:r};a&&Si(Kn(\"boxplot\"));const u=c.extent??i.boxplot.extent,d=En(\"size\",c,i),m=c.invalid,p=ss(u),{bins:g,timeUnits:h,transform:y,continuousAxisChannelDef:v,continuousAxis:b,groupby:x,aggregate:$,encodingWithoutContinuousAxis:w,ticksOrient:k,boxOrient:D,customTooltipWithoutAggregatedField:F}=function(e,n,i){const r=rs(e,os),{continuousAxisChannelDef:o,continuousAxis:a}=ns(e,r,os),s=o.field,l=L(s),c=ss(n),u=[...cs(s),{op:\"median\",field:s,as:`mid_box_${l}`},{op:\"min\",field:s,as:(\"min-max\"===c?\"lower_whisker_\":\"min_\")+l},{op:\"max\",field:s,as:(\"min-max\"===c?\"upper_whisker_\":\"max_\")+l}],f=\"min-max\"===c||\"tukey\"===c?[]:[{calculate:`${j(`upper_box_${l}`)} - ${j(`lower_box_${l}`)}`,as:`iqr_${l}`},{calculate:`min(${j(`upper_box_${l}`)} + ${j(`iqr_${l}`)} * ${n}, ${j(`max_${l}`)})`,as:`upper_whisker_${l}`},{calculate:`max(${j(`lower_box_${l}`)} - ${j(`iqr_${l}`)} * ${n}, ${j(`min_${l}`)})`,as:`low"
-  , "er_whisker_${l}`}],{[a]:d,...m}=e.encoding,{customTooltipWithoutAggregatedField:p,filteredEncoding:g}=function(e){const{tooltip:n,...i}=e;if(!n)return{filteredEncoding:i};let r,o;if(t.isArray(n)){for(const e of n)e.aggregate?(r||(r=[]),r.push(e)):(o||(o=[]),o.push(e));r&&(i.tooltip=r)}else n.aggregate?i.tooltip=n:o=n;return t.isArray(o)&&1===o.length&&(o=o[0]),{customTooltipWithoutAggregatedField:o,filteredEncoding:i}}(m),{bins:h,timeUnits:y,aggregate:v,groupby:b,encoding:x}=Ga(g,i),$=\"vertical\"===r?\"horizontal\":\"vertical\",w=r,k=[...h,...y,{aggregate:[...v,...u],groupby:b},...f];return{bins:h,timeUnits:y,transform:k,groupby:b,aggregate:v,continuousAxisChannelDef:o,continuousAxis:a,encodingWithoutContinuousAxis:x,ticksOrient:$,boxOrient:w,customTooltipWithoutAggregatedField:p}}(e,u,i),O=L(v.field),{color:z,size:C,..._}=w,P=e=>es(c,b,v,e,i.boxplot),N=P(_),A=P(w),T=(t.isObject(i.boxplot.box)?i.boxplot.box.color:i.mark.color)||\"#4c78a8\",E=P({..._,...C?{size:C}:{},color:{condition:{test:`${j(`lower_box_${v.field}`)} >= ${j(`upper_box_${v.field}`)}`,...z||{value:T}}}}),M=Ka([{fieldPrefix:\"min-max\"===p?\"upper_whisker_\":\"max_\",titlePrefix:\"Max\"},{fieldPrefix:\"upper_box_\",titlePrefix:\"Q3\"},{fieldPrefix:\"mid_box_\",titlePrefix:\"Median\"},{fieldPrefix:\"lower_box_\",titlePrefix:\"Q1\"},{fieldPrefix:\"min-max\"===p?\"lower_whisker_\":\"min_\",titlePrefix:\"Min\"}],v,w),R={type:\"tick\",color:\"black\",opacity:1,orient:k,invalid:m,aria:!1},q=\"min-max\"===p?M:Ka([{fieldPrefix:\"upper_whisker_\",titlePrefix:\"Upper Whisker\"},{fieldPrefix:\"lower_whisker_\",titlePrefix:\"Lower Whisker\"}],v,w),U=[...N({partName:\"rule\",mark:{type:\"rule\",invalid:m,aria:!1},positionPrefix:\"lower_whisker\",endPositionPrefix:\"lower_box\",extraEncoding:q}),...N({partName:\"rule\",mark:{type:\"rule\",invalid:m,aria:!1},positionPrefix:\"upper_box\",endPositionPrefix:\"upper_whisker\",extraEncoding:q}),...N({partName:\"ticks\",mark:R,positionPrefix:\"lower_whisker\",extraEncoding:q}),...N({partName:\"ticks\",mark:R,positionPrefix:\"upper_whisker\",extraEncoding:q})],W=[...\"tukey\"!==p?U:[],...A({partName:\"box\",mark:{type:\"bar\",...d?{size:d}:{},orient:D,invalid:m,ariaRoleDescription:\"box\"},positionPrefix:\"lower_box\",endPositionPrefix:\"upper_box\",extraEncoding:M}),...E({partName:\"median\",mark:{type:\"tick\",invalid:m,...t.isObject(i.boxplot.median)&&i.boxplot.median.color?{color:i.boxplot.median.color}:{},...d?{size:d}:{},orient:k,aria:!1},positionPrefix:\"mid_box\",extraEncoding:M})];if(\"min-max\"===p)return{...l,transform:(l.transform??[]).concat(y),layer:W};const I=j(`lower_box_${v.field}`),B=j(`upper_box_${v.field}`),V=`(${B} - ${I})`,H=`${I} - ${u} * ${V}`,G=`${B} + ${u} * ${V}`,Y=j(v.field),X={joinaggregate:cs(v.field),groupby:x},Q={transform:[{filter:`(${H} <= ${Y}) && (${Y} <= ${G})`},{aggregate:[{op:\"min\",field:v.field,as:`lower_whisker_${O}`},{op:\"max\",field:v.field,as:`upper_whisker_${O}`},{op:\"min\",field:`lower_box_${v.field}`,as:`lower_box_${O}`},{op:\"max\",field:`upper_box_${v.field}`,as:`upper_box_${O}`},...$],groupby:x}],layer:U},{tooltip:J,...K}=_,{scale:Z,axis:ee}=v,te=Za(v),ne=f(ee,[\"title\"]),ie=ts(c,\"outliers\",i.boxplot,{transform:[{filter:`(${Y} < ${H}) || (${Y} > ${G})`}],mark:\"point\",encoding:{[b]:{field:v.field,type:v.type,...void 0!==te?{title:te}:{},...void 0!==Z?{scale:Z}:{},...S(ne)?{}:{axis:ne}},...K,...z?{color:z}:{},...F?{tooltip:F}:{}}})[0];let re;const oe=[...g,...h,X];return ie?re={transform:oe,layer:[ie,Q]}:(re=Q,re.transform.unshift(...oe)),{...l,layer:[re,{transform:y,layer:W}]}}function cs(e){const t=L(e);return[{op:\"q1\",field:e,as:`lower_box_${t}`},{op:\"q3\",field:e,as:`upper_box_${t}`}]}const us=\"errorbar\",fs=new Wa(us,ds);function ds(e,t){let{config:n}=t;e={...e,encoding:Xa(e.encoding,n)};const{transform:i,continuousAxisChannelDef:r,continuousAxis:o,encodingWithoutContinuousAxis:a,ticksOrient:s,markDef:l,outerSpec:c,tooltipEncoding:u}=ps(e,us,n);delete a.size;const f=es(l,o,r,a,n.errorbar),d=l.thickness,m=l.size,p={type:\"tick\",orient:s,aria:!1,...void 0!==d?{thickness:d}:{},...void 0!==m?{size:m}:{}},g=[...f({partName:\"ticks\",mark:p,positionPrefix:\"lower\",extraEncoding:u}),."
-  , "..f({partName:\"ticks\",mark:p,positionPrefix:\"upper\",extraEncoding:u}),...f({partName:\"rule\",mark:{type:\"rule\",ariaRoleDescription:\"errorbar\",...void 0!==d?{size:d}:{}},positionPrefix:\"lower\",endPositionPrefix:\"upper\",extraEncoding:u})];return{...c,transform:i,...g.length>1?{layer:g}:{...g[0]}}}function ms(e,t){const{encoding:n}=e;if(function(e){return(oa(e.x)||oa(e.y))&&!oa(e.x2)&&!oa(e.y2)&&!oa(e.xError)&&!oa(e.xError2)&&!oa(e.yError)&&!oa(e.yError2)}(n))return{orient:rs(e,t),inputType:\"raw\"};const i=function(e){return oa(e.x2)||oa(e.y2)}(n),r=function(e){return oa(e.xError)||oa(e.xError2)||oa(e.yError)||oa(e.yError2)}(n),o=n.x,a=n.y;if(i){if(r)throw new Error(`${t} cannot be both type aggregated-upper-lower and aggregated-error`);const e=n.x2,i=n.y2;if(oa(e)&&oa(i))throw new Error(`${t} cannot have both x2 and y2`);if(oa(e)){if(na(o))return{orient:\"horizontal\",inputType:\"aggregated-upper-lower\"};throw new Error(`Both x and x2 have to be quantitative in ${t}`)}if(oa(i)){if(na(a))return{orient:\"vertical\",inputType:\"aggregated-upper-lower\"};throw new Error(`Both y and y2 have to be quantitative in ${t}`)}throw new Error(\"No ranged axis\")}{const e=n.xError,i=n.xError2,r=n.yError,s=n.yError2;if(oa(i)&&!oa(e))throw new Error(`${t} cannot have xError2 without xError`);if(oa(s)&&!oa(r))throw new Error(`${t} cannot have yError2 without yError`);if(oa(e)&&oa(r))throw new Error(`${t} cannot have both xError and yError with both are quantiative`);if(oa(e)){if(na(o))return{orient:\"horizontal\",inputType:\"aggregated-error\"};throw new Error(\"All x, xError, and xError2 (if exist) have to be quantitative\")}if(oa(r)){if(na(a))return{orient:\"vertical\",inputType:\"aggregated-error\"};throw new Error(\"All y, yError, and yError2 (if exist) have to be quantitative\")}throw new Error(\"No ranged axis\")}}function ps(e,t,n){const{mark:i,encoding:r,params:o,projection:a,...s}=e,l=no(i)?i:{type:i};o&&Si(Kn(t));const{orient:c,inputType:u}=ms(e,t),{continuousAxisChannelDef:f,continuousAxisChannelDef2:d,continuousAxisChannelDefError:m,continuousAxisChannelDefError2:p,continuousAxis:g}=ns(e,c,t),{errorBarSpecificAggregate:h,postAggregateCalculates:y,tooltipSummary:v,tooltipTitleWithFieldName:b}=function(e,t,n,i,r,o,a,s){let l=[],c=[];const u=t.field;let f,d=!1;if(\"raw\"===o){const t=e.center?e.center:e.extent?\"iqr\"===e.extent?\"median\":\"mean\":s.errorbar.center,n=e.extent?e.extent:\"mean\"===t?\"stderr\":\"iqr\";if(\"median\"===t!=(\"iqr\"===n)&&Si(function(e,t,n){return`${e} is not usually used with ${t} for ${n}.`}(t,n,a)),\"stderr\"===n||\"stdev\"===n)l=[{op:n,field:u,as:`extent_${u}`},{op:t,field:u,as:`center_${u}`}],c=[{calculate:`${j(`center_${u}`)} + ${j(`extent_${u}`)}`,as:`upper_${u}`},{calculate:`${j(`center_${u}`)} - ${j(`extent_${u}`)}`,as:`lower_${u}`}],f=[{fieldPrefix:\"center_\",titlePrefix:N(t)},{fieldPrefix:\"upper_\",titlePrefix:gs(t,n,\"+\")},{fieldPrefix:\"lower_\",titlePrefix:gs(t,n,\"-\")}],d=!0;else{let e,t,i;\"ci\"===n?(e=\"mean\",t=\"ci0\",i=\"ci1\"):(e=\"median\",t=\"q1\",i=\"q3\"),l=[{op:t,field:u,as:`lower_${u}`},{op:i,field:u,as:`upper_${u}`},{op:e,field:u,as:`center_${u}`}],f=[{fieldPrefix:\"upper_\",titlePrefix:va({field:u,aggregate:i,type:\"quantitative\"},s,{allowDisabling:!1})},{fieldPrefix:\"lower_\",titlePrefix:va({field:u,aggregate:t,type:\"quantitative\"},s,{allowDisabling:!1})},{fieldPrefix:\"center_\",titlePrefix:va({field:u,aggregate:e,type:\"quantitative\"},s,{allowDisabling:!1})}]}}else{(e.center||e.extent)&&Si((m=e.center,`${(p=e.extent)?\"extent \":\"\"}${p&&m?\"and \":\"\"}${m?\"center \":\"\"}${p&&m?\"are \":\"is \"}not needed when data are aggregated.`)),\"aggregated-upper-lower\"===o?(f=[],c=[{calculate:j(n.field),as:`upper_${u}`},{calculate:j(u),as:`lower_${u}`}]):\"aggregated-error\"===o&&(f=[{fieldPrefix:\"\",titlePrefix:u}],c=[{calculate:`${j(u)} + ${j(i.field)}`,as:`upper_${u}`}],r?c.push({calculate:`${j(u)} + ${j(r.field)}`,as:`lower_${u}`}):c.push({calculate:`${j(u)} - ${j(i.field)}`,as:`lower_${u}`}));for(const e of c)f.push({fieldPrefix:e.as.substring(0,6),titlePrefix:R(R(e.calculate,\"datum['\",\"\"),\"']\",\"\")})}var m,p;return{postAggregateCalculates:c,errorBarSpecificAggrega"
-  , "te:l,tooltipSummary:f,tooltipTitleWithFieldName:d}}(l,f,d,m,p,u,t,n),{[g]:x,[\"x\"===g?\"x2\":\"y2\"]:$,[\"x\"===g?\"xError\":\"yError\"]:w,[\"x\"===g?\"xError2\":\"yError2\"]:k,...S}=r,{bins:D,timeUnits:F,aggregate:O,groupby:z,encoding:C}=Ga(S,n),_=[...O,...h],P=\"raw\"!==u?[]:z,A=Ka(v,f,C,b);return{transform:[...s.transform??[],...D,...F,...0===_.length?[]:[{aggregate:_,groupby:P}],...y],groupby:P,continuousAxisChannelDef:f,continuousAxis:g,encodingWithoutContinuousAxis:C,ticksOrient:\"vertical\"===c?\"horizontal\":\"vertical\",markDef:l,outerSpec:s,tooltipEncoding:A}}function gs(e,t,n){return`${N(e)} ${n} ${t}`}const hs=\"errorband\",ys=new Wa(hs,vs);function vs(e,t){let{config:n}=t;e={...e,encoding:Xa(e.encoding,n)};const{transform:i,continuousAxisChannelDef:r,continuousAxis:o,encodingWithoutContinuousAxis:a,markDef:s,outerSpec:l,tooltipEncoding:c}=ps(e,hs,n),u=s,f=es(u,o,r,a,n.errorband),d=void 0!==e.encoding.x&&void 0!==e.encoding.y;let m={type:d?\"area\":\"rect\"},p={type:d?\"line\":\"rule\"};const g={...u.interpolate?{interpolate:u.interpolate}:{},...u.tension&&u.interpolate?{tension:u.tension}:{}};return d?(m={...m,...g,ariaRoleDescription:\"errorband\"},p={...p,...g,aria:!1}):u.interpolate?Si(bi(\"interpolate\")):u.tension&&Si(bi(\"tension\")),{...l,transform:i,layer:[...f({partName:\"band\",mark:m,positionPrefix:\"lower\",endPositionPrefix:\"upper\",extraEncoding:c}),...f({partName:\"borders\",mark:p,positionPrefix:\"lower\",extraEncoding:c}),...f({partName:\"borders\",mark:p,positionPrefix:\"upper\",extraEncoding:c})]}}const bs={};function xs(e,t,n){const i=new Wa(e,t);bs[e]={normalizer:i,parts:n}}xs(os,ls,[\"box\",\"median\",\"outliers\",\"rule\",\"ticks\"]),xs(us,ds,[\"ticks\",\"rule\"]),xs(hs,vs,[\"band\",\"borders\"]);const $s=[\"gradientHorizontalMaxLength\",\"gradientHorizontalMinLength\",\"gradientVerticalMaxLength\",\"gradientVerticalMinLength\",\"unselectedOpacity\"],ws={titleAlign:\"align\",titleAnchor:\"anchor\",titleAngle:\"angle\",titleBaseline:\"baseline\",titleColor:\"color\",titleFont:\"font\",titleFontSize:\"fontSize\",titleFontStyle:\"fontStyle\",titleFontWeight:\"fontWeight\",titleLimit:\"limit\",titleLineHeight:\"lineHeight\",titleOrient:\"orient\",titlePadding:\"offset\"},ks={labelAlign:\"align\",labelAnchor:\"anchor\",labelAngle:\"angle\",labelBaseline:\"baseline\",labelColor:\"color\",labelFont:\"font\",labelFontSize:\"fontSize\",labelFontStyle:\"fontStyle\",labelFontWeight:\"fontWeight\",labelLimit:\"limit\",labelLineHeight:\"lineHeight\",labelOrient:\"orient\",labelPadding:\"offset\"},Ss=D(ws),Ds=D(ks),Fs=D({header:1,headerRow:1,headerColumn:1,headerFacet:1}),Os=[\"size\",\"shape\",\"fill\",\"stroke\",\"strokeDash\",\"strokeWidth\",\"opacity\"],zs=\"_vgsid_\",Cs={point:{on:\"click\",fields:[zs],toggle:\"event.shiftKey\",resolve:\"global\",clear:\"dblclick\"},interval:{on:\"[pointerdown, window:pointerup] > window:pointermove!\",encodings:[\"x\",\"y\"],translate:\"[pointerdown, window:pointerup] > window:pointermove!\",zoom:\"wheel!\",mark:{fill:\"#333\",fillOpacity:.125,stroke:\"white\"},resolve:\"global\",clear:\"dblclick\"}};function _s(e){return\"legend\"===e||!!e?.legend}function Ps(e){return _s(e)&&t.isObject(e)}function Ns(e){return!!e?.select}function As(e){const t=[];for(const n of e||[]){if(Ns(n))continue;const{expr:e,bind:i,...r}=n;if(i&&e){const n={...r,bind:i,init:e};t.push(n)}else{const n={...r,...e?{update:e}:{},...i?{bind:i}:{}};t.push(n)}}return t}function Ts(e){return J(e,\"concat\")}function js(e){return J(e,\"vconcat\")}function Es(e){return J(e,\"hconcat\")}function Ms(e){let{step:t,offsetIsDiscrete:n}=e;return n?t.for??\"offset\":\"position\"}function Rs(e){return J(e,\"step\")}function Ls(e){return J(e,\"view\")||J(e,\"width\")||J(e,\"height\")}const qs=D({align:1,bounds:1,center:1,columns:1,spacing:1});function Us(e,t){return e[t]??e[\"width\"===t?\"continuousWidth\":\"continuousHeight\"]}function Ws(e,t){const n=Is(e,t);return Rs(n)?n.step:Bs}function Is(e,t){return U(e[t]??e[\"width\"===t?\"discreteWidth\":\"discreteHeight\"],{step:e.step})}const Bs=20,Vs={background:\"white\",padding:5,timeFormat:\"%b %d, %Y\",countTitle:\"Count of Records\",view:{continuousWidth:200,continuousHeight:200,step:Bs},mark:{color:\"#4c78a8\",invalid:\"break-paths-show-path-domains\",timeUnitBandS"
-  , "ize:1},arc:{},area:{},bar:fo,circle:{},geoshape:{},image:{},line:{},point:{},rect:uo,rule:{color:\"black\"},square:{},text:{color:\"black\"},tick:mo,trail:{},boxplot:{size:14,extent:1.5,box:{},median:{color:\"white\"},outliers:{},rule:{},ticks:null},errorbar:{center:\"mean\",rule:!0,ticks:!1},errorband:{band:{opacity:.3},borders:!1},scale:{pointPadding:.5,barBandPaddingInner:.1,rectBandPaddingInner:0,tickBandPaddingInner:.25,bandWithNestedOffsetPaddingInner:.2,bandWithNestedOffsetPaddingOuter:.2,minBandSize:2,minFontSize:8,maxFontSize:40,minOpacity:.3,maxOpacity:.8,minSize:4,minStrokeWidth:1,maxStrokeWidth:4,quantileCount:4,quantizeCount:4,zero:!0,framesPerSecond:2,animationDuration:5},projection:{},legend:{gradientHorizontalMaxLength:200,gradientHorizontalMinLength:100,gradientVerticalMaxLength:200,gradientVerticalMinLength:64,unselectedOpacity:.35},header:{titlePadding:10,labelPadding:10},headerColumn:{},headerRow:{},headerFacet:{},selection:Cs,style:{},title:{},facet:{spacing:20},concat:{spacing:20},normalizedNumberFormat:\".0%\"},Hs=[\"#4c78a8\",\"#f58518\",\"#e45756\",\"#72b7b2\",\"#54a24b\",\"#eeca3b\",\"#b279a2\",\"#ff9da6\",\"#9d755d\",\"#bab0ac\"],Gs={text:11,guideLabel:10,guideTitle:11,groupTitle:13,groupSubtitle:12},Ys={blue:Hs[0],orange:Hs[1],red:Hs[2],teal:Hs[3],green:Hs[4],yellow:Hs[5],purple:Hs[6],pink:Hs[7],brown:Hs[8],gray0:\"#000\",gray1:\"#111\",gray2:\"#222\",gray3:\"#333\",gray4:\"#444\",gray5:\"#555\",gray6:\"#666\",gray7:\"#777\",gray8:\"#888\",gray9:\"#999\",gray10:\"#aaa\",gray11:\"#bbb\",gray12:\"#ccc\",gray13:\"#ddd\",gray14:\"#eee\",gray15:\"#fff\"};function Xs(e){const t=D(e||{}),n={};for(const i of t){const t=e[i];n[i]=Ta(t)?zn(t):Cn(t)}return n}const Qs=[...so,...qa,...Fs,\"background\",\"padding\",\"legend\",\"lineBreak\",\"scale\",\"style\",\"title\",\"view\"];function Js(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const{color:n,font:i,fontSize:r,selection:o,...a}=e,s=t.mergeConfig({},l(Vs),i?function(e){return{text:{font:e},style:{\"guide-label\":{font:e},\"guide-title\":{font:e},\"group-title\":{font:e},\"group-subtitle\":{font:e}}}}(i):{},n?function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return{signals:[{name:\"color\",value:t.isObject(e)?{...Ys,...e}:Ys}],mark:{color:{signal:\"color.blue\"}},rule:{color:{signal:\"color.gray0\"}},text:{color:{signal:\"color.gray0\"}},style:{\"guide-label\":{fill:{signal:\"color.gray0\"}},\"guide-title\":{fill:{signal:\"color.gray0\"}},\"group-title\":{fill:{signal:\"color.gray0\"}},\"group-subtitle\":{fill:{signal:\"color.gray0\"}},cell:{stroke:{signal:\"color.gray8\"}}},axis:{domainColor:{signal:\"color.gray13\"},gridColor:{signal:\"color.gray8\"},tickColor:{signal:\"color.gray13\"}},range:{category:[{signal:\"color.blue\"},{signal:\"color.orange\"},{signal:\"color.red\"},{signal:\"color.teal\"},{signal:\"color.green\"},{signal:\"color.yellow\"},{signal:\"color.purple\"},{signal:\"color.pink\"},{signal:\"color.brown\"},{signal:\"color.grey8\"}]}}}(n):{},r?function(e){return{signals:[{name:\"fontSize\",value:t.isObject(e)?{...Gs,...e}:Gs}],text:{fontSize:{signal:\"fontSize.text\"}},style:{\"guide-label\":{fontSize:{signal:\"fontSize.guideLabel\"}},\"guide-title\":{fontSize:{signal:\"fontSize.guideTitle\"}},\"group-title\":{fontSize:{signal:\"fontSize.groupTitle\"}},\"group-subtitle\":{fontSize:{signal:\"fontSize.groupSubtitle\"}}}}}(r):{},a||{});o&&t.writeConfig(s,\"selection\",o,!0);const c=f(s,Qs);for(const e of[\"background\",\"lineBreak\",\"padding\"])s[e]&&(c[e]=Cn(s[e]));for(const e of so)s[e]&&(c[e]=bn(s[e]));for(const e of qa)s[e]&&(c[e]=Xs(s[e]));for(const e of Fs)s[e]&&(c[e]=bn(s[e]));if(s.legend&&(c.legend=bn(s.legend)),s.scale){const{invalid:e,...t}=s.scale,n=bn(e,{level:1});c.scale={...bn(t),...D(n).length>0?{invalid:n}:{}}}return s.style&&(c.style=function(e){const t=D(e),n={};for(const i of t)n[i]=Xs(e[i]);return n}(s.style)),s.title&&(c.title=bn(s.title)),s.view&&(c.view=bn(s.view)),c}const Ks=new Set([\"view\",...to]),Zs=[\"color\",\"fontSize\",\"background\",\"padding\",\"facet\",\"concat\",\"numberFormat\",\"numberFormatType\",\"normalizedNumberFormat\",\"normalizedNumberFormatType\",\"timeFormat\",\"countTitle\",\"header\",\"axisQuantitative\",\"axisTemporal\",\"axisDiscrete\",\"ax"
-  , "isPoint\",\"axisXBand\",\"axisXPoint\",\"axisXDiscrete\",\"axisXQuantitative\",\"axisXTemporal\",\"axisYBand\",\"axisYPoint\",\"axisYDiscrete\",\"axisYQuantitative\",\"axisYTemporal\",\"scale\",\"selection\",\"overlay\"],el={view:[\"continuousWidth\",\"continuousHeight\",\"discreteWidth\",\"discreteHeight\",\"step\"],...ao};function tl(e){e=l(e);for(const t of Zs)delete e[t];if(e.axis)for(const t in e.axis)Ta(e.axis[t])&&delete e.axis[t];if(e.legend)for(const t of $s)delete e.legend[t];if(e.mark){for(const t of ro)delete e.mark[t];e.mark.tooltip&&t.isObject(e.mark.tooltip)&&delete e.mark.tooltip}e.params&&(e.signals=(e.signals||[]).concat(As(e.params)),delete e.params);for(const t of Ks){for(const n of ro)delete e[t][n];const n=el[t];if(n)for(const i of n)delete e[t][i];nl(e,t)}for(const t of D(bs))delete e[t];!function(e){const{titleMarkConfig:t,subtitleMarkConfig:n,subtitle:i}=xn(e.title);S(t)||(e.style[\"group-title\"]={...e.style[\"group-title\"],...t});S(n)||(e.style[\"group-subtitle\"]={...e.style[\"group-subtitle\"],...n});S(i)?delete e.title:e.title=i}(e);for(const n in e)t.isObject(e[n])&&S(e[n])&&delete e[n];return S(e)?void 0:e}function nl(e,t,n,i){\"view\"===t&&(n=\"cell\");const r={...e[t],...e.style[n??t]};S(r)||(e.style[n??t]=r),delete e[t]}function il(e){return J(e,\"layer\")}class rl{map(e,t){return Io(e)?this.mapFacet(e,t):function(e){return J(e,\"repeat\")}(e)?this.mapRepeat(e,t):Es(e)?this.mapHConcat(e,t):js(e)?this.mapVConcat(e,t):Ts(e)?this.mapConcat(e,t):this.mapLayerOrUnit(e,t)}mapLayerOrUnit(e,t){if(il(e))return this.mapLayer(e,t);if(Ua(e))return this.mapUnit(e,t);throw new Error(Bn(e))}mapLayer(e,t){return{...e,layer:e.layer.map((e=>this.mapLayerOrUnit(e,t)))}}mapHConcat(e,t){return{...e,hconcat:e.hconcat.map((e=>this.map(e,t)))}}mapVConcat(e,t){return{...e,vconcat:e.vconcat.map((e=>this.map(e,t)))}}mapConcat(e,t){const{concat:n,...i}=e;return{...i,concat:n.map((e=>this.map(e,t)))}}mapFacet(e,t){return{...e,spec:this.map(e.spec,t)}}mapRepeat(e,t){return{...e,spec:this.map(e.spec,t)}}}const ol={zero:1,center:1,normalize:1};const al=new Set([Lr,Ur,qr,Hr,Br,Qr,Jr,Ir,Gr,Yr]),sl=new Set([Ur,qr,Lr]);function ll(e){return Zo(e)&&\"quantitative\"===ea(e)&&!e.bin}function cl(e,t,n){let{orient:i,type:r}=n;const o=\"x\"===t?\"y\":\"radius\",a=\"x\"===t&&[\"bar\",\"area\"].includes(r),s=e[t],l=e[o];if(Zo(s)&&Zo(l))if(ll(s)&&ll(l)){if(s.stack)return t;if(l.stack)return o;const e=Zo(s)&&!!s.aggregate;if(e!==(Zo(l)&&!!l.aggregate))return e?t:o;if(a){if(\"vertical\"===i)return o;if(\"horizontal\"===i)return t}}else{if(ll(s))return t;if(ll(l))return o}else{if(ll(s)){if(a&&\"vertical\"===i)return;return t}if(ll(l)){if(a&&\"horizontal\"===i)return;return o}}}function ul(e,n){const i=no(e)?e:{type:e},r=i.type;if(!al.has(r))return null;const o=cl(n,\"x\",i)||cl(n,\"theta\",i);if(!o)return null;const a=n[o],s=Zo(a)?ma(a,{}):void 0,l=function(e){switch(e){case\"x\":return\"y\";case\"y\":return\"x\";case\"theta\":return\"radius\";case\"radius\":return\"theta\"}}(o),c=[],u=new Set;if(n[l]){const e=n[l],t=Zo(e)?ma(e,{}):void 0;t&&t!==s&&(c.push(l),u.add(t))}const f=\"x\"===l?\"xOffset\":\"yOffset\",d=n[f],m=Zo(d)?ma(d,{}):void 0;m&&m!==s&&(c.push(f),u.add(m));const p=Ot.reduce(((e,i)=>{if(\"tooltip\"!==i&&Ia(n,i)){const r=n[i];for(const n of t.array(r)){const t=wa(n);if(t.aggregate)continue;const r=ma(t,{});r&&u.has(r)||e.push({channel:i,fieldDef:t})}}return e}),[]);let g;return void 0!==a.stack?g=t.isBoolean(a.stack)?a.stack?\"zero\":null:a.stack:sl.has(r)&&(g=\"zero\"),g&&(h=g,t.hasOwnProperty(ol,h))?Ha(n)&&0===p.length?null:(a?.scale?.type&&a?.scale?.type!==dr.LINEAR&&a?.stack&&Si(function(e){return`Stack is applied to a non-linear scale (${e}).`}(a.scale.type)),oa(n[at(o)])?(void 0!==a.stack&&Si(`Cannot stack \"${y=o}\" if there is already \"${y}2\".`),null):(Zo(a)&&a.aggregate&&!un.has(a.aggregate)&&Si(`Stacking is applied even though the aggregate function is non-summative (\"${a.aggregate}\").`),{groupbyChannels:c,groupbyFields:u,fieldChannel:o,impute:null!==a.impute&&Zr(r),stackBy:p,offset:g})):null;var h,y}function fl(e,t,n){const i=bn(e),r=En(\"orient\",i,n);if(i.orient=function(e,t,n){switch(e){case Br:case Qr:case Jr:case"
-  , " Gr:case Vr:case Wr:return}const{x:i,y:r,x2:o,y2:a}=t;switch(e){case Ur:if(Zo(i)&&(pn(i.bin)||Zo(r)&&r.aggregate&&!i.aggregate))return\"vertical\";if(Zo(r)&&(pn(r.bin)||Zo(i)&&i.aggregate&&!r.aggregate))return\"horizontal\";if(a||o){if(n)return n;if(!o)return(Zo(i)&&i.type===sr&&!mn(i.bin)||ra(i))&&Zo(r)&&pn(r.bin)?\"horizontal\":\"vertical\";if(!a)return(Zo(r)&&r.type===sr&&!mn(r.bin)||ra(r))&&Zo(i)&&pn(i.bin)?\"vertical\":\"horizontal\"}case Hr:if(o&&(!Zo(i)||!pn(i.bin))&&a&&(!Zo(r)||!pn(r.bin)))return;case qr:if(a)return Zo(r)&&pn(r.bin)?\"horizontal\":\"vertical\";if(o)return Zo(i)&&pn(i.bin)?\"vertical\":\"horizontal\";if(e===Hr){if(i&&!r)return\"vertical\";if(r&&!i)return\"horizontal\"}case Ir:case Yr:{const t=ia(i),o=ia(r);if(n)return n;if(t&&!o)return\"tick\"!==e?\"horizontal\":\"vertical\";if(!t&&o)return\"tick\"!==e?\"vertical\":\"horizontal\";if(t&&o)return\"vertical\";{const e=aa(i)&&i.type===cr,t=aa(r)&&r.type===cr;if(e&&!t)return\"vertical\";if(!e&&t)return\"horizontal\"}return}}return\"vertical\"}(i.type,t,r),void 0!==r&&r!==i.orient&&Si(`Specified orient \"${i.orient}\" overridden with \"${r}\".`),\"bar\"===i.type&&i.orient){const e=En(\"cornerRadiusEnd\",i,n);if(void 0!==e){const n=\"horizontal\"===i.orient&&t.x2||\"vertical\"===i.orient&&t.y2?[\"cornerRadius\"]:co[i.orient];for(const t of n)i[t]=e;void 0!==i.cornerRadiusEnd&&delete i.cornerRadiusEnd}}const o=En(\"opacity\",i,n),a=En(\"fillOpacity\",i,n);void 0===o&&void 0===a&&(i.opacity=function(e,t){if(p([Br,Yr,Qr,Jr],e)&&!Ha(t))return.7;return}(i.type,t));return void 0===En(\"cursor\",i,n)&&(i.cursor=function(e,t,n){if(t.href||e.href||En(\"href\",e,n))return\"pointer\";return e.cursor}(i,t,n)),i}function dl(e){const{point:t,line:n,...i}=e;return D(i).length>1?i:i.type}function ml(e){for(const t of[\"line\",\"area\",\"rule\",\"trail\"])e[t]&&(e={...e,[t]:f(e[t],[\"point\",\"line\"])});return e}function pl(e){let n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=arguments.length>2?arguments[2]:void 0;return\"transparent\"===e.point?{opacity:0}:e.point?t.isObject(e.point)?e.point:{}:void 0!==e.point?null:n.point||i.shape?t.isObject(n.point)?n.point:{}:void 0}function gl(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.line?!0===e.line?{}:e.line:void 0!==e.line?null:t.line?!0===t.line?{}:t.line:void 0}class hl{name=\"path-overlay\";hasMatchingType(e,t){if(Ua(e)){const{mark:n,encoding:i}=e,r=no(n)?n:{type:n};switch(r.type){case\"line\":case\"rule\":case\"trail\":return!!pl(r,t[r.type],i);case\"area\":return!!pl(r,t[r.type],i)||!!gl(r,t[r.type])}}return!1}run(e,t,n){const{config:i}=t,{params:r,projection:o,mark:a,name:s,encoding:l,...c}=e,d=Xa(l,i),m=no(a)?a:{type:a},p=pl(m,i[m.type],d),g=\"area\"===m.type&&gl(m,i[m.type]),h=[{name:s,...r?{params:r}:{},mark:dl({...\"area\"===m.type&&void 0===m.opacity&&void 0===m.fillOpacity?{opacity:.7}:{},...m}),encoding:f(d,[\"shape\"])}],y=ul(fl(m,d,i),d);let v=d;if(y){const{fieldChannel:e,offset:t}=y;v={...d,[e]:{...d[e],...t?{stack:t}:{}}}}return v=f(v,[\"y2\",\"x2\"]),g&&h.push({...o?{projection:o}:{},mark:{type:\"line\",...u(m,[\"clip\",\"interpolate\",\"tension\",\"tooltip\"]),...g},encoding:v}),p&&h.push({...o?{projection:o}:{},mark:{type:\"point\",opacity:1,filled:!0,...u(m,[\"clip\",\"tooltip\"]),...p},encoding:v}),n({...c,layer:h},{...t,config:ml(i)})}}function yl(e,t){return t?Uo(e)?kl(e,t):xl(e,t):e}function vl(e,t){return t?kl(e,t):e}function bl(e,n,i){const r=n[e];return o=r,!t.isString(o)&&J(o,\"repeat\")?r.repeat in i?{...n,[e]:i[r.repeat]}:void Si(function(e){return`Unknown repeated value \"${e}\".`}(r.repeat)):n;var o}function xl(e,t){if(void 0!==(e=bl(\"field\",e,t))){if(null===e)return null;if(Vo(e)&&Lo(e.sort)){const n=bl(\"field\",e.sort,t);e={...e,...n?{sort:n}:{}}}return e}}function $l(e,t){if(Zo(e))return xl(e,t);{const n=bl(\"datum\",e,t);return n===e||n.type||(n.type=\"nominal\"),n}}function wl(e,t){if(!oa(e)){if(Ko(e)){const n=$l(e.condition,t);if(n)return{...e,condition:n};{const{condition:t,...n}=e;return n}}return e}{const n=$l(e,t);if(n)return n;if(Qo(e))return{condition:e.condition}}}function kl(e,n){const i={};for(const r in e)if(J(e,r)){const o=e[r];if(t.isArray(o))i[r]=o.ma"
-  , "p((e=>wl(e,n))).filter((e=>e));else{const e=wl(o,n);void 0!==e&&(i[r]=e)}}return i}class Sl{name=\"RuleForRangedLine\";hasMatchingType(e){if(Ua(e)){const{encoding:t,mark:n}=e;if(\"line\"===n||no(n)&&\"line\"===n.type)for(const e of nt){const n=t[rt(e)];if(t[e]&&(Zo(n)&&!pn(n.bin)||ta(n)))return!0}}return!1}run(e,n,i){const{encoding:r,mark:o}=e;var a,s;return Si((a=!!r.x2,s=!!r.y2,`Line mark is for continuous lines and thus cannot be used with ${a&&s?\"x2 and y2\":a?\"x2\":\"y2\"}. We will use the rule mark (line segments) instead.`)),i({...e,mark:t.isObject(o)?{...o,type:\"rule\"}:\"rule\"},n)}}function Dl(e){let{parentEncoding:n,encoding:i={},layer:r}=e,o={};if(n){const e=new Set([...D(n),...D(i)]);for(const a of e){const e=i[a],s=n[a];if(oa(e)){const t={...s,...e};o[a]=t}else Ko(e)?o[a]={...e,condition:{...s,...e.condition}}:e||null===e?o[a]=e:(r||sa(s)||wn(s)||oa(s)||t.isArray(s))&&(o[a]=s)}}else o=i;return!o||S(o)?void 0:o}function Fl(e){const{parentProjection:t,projection:n}=e;return t&&n&&Si(function(e){const{parentProjection:t,projection:n}=e;return`Layer's shared projection ${Q(t)} is overridden by a child projection ${Q(n)}.`}({parentProjection:t,projection:n})),n??t}function Ol(e){return J(e,\"filter\")}function zl(e){return J(e,\"lookup\")}function Cl(e){return J(e,\"pivot\")}function _l(e){return J(e,\"density\")}function Pl(e){return J(e,\"quantile\")}function Nl(e){return J(e,\"regression\")}function Al(e){return J(e,\"loess\")}function Tl(e){return J(e,\"sample\")}function jl(e){return J(e,\"window\")}function El(e){return J(e,\"joinaggregate\")}function Ml(e){return J(e,\"flatten\")}function Rl(e){return J(e,\"calculate\")}function Ll(e){return J(e,\"bin\")}function ql(e){return J(e,\"impute\")}function Ul(e){return J(e,\"timeUnit\")}function Wl(e){return J(e,\"aggregate\")}function Il(e){return J(e,\"stack\")}function Bl(e){return J(e,\"fold\")}function Vl(e){return J(e,\"extent\")&&!J(e,\"density\")&&!J(e,\"regression\")}function Hl(e,t){const{transform:n,...i}=e;if(n){return{...i,transform:n.map((e=>{if(Ol(e))return{filter:Xl(e,t)};if(Ll(e)&&gn(e.bin))return{...e,bin:Yl(e.bin)};if(zl(e)){const{selection:t,...n}=e.from;return t?{...e,from:{param:t,...n}}:e}return e}))}}return e}function Gl(e,n){const i=l(e);if(Zo(i)&&gn(i.bin)&&(i.bin=Yl(i.bin)),la(i)&&i.scale?.domain?.selection){const{selection:e,...t}=i.scale.domain;i.scale.domain={...t,...e?{param:e}:{}}}if(Qo(i))if(t.isArray(i.condition))i.condition=i.condition.map((e=>{const{selection:t,param:i,test:r,...o}=e;return i?e:{...o,test:Xl(e,n)}}));else{const{selection:e,param:t,test:r,...o}=Gl(i.condition,n);i.condition=t?i.condition:{...o,test:Xl(i.condition,n)}}return i}function Yl(e){const t=e.extent;if(t?.selection){const{selection:n,...i}=t;return{...e,extent:{...i,param:n}}}return e}function Xl(e,t){const n=e=>s(e,(e=>{const n={param:e,empty:t.emptySelections[e]??!0};return t.selectionPredicates[e]??=[],t.selectionPredicates[e].push(n),n}));return e.selection?n(e.selection):s(e.test||e.filter,(e=>e.selection?n(e.selection):e))}class Ql extends rl{map(e,t){const n=t.selections??[];if(e.params&&!Ua(e)){const t=[];for(const i of e.params)Ns(i)?n.push(i):t.push(i);e.params=t}return t.selections=n,super.map(e,t)}mapUnit(e,n){const i=n.selections;if(!i||!i.length)return e;const r=(n.path??[]).concat(e.name),o=[];for(const n of i)if(n.views&&n.views.length)for(const i of n.views)(t.isString(i)&&(i===e.name||r.includes(i))||t.isArray(i)&&i.map((e=>r.indexOf(e))).every(((e,t,n)=>-1!==e&&(0===t||e>n[t-1]))))&&o.push(n);else o.push(n);return o.length&&(e.params=o),e}}for(const e of[\"mapFacet\",\"mapRepeat\",\"mapHConcat\",\"mapVConcat\",\"mapLayer\"]){const t=Ql.prototype[e];Ql.prototype[e]=function(e,n){return t.call(this,e,Jl(e,n))}}function Jl(e,t){return e.name?{...t,path:(t.path??[]).concat(e.name)}:t}function Kl(e,t){void 0===t&&(t=Js(e.config));const n=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const n={config:t};return tc.map(Zl.map(ec.map(e,n),n),n)}(e,t),{width:i,height:r}=e,o=function(e,t,n){let{width:i,height:r}=t;const o=Ua(e)||il(e),a={};o?\"container\"==i&&\"container\"==r?(a.type"
-  , "=\"fit\",a.contains=\"padding\"):\"container\"==i?(a.type=\"fit-x\",a.contains=\"padding\"):\"container\"==r&&(a.type=\"fit-y\",a.contains=\"padding\"):(\"container\"==i&&(Si(Hn(\"width\")),i=void 0),\"container\"==r&&(Si(Hn(\"height\")),r=void 0));const s={type:\"pad\",...a,...n?nc(n.autosize):{},...nc(e.autosize)};\"fit\"!==s.type||o||(Si(Vn),s.type=\"pad\");\"container\"==i&&\"fit\"!=s.type&&\"fit-x\"!=s.type&&Si(Gn(\"width\"));\"container\"==r&&\"fit\"!=s.type&&\"fit-y\"!=s.type&&Si(Gn(\"height\"));if(X(s,{type:\"pad\"}))return;return s}(n,{width:i,height:r,autosize:e.autosize},t);return{...n,...o?{autosize:o}:{}}}const Zl=new class extends rl{nonFacetUnitNormalizers=(()=>[as,fs,ys,new hl,new Sl])();map(e,t){if(Ua(e)){const n=Ia(e.encoding,K),i=Ia(e.encoding,Z),r=Ia(e.encoding,ee);if(n||i||r)return this.mapFacetedUnit(e,t)}return super.map(e,t)}mapUnit(e,t){const{parentEncoding:n,parentProjection:i}=t,r=vl(e.encoding,t.repeater),o={...e,...e.name?{name:[t.repeaterPrefix,e.name].filter((e=>e)).join(\"_\")}:{},...r?{encoding:r}:{}};if(n||i)return this.mapUnitWithParentEncodingOrProjection(o,t);const a=this.mapLayerOrUnit.bind(this);for(const e of this.nonFacetUnitNormalizers)if(e.hasMatchingType(o,t.config))return e.run(o,t,a);return o}mapRepeat(e,n){return function(e){return!t.isArray(e.repeat)&&J(e.repeat,\"layer\")}(e)?this.mapLayerRepeat(e,n):this.mapNonLayerRepeat(e,n)}mapLayerRepeat(e,t){const{repeat:n,spec:i,...r}=e,{row:o,column:a,layer:s}=n,{repeater:l={},repeaterPrefix:c=\"\"}=t;return o||a?this.mapRepeat({...e,repeat:{...o?{row:o}:{},...a?{column:a}:{}},spec:{repeat:{layer:s},spec:i}},t):{...r,layer:s.map((e=>{const n={...l,layer:e},r=`${(i.name?`${i.name}_`:\"\")+c}child__layer_${C(e)}`,o=this.mapLayerOrUnit(i,{...t,repeater:n,repeaterPrefix:r});return o.name=r,o}))}}mapNonLayerRepeat(e,n){const{repeat:i,spec:r,data:o,...a}=e;!t.isArray(i)&&e.columns&&(e=f(e,[\"columns\"]),Si(ei(\"repeat\")));const s=[],{repeater:l={},repeaterPrefix:c=\"\"}=n,u=!t.isArray(i)&&i.row||[l?l.row:null],d=!t.isArray(i)&&i.column||[l?l.column:null],m=t.isArray(i)&&i||[l?l.repeat:null];for(const e of m)for(const o of u)for(const a of d){const u={repeat:e,row:o,column:a,layer:l.layer},d=(r.name?`${r.name}_`:\"\")+c+\"child__\"+(t.isArray(i)?`${C(e)}`:(i.row?`row_${C(o)}`:\"\")+(i.column?`column_${C(a)}`:\"\")),m=this.map(r,{...n,repeater:u,repeaterPrefix:d});m.name=d,s.push(f(m,[\"data\"]))}const p=t.isArray(i)?e.columns:i.column?i.column.length:1;return{data:r.data??o,align:\"all\",...a,columns:p,concat:s}}mapFacet(e,t){const{facet:n}=e;return Uo(n)&&e.columns&&(e=f(e,[\"columns\"]),Si(ei(\"facet\"))),super.mapFacet(e,t)}mapUnitWithParentEncodingOrProjection(e,t){const{encoding:n,projection:i}=e,{parentEncoding:r,parentProjection:o,config:a}=t,s=Fl({parentProjection:o,projection:i}),l=Dl({parentEncoding:r,encoding:vl(n,t.repeater)});return this.mapUnit({...e,...s?{projection:s}:{},...l?{encoding:l}:{}},{config:a})}mapFacetedUnit(e,t){const{row:n,column:i,facet:r,...o}=e.encoding,{mark:a,width:s,projection:l,height:c,view:u,params:f,encoding:d,...m}=e,{facetMapping:p,layout:g}=this.getFacetMappingAndLayout({row:n,column:i,facet:r},t),h=vl(o,t.repeater);return this.mapFacet({...m,...g,facet:p,spec:{...s?{width:s}:{},...c?{height:c}:{},...u?{view:u}:{},...l?{projection:l}:{},mark:a,encoding:h,...f?{params:f}:{}}},t)}getFacetMappingAndLayout(e,t){const{row:n,column:i,facet:r}=e;if(n||i){r&&Si(`Facet encoding dropped as ${(o=[...n?[K]:[],...i?[Z]:[]]).join(\" and \")} ${o.length>1?\"are\":\"is\"} also specified.`);const t={},a={};for(const n of[K,Z]){const i=e[n];if(i){const{align:e,center:r,spacing:o,columns:s,...l}=i;t[n]=l;for(const e of[\"align\",\"center\",\"spacing\"])void 0!==i[e]&&(a[e]??={},a[e][n]=i[e])}}return{facetMapping:t,layout:a}}{const{align:e,center:n,spacing:i,columns:o,...a}=r;return{facetMapping:yl(a,t.repeater),layout:{...e?{align:e}:{},...n?{center:n}:{},...i?{spacing:i}:{},...o?{columns:o}:{}}}}var o}mapLayer(e,t){let{parentEncoding:n,parentProjection:i,...r}=t;const{encoding:o,projection:a,...s}=e,l={...r,parentEncoding:Dl({parentEncoding:n,encoding:o,layer:!0}),parentProjection:Fl({parentProjection:"
-  , "i,projection:a})};return super.mapLayer({...s,...e.name?{name:[l.repeaterPrefix,e.name].filter((e=>e)).join(\"_\")}:{}},l)}},ec=new class extends rl{map(e,t){return t.emptySelections??={},t.selectionPredicates??={},e=Hl(e,t),super.map(e,t)}mapLayerOrUnit(e,t){if((e=Hl(e,t)).encoding){const n={};for(const[i,r]of O(e.encoding))n[i]=Gl(r,t);e={...e,encoding:n}}return super.mapLayerOrUnit(e,t)}mapUnit(e,t){const{selection:n,...i}=e;return n?{...i,params:O(n).map((e=>{let[n,i]=e;const{init:r,bind:o,empty:a,...s}=i;\"single\"===s.type?(s.type=\"point\",s.toggle=!1):\"multi\"===s.type&&(s.type=\"point\"),t.emptySelections[n]=\"none\"!==a;for(const e of F(t.selectionPredicates[n]??{}))e.empty=\"none\"!==a;return{name:n,value:r,select:s,bind:o}}))}:e}},tc=new Ql;function nc(e){return t.isString(e)?{type:e}:e??{}}const ic=[\"background\",\"padding\"];function rc(e,t){const n={};for(const t of ic)e&&void 0!==e[t]&&(n[t]=Cn(e[t]));return t&&(n.params=e.params),n}class oc{constructor(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.explicit=e,this.implicit=t}clone(){return new oc(l(this.explicit),l(this.implicit))}combine(){return{...this.explicit,...this.implicit}}get(e){return U(this.explicit[e],this.implicit[e])}getWithExplicit(e){return void 0!==this.explicit[e]?{explicit:!0,value:this.explicit[e]}:void 0!==this.implicit[e]?{explicit:!1,value:this.implicit[e]}:{explicit:!1,value:void 0}}setWithExplicit(e,t){let{value:n,explicit:i}=t;void 0!==n&&this.set(e,n,i)}set(e,t,n){return delete this[n?\"implicit\":\"explicit\"][e],this[n?\"explicit\":\"implicit\"][e]=t,this}copyKeyFromSplit(e,t){let{explicit:n,implicit:i}=t;void 0!==n[e]?this.set(e,n[e],!0):void 0!==i[e]&&this.set(e,i[e],!1)}copyKeyFromObject(e,t){void 0!==t[e]&&this.set(e,t[e],!0)}copyAll(e){for(const t of D(e.combine())){const n=e.getWithExplicit(t);this.setWithExplicit(t,n)}}}function ac(e){return{explicit:!0,value:e}}function sc(e){return{explicit:!1,value:e}}function lc(e){return(t,n,i,r)=>{const o=e(t.value,n.value);return o>0?t:o<0?n:cc(t,n,i,r)}}function cc(e,t,n,i){return e.explicit&&t.explicit&&Si(function(e,t,n,i){return`Conflicting ${t.toString()} property \"${e.toString()}\" (${Q(n)} and ${Q(i)}). Using ${Q(n)}.`}(n,i,e.value,t.value)),e}function uc(e,t,n,i){let r=arguments.length>4&&void 0!==arguments[4]?arguments[4]:cc;return void 0===e||void 0===e.value?t:e.explicit&&!t.explicit?e:t.explicit&&!e.explicit?t:X(e.value,t.value)?e:r(e,t,n,i)}class fc extends oc{constructor(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];super(e,t),this.explicit=e,this.implicit=t,this.parseNothing=n}clone(){const e=super.clone();return e.parseNothing=this.parseNothing,e}}function dc(e){return J(e,\"url\")}function mc(e){return J(e,\"values\")}function pc(e){return J(e,\"name\")&&!dc(e)&&!mc(e)&&!gc(e)}function gc(e){return e&&(hc(e)||yc(e)||vc(e))}function hc(e){return J(e,\"sequence\")}function yc(e){return J(e,\"sphere\")}function vc(e){return J(e,\"graticule\")}let bc=function(e){return e[e.Raw=0]=\"Raw\",e[e.Main=1]=\"Main\",e[e.Row=2]=\"Row\",e[e.Column=3]=\"Column\",e[e.Lookup=4]=\"Lookup\",e[e.PreFilterInvalid=5]=\"PreFilterInvalid\",e[e.PostFilterInvalid=6]=\"PostFilterInvalid\",e}({});function xc(e){let{invalid:t,isPath:n}=e;switch(po(t,{isPath:n})){case\"filter\":return{marks:\"exclude-invalid-values\",scales:\"exclude-invalid-values\"};case\"break-paths-show-domains\":return{marks:n?\"include-invalid-values\":\"exclude-invalid-values\",scales:\"include-invalid-values\"};case\"break-paths-filter-domains\":return{marks:n?\"include-invalid-values\":\"exclude-invalid-values\",scales:\"exclude-invalid-values\"};case\"show\":return{marks:\"include-invalid-values\",scales:\"include-invalid-values\"}}}class $c{_children=[];_parent=null;constructor(e,t){this.debugName=t,e&&(this.parent=e)}clone(){throw new Error(\"Cannot clone node\")}get parent(){return this._parent}set parent(e){this._parent=e,e&&e.addChild(this)}get children(){return this._ch"
-  , "ildren}numChildren(){return this._children.length}addChild(e,t){this._children.includes(e)?Si(\"Attempt to add the same child twice.\"):void 0!==t?this._children.splice(t,0,e):this._children.push(e)}removeChild(e){const t=this._children.indexOf(e);return this._children.splice(t,1),t}remove(){let e=this._parent.removeChild(this);for(const t of this._children)t._parent=this._parent,this._parent.addChild(t,e++)}insertAsParentOf(e){const t=e.parent;t.removeChild(this),this.parent=t,e.parent=this}swapWithParent(){const e=this._parent,t=e.parent;for(const t of this._children)t.parent=e;this._children=[],e.removeChild(this);const n=e.parent.removeChild(e);this._parent=t,t.addChild(this,n),e.parent=this}}class wc extends $c{clone(){const e=new this.constructor;return e.debugName=`clone_${this.debugName}`,e._source=this._source,e._name=`clone_${this._name}`,e.type=this.type,e.refCounts=this.refCounts,e.refCounts[e._name]=0,e}constructor(e,t,n,i){super(e,t),this.type=n,this.refCounts=i,this._source=this._name=t,this.refCounts&&!(this._name in this.refCounts)&&(this.refCounts[this._name]=0)}dependentFields(){return new Set}producedFields(){return new Set}hash(){return void 0===this._hash&&(this._hash=`Output ${I()}`),this._hash}getSource(){return this.refCounts[this._name]++,this._source}isRequired(){return!!this.refCounts[this._name]}setSource(e){this._source=e}}function kc(e){return void 0!==e.as}function Sc(e){return`${e}_end`}class Dc extends $c{clone(){return new Dc(null,l(this.timeUnits))}constructor(e,t){super(e),this.timeUnits=t}static makeFromEncoding(e,t){const n=t.reduceFieldDef(((e,n,i)=>{const{field:r,timeUnit:o}=n;if(o){let a;if(Ti(o)){if(qm(t)){const{mark:e,markDef:i,config:s}=t,l=Ho({fieldDef:n,markDef:i,config:s});(eo(e)||l)&&(a={timeUnit:Ii(o),field:r})}}else a={as:ma(n,{forAs:!0}),field:r,timeUnit:o};if(qm(t)){const{mark:e,markDef:r,config:o}=t,s=Ho({fieldDef:n,markDef:r,config:o});eo(e)&&_t(i)&&.5!==s&&(a.rectBandPosition=s)}a&&(e[d(a)]=a)}return e}),{});return S(n)?null:new Dc(e,n)}static makeFromTransform(e,t){const{timeUnit:n,...i}={...t},r={...i,timeUnit:Ii(n)};return new Dc(e,{[d(r)]:r})}merge(e){this.timeUnits={...this.timeUnits};for(const t in e.timeUnits)this.timeUnits[t]||(this.timeUnits[t]=e.timeUnits[t]);for(const t of e.children)e.removeChild(t),t.parent=this;e.remove()}removeFormulas(e){const t={};for(const[n,i]of O(this.timeUnits)){const r=kc(i)?i.as:`${i.field}_end`;e.has(r)||(t[n]=i)}this.timeUnits=t}producedFields(){return new Set(F(this.timeUnits).map((e=>kc(e)?e.as:Sc(e.field))))}dependentFields(){return new Set(F(this.timeUnits).map((e=>e.field)))}hash(){return`TimeUnit ${d(this.timeUnits)}`}assemble(){const e=[];for(const t of F(this.timeUnits)){const{rectBandPosition:n}=t,i=Ii(t.timeUnit);if(kc(t)){const{field:r,as:o}=t,{unit:a,utc:s,...l}=i,c=[o,`${o}_end`];e.push({field:M(r),type:\"timeunit\",...a?{units:Ri(a)}:{},...s?{timezone:\"utc\"}:{},...l,as:c}),e.push(...Cc(c,n,i))}else if(t){const{field:r}=t,o=r.replaceAll(\"\\\\.\",\".\"),a=zc({timeUnit:i,field:o}),s=Sc(o);e.push({type:\"formula\",expr:a,as:s}),e.push(...Cc([o,s],n,i))}}return e}}const Fc=\"offsetted_rect_start\",Oc=\"offsetted_rect_end\";function zc(e){let{timeUnit:t,field:n,reverse:i}=e;const{unit:r,utc:o}=t,a=Li(r),{part:s,step:l}=Hi(a,t.step);return`${o?\"utcOffset\":\"timeOffset\"}('${s}', ${j(n)}, ${i?-l:l})`}function Cc(e,t,n){let[i,r]=e;if(void 0!==t&&.5!==t){const e=j(i),o=j(r);return[{type:\"formula\",expr:_c([zc({timeUnit:n,field:i,reverse:!0}),e],t+.5),as:`${i}_${Fc}`},{type:\"formula\",expr:_c([e,o],t+.5),as:`${i}_${Oc}`}]}return[]}function _c(e,t){let[n,i]=e;return`${1-t} * ${n} + ${t} * ${i}`}const Pc=\"_tuple_fields\";class Nc{constructor(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];this.items=t,this.hasChannel={},this.hasField={},this.hasSelectionId=!1}}const Ac={defined:()=>!0,parse:(e,n,i)=>{const r=n.name,o=n.project??=new Nc,a={},s={},l=new Set,c=(e,t)=>{const n=\"visual\"===t?e.channel:e.field;let i=C(`${r}_${n}`);for(let e=1;l.has(i);e++)i=C(`${r}_${n}_${e}`);return l.add(i),{[t]:i}},u=n.type,f=e.config.sel"
-  , "ection[u],m=void 0!==i.value?t.array(i.value):null;let{fields:p,encodings:g}=t.isObject(i.select)?i.select:{};if(!p&&!g&&m)for(const e of m)if(t.isObject(e))for(const n of D(e))h=n,t.hasOwnProperty(et,h)?(g||(g=[])).push(n):\"interval\"===u?(Si('Interval selections should be initialized using \"x\", \"y\", \"longitude\", or \"latitude\" keys.'),g=f.encodings):(p??=[]).push(n);var h;p||g||(g=f.encodings,\"fields\"in f&&(p=f.fields));for(const t of g??[]){const n=e.fieldDef(t);if(n){let i=n.field;if(n.aggregate){Si(Jn(t,n.aggregate));continue}if(!i){Si(Qn(t));continue}if(n.timeUnit&&!Ti(n.timeUnit)){i=e.vgField(t);const r={timeUnit:n.timeUnit,as:i,field:n.field};s[d(r)]=r}if(!a[i]){const r={field:i,channel:t,type:\"interval\"===u&&Qt(t)&&Sr(e.getScaleComponent(t).get(\"type\"))?\"R\":n.bin?\"R-RE\":\"E\",index:o.items.length};r.signals={...c(r,\"data\"),...c(r,\"visual\")},o.items.push(a[i]=r),o.hasField[i]=a[i],o.hasSelectionId=o.hasSelectionId||i===zs,Le(t)?(r.geoChannel=t,r.channel=Re(t),o.hasChannel[r.channel]=a[i]):o.hasChannel[t]=a[i]}}else Si(Qn(t))}for(const e of p??[]){if(o.hasField[e])continue;const t={type:\"E\",field:e,index:o.items.length};t.signals={...c(t,\"data\")},o.items.push(t),o.hasField[e]=t,o.hasSelectionId=o.hasSelectionId||e===zs}m&&(n.init=m.map((e=>o.items.map((n=>t.isObject(e)?void 0!==e[n.geoChannel||n.channel]?e[n.geoChannel||n.channel]:e[n.field]:e))))),S(s)||(o.timeUnit=new Dc(null,s))},signals:(e,t,n)=>{const i=t.name+Pc;return n.filter((e=>e.name===i)).length>0||t.project.hasSelectionId?n:n.concat({name:i,value:t.project.items.map(Bc)})}},Tc=\"_curr\",jc=\"anim_value\",Ec=\"anim_clock\",Mc=\"eased_anim_clock\",Rc=\"min_extent\",Lc=\"max_range_extent\",qc=\"last_tick_at\",Uc=\"is_playing\",Wc=1/60*1e3,Ic={defined:e=>\"point\"===e.type,topLevelSignals:(e,t,n)=>(af(t)&&(n=n.concat([{name:Ec,init:\"0\",on:[{events:{type:\"timer\",throttle:Wc},update:`${Uc} ? (${Ec} + (now() - ${qc}) > ${Lc} ? 0 : ${Ec} + (now() - ${qc})) : ${Ec}`}]},{name:qc,init:\"now()\",on:[{events:[{signal:Ec},{signal:Uc}],update:\"now()\"}]},{name:Uc,init:\"true\"}])),n),signals:(e,n,i)=>{const r=n.name,o=r+Pc,a=n.project,s=\"(item().isVoronoi ? datum.datum : datum)\",l=F(e.component.selection??{}).reduce(((e,t)=>\"interval\"===t.type?e.concat(t.name+Zc):e),[]).map((e=>`indexof(item().mark.name, '${e}') < 0`)).join(\" && \"),c=\"datum && item().mark.marktype !== 'group' && indexof(item().mark.role, 'legend') < 0\"+(l?` && ${l}`:\"\");let u=`unit: ${nf(e)}, `;if(n.project.hasSelectionId)u+=`${zs}: ${s}[${t.stringValue(zs)}]`;else if(af(n))u+=`fields: ${o}, values: [${jc} ? ${jc} : ${Rc}]`;else{u+=`fields: ${o}, values: [${a.items.map((n=>{const i=e.fieldDef(n.channel);return i?.bin?`[${s}[${t.stringValue(e.vgField(n.channel,{}))}], ${s}[${t.stringValue(e.vgField(n.channel,{binSuffix:\"end\"}))}]]`:`${s}[${t.stringValue(n.field)}]`})).join(\", \")}]`}if(af(n))return i.concat((f=n.name,d=e.scaleName(ge),[{name:Mc,update:Ec},{name:`${f}_domain`,init:`domain('${d}')`},{name:Rc,init:`extent(${f}_domain)[0]`},{name:Lc,init:`extent(range('${d}'))[1]`},{name:jc,update:`invert('${d}', ${Mc})`}]),[{name:r+Ku,on:[{events:[{signal:Mc},{signal:jc}],update:`{${u}}`,force:!0}]}]);{const e=n.events;return i.concat([{name:r+Ku,on:e?[{events:e,update:`${c} ? {${u}} : null`,force:!0}]:[]}])}var f,d}};function Bc(e){const{signals:t,hasLegend:n,index:i,...r}=e;return r.field=M(r.field),r}function Vc(e){let n=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:t.identity;if(t.isArray(e)){const t=e.map((e=>Vc(e,n,i)));return n?`[${t.join(\", \")}]`:t}return Di(e)?i(n?Pi(e):function(e){const t=_i(e,!0);return e.utc?+new Date(Date.UTC(...t)):+new Date(...t)}(e)):n?i(Q(e)):e}function Hc(e,n){for(const i of F(e.component.selection??{})){const r=i.name;let o=`${r}${Ku}, ${\"global\"===i.resolve?\"true\":`{unit: ${nf(e)}}`}`;for(const t of tf)t.defined(i)&&(t.signals&&(n=t.signals(e,i,n)),t.modifyExpr&&(o=t.modifyExpr(e,i,o)));n.push({name:r+Zu,on:[{events:{signal:i.name+Ku},update:`modify(${t.stringValue(i.name+Ju)}, ${o})`}]})}return Xc(n)}function Gc(e,n){if(e.co"
-  , "mponent.selection&&D(e.component.selection).length){const i=t.stringValue(e.getName(\"cell\"));n.unshift({name:\"facet\",value:{},on:[{events:t.parseSelector(\"pointermove\",\"scope\"),update:`isTuple(facet) ? facet : group(${i}).datum`}]})}return Xc(n)}function Yc(e,t){for(const n of F(e.component.selection??{}))for(const i of tf)i.defined(n)&&i.marks&&(t=i.marks(e,n,t));return t}function Xc(e){return e.map((e=>(e.on&&!e.on.length&&delete e.on,e)))}const Qc={defined:e=>\"interval\"===e.type&&\"global\"===e.resolve&&e.bind&&\"scales\"===e.bind,parse:(e,t)=>{const n=t.scales=[];for(const i of t.project.items){const r=i.channel;if(!Qt(r))continue;const o=e.getScaleComponent(r),a=o?o.get(\"type\"):void 0;\"sequential\"==a&&Si(\"Sequntial scales are deprecated. The available quantitative scale type values are linear, log, pow, sqrt, symlog, time and utc\"),o&&Sr(a)?(o.set(\"selectionExtent\",{param:t.name,field:i.field},!0),n.push(i)):Si(\"Scale bindings are currently only supported for scales with unbinned, continuous domains.\")}},topLevelSignals:(e,n,i)=>{const r=n.scales.filter((e=>0===i.filter((t=>t.name===e.signals.data)).length));if(!e.parent||Kc(e)||0===r.length)return i;const o=i.find((e=>e.name===n.name));let a=o.update;if(a.includes(ef))o.update=`{${r.map((e=>`${t.stringValue(M(e.field))}: ${e.signals.data}`)).join(\", \")}}`;else{for(const e of r){const n=`${t.stringValue(M(e.field))}: ${e.signals.data}`;a.includes(n)||(a=`${a.substring(0,a.length-1)}, ${n}}`)}o.update=a}return i.concat(r.map((e=>({name:e.signals.data}))))},signals:(e,t,n)=>{if(e.parent&&!Kc(e))for(const e of t.scales){const t=n.find((t=>t.name===e.signals.data));t.push=\"outer\",delete t.value,delete t.update}return n}};function Jc(e,n){return`domain(${t.stringValue(e.scaleName(n))})`}function Kc(e){return e.parent&&Im(e.parent)&&(!e.parent.parent||Kc(e.parent.parent))}const Zc=\"_brush\",eu=\"_scale_trigger\",tu=\"geo_interval_init_tick\",nu=\"_init\",iu={defined:e=>\"interval\"===e.type,parse:(e,n,i)=>{if(e.hasProjection){const e={...t.isObject(i.select)?i.select:{}};e.fields=[zs],e.encodings||(e.encodings=i.value?D(i.value):[de,fe]),i.select={type:\"interval\",...e}}if(n.translate&&!Qc.defined(n)){const e=`!event.item || event.item.mark.name !== ${t.stringValue(n.name+Zc)}`;for(const i of n.events){if(!i.between){Si(`${i} is not an ordered event stream for interval selections.`);continue}const n=t.array(i.between[0].filter??=[]);n.includes(e)||n.push(e)}}},signals:(e,n,i)=>{const r=n.name,o=r+Ku,a=F(n.project.hasChannel).filter((e=>e.channel===te||e.channel===ne)),s=n.init?n.init[0]:null;if(i.push(...a.reduce(((i,r)=>i.concat(function(e,n,i,r){const o=!e.hasProjection,a=i.channel,s=i.signals.visual,l=t.stringValue(o?e.scaleName(a):e.projectionName()),c=e=>`scale(${l}, ${e})`,u=e.getSizeSignalRef(a===te?\"width\":\"height\").signal,f=`${a}(unit)`,d=n.events.reduce(((e,t)=>[...e,{events:t.between[0],update:`[${f}, ${f}]`},{events:t,update:`[${s}[0], clamp(${f}, 0, ${u})]`}]),[]);if(o){const t=i.signals.data,o=Qc.defined(n),u=e.getScaleComponent(a),f=u?u.get(\"type\"):void 0,m=r?{init:Vc(r,!0,c)}:{value:[]};return d.push({events:{signal:n.name+eu},update:Sr(f)?`[${c(`${t}[0]`)}, ${c(`${t}[1]`)}]`:\"[0, 0]\"}),o?[{name:t,on:[]}]:[{name:s,...m,on:d},{name:t,...r?{init:Vc(r)}:{},on:[{events:{signal:s},update:`${s}[0] === ${s}[1] ? null : invert(${l}, ${s})`}]}]}{const e=a===te?0:1,t=n.name+nu;return[{name:s,...r?{init:`[${t}[0][${e}], ${t}[1][${e}]]`}:{value:[]},on:d}]}}(e,n,r,s&&s[r.index]))),[])),e.hasProjection){const l=t.stringValue(e.projectionName()),c=e.projectionName()+\"_center\",{x:u,y:f}=n.project.hasChannel,d=u&&u.signals.visual,m=f&&f.signals.visual,p=u?s&&s[u.index]:`${c}[0]`,g=f?s&&s[f.index]:`${c}[1]`,h=t=>e.getSizeSignalRef(t).signal,y=`[[${d?d+\"[0]\":\"0\"}, ${m?m+\"[0]\":\"0\"}],[${d?d+\"[1]\":h(\"width\")}, ${m?m+\"[1]\":h(\"height\")}]]`;if(s&&(i.unshift({name:r+nu,init:`[scale(${l}, [${u?p[0]:p}, ${f?g[0]:g}]), scale(${l}, [${u?p[1]:p}, ${f?g[1]:g}])]`}),!u||!f)){i.find((e=>e.name===c))||i.unshift({name:c,update:`invert(${l}, [${h(\"width\")}/2, ${h(\"height\")}/2])`})}const v=`vlSelectionTuple"
-  , "s(${`intersect(${y}, {markname: ${t.stringValue(e.getName(\"marks\"))}}, unit.mark)`}, ${`{unit: ${nf(e)}}`})`,b=a.map((e=>e.signals.visual));return i.concat({name:o,on:[{events:[...b.length?[{signal:b.join(\" || \")}]:[],...s?[{signal:tu}]:[]],update:v}]})}{if(!Qc.defined(n)){const n=r+eu,o=a.map((n=>{const i=n.channel,{data:r,visual:o}=n.signals,a=t.stringValue(e.scaleName(i)),s=Sr(e.getScaleComponent(i).get(\"type\"))?\"+\":\"\";return`(!isArray(${r}) || (${s}invert(${a}, ${o})[0] === ${s}${r}[0] && ${s}invert(${a}, ${o})[1] === ${s}${r}[1]))`}));o.length&&i.push({name:n,value:{},on:[{events:a.map((t=>({scale:e.scaleName(t.channel)}))),update:o.join(\" && \")+` ? ${n} : {}`}]})}const l=a.map((e=>e.signals.data)),c=`unit: ${nf(e)}, fields: ${r+Pc}, values`;return i.concat({name:o,...s?{init:`{${c}: ${Vc(s)}}`}:{},...l.length?{on:[{events:[{signal:l.join(\" || \")}],update:`${l.join(\" && \")} ? {${c}: [${l}]} : null`}]}:{}})}},topLevelSignals:(e,t,n)=>{if(qm(e)&&e.hasProjection&&t.init){n.filter((e=>e.name===tu)).length||n.unshift({name:tu,value:null,on:[{events:\"timer{1}\",update:`${tu} === null ? {} : ${tu}`}]})}return n},marks:(e,n,i)=>{const r=n.name,{x:o,y:a}=n.project.hasChannel,s=o?.signals.visual,l=a?.signals.visual,c=`data(${t.stringValue(n.name+Ju)})`;if(Qc.defined(n)||!o&&!a)return i;const u={x:void 0!==o?{signal:`${s}[0]`}:{value:0},y:void 0!==a?{signal:`${l}[0]`}:{value:0},x2:void 0!==o?{signal:`${s}[1]`}:{field:{group:\"width\"}},y2:void 0!==a?{signal:`${l}[1]`}:{field:{group:\"height\"}}};if(\"global\"===n.resolve)for(const t of D(u))u[t]=[{test:`${c}.length && ${c}[0].unit === ${nf(e)}`,...u[t]},{value:0}];const{fill:f,fillOpacity:d,cursor:m,...p}=n.mark,g=D(p).reduce(((e,t)=>(e[t]=[{test:[void 0!==o&&`${s}[0] !== ${s}[1]`,void 0!==a&&`${l}[0] !== ${l}[1]`].filter((e=>e)).join(\" && \"),value:p[t]},{value:null}],e)),{}),h=m??(n.translate?\"move\":null);return[{name:`${r+Zc}_bg`,type:\"rect\",clip:!0,encode:{enter:{fill:{value:f},fillOpacity:{value:d}},update:u}},...i,{name:r+Zc,type:\"rect\",clip:!0,encode:{enter:{...h?{cursor:{value:h}}:{},fill:{value:\"transparent\"}},update:{...u,...g}}}]}};function ru(e){let{model:n,channelDef:i,vgChannel:r,invalidValueRef:o,mainRefFn:a}=e;const s=Qo(i)&&i.condition;let l=[];if(s){l=t.array(s).map((e=>{const t=a(e);if(function(e){return J(e,\"param\")}(e)){const{param:i,empty:r}=e;return{test:ff(n,{param:i,empty:r}),...t}}return{test:mf(n,e.test),...t}}))}void 0!==o&&l.push(o);const c=a(i);return void 0!==c&&l.push(c),l.length>1||1===l.length&&Boolean(l[0].test)?{[r]:l}:1===l.length?{[r]:l[0]}:{}}function ou(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:\"text\";const n=e.encoding[t];return ru({model:e,channelDef:n,vgChannel:t,mainRefFn:t=>au(t,e.config),invalidValueRef:void 0})}function au(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:\"datum\";if(e){if(sa(e))return Pn(e.value);if(oa(e)){const{format:i,formatType:r}=$a(e);return Oo({fieldOrDatumDef:e,format:i,formatType:r,expr:n,config:t})}}}function su(e){let n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{encoding:i,markDef:r,config:o,stack:a}=e,s=i.tooltip;if(t.isArray(s))return{tooltip:cu({tooltip:s},a,o,n)};{const l=n.reactiveGeom?\"datum.datum\":\"datum\";return ru({model:e,channelDef:s,vgChannel:\"tooltip\",mainRefFn:e=>{const s=au(e,o,l);if(s)return s;if(null===e)return;let c=En(\"tooltip\",r,o);return!0===c&&(c={content:\"encoding\"}),t.isString(c)?{value:c}:t.isObject(c)?wn(c)?c:\"encoding\"===c.content?cu(i,a,o,n):{signal:l}:void 0},invalidValueRef:void 0})}}function lu(e,n,i){let{reactiveGeom:r}=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};const o={...i,...i.tooltipFormat},a=new Set,s=r?\"datum.datum\":\"datum\",l=[];function c(i,r){const c=rt(r),u=aa(i)?i:{...i,type:e[c].type},f=u.title||xa(u,o),d=t.array(f).join(\", \").replaceAll(/\"/g,'\\\\\"');let m;if(_t(r)){const t=\"x\"===r?\"x2\":\"y2\",n=wa(e[t]);if(pn(u.bin)&&n){const e=ma(u,{expr:s}),i=ma(n,{expr:s}),{format:r,formatType:l}=$a(u);m=jo(e,i,r,l,o),a.add(t)}}if((_t(r)||r===ce||r===se)&&n&&n.fieldChannel===r&&\"normalize\"===n.offset){const{form"
-  , "at:e,formatType:t}=$a(u);m=Oo({fieldOrDatumDef:u,format:e,formatType:t,expr:s,config:o,normalizeStack:!0}).signal}m??=au(u,o,s).signal,l.push({channel:r,key:d,value:m})}Qa(e,((e,t)=>{Zo(e)?c(e,t):Jo(e)&&c(e.condition,t)}));const u={};for(const{channel:e,key:t,value:n}of l)a.has(e)||u[t]||(u[t]=n);return u}function cu(e,t,n){let{reactiveGeom:i}=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};const r=lu(e,t,n,{reactiveGeom:i}),o=O(r).map((e=>{let[t,n]=e;return`\"${t}\": ${n}`}));return o.length>0?{signal:`{${o.join(\", \")}}`}:void 0}function uu(e){const{markDef:t,config:n}=e,i=En(\"aria\",t,n);return!1===i?{}:{...i?{aria:i}:{},...fu(e),...du(e)}}function fu(e){const{mark:n,markDef:i,config:r}=e;if(!1===r.aria)return{};const o=En(\"ariaRoleDescription\",i,r);return null!=o?{ariaRoleDescription:{value:o}}:t.hasOwnProperty(Fn,n)?{}:{ariaRoleDescription:{value:n}}}function du(e){const{encoding:t,markDef:n,config:i,stack:r}=e,o=t.description;if(o)return ru({model:e,channelDef:o,vgChannel:\"description\",mainRefFn:t=>au(t,e.config),invalidValueRef:void 0});const a=En(\"description\",n,i);if(null!=a)return{description:Pn(a)};if(!1===i.aria)return{};const s=lu(t,r,i);return S(s)?void 0:{description:{signal:O(s).map(((e,t)=>{let[n,i]=e;return`\"${t>0?\"; \":\"\"}${n}: \" + (${i})`})).join(\" + \")}}}function mu(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{markDef:i,encoding:r,config:o}=t,{vgChannel:a}=n;let{defaultRef:s,defaultValue:l}=n;const c=r[e];void 0===s&&(l??=En(e,i,o,{vgChannel:a,ignoreVgConfig:!Qo(c)}),void 0!==l&&(s=Pn(l)));const u={markDef:i,config:o,scaleName:t.scaleName(e),scale:t.getScaleComponent(e)},f=yo({...u,scaleChannel:e,channelDef:c});return ru({model:t,channelDef:c,vgChannel:a??e,invalidValueRef:f,mainRefFn:t=>wo({...u,channel:e,channelDef:t,stack:null,defaultRef:s})})}function pu(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{filled:void 0};const{markDef:n,encoding:i,config:r}=e,{type:o}=n,a=t.filled??En(\"filled\",n,r),s=p([\"bar\",\"point\",\"circle\",\"square\",\"geoshape\"],o)?\"transparent\":void 0,l=En(!0===a?\"color\":void 0,n,r,{vgChannel:\"fill\"})??r.mark[!0===a&&\"color\"]??s,c=En(!1===a?\"color\":void 0,n,r,{vgChannel:\"stroke\"})??r.mark[!1===a&&\"color\"],u=a?\"fill\":\"stroke\",f={...l?{fill:Pn(l)}:{},...c?{stroke:Pn(c)}:{}};return n.color&&(a?n.fill:n.stroke)&&Si(ai(\"property\",{fill:\"fill\"in n,stroke:\"stroke\"in n})),{...f,...mu(\"color\",e,{vgChannel:u,defaultValue:a?l:c}),...mu(\"fill\",e,{defaultValue:i.fill?l:void 0}),...mu(\"stroke\",e,{defaultValue:i.stroke?c:void 0})}}function gu(e){const{encoding:t,mark:n}=e,i=t.order;return!Zr(n)&&sa(i)?ru({model:e,channelDef:i,vgChannel:\"zindex\",mainRefFn:e=>Pn(e.value),invalidValueRef:void 0}):{}}function hu(e){let{channel:t,markDef:n,encoding:i={},model:r,bandPosition:o}=e;const a=`${t}Offset`,s=n[a],l=i[a];if((\"xOffset\"===a||\"yOffset\"===a)&&l){return{offsetType:\"encoding\",offset:wo({channel:a,channelDef:l,markDef:n,config:r?.config,scaleName:r.scaleName(a),scale:r.getScaleComponent(a),stack:null,defaultRef:Pn(s),bandPosition:o})}}const c=n[a];return c?{offsetType:\"visual\",offset:c}:{}}function yu(e,t,n){let{defaultPos:i,vgChannel:r}=n;const{encoding:o,markDef:a,config:s,stack:l}=t,c=o[e],u=o[at(e)],f=t.scaleName(e),d=t.getScaleComponent(e),{offset:m,offsetType:p}=hu({channel:e,markDef:a,encoding:o,model:t,bandPosition:.5}),g=vu({model:t,defaultPos:i,channel:e,scaleName:f,scale:d}),h=!c&&_t(e)&&(o.latitude||o.longitude)?{field:t.getName(e)}:function(e){const{channel:t,channelDef:n,scaleName:i,stack:r,offset:o,markDef:a}=e;if(oa(n)&&r&&t===r.fieldChannel){if(Zo(n)){let e=n.bandPosition;if(void 0!==e||\"text\"!==a.type||\"radius\"!==t&&\"theta\"!==t||(e=.5),void 0!==e)return $o({scaleName:i,fieldOrDatumDef:n,startSuffix:\"start\",bandPosition:e,offset:o})}return xo(n,i,{suffix:\"end\"},{offset:o})}return bo(e)}({channel:e,channelDef:c,channel2Def:u,markDef:a,config:s,scaleName:f,scale:d,stack:l,offset:m,defaultRef:g,bandPosition:\"encoding\"===p?0:void 0});return h?{[r||e]:h}:void 0}function vu(e){let{model:t,defaultPos:n,channel:i,scaleName:r,scale:o}=e;cons"
-  , "t{markDef:a,config:s}=t;return()=>{const e=rt(i),l=ot(i),c=En(i,a,s,{vgChannel:l});if(void 0!==c)return ko(i,c);switch(n){case\"zeroOrMin\":return bu({scaleName:r,scale:o,mode:\"zeroOrMin\",mainChannel:e,config:s});case\"zeroOrMax\":return bu({scaleName:r,scale:o,mode:{zeroOrMax:{widthSignal:t.width.signal,heightSignal:t.height.signal}},mainChannel:e,config:s});case\"mid\":return{...t[st(i)],mult:.5}}}}function bu(e){let{mainChannel:t,config:n,...i}=e;const r=ho(i),{mode:o}=i;if(r)return r;switch(t){case\"radius\":{if(\"zeroOrMin\"===o)return{value:0};const{widthSignal:e,heightSignal:t}=o.zeroOrMax;return{signal:`min(${e},${t})/2`}}case\"theta\":return\"zeroOrMin\"===o?{value:0}:{signal:\"2*PI\"};case\"x\":return\"zeroOrMin\"===o?{value:0}:{field:{group:\"width\"}};case\"y\":return\"zeroOrMin\"===o?{field:{group:\"height\"}}:{value:0}}}const xu={left:\"x\",center:\"xc\",right:\"x2\"},$u={top:\"y\",middle:\"yc\",bottom:\"y2\"};function wu(e,t,n){let i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:\"middle\";if(\"radius\"===e||\"theta\"===e)return ot(e);const r=\"x\"===e?\"align\":\"baseline\",o=En(r,t,n);let a;return wn(o)?(Si(function(e){return`The ${e} for range marks cannot be an expression`}(r)),a=void 0):a=o,\"x\"===e?xu[a||(\"top\"===i?\"left\":\"center\")]:$u[a||i]}function ku(e,t,n){let{defaultPos:i,defaultPos2:r,range:o}=n;return o?Su(e,t,{defaultPos:i,defaultPos2:r}):yu(e,t,{defaultPos:i})}function Su(e,t,n){let{defaultPos:i,defaultPos2:r}=n;const{markDef:o,config:a}=t,s=at(e),l=st(e),c=function(e,t,n){const{encoding:i,mark:r,markDef:o,stack:a,config:s}=e,l=rt(n),c=st(n),u=ot(n),f=i[l],d=e.scaleName(l),m=e.getScaleComponent(l),{offset:p}=hu(n in i||n in o?{channel:n,markDef:o,encoding:i,model:e}:{channel:l,markDef:o,encoding:i,model:e});if(!f&&(\"x2\"===n||\"y2\"===n)&&(i.latitude||i.longitude)){const t=st(n),i=e.markDef[t];return null!=i?{[t]:{value:i}}:{[u]:{field:e.getName(n)}}}const g=function(e){let{channel:t,channelDef:n,channel2Def:i,markDef:r,config:o,scaleName:a,scale:s,stack:l,offset:c,defaultRef:u}=e;if(oa(n)&&l&&t.charAt(0)===l.fieldChannel.charAt(0))return xo(n,a,{suffix:\"start\"},{offset:c});return bo({channel:t,channelDef:i,scaleName:a,scale:s,stack:l,markDef:r,config:o,offset:c,defaultRef:u})}({channel:n,channelDef:f,channel2Def:i[n],markDef:o,config:s,scaleName:d,scale:m,stack:a,offset:p,defaultRef:void 0});if(void 0!==g)return{[u]:g};return Du(n,o)||Du(n,{[n]:Rn(n,o,s.style),[c]:Rn(c,o,s.style)})||Du(n,s[r])||Du(n,s.mark)||{[u]:vu({model:e,defaultPos:t,channel:n,scaleName:d,scale:m})()}}(t,r,s);return{...yu(e,t,{defaultPos:i,vgChannel:c[l]?wu(e,o,a):ot(e)}),...c}}function Du(e,t){const n=st(e),i=ot(e);if(void 0!==t[i])return{[i]:ko(e,t[i])};if(void 0!==t[e])return{[i]:ko(e,t[e])};if(t[n]){const i=t[n];if(!lo(i))return{[n]:ko(e,i)};Si(function(e){return`Position range does not support relative band size for ${e}.`}(n))}}function Fu(e,n){const{config:i,encoding:r,markDef:o}=e,a=o.type,s=at(n),l=st(n),c=r[n],u=r[s],f=e.getScaleComponent(n),d=f?f.get(\"type\"):void 0,m=o.orient,p=r[l]??r.size??En(\"size\",o,i,{vgChannel:l}),g=lt(n),h=\"bar\"===a&&(\"x\"===n?\"vertical\"===m:\"horizontal\"===m)||\"tick\"===a&&(\"y\"===n?\"vertical\"===m:\"horizontal\"===m);return!Zo(c)||!(mn(c.bin)||pn(c.bin)||c.timeUnit&&!u)||p&&!lo(p)||r[g]||kr(d)?(oa(c)&&kr(d)||h)&&!u?function(e,n,i){const{markDef:r,encoding:o,config:a,stack:s}=i,l=r.orient,c=i.scaleName(n),u=i.getScaleComponent(n),f=st(n),d=at(n),m=lt(n),p=i.scaleName(m),g=i.getScaleComponent(ct(n)),h=\"tick\"===r.type||\"horizontal\"===l&&\"y\"===n||\"vertical\"===l&&\"x\"===n;let y;(o.size||r.size)&&(h?y=mu(\"size\",i,{vgChannel:f,defaultRef:Pn(r.size)}):Si(function(e){return`Cannot apply size to non-oriented mark \"${e}\".`}(r.type)));const v=!!y,b=Go({channel:n,fieldDef:e,markDef:r,config:a,scaleType:(u||g)?.get(\"type\"),useVlSizeChannel:h});y=y||{[f]:Ou(f,p||c,g||u,a,b,!!e,r.type)};const x=\"band\"===(u||g)?.get(\"type\")&&lo(b)&&!v?\"top\":\"middle\",$=wu(n,r,a,x),w=\"xc\"===$||\"yc\"===$,{offset:k,offsetType:S}=hu({channel:n,markDef:r,encoding:o,model:i,bandPosition:w?.5:0}),D=bo({channel:n,channelDef:e,markDef:r,config:a,scaleName:c,scale:u,stack:s,offset:k"
-  , ",defaultRef:vu({model:i,defaultPos:\"mid\",channel:n,scaleName:c,scale:u}),bandPosition:w?\"encoding\"===S?0:.5:wn(b)?{signal:`(1-${b})/2`}:lo(b)?(1-b.band)/2:0});if(f)return{[$]:D,...y};{const e=ot(d),n=y[f],i=k?{...n,offset:k}:n;return{[$]:D,[e]:t.isArray(D)?[D[0],{...D[1],offset:i}]:{...D,offset:i}}}}(c,n,e):Su(n,e,{defaultPos:\"zeroOrMax\",defaultPos2:\"zeroOrMin\"}):function(e){let{fieldDef:t,fieldDef2:n,channel:i,model:r}=e;const{config:o,markDef:a,encoding:s}=r,l=r.getScaleComponent(i),c=r.scaleName(i),u=l?l.get(\"type\"):void 0,f=l.get(\"reverse\"),d=Go({channel:i,fieldDef:t,markDef:a,config:o,scaleType:u}),m=r.component.axes[i]?.[0],p=m?.get(\"translate\")??.5,g=_t(i)?En(\"binSpacing\",a,o)??0:0,h=at(i),y=ot(i),v=ot(h),b=Mn(\"minBandSize\",a,o),{offset:x}=hu({channel:i,markDef:a,encoding:s,model:r,bandPosition:0}),{offset:$}=hu({channel:h,markDef:a,encoding:s,model:r,bandPosition:0}),w=function(e){let{scaleName:t,fieldDef:n}=e;const i=ma(n,{expr:\"datum\"});return`abs(scale(\"${t}\", ${ma(n,{expr:\"datum\",suffix:\"end\"})}) - scale(\"${t}\", ${i}))`}({fieldDef:t,scaleName:c}),k=zu(i,g,f,p,x,b,w),S=zu(h,g,f,p,$??x,b,w),D=wn(d)?{signal:`(1-${d.signal})/2`}:lo(d)?(1-d.band)/2:.5,F=Ho({fieldDef:t,fieldDef2:n,markDef:a,config:o});if(mn(t.bin)||t.timeUnit){const e=t.timeUnit&&.5!==F;return{[v]:Cu({fieldDef:t,scaleName:c,bandPosition:D,offset:S,useRectOffsetField:e}),[y]:Cu({fieldDef:t,scaleName:c,bandPosition:wn(D)?{signal:`1-${D.signal}`}:1-D,offset:k,useRectOffsetField:e})}}if(pn(t.bin)){const e=xo(t,c,{},{offset:S});if(Zo(n))return{[v]:e,[y]:xo(n,c,{},{offset:k})};if(gn(t.bin)&&t.bin.step)return{[v]:e,[y]:{signal:`scale(\"${c}\", ${ma(t,{expr:\"datum\"})} + ${t.bin.step})`,offset:k}}}return void Si(xi(h))}({fieldDef:c,fieldDef2:u,channel:n,model:e})}function Ou(e,n,i,r,o,a,s){if(lo(o)){if(!i)return{mult:o.band,field:{group:e}};{const e=i.get(\"type\");if(\"band\"===e){let e=`bandwidth('${n}')`;1!==o.band&&(e=`${o.band} * ${e}`);const t=Mn(\"minBandSize\",{type:s},r);return{signal:t?`max(${An(t)}, ${e})`:e}}1!==o.band&&(Si(function(e){return`Cannot use the relative band size with ${e} scale.`}(e)),o=void 0)}}else{if(wn(o))return o;if(o)return{value:o}}if(i){const e=i.get(\"range\");if(kn(e)&&t.isNumber(e.step))return{value:e.step-2}}if(!a){const{bandPaddingInner:n,barBandPaddingInner:i,rectBandPaddingInner:o,tickBandPaddingInner:a}=r.scale,l=U(n,\"tick\"===s?a:\"bar\"===s?i:o);if(wn(l))return{signal:`(1 - (${l.signal})) * ${e}`};if(t.isNumber(l))return{signal:`${1-l} * ${e}`}}return{value:Ws(r.view,e)-2}}function zu(e,t,n,i,r,o,a){if(Ee(e))return 0;const s=\"x\"===e||\"y2\"===e,l=s?-t/2:t/2;if(wn(n)||wn(r)||wn(i)||o){const e=An(n),t=An(r),c=An(i),u=An(o),f=o?`(${a} < ${u} ? ${s?\"\":\"-\"}0.5 * (${u} - (${a})) : ${l})`:l;return{signal:(c?`${c} + `:\"\")+(e?`(${e} ? -1 : 1) * `:\"\")+(t?`(${t} + ${f})`:f)}}return r=r||0,i+(n?-r-l:+r+l)}function Cu(e){let{fieldDef:t,scaleName:n,bandPosition:i,offset:r,useRectOffsetField:o}=e;return $o({scaleName:n,fieldOrDatumDef:t,bandPosition:i,offset:r,...o?{startSuffix:Fc,endSuffix:Oc}:{}})}const _u=new Set([\"aria\",\"width\",\"height\"]);function Pu(e,t){const{fill:n,stroke:i}=\"include\"===t.color?pu(e):{};return{...Au(e.markDef,t),...Nu(\"fill\",n),...Nu(\"stroke\",i),...mu(\"opacity\",e),...mu(\"fillOpacity\",e),...mu(\"strokeOpacity\",e),...mu(\"strokeWidth\",e),...mu(\"strokeDash\",e),...gu(e),...su(e),...ou(e,\"href\"),...uu(e)}}function Nu(e,t){return t?{[e]:t}:{}}function Au(e,t){return Dn.reduce(((n,i)=>(!_u.has(i)&&J(e,i)&&\"ignore\"!==t[i]&&(n[i]=Pn(e[i])),n)),{})}function Tu(e){const{config:t,markDef:n}=e,i=new Set;if(e.forEachFieldDef(((r,o)=>{let a;if(!Qt(o)||!(a=e.getScaleType(o)))return;const s=cn(r.aggregate),l=go({scaleChannel:o,markDef:n,config:t,scaleType:a,isCountAggregate:s});if(\"break-paths-filter-domains\"===(c=l)||\"break-paths-show-domains\"===c){const t=e.vgField(o,{expr:\"datum\",binSuffix:e.stack?.impute?\"mid\":void 0});t&&i.add(t)}var c})),i.size>0){return{defined:{signal:[...i].map((e=>ir(e,!0))).join(\" && \")}}}}function ju(e,t){if(void 0!==t)return{[e]:Pn(t)}}const Eu=\"voronoi\",Mu={defined:e=>\"point\"===e.type&&e.nearest,parse:(e,t)="
-  , ">{if(t.events)for(const n of t.events)n.markname=e.getName(Eu)},marks:(e,t,n)=>{const{x:i,y:r}=t.project.hasChannel,o=e.mark;if(Zr(o))return Si(`The \"nearest\" transform is not supported for ${o} marks.`),n;const a={name:e.getName(Eu),type:\"path\",interactive:!0,from:{data:e.getName(\"marks\")},encode:{update:{fill:{value:\"transparent\"},strokeWidth:{value:.35},stroke:{value:\"transparent\"},isVoronoi:{value:!0},...su(e,{reactiveGeom:!0})}},transform:[{type:\"voronoi\",x:{expr:i||!r?\"datum.datum.x || 0\":\"0\"},y:{expr:r||!i?\"datum.datum.y || 0\":\"0\"},size:[e.getSizeSignalRef(\"width\"),e.getSizeSignalRef(\"height\")]}]};let s=0,l=!1;return n.forEach(((t,n)=>{const i=t.name??\"\";i===e.component.mark[0].name?s=n:i.includes(Eu)&&(l=!0)})),l||n.splice(s+1,0,a),n}},Ru={defined:e=>\"point\"===e.type&&\"global\"===e.resolve&&e.bind&&\"scales\"!==e.bind&&!_s(e.bind),parse:(e,t,n)=>of(t,n),topLevelSignals:(e,n,i)=>{const r=n.name,o=n.project,a=n.bind,s=n.init&&n.init[0],l=Mu.defined(n)?\"(item().isVoronoi ? datum.datum : datum)\":\"datum\";return o.items.forEach(((e,o)=>{const c=C(`${r}_${e.field}`);i.filter((e=>e.name===c)).length||i.unshift({name:c,...s?{init:Vc(s[o])}:{value:null},on:n.events?[{events:n.events,update:`datum && item().mark.marktype !== 'group' ? ${l}[${t.stringValue(e.field)}] : null`}]:[],bind:a[e.field]??a[e.channel]??a})})),i},signals:(e,t,n)=>{const i=t.name,r=t.project,o=n.find((e=>e.name===i+Ku)),a=i+Pc,s=r.items.map((e=>C(`${i}_${e.field}`))),l=s.map((e=>`${e} !== null`)).join(\" && \");return s.length&&(o.update=`${l} ? {fields: ${a}, values: [${s.join(\", \")}]} : null`),delete o.value,delete o.on,n}},Lu=\"_toggle\",qu={defined:e=>\"point\"===e.type&&!af(e)&&!!e.toggle,signals:(e,t,n)=>n.concat({name:t.name+Lu,value:!1,on:[{events:t.events,update:t.toggle}]}),modifyExpr:(e,t)=>{const n=t.name+Ku,i=t.name+Lu;return`${i} ? null : ${n}, `+(\"global\"===t.resolve?`${i} ? null : true, `:`${i} ? null : {unit: ${nf(e)}}, `)+`${i} ? ${n} : null`}},Uu={defined:e=>void 0!==e.clear&&!1!==e.clear&&!af(e),parse:(e,n)=>{n.clear&&(n.clear=t.isString(n.clear)?t.parseSelector(n.clear,\"view\"):n.clear)},topLevelSignals:(e,t,n)=>{if(Ru.defined(t))for(const e of t.project.items){const i=n.findIndex((n=>n.name===C(`${t.name}_${e.field}`)));-1!==i&&n[i].on.push({events:t.clear,update:\"null\"})}return n},signals:(e,t,n)=>{function i(e,i){-1!==e&&n[e].on&&n[e].on.push({events:t.clear,update:i})}if(\"interval\"===t.type)for(const e of t.project.items){const t=n.findIndex((t=>t.name===e.signals.visual));if(i(t,\"[0, 0]\"),-1===t){i(n.findIndex((t=>t.name===e.signals.data)),\"null\")}}else{let e=n.findIndex((e=>e.name===t.name+Ku));i(e,\"null\"),qu.defined(t)&&(e=n.findIndex((e=>e.name===t.name+Lu)),i(e,\"false\"))}return n}},Wu={defined:e=>{const t=\"global\"===e.resolve&&e.bind&&_s(e.bind),n=1===e.project.items.length&&e.project.items[0].field!==zs;return t&&!n&&Si(\"Legend bindings are only supported for selections over an individual field or encoding channel.\"),t&&n},parse:(e,n,i)=>{const r=l(i);if(r.select=t.isString(r.select)?{type:r.select,toggle:n.toggle}:{...r.select,toggle:n.toggle},of(n,r),t.isObject(i.select)&&(i.select.on||i.select.clear)){const e='event.item && indexof(event.item.mark.role, \"legend\") < 0';for(const i of n.events)i.filter=t.array(i.filter??[]),i.filter.includes(e)||i.filter.push(e)}const o=Ps(n.bind)?n.bind.legend:\"click\",a=t.isString(o)?t.parseSelector(o,\"view\"):t.array(o);n.bind={legend:{merge:a}}},topLevelSignals:(e,t,n)=>{const i=t.name,r=Ps(t.bind)&&t.bind.legend,o=e=>t=>{const n=l(t);return n.markname=e,n};for(const e of t.project.items){if(!e.hasLegend)continue;const a=`${C(e.field)}_legend`,s=`${i}_${a}`;if(0===n.filter((e=>e.name===s)).length){const e=r.merge.map(o(`${a}_symbols`)).concat(r.merge.map(o(`${a}_labels`))).concat(r.merge.map(o(`${a}_entries`)));n.unshift({name:s,...t.init?{}:{value:null},on:[{events:e,update:\"isDefined(datum.value) ? datum.value : item().items[0].items[0].datum.value\",force:!0},{events:r.merge,update:`!event.item || !datum ? null : ${s}`,force:!0}]})}}return n},signals:(e,t,n)=>{const i=t.name,r=t.project,o=n.fi"
-  , "nd((e=>e.name===i+Ku)),a=i+Pc,s=r.items.filter((e=>e.hasLegend)).map((e=>C(`${i}_${C(e.field)}_legend`))),l=`${s.map((e=>`${e} !== null`)).join(\" && \")} ? {fields: ${a}, values: [${s.join(\", \")}]} : null`;t.events&&s.length>0?o.on.push({events:s.map((e=>({signal:e}))),update:l}):s.length>0&&(o.update=l,delete o.value,delete o.on);const c=n.find((e=>e.name===i+Lu)),u=Ps(t.bind)&&t.bind.legend;return c&&(t.events?c.on.push({...c.on[0],events:u}):c.on[0].events=u),n}};const Iu=\"_translate_anchor\",Bu=\"_translate_delta\",Vu={defined:e=>\"interval\"===e.type&&e.translate,signals:(e,n,i)=>{const r=n.name,o=Qc.defined(n),a=r+Iu,{x:s,y:l}=n.project.hasChannel;let c=t.parseSelector(n.translate,\"scope\");return o||(c=c.map((e=>(e.between[0].markname=r+Zc,e)))),i.push({name:a,value:{},on:[{events:c.map((e=>e.between[0])),update:\"{x: x(unit), y: y(unit)\"+(void 0!==s?`, extent_x: ${o?Jc(e,te):`slice(${s.signals.visual})`}`:\"\")+(void 0!==l?`, extent_y: ${o?Jc(e,ne):`slice(${l.signals.visual})`}`:\"\")+\"}\"}]},{name:r+Bu,value:{},on:[{events:c,update:`{x: ${a}.x - x(unit), y: ${a}.y - y(unit)}`}]}),void 0!==s&&Hu(e,n,s,\"width\",i),void 0!==l&&Hu(e,n,l,\"height\",i),i}};function Hu(e,t,n,i,r){const o=t.name,a=o+Iu,s=o+Bu,l=n.channel,c=Qc.defined(t),u=r.find((e=>e.name===n.signals[c?\"data\":\"visual\"])),f=e.getSizeSignalRef(i).signal,d=e.getScaleComponent(l),m=d&&d.get(\"type\"),p=d&&d.get(\"reverse\"),g=`${a}.extent_${l}`,h=`${c&&d?\"log\"===m?\"panLog\":\"symlog\"===m?\"panSymlog\":\"pow\"===m?\"panPow\":\"panLinear\":\"panLinear\"}(${g}, ${`${c?l===te?p?\"\":\"-\":p?\"-\":\"\":\"\"}${s}.${l} / ${c?`${f}`:`span(${g})`}`}${c?\"pow\"===m?`, ${d.get(\"exponent\")??1}`:\"symlog\"===m?`, ${d.get(\"constant\")??1}`:\"\":\"\"})`;u.on.push({events:{signal:s},update:c?h:`clampRange(${h}, 0, ${f})`})}const Gu=\"_zoom_anchor\",Yu=\"_zoom_delta\",Xu={defined:e=>\"interval\"===e.type&&e.zoom,signals:(e,n,i)=>{const r=n.name,o=Qc.defined(n),a=r+Yu,{x:s,y:l}=n.project.hasChannel,c=t.stringValue(e.scaleName(te)),u=t.stringValue(e.scaleName(ne));let f=t.parseSelector(n.zoom,\"scope\");return o||(f=f.map((e=>(e.markname=r+Zc,e)))),i.push({name:r+Gu,on:[{events:f,update:o?\"{\"+[c?`x: invert(${c}, x(unit))`:\"\",u?`y: invert(${u}, y(unit))`:\"\"].filter((e=>e)).join(\", \")+\"}\":\"{x: x(unit), y: y(unit)}\"}]},{name:a,on:[{events:f,force:!0,update:\"pow(1.001, event.deltaY * pow(16, event.deltaMode))\"}]}),void 0!==s&&Qu(e,n,s,\"width\",i),void 0!==l&&Qu(e,n,l,\"height\",i),i}};function Qu(e,t,n,i,r){const o=t.name,a=n.channel,s=Qc.defined(t),l=r.find((e=>e.name===n.signals[s?\"data\":\"visual\"])),c=e.getSizeSignalRef(i).signal,u=e.getScaleComponent(a),f=u&&u.get(\"type\"),d=s?Jc(e,a):l.name,m=o+Yu,p=`${s&&u?\"log\"===f?\"zoomLog\":\"symlog\"===f?\"zoomSymlog\":\"pow\"===f?\"zoomPow\":\"zoomLinear\":\"zoomLinear\"}(${d}, ${`${o}${Gu}.${a}`}, ${m}${s?\"pow\"===f?`, ${u.get(\"exponent\")??1}`:\"symlog\"===f?`, ${u.get(\"constant\")??1}`:\"\":\"\"})`;l.on.push({events:{signal:m},update:s?p:`clampRange(${p}, 0, ${c})`})}const Ju=\"_store\",Ku=\"_tuple\",Zu=\"_modify\",ef=\"vlSelectionResolve\",tf=[Ic,iu,Ac,qu,Ru,Qc,Wu,Uu,Vu,Xu,Mu];function nf(e){let{escape:n}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{escape:!0},i=n?t.stringValue(e.name):e.name;const r=function(e){let t=e.parent;for(;t&&!Um(t);)t=t.parent;return t}(e);if(r){const{facet:e}=r;for(const n of Be)e[n]&&(i+=` + '__facet_${n}_' + (facet[${t.stringValue(r.vgField(n))}])`)}return i}function rf(e){return F(e.component.selection??{}).reduce(((e,t)=>e||t.project.hasSelectionId),!1)}function of(e,n){!t.isString(n.select)&&n.select.on||delete e.events,!t.isString(n.select)&&n.select.clear||delete e.clear,!t.isString(n.select)&&n.select.toggle||delete e.toggle}function af(e){return e.events?.find((e=>\"type\"in e&&\"timer\"===e.type))}function sf(e){const t=[];return\"Identifier\"===e.type?[e.name]:\"Literal\"===e.type?[e.value]:(\"MemberExpression\"===e.type&&(t.push(...sf(e.object)),t.push(...sf(e.property))),t)}function lf(e){return\"MemberExpression\"===e.object.type?lf(e.object):\"datum\"===e.object.name}function cf(e){const n=t.parseExpression(e),i=new Set;return n.visit((e=>{\"MemberExpression\"===e.type&&lf(e)&&i.add(sf"
-  , "(e).slice(1).join(\".\"))})),i}class uf extends $c{clone(){return new uf(null,this.model,l(this.filter))}constructor(e,t,n){super(e),this.model=t,this.filter=n,this.expr=mf(this.model,this.filter,this),this._dependentFields=cf(this.expr)}dependentFields(){return this._dependentFields}producedFields(){return new Set}assemble(){return{type:\"filter\",expr:this.expr}}hash(){return`Filter ${this.expr}`}}function ff(e,n,i){let r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:\"datum\";const o=t.isString(n)?n:n.param,a=C(o),s=t.stringValue(a+Ju);let l;try{l=e.getSelectionComponent(a,o)}catch(e){return`!!${a}`}if(l.project.timeUnit){const t=i??e.component.data.raw,n=l.project.timeUnit.clone();t.parent?n.insertAsParentOf(t):t.parent=n}const c=`${l.project.hasSelectionId?\"vlSelectionIdTest(\":\"vlSelectionTest(\"}${s}, ${r}${\"global\"===l.resolve?\")\":`, ${t.stringValue(l.resolve)})`}`,u=`length(data(${s}))`;return!1===n.empty?`${u} && ${c}`:`!${u} || ${c}`}function df(e,n,i){const r=C(n),o=i.encoding;let a,s=i.field;try{a=e.getSelectionComponent(r,n)}catch(e){return r}if(o||s){if(o&&!s){const e=a.project.items.filter((e=>e.channel===o));!e.length||e.length>1?(s=a.project.items[0].field,Si(function(e,n,i,r){return(e.length?\"Multiple \":\"No \")+`matching ${t.stringValue(n)} encoding found for selection ${t.stringValue(i.param)}. `+`Using \"field\": ${t.stringValue(r)}.`}(e,o,i,s))):s=e[0].field}}else s=a.project.items[0].field,a.project.items.length>1&&Si(function(e){return`A \"field\" or \"encoding\" must be specified when using a selection as a scale domain. Using \"field\": ${t.stringValue(e)}.`}(s));return`${a.name}[${t.stringValue(M(s))}]`}function mf(e,n,i){return _(n,(n=>t.isString(n)?n:function(e){return J(e,\"param\")}(n)?ff(e,n,i):nr(n)))}function pf(e,t,n,i){e.encode??={},e.encode[t]??={},e.encode[t].update??={},e.encode[t].update[n]=i}function gf(e,n,i){let r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{header:!1};const{disable:o,orient:a,scale:s,labelExpr:l,title:c,zindex:u,...f}=e.combine();if(!o){for(const e in f){const i=e,r=Ea[i],o=f[i];if(r&&r!==n&&\"both\"!==r)delete f[i];else if(Ta(o)){const{condition:e,...n}=o,r=t.array(e),a=Aa[i];if(a){const{vgProp:e,part:t}=a;pf(f,t,e,[...r.map((e=>{const{test:t,...n}=e;return{test:mf(null,t),...n}})),n]),delete f[i]}else if(null===a){const e={signal:r.map((e=>{const{test:t,...n}=e;return`${mf(null,t)} ? ${Nn(n)} : `})).join(\"\")+Nn(n)};f[i]=e}}else if(wn(o)){const e=Aa[i];if(e){const{vgProp:t,part:n}=e;pf(f,n,t,o),delete f[i]}}p([\"labelAlign\",\"labelBaseline\"],i)&&null===f[i]&&delete f[i]}if(\"grid\"===n){if(!f.grid)return;if(f.encode){const{grid:e}=f.encode;f.encode={...e?{grid:e}:{}},S(f.encode)&&delete f.encode}return{scale:s,orient:a,...f,domain:!1,labels:!1,aria:!1,maxExtent:0,minExtent:0,ticks:!1,zindex:U(u,0)}}{if(!r.header&&e.mainExtracted)return;if(void 0!==l){let e=l;f.encode?.labels?.update&&wn(f.encode.labels.update.text)&&(e=R(l,\"datum.label\",f.encode.labels.update.text.signal)),pf(f,\"labels\",\"text\",{signal:e})}if(null===f.labelAlign&&delete f.labelAlign,f.encode){for(const t of ja)e.hasAxisPart(t)||delete f.encode[t];S(f.encode)&&delete f.encode}const n=function(e,n){if(e)return t.isArray(e)&&!$n(e)?e.map((e=>xa(e,n))).join(\", \"):e}(c,i);return{scale:s,orient:a,grid:!1,...n?{title:n}:{},...f,...!1===i.aria?{aria:!1}:{},zindex:U(u,0)}}}}function hf(e){const{axes:t}=e.component,n=[];for(const i of Ct)if(t[i])for(const r of t[i])if(!r.get(\"disable\")&&!r.get(\"gridScale\")){const t=\"x\"===i?\"height\":\"width\",r=e.getSizeSignalRef(t).signal;t!==r&&n.push({name:t,update:r})}return n}function yf(e,t,n,i){return Object.assign.apply(null,[{},...e.map((e=>{if(\"axisOrient\"===e){const e=\"x\"===n?\"bottom\":\"left\",r=t[\"x\"===n?\"axisBottom\":\"axisLeft\"]||{},o=t[\"x\"===n?\"axisTop\":\"axisRight\"]||{},a=new Set([...D(r),...D(o)]),s={};for(const t of a.values())s[t]={signal:`${i.signal} === \"${e}\" ? ${An(r[t])} : ${An(o[t])}`};return s}return t[e]}))])}function vf(e,n){const i=[{}];for(const r of e){let e=n[r]?.style;if(e){e=t.array(e);for(const t of e)i.push(n.style[t])}}return Object.assign.apply(n"
-  , "ull,i)}function bf(e,t,n){let i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};const r=Ln(e,n,t);if(void 0!==r)return{configFrom:\"style\",configValue:r};for(const t of[\"vlOnlyAxisConfig\",\"vgAxisConfig\",\"axisConfigStyle\"])if(void 0!==i[t]?.[e])return{configFrom:t,configValue:i[t][e]};return{}}const xf={scale:e=>{let{model:t,channel:n}=e;return t.scaleName(n)},format:e=>{let{format:t}=e;return t},formatType:e=>{let{formatType:t}=e;return t},grid:e=>{let{fieldOrDatumDef:t,axis:n,scaleType:i}=e;return n.grid??function(e,t){return!kr(e)&&Zo(t)&&!mn(t?.bin)&&!pn(t?.bin)}(i,t)},gridScale:e=>{let{model:t,channel:n}=e;return function(e,t){const n=\"x\"===t?\"y\":\"x\";if(e.getScaleComponent(n))return e.scaleName(n);return}(t,n)},labelAlign:e=>{let{axis:t,labelAngle:n,orient:i,channel:r}=e;return t.labelAlign||kf(n,i,r)},labelAngle:e=>{let{labelAngle:t}=e;return t},labelBaseline:e=>{let{axis:t,labelAngle:n,orient:i,channel:r}=e;return t.labelBaseline||wf(n,i,r)},labelFlush:e=>{let{axis:t,fieldOrDatumDef:n,channel:i}=e;return t.labelFlush??function(e,t){if(\"x\"===t&&p([\"quantitative\",\"temporal\"],e))return!0;return}(n.type,i)},labelOverlap:e=>{let{axis:n,fieldOrDatumDef:i,scaleType:r}=e;return n.labelOverlap??function(e,n,i,r){if(i&&!t.isObject(r)||\"nominal\"!==e&&\"ordinal\"!==e)return\"log\"!==n&&\"symlog\"!==n||\"greedy\";return}(i.type,r,Zo(i)&&!!i.timeUnit,Zo(i)?i.sort:void 0)},orient:e=>{let{orient:t}=e;return t},tickCount:e=>{let{channel:t,model:n,axis:i,fieldOrDatumDef:r,scaleType:o}=e;const a=\"x\"===t?\"width\":\"y\"===t?\"height\":void 0,s=a?n.getSizeSignalRef(a):void 0;return i.tickCount??function(e){let{fieldOrDatumDef:t,scaleType:n,size:i,values:r}=e;if(!r&&!kr(n)&&\"log\"!==n){if(Zo(t)){if(mn(t.bin))return{signal:`ceil(${i.signal}/10)`};if(t.timeUnit&&p([\"month\",\"hours\",\"day\",\"quarter\"],Ii(t.timeUnit)?.unit))return}return{signal:`ceil(${i.signal}/40)`}}return}({fieldOrDatumDef:r,scaleType:o,size:s,values:i.values})},tickMinStep:function(e){let{format:t,fieldOrDatumDef:n}=e;if(\"d\"===t)return 1;if(Zo(n)){const{timeUnit:e}=n;if(e){const t=Bi(e);if(t)return{signal:t}}}return},title:e=>{let{axis:t,model:n,channel:i}=e;if(void 0!==t.title)return t.title;const r=Sf(n,i);if(void 0!==r)return r;const o=n.typedFieldDef(i),a=\"x\"===i?\"x2\":\"y2\",s=n.fieldDef(a);return Un(o?[Bo(o)]:[],Zo(s)?[Bo(s)]:[])},values:e=>{let{axis:n,fieldOrDatumDef:i}=e;return function(e,n){const i=e.values;if(t.isArray(i))return Pa(n,i);if(wn(i))return i;return}(n,i)},zindex:e=>{let{axis:t,fieldOrDatumDef:n,mark:i}=e;return t.zindex??function(e,t){if(\"rect\"===e&&pa(t))return 1;return 0}(i,n)}};function $f(e){return`(((${e.signal} % 360) + 360) % 360)`}function wf(e,t,n,i){if(void 0!==e){if(\"x\"===n){if(wn(e)){const n=$f(e);return{signal:`(45 < ${n} && ${n} < 135) || (225 < ${n} && ${n} < 315) ? \"middle\" :(${n} <= 45 || 315 <= ${n}) === ${wn(t)?`(${t.signal} === \"top\")`:\"top\"===t} ? \"bottom\" : \"top\"`}}if(45<e&&e<135||225<e&&e<315)return\"middle\";if(wn(t)){const n=e<=45||315<=e?\"===\":\"!==\";return{signal:`${t.signal} ${n} \"top\" ? \"bottom\" : \"top\"`}}return(e<=45||315<=e)==(\"top\"===t)?\"bottom\":\"top\"}if(wn(e)){const n=$f(e);return{signal:`${n} <= 45 || 315 <= ${n} || (135 <= ${n} && ${n} <= 225) ? ${i?'\"middle\"':\"null\"} : (45 <= ${n} && ${n} <= 135) === ${wn(t)?`(${t.signal} === \"left\")`:\"left\"===t} ? \"top\" : \"bottom\"`}}if(e<=45||315<=e||135<=e&&e<=225)return i?\"middle\":null;if(wn(t)){const n=45<=e&&e<=135?\"===\":\"!==\";return{signal:`${t.signal} ${n} \"left\" ? \"top\" : \"bottom\"`}}return(45<=e&&e<=135)==(\"left\"===t)?\"top\":\"bottom\"}}function kf(e,t,n){if(void 0===e)return;const i=\"x\"===n,r=i?0:90,o=i?\"bottom\":\"left\";if(wn(e)){const n=$f(e);return{signal:`(${r?`(${n} + 90)`:n} % 180 === 0) ? ${i?null:'\"center\"'} :(${r} < ${n} && ${n} < ${180+r}) === ${wn(t)?`(${t.signal} === \"${o}\")`:t===o} ? \"left\" : \"right\"`}}if((e+r)%180==0)return i?null:\"center\";if(wn(t)){const n=r<e&&e<180+r?\"===\":\"!==\";return{signal:`${`${t.signal} ${n} \"${o}\"`} ? \"left\" : \"right\"`}}return(r<e&&e<180+r)==(t===o)?\"left\":\"right\"}function Sf(e,t){const n=\"x\"===t?\"x2\":\"y2\",i=e.fieldDef(t),r=e.fieldDef(n),o=i?i.title:"
-  , "void 0,a=r?r.title:void 0;return o&&a?Wn(o,a):o||(a||(void 0!==o?o:void 0!==a?a:void 0))}class Df extends $c{clone(){return new Df(null,l(this.transform))}constructor(e,t){super(e),this.transform=t,this._dependentFields=cf(this.transform.calculate)}static parseAllForSortIndex(e,t){return t.forEachFieldDef(((t,n)=>{if(la(t)&&qo(t.sort)){const{field:i,timeUnit:r}=t,o=t.sort,a=o.map(((e,t)=>`${nr({field:i,timeUnit:r,equal:e})} ? ${t} : `)).join(\"\")+o.length;e=new Df(e,{calculate:a,as:Ff(t,n,{forAs:!0})})}})),e}producedFields(){return new Set([this.transform.as])}dependentFields(){return this._dependentFields}assemble(){return{type:\"formula\",expr:this.transform.calculate,as:this.transform.as}}hash(){return`Calculate ${d(this.transform)}`}}function Ff(e,t,n){return ma(e,{prefix:t,suffix:\"sort_index\",...n})}function Of(e,t){return p([\"top\",\"bottom\"],t)?\"column\":p([\"left\",\"right\"],t)||\"row\"===e?\"row\":\"column\"}function zf(e,t,n,i){const r=\"row\"===i?n.headerRow:\"column\"===i?n.headerColumn:n.headerFacet;return U((t||{})[e],r[e],n.header[e])}function Cf(e,t,n,i){const r={};for(const o of e){const e=zf(o,t||{},n,i);void 0!==e&&(r[o]=e)}return r}const _f=[\"row\",\"column\"],Pf=[\"header\",\"footer\"];function Nf(e,t){const n=e.component.layoutHeaders[t].title,i=e.config?e.config:void 0,r=e.component.layoutHeaders[t].facetFieldDef?e.component.layoutHeaders[t].facetFieldDef:void 0,{titleAnchor:o,titleAngle:a,titleOrient:s}=Cf([\"titleAnchor\",\"titleAngle\",\"titleOrient\"],r.header,i,t),l=Of(t,s),c=H(a);return{name:`${t}-title`,type:\"group\",role:`${l}-title`,title:{text:n,...\"row\"===t?{orient:\"left\"}:{},style:\"guide-title\",...Tf(c,l),...Af(l,c,o),...Uf(i,r,t,Ss,ws)}}}function Af(e,t){switch(arguments.length>2&&void 0!==arguments[2]?arguments[2]:\"middle\"){case\"start\":return{align:\"left\"};case\"end\":return{align:\"right\"}}const n=kf(t,\"row\"===e?\"left\":\"top\",\"row\"===e?\"y\":\"x\");return n?{align:n}:{}}function Tf(e,t){const n=wf(e,\"row\"===t?\"left\":\"top\",\"row\"===t?\"y\":\"x\",!0);return n?{baseline:n}:{}}function jf(e,t){const n=e.component.layoutHeaders[t],i=[];for(const r of Pf)if(n[r])for(const o of n[r]){const a=Rf(e,t,r,n,o);null!=a&&i.push(a)}return i}function Ef(e,n){const{sort:i}=e;return Lo(i)?{field:ma(i,{expr:\"datum\"}),order:i.order??\"ascending\"}:t.isArray(i)?{field:Ff(e,n,{expr:\"datum\"}),order:\"ascending\"}:{field:ma(e,{expr:\"datum\"}),order:i??\"ascending\"}}function Mf(e,t,n){const{format:i,formatType:r,labelAngle:o,labelAnchor:a,labelOrient:s,labelExpr:l}=Cf([\"format\",\"formatType\",\"labelAngle\",\"labelAnchor\",\"labelOrient\",\"labelExpr\"],e.header,n,t),c=Oo({fieldOrDatumDef:e,format:i,formatType:r,expr:\"parent\",config:n}).signal,u=Of(t,s);return{text:{signal:l?R(R(l,\"datum.label\",c),\"datum.value\",ma(e,{expr:\"parent\"})):c},...\"row\"===t?{orient:\"left\"}:{},style:\"guide-label\",frame:\"group\",...Tf(o,u),...Af(u,o,a),...Uf(n,e,t,Ds,ks)}}function Rf(e,t,n,i,r){if(r){let o=null;const{facetFieldDef:a}=i,s=e.config?e.config:void 0;if(a&&r.labels){const{labelOrient:e}=Cf([\"labelOrient\"],a.header,s,t);(\"row\"===t&&!p([\"top\",\"bottom\"],e)||\"column\"===t&&!p([\"left\",\"right\"],e))&&(o=Mf(a,t,s))}const l=Um(e)&&!Uo(e.facet),c=r.axes,u=c?.length>0;if(o||u){const s=\"row\"===t?\"height\":\"width\";return{name:e.getName(`${t}_${n}`),type:\"group\",role:`${t}-${n}`,...i.facetFieldDef?{from:{data:e.getName(`${t}_domain`)},sort:Ef(a,t)}:{},...u&&l?{from:{data:e.getName(`facet_domain_${t}`)}}:{},...o?{title:o}:{},...r.sizeSignal?{encode:{update:{[s]:r.sizeSignal}}}:{},...u?{axes:c}:{}}}}return null}const Lf={column:{start:0,end:1},row:{start:1,end:0}};function qf(e,t){return Lf[t][e]}function Uf(e,t,n,i,r){const o={};for(const a of i){if(!r[a])continue;const i=zf(a,t?.header,e,n);void 0!==i&&(o[r[a]]=i)}return o}function Wf(e){return[...If(e,\"width\"),...If(e,\"height\"),...If(e,\"childWidth\"),...If(e,\"childHeight\")]}function If(e,t){const n=\"width\"===t?\"x\":\"y\",i=e.component.layoutSize.get(t);if(!i||\"merged\"===i)return[];const r=e.getSizeSignalRef(t).signal;if(\"step\"===i){const t=e.getScaleComponent(n);if(t){const i=t.get(\"type\"),o=t.get(\"range\");if(kr(i)&&kn(o)){const i=e.scaleName(n);if(Um(e"
-  , ".parent)){if(\"independent\"===e.parent.component.resolve.scale[n])return[Bf(i,o)]}return[Bf(i,o),{name:r,update:Vf(i,t,`domain('${i}').length`)}]}}throw new Error(\"layout size is step although width/height is not step.\")}if(\"container\"==i){const t=r.endsWith(\"width\"),n=t?\"containerSize()[0]\":\"containerSize()[1]\",i=`isFinite(${n}) ? ${n} : ${Us(e.config.view,t?\"width\":\"height\")}`;return[{name:r,init:i,on:[{update:i,events:\"window:resize\"}]}]}return[{name:r,value:i}]}function Bf(e,t){const n=`${e}_step`;return wn(t.step)?{name:n,update:t.step.signal}:{name:n,value:t.step}}function Vf(e,t,n){const i=t.get(\"type\"),r=t.get(\"padding\"),o=U(t.get(\"paddingOuter\"),r);let a=t.get(\"paddingInner\");return a=\"band\"===i?void 0!==a?a:r:1,`bandspace(${n}, ${An(a)}, ${An(o)}) * ${e}_step`}function Hf(e){return\"childWidth\"===e?\"width\":\"childHeight\"===e?\"height\":e}function Gf(e,t){return D(e).reduce(((n,i)=>({...n,...ru({model:t,channelDef:e[i],vgChannel:i,mainRefFn:e=>Pn(e.value),invalidValueRef:void 0})})),{})}function Yf(e,t){if(Um(t))return\"theta\"===e?\"independent\":\"shared\";if(Im(t))return\"shared\";if(Wm(t))return _t(e)||\"theta\"===e||\"radius\"===e?\"independent\":\"shared\";throw new Error(\"invalid model type for resolve\")}function Xf(e,t){const n=e.scale[t],i=_t(t)?\"axis\":\"legend\";return\"independent\"===n?(\"shared\"===e[i][t]&&Si(function(e){return`Setting the scale to be independent for \"${e}\" means we also have to set the guide (axis or legend) to be independent.`}(t)),\"independent\"):e[i][t]||\"shared\"}const Qf=D({aria:1,clipHeight:1,columnPadding:1,columns:1,cornerRadius:1,description:1,direction:1,fillColor:1,format:1,formatType:1,gradientLength:1,gradientOpacity:1,gradientStrokeColor:1,gradientStrokeWidth:1,gradientThickness:1,gridAlign:1,labelAlign:1,labelBaseline:1,labelColor:1,labelFont:1,labelFontSize:1,labelFontStyle:1,labelFontWeight:1,labelLimit:1,labelOffset:1,labelOpacity:1,labelOverlap:1,labelPadding:1,labelSeparation:1,legendX:1,legendY:1,offset:1,orient:1,padding:1,rowPadding:1,strokeColor:1,symbolDash:1,symbolDashOffset:1,symbolFillColor:1,symbolLimit:1,symbolOffset:1,symbolOpacity:1,symbolSize:1,symbolStrokeColor:1,symbolStrokeWidth:1,symbolType:1,tickCount:1,tickMinStep:1,title:1,titleAlign:1,titleAnchor:1,titleBaseline:1,titleColor:1,titleFont:1,titleFontSize:1,titleFontStyle:1,titleFontWeight:1,titleLimit:1,titleLineHeight:1,titleOpacity:1,titleOrient:1,titlePadding:1,type:1,values:1,zindex:1,disable:1,labelExpr:1,selections:1,opacity:1,shape:1,stroke:1,fill:1,size:1,strokeWidth:1,strokeDash:1,encode:1});class Jf extends oc{}const Kf={symbols:function(e,n){let{fieldOrDatumDef:i,model:r,channel:o,legendCmpt:a,legendType:s}=n;if(\"symbol\"!==s)return;const{markDef:l,encoding:c,config:u,mark:f}=r,d=l.filled&&\"trail\"!==f;let m={...Tn({},r,io),...pu(r,{filled:d})};const p=a.get(\"symbolOpacity\")??u.legend.symbolOpacity,g=a.get(\"symbolFillColor\")??u.legend.symbolFillColor,h=a.get(\"symbolStrokeColor\")??u.legend.symbolStrokeColor,y=void 0===p?Zf(c.opacity)??l.opacity:void 0;if(m.fill)if(\"fill\"===o||d&&o===he)delete m.fill;else if(J(m.fill,\"field\"))g?delete m.fill:(m.fill=Pn(u.legend.symbolBaseFillColor??\"black\"),m.fillOpacity=Pn(y??1));else if(t.isArray(m.fill)){const e=ed(c.fill??c.color)??l.fill??(d&&l.color);e&&(m.fill=Pn(e))}if(m.stroke)if(\"stroke\"===o||!d&&o===he)delete m.stroke;else if(J(m.stroke,\"field\")||h)delete m.stroke;else if(t.isArray(m.stroke)){const e=U(ed(c.stroke||c.color),l.stroke,d?l.color:void 0);e&&(m.stroke={value:e})}if(o!==we){const e=Zo(i)&&nd(r,a,i);e?m.opacity=[{test:e,...Pn(y??1)},Pn(u.legend.unselectedOpacity)]:y&&(m.opacity=Pn(y))}return m={...m,...e},S(m)?void 0:m},gradient:function(e,t){let{model:n,legendType:i,legendCmpt:r}=t;if(\"gradient\"!==i)return;const{config:o,markDef:a,encoding:s}=n;let l={};const c=void 0===(r.get(\"gradientOpacity\")??o.legend.gradientOpacity)?Zf(s.opacity)||a.opacity:void 0;c&&(l.opacity=Pn(c));return l={...l,...e},S(l)?void 0:l},labels:function(e,t){let{fieldOrDatumDef:n,model:i,channel:r,legendCmpt:o}=t;const a=i.legend(r)||{},s=i.config,l=Zo(n)?nd(i,o,n):void 0,c=l?[{test:l,va"
-  , "lue:1},{value:s.legend.unselectedOpacity}]:void 0,{format:u,formatType:f}=a;let d;So(f)?d=Co({fieldOrDatumDef:n,field:\"datum.value\",format:u,formatType:f,config:s}):void 0===u&&void 0===f&&s.customFormatTypes&&(\"quantitative\"===n.type&&s.numberFormatType?d=Co({fieldOrDatumDef:n,field:\"datum.value\",format:s.numberFormat,formatType:s.numberFormatType,config:s}):\"temporal\"===n.type&&s.timeFormatType&&Zo(n)&&void 0===n.timeUnit&&(d=Co({fieldOrDatumDef:n,field:\"datum.value\",format:s.timeFormat,formatType:s.timeFormatType,config:s})));const m={...c?{opacity:c}:{},...d?{text:d}:{},...e};return S(m)?void 0:m},entries:function(e,t){let{legendCmpt:n}=t;const i=n.get(\"selections\");return i?.length?{...e,fill:{value:\"transparent\"}}:e}};function Zf(e){return td(e,((e,t)=>Math.max(e,t.value)))}function ed(e){return td(e,((e,t)=>U(e,t.value)))}function td(e,n){return function(e){const n=e?.condition;return!!n&&(t.isArray(n)||sa(n))}(e)?t.array(e.condition).reduce(n,e.value):sa(e)?e.value:void 0}function nd(e,n,i){const r=n.get(\"selections\");if(!r?.length)return;const o=t.stringValue(i.field);return r.map((e=>`(!length(data(${t.stringValue(C(e)+Ju)})) || (${e}[${o}] && indexof(${e}[${o}], datum.value) >= 0))`)).join(\" || \")}const id={direction:e=>{let{direction:t}=e;return t},format:e=>{let{fieldOrDatumDef:t,legend:n,config:i}=e;const{format:r,formatType:o}=n;return _o(t,t.type,r,o,i,!1)},formatType:e=>{let{legend:t,fieldOrDatumDef:n,scaleType:i}=e;const{formatType:r}=t;return Po(r,n,i)},gradientLength:e=>{const{legend:t,legendConfig:n}=e;return t.gradientLength??n.gradientLength??function(e){let{legendConfig:t,model:n,direction:i,orient:r,scaleType:o}=e;const{gradientHorizontalMaxLength:a,gradientHorizontalMinLength:s,gradientVerticalMaxLength:l,gradientVerticalMinLength:c}=t;if(Dr(o))return\"horizontal\"===i?\"top\"===r||\"bottom\"===r?ad(n,\"width\",s,a):s:ad(n,\"height\",c,l);return}(e)},labelOverlap:e=>{let{legend:t,legendConfig:n,scaleType:i}=e;return t.labelOverlap??n.labelOverlap??function(e){if(p([\"quantile\",\"threshold\",\"log\",\"symlog\"],e))return\"greedy\";return}(i)},symbolType:e=>{let{legend:t,markDef:n,channel:i,encoding:r}=e;return t.symbolType??function(e,t,n,i){if(\"shape\"!==t){const e=ed(n)??i;if(e)return e}switch(e){case\"bar\":case\"rect\":case\"image\":case\"square\":return\"square\";case\"line\":case\"trail\":case\"rule\":return\"stroke\";case\"arc\":case\"point\":case\"circle\":case\"tick\":case\"geoshape\":case\"area\":case\"text\":return\"circle\"}}(n.type,i,r.shape,n.shape)},title:e=>{let{fieldOrDatumDef:t,config:n}=e;return va(t,n,{allowDisabling:!0})},type:e=>{let{legendType:t,scaleType:n,channel:i}=e;if(We(i)&&Dr(n)){if(\"gradient\"===t)return}else if(\"symbol\"===t)return;return t},values:e=>{let{fieldOrDatumDef:n,legend:i}=e;return function(e,n){const i=e.values;if(t.isArray(i))return Pa(n,i);if(wn(i))return i;return}(i,n)}};function rd(e){const{legend:t}=e;return U(t.type,function(e){let{channel:t,timeUnit:n,scaleType:i}=e;if(We(t)){if(p([\"quarter\",\"month\",\"day\"],n))return\"symbol\";if(Dr(i))return\"gradient\"}return\"symbol\"}(e))}function od(e){let{legendConfig:t,legendType:n,orient:i,legend:r}=e;return r.direction??t[n?\"gradientDirection\":\"symbolDirection\"]??function(e,t){switch(e){case\"top\":case\"bottom\":return\"horizontal\";case\"left\":case\"right\":case\"none\":case void 0:return;default:return\"gradient\"===t?\"horizontal\":void 0}}(i,n)}function ad(e,t,n,i){return{signal:`clamp(${e.getSizeSignalRef(t).signal}, ${n}, ${i})`}}function sd(e){const t=qm(e)?function(e){const{encoding:t}=e,n={};for(const i of[he,...Os]){const r=ka(t[i]);r&&e.getScaleComponent(i)&&(i===be&&Zo(r)&&r.type===fr||(n[i]=cd(e,i)))}return n}(e):function(e){const{legends:t,resolve:n}=e.component;for(const i of e.children){sd(i);for(const r of D(i.component.legends))n.legend[r]=Xf(e.component.resolve,r),\"shared\"===n.legend[r]&&(t[r]=ud(t[r],i.component.legends[r]),t[r]||(n.legend[r]=\"independent\",delete t[r]))}for(const i of D(t))for(const t of e.children)t.component.legends[i]&&\"shared\"===n.legend[i]&&delete t.component.legends[i];return t}(e);return e.component.legends=t,t}function ld(e,t,n,i){swit"
-  , "ch(t){case\"disable\":return void 0!==n;case\"values\":return!!n?.values;case\"title\":if(\"title\"===t&&e===i?.title)return!0}return e===(n||{})[t]}function cd(e,t){let n=e.legend(t);const{markDef:i,encoding:r,config:o}=e,a=o.legend,s=new Jf({},function(e,t){const n=e.scaleName(t);if(\"trail\"===e.mark){if(\"color\"===t)return{stroke:n};if(\"size\"===t)return{strokeWidth:n}}return\"color\"===t?e.markDef.filled?{fill:n}:{stroke:n}:{[t]:n}}(e,t));!function(e,t,n){const i=e.fieldDef(t)?.field;for(const r of F(e.component.selection??{})){const e=r.project.hasField[i]??r.project.hasChannel[t];if(e&&Wu.defined(r)){const t=n.get(\"selections\")??[];t.push(r.name),n.set(\"selections\",t,!1),e.hasLegend=!0}}}(e,t,s);const l=void 0!==n?!n:a.disable;if(s.set(\"disable\",l,void 0!==n),l)return s;n=n||{};const c=e.getScaleComponent(t).get(\"type\"),u=ka(r[t]),f=Zo(u)?Ii(u.timeUnit)?.unit:void 0,d=n.orient||o.legend.orient||\"right\",m=rd({legend:n,channel:t,timeUnit:f,scaleType:c}),p={legend:n,channel:t,model:e,markDef:i,encoding:r,fieldOrDatumDef:u,legendConfig:a,config:o,scaleType:c,orient:d,legendType:m,direction:od({legend:n,legendType:m,orient:d,legendConfig:a})};for(const i of Qf){if(\"gradient\"===m&&i.startsWith(\"symbol\")||\"symbol\"===m&&i.startsWith(\"gradient\"))continue;const r=i in id?id[i](p):n[i];if(void 0!==r){const a=ld(r,i,n,e.fieldDef(t));(a||void 0===o.legend[i])&&s.set(i,r,a)}}const g=n?.encoding??{},h=s.get(\"selections\"),y={},v={fieldOrDatumDef:u,model:e,channel:t,legendCmpt:s,legendType:m};for(const t of[\"labels\",\"legend\",\"title\",\"symbols\",\"gradient\",\"entries\"]){const n=Gf(g[t]??{},e),i=t in Kf?Kf[t](n,v):n;void 0===i||S(i)||(y[t]={...h?.length&&Zo(u)?{name:`${C(u.field)}_legend_${t}`}:{},...h?.length?{interactive:!!h}:{},update:i})}return S(y)||s.set(\"encode\",y,!!n?.encoding),s}function ud(e,t){if(!e)return t.clone();const n=e.getWithExplicit(\"orient\"),i=t.getWithExplicit(\"orient\");if(n.explicit&&i.explicit&&n.value!==i.value)return;let r=!1;for(const n of Qf){const i=uc(e.getWithExplicit(n),t.getWithExplicit(n),n,\"legend\",((e,t)=>{switch(n){case\"symbolType\":return fd(e,t);case\"title\":return In(e,t);case\"type\":return r=!0,sc(\"symbol\")}return cc(e,t,n,\"legend\")}));e.setWithExplicit(n,i)}return r&&(e.implicit?.encode?.gradient&&P(e.implicit,[\"encode\",\"gradient\"]),e.explicit?.encode?.gradient&&P(e.explicit,[\"encode\",\"gradient\"])),e}function fd(e,t){return\"circle\"===t.value?t:e}function dd(e){const t=e.component.legends,n={};for(const i of D(t)){const r=Q(e.getScaleComponent(i).get(\"domains\"));if(n[r])for(const e of n[r]){ud(e,t[i])||n[r].push(t[i])}else n[r]=[t[i].clone()]}return F(n).flat().map((t=>function(e,t){const{disable:n,labelExpr:i,selections:r,...o}=e.combine();if(n)return;!1===t.aria&&null==o.aria&&(o.aria=!1);if(o.encode?.symbols){const e=o.encode.symbols.update;!e.fill||\"transparent\"===e.fill.value||e.stroke||o.stroke||(e.stroke={value:\"transparent\"});for(const t of Os)o[t]&&delete e[t]}o.title||delete o.title;if(void 0!==i){let e=i;o.encode?.labels?.update&&wn(o.encode.labels.update.text)&&(e=R(i,\"datum.label\",o.encode.labels.update.text.signal)),function(e,t,n,i){e.encode??={},e.encode[t]??={},e.encode[t].update??={},e.encode[t].update[n]=i}(o,\"labels\",\"text\",{signal:e})}return o}(t,e.config))).filter((e=>void 0!==e))}function md(e){return Im(e)||Wm(e)?function(e){return e.children.reduce(((e,t)=>e.concat(t.assembleProjections())),pd(e))}(e):pd(e)}function pd(e){const t=e.component.projection;if(!t||t.merged)return[];const n=t.combine(),{name:i}=n;if(t.data){const r={signal:`[${t.size.map((e=>e.signal)).join(\", \")}]`},o=t.data.reduce(((t,n)=>{const i=wn(n)?n.signal:`data('${e.lookupDataSource(n)}')`;return p(t,i)||t.push(i),t}),[]);if(o.length<=0)throw new Error(\"Projection's fit didn't find any data sources\");return[{name:i,size:r,fit:{signal:o.length>1?`[${o.join(\", \")}]`:o[0]},...n}]}return[{name:i,translate:{signal:\"[width / 2, height / 2]\"},...n}]}const gd=[\"type\",\"clipAngle\",\"clipExtent\",\"center\",\"rotate\",\"precision\",\"reflectX\",\"reflectY\",\"coefficient\",\"distance\",\"fraction\",\"lobes\",\"parallel\",\"radius\",\"ratio\",\"spacing\",\"ti"
-  , "lt\"];class hd extends oc{merged=!1;constructor(e,t,n,i){super({...t},{name:e}),this.specifiedProjection=t,this.size=n,this.data=i}get isFit(){return!!this.data}}function yd(e){e.component.projection=qm(e)?function(e){if(e.hasProjection){const t=bn(e.specifiedProjection),n=!(t&&(null!=t.scale||null!=t.translate)),i=n?[e.getSizeSignalRef(\"width\"),e.getSizeSignalRef(\"height\")]:void 0,r=n?function(e){const t=[],{encoding:n}=e;for(const i of[[de,fe],[pe,me]])(ka(n[i[0]])||ka(n[i[1]]))&&t.push({signal:e.getName(`geojson_${t.length}`)});e.channelHasField(be)&&e.typedFieldDef(be).type===fr&&t.push({signal:e.getName(`geojson_${t.length}`)});0===t.length&&t.push(e.requestDataName(bc.Main));return t}(e):void 0,o=new hd(e.projectionName(!0),{...bn(e.config.projection),...t},i,r);return o.get(\"type\")||o.set(\"type\",\"equalEarth\",!1),o}return}(e):function(e){if(0===e.children.length)return;let n;for(const t of e.children)yd(t);const i=h(e.children,(e=>{const i=e.component.projection;if(i){if(n){const e=function(e,n){const i=h(gd,(i=>!t.hasOwnProperty(e.explicit,i)&&!t.hasOwnProperty(n.explicit,i)||!!(t.hasOwnProperty(e.explicit,i)&&t.hasOwnProperty(n.explicit,i)&&X(e.get(i),n.get(i)))));if(X(e.size,n.size)){if(i)return e;if(X(e.explicit,{}))return n;if(X(n.explicit,{}))return e}return null}(n,i);return e&&(n=e),!!e}return n=i,!0}return!0}));if(n&&i){const t=e.projectionName(!0),i=new hd(t,n.specifiedProjection,n.size,l(n.data));for(const n of e.children){const e=n.component.projection;e&&(e.isFit&&i.data.push(...n.component.projection.data),n.renameProjection(e.get(\"name\"),t),e.merged=!0)}return i}return}(e)}function vd(e,t,n,i){if(Na(t,n)){const r=qm(e)?e.axis(n)??e.legend(n)??{}:{},o=ma(t,{expr:\"datum\"}),a=ma(t,{expr:\"datum\",binSuffix:\"end\"});return{formulaAs:ma(t,{binSuffix:\"range\",forAs:!0}),formula:jo(o,a,r.format,r.formatType,i)}}return{}}function bd(e,t){return`${dn(e)}_${t}`}function xd(e,t,n){const i=bd(Oa(n,void 0)??{},t);return e.getName(`${i}_bins`)}function $d(e,n,i){let r,o;r=function(e){return\"as\"in e}(e)?t.isString(e.as)?[e.as,`${e.as}_end`]:[e.as[0],e.as[1]]:[ma(e,{forAs:!0}),ma(e,{binSuffix:\"end\",forAs:!0})];const a={...Oa(n,void 0)},s=bd(a,e.field),{signal:l,extentSignal:c}=function(e,t){return{signal:e.getName(`${t}_bins`),extentSignal:e.getName(`${t}_extent`)}}(i,s);if(hn(a.extent)){const e=a.extent;o=df(i,e.param,e),delete a.extent}return{key:s,binComponent:{bin:a,field:e.field,as:[r],...l?{signal:l}:{},...c?{extentSignal:c}:{},...o?{span:o}:{}}}}class wd extends $c{clone(){return new wd(null,l(this.bins))}constructor(e,t){super(e),this.bins=t}static makeFromEncoding(e,t){const n=t.reduceFieldDef(((e,n,i)=>{if(aa(n)&&mn(n.bin)){const{key:r,binComponent:o}=$d(n,n.bin,t);e[r]={...o,...e[r],...vd(t,n,i,t.config)}}return e}),{});return S(n)?null:new wd(e,n)}static makeFromTransform(e,t,n){const{key:i,binComponent:r}=$d(t,t.bin,n);return new wd(e,{[i]:r})}merge(e,t){for(const n of D(e.bins))n in this.bins?(t(e.bins[n].signal,this.bins[n].signal),this.bins[n].as=b([...this.bins[n].as,...e.bins[n].as],d)):this.bins[n]=e.bins[n];for(const t of e.children)e.removeChild(t),t.parent=this;e.remove()}producedFields(){return new Set(F(this.bins).map((e=>e.as)).flat(2))}dependentFields(){return new Set(F(this.bins).map((e=>e.field)))}hash(){return`Bin ${d(this.bins)}`}assemble(){return F(this.bins).flatMap((e=>{const t=[],[n,...i]=e.as,{extent:r,...o}=e.bin,a={type:\"bin\",field:M(e.field),as:n,signal:e.signal,...hn(r)?{extent:null}:{extent:r},...e.span?{span:{signal:`span(${e.span})`}}:{},...o};!r&&e.extentSignal&&(t.push({type:\"extent\",field:M(e.field),signal:e.extentSignal}),a.extent={signal:e.extentSignal}),t.push(a);for(const e of i)for(let i=0;i<2;i++)t.push({type:\"formula\",expr:ma({field:n[i]},{expr:\"datum\"}),as:e[i]});return e.formula&&t.push({type:\"formula\",expr:e.formula,as:e.formulaAs}),t}))}}function kd(e,n,i,r){const o=qm(r)?r.encoding[at(n)]:void 0;if(aa(i)&&qm(r)&&Yo(i,o,r.markDef,r.config)){e.add(ma(i,{})),e.add(ma(i,{suffix:\"end\"}));const{mark:t,markDef:o,config:a}=r,s=Ho({fieldDef:i,markDef:o,config:a});eo(t)&&.5!"
-  , "==s&&_t(n)&&(e.add(ma(i,{suffix:Fc})),e.add(ma(i,{suffix:Oc}))),i.bin&&Na(i,n)&&e.add(ma(i,{binSuffix:\"range\"}))}else if(Le(n)){const t=Re(n);e.add(r.getName(t))}else e.add(ma(i));return la(i)&&function(e){return t.isObject(e)&&\"field\"in e}(i.scale?.range)&&e.add(i.scale.range.field),e}class Sd extends $c{clone(){return new Sd(null,new Set(this.dimensions),l(this.measures))}constructor(e,t,n){super(e),this.dimensions=t,this.measures=n}get groupBy(){return this.dimensions}static makeFromEncoding(e,t){let n=!1;t.forEachFieldDef((e=>{e.aggregate&&(n=!0)}));const i={},r=new Set;return n?(t.forEachFieldDef(((e,n)=>{const{aggregate:o,field:a}=e;if(o)if(\"count\"===o)i[\"*\"]??={},i[\"*\"].count=new Set([ma(e,{forAs:!0})]);else{if(on(o)||an(o)){const e=on(o)?\"argmin\":\"argmax\",t=o[e];i[t]??={},i[t][e]=new Set([ma({op:e,field:t},{forAs:!0})])}else i[a]??={},i[a][o]=new Set([ma(e,{forAs:!0})]);Qt(n)&&\"unaggregated\"===t.scaleDomain(n)&&(i[a]??={},i[a].min=new Set([ma({field:a,aggregate:\"min\"},{forAs:!0})]),i[a].max=new Set([ma({field:a,aggregate:\"max\"},{forAs:!0})]))}else kd(r,n,e,t)})),r.size+D(i).length===0?null:new Sd(e,r,i)):null}static makeFromTransform(e,t){const n=new Set,i={};for(const e of t.aggregate){const{op:t,field:n,as:r}=e;t&&(\"count\"===t?(i[\"*\"]??={},i[\"*\"].count=new Set([r||ma(e,{forAs:!0})])):(i[n]??={},i[n][t]??=new Set,i[n][t].add(r||ma(e,{forAs:!0}))))}for(const e of t.groupby??[])n.add(e);return n.size+D(i).length===0?null:new Sd(e,n,i)}merge(e){return x(this.dimensions,e.dimensions)?(function(e,t){for(const n of D(t)){const i=t[n];for(const t of D(i))n in e?e[n][t]=new Set([...e[n][t]??[],...i[t]]):e[n]={[t]:i[t]}}}(this.measures,e.measures),!0):(function(){wi.debug(...arguments)}(\"different dimensions, cannot merge\"),!1)}addDimensions(e){e.forEach(this.dimensions.add,this.dimensions)}dependentFields(){return new Set([...this.dimensions,...D(this.measures)])}producedFields(){const e=new Set;for(const t of D(this.measures))for(const n of D(this.measures[t])){const i=this.measures[t][n];0===i.size?e.add(`${n}_${t}`):i.forEach(e.add,e)}return e}hash(){return`Aggregate ${d({dimensions:this.dimensions,measures:this.measures})}`}assemble(){const e=[],t=[],n=[];for(const i of D(this.measures))for(const r of D(this.measures[i]))for(const o of this.measures[i][r])n.push(o),e.push(r),t.push(\"*\"===i?null:M(i));return{type:\"aggregate\",groupby:[...this.dimensions].map(M),ops:e,fields:t,as:n}}}class Dd extends $c{constructor(e,n,i,r){super(e),this.model=n,this.name=i,this.data=r;for(const e of Be){const i=n.facet[e];if(i){const{bin:r,sort:o}=i;this[e]={name:n.getName(`${e}_domain`),fields:[ma(i),...mn(r)?[ma(i,{binSuffix:\"end\"})]:[]],...Lo(o)?{sortField:o}:t.isArray(o)?{sortIndexField:Ff(i,e)}:{}}}}this.childModel=n.child}hash(){let e=\"Facet\";for(const t of Be)this[t]&&(e+=` ${t.charAt(0)}:${d(this[t])}`);return e}get fields(){const e=[];for(const t of Be)this[t]?.fields&&e.push(...this[t].fields);return e}dependentFields(){const e=new Set(this.fields);for(const t of Be)this[t]&&(this[t].sortField&&e.add(this[t].sortField.field),this[t].sortIndexField&&e.add(this[t].sortIndexField));return e}producedFields(){return new Set}getSource(){return this.name}getChildIndependentFieldsWithStep(){const e={};for(const t of Ct){const n=this.childModel.component.scales[t];if(n&&!n.merged){const i=n.get(\"type\"),r=n.get(\"range\");if(kr(i)&&kn(r)){const n=ym(vm(this.childModel,t));n?e[t]=n:Si(Xn(t))}}}return e}assembleRowColumnHeaderData(e,t,n){const i={row:\"y\",column:\"x\",facet:void 0}[e],r=[],o=[],a=[];i&&n&&n[i]&&(t?(r.push(`distinct_${n[i]}`),o.push(\"max\")):(r.push(n[i]),o.push(\"distinct\")),a.push(`distinct_${n[i]}`));const{sortField:s,sortIndexField:l}=this[e];if(s){const{op:e=Eo,field:t}=s;r.push(t),o.push(e),a.push(ma(s,{forAs:!0}))}else l&&(r.push(l),o.push(\"max\"),a.push(l));return{name:this[e].name,source:t??this.data,transform:[{type:\"aggregate\",groupby:this[e].fields,...r.length?{fields:r,ops:o,as:a}:{}}]}}assembleFacetHeaderData(e){const{columns:t}=this.model.layout,{layoutHeaders:n}=this.model.component,i=[],r={};for(const e of _f){f"
-  , "or(const t of Pf){const i=(n[e]&&n[e][t])??[];for(const t of i)if(t.axes?.length>0){r[e]=!0;break}}if(r[e]){const n=`length(data(\"${this.facet.name}\"))`,r=\"row\"===e?t?{signal:`ceil(${n} / ${t})`}:1:t?{signal:`min(${n}, ${t})`}:{signal:n};i.push({name:`${this.facet.name}_${e}`,transform:[{type:\"sequence\",start:0,stop:r}]})}}const{row:o,column:a}=r;return(o||a)&&i.unshift(this.assembleRowColumnHeaderData(\"facet\",null,e)),i}assemble(){const e=[];let t=null;const n=this.getChildIndependentFieldsWithStep(),{column:i,row:r,facet:o}=this;if(i&&r&&(n.x||n.y)){t=`cross_${this.column.name}_${this.row.name}`;const i=[].concat(n.x??[],n.y??[]),r=i.map((()=>\"distinct\"));e.push({name:t,source:this.data,transform:[{type:\"aggregate\",groupby:this.fields,fields:i,ops:r}]})}for(const i of[Z,K])this[i]&&e.push(this.assembleRowColumnHeaderData(i,t,n));if(o){const t=this.assembleFacetHeaderData(n);t&&e.push(...t)}return e}}function Fd(e){return e.startsWith(\"'\")&&e.endsWith(\"'\")||e.startsWith('\"')&&e.endsWith('\"')?e.slice(1,-1):e}function Od(e){const n={};return a(e.filter,(e=>{if(er(e)){let i=null;Gi(e)?i=Cn(e.equal):Xi(e)?i=Cn(e.lte):Yi(e)?i=Cn(e.lt):Qi(e)?i=Cn(e.gt):Ji(e)?i=Cn(e.gte):Ki(e)?i=e.range[0]:Zi(e)&&(i=(e.oneOf??e.in)[0]),i&&(Di(i)?n[e.field]=\"date\":t.isNumber(i)?n[e.field]=\"number\":t.isString(i)&&(n[e.field]=\"string\")),e.timeUnit&&(n[e.field]=\"date\")}})),n}function zd(e){const n={};function i(e){var i;Ca(e)?n[e.field]=\"date\":\"quantitative\"===e.type&&(i=e.aggregate,t.isString(i)&&p([\"min\",\"max\"],i))?n[e.field]=\"number\":q(e.field)>1?e.field in n||(n[e.field]=\"flatten\"):la(e)&&Lo(e.sort)&&q(e.sort.field)>1&&(e.sort.field in n||(n[e.sort.field]=\"flatten\"))}if((qm(e)||Um(e))&&e.forEachFieldDef(((t,n)=>{if(aa(t))i(t);else{const r=rt(n),o=e.fieldDef(r);i({...t,type:o.type})}})),qm(e)){const{mark:t,markDef:i,encoding:r}=e;if(Zr(t)&&!e.encoding.order){const e=r[\"horizontal\"===i.orient?\"y\":\"x\"];Zo(e)&&\"quantitative\"===e.type&&!(e.field in n)&&(n[e.field]=\"number\")}}return n}class Cd extends $c{clone(){return new Cd(null,l(this._parse))}constructor(e,t){super(e),this._parse=t}hash(){return`Parse ${d(this._parse)}`}static makeExplicit(e,t,n){let i={};const r=t.data;return!gc(r)&&r?.format?.parse&&(i=r.format.parse),this.makeWithAncestors(e,i,{},n)}static makeWithAncestors(e,t,n,i){for(const e of D(n)){const t=i.getWithExplicit(e);void 0!==t.value&&(t.explicit||t.value===n[e]||\"derived\"===t.value||\"flatten\"===n[e]?delete n[e]:Si(ni(e,n[e],t.value)))}for(const e of D(t)){const n=i.get(e);void 0!==n&&(n===t[e]?delete t[e]:Si(ni(e,t[e],n)))}const r=new oc(t,n);i.copyAll(r);const o={};for(const e of D(r.combine())){const t=r.get(e);null!==t&&(o[e]=t)}return 0===D(o).length||i.parseNothing?null:new Cd(e,o)}get parse(){return this._parse}merge(e){this._parse={...this._parse,...e.parse},e.remove()}assembleFormatParse(){const e={};for(const t of D(this._parse)){const n=this._parse[t];1===q(t)&&(e[t]=n)}return e}producedFields(){return new Set(D(this._parse))}dependentFields(){return new Set(D(this._parse))}assembleTransforms(){let e=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return D(this._parse).filter((t=>!e||q(t)>1)).map((e=>{const t=function(e,t){const n=A(e);if(\"number\"===t)return`toNumber(${n})`;if(\"boolean\"===t)return`toBoolean(${n})`;if(\"string\"===t)return`toString(${n})`;if(\"date\"===t)return`toDate(${n})`;if(\"flatten\"===t)return n;if(t.startsWith(\"date:\"))return`timeParse(${n},'${Fd(t.slice(5,t.length))}')`;if(t.startsWith(\"utc:\"))return`utcParse(${n},'${Fd(t.slice(4,t.length))}')`;return Si(`Unrecognized parse \"${t}\".`),null}(e,this._parse[e]);if(!t)return null;return{type:\"formula\",expr:t,as:L(e)}})).filter((e=>null!==e))}}class _d extends $c{clone(){return new _d(null)}constructor(e){super(e)}dependentFields(){return new Set}producedFields(){return new Set([zs])}hash(){return\"Identifier\"}assemble(){return{type:\"identifier\",as:zs}}}class Pd extends $c{clone(){return new Pd(null,this.params)}constructor(e,t){super(e),this.params=t}dependentFields(){return new Set}producedFields(){}hash(){return`Graticule ${d(this.params)}`}ass"
-  , "emble(){return{type:\"graticule\",...!0===this.params?{}:this.params}}}class Nd extends $c{clone(){return new Nd(null,this.params)}constructor(e,t){super(e),this.params=t}dependentFields(){return new Set}producedFields(){return new Set([this.params.as??\"data\"])}hash(){return`Hash ${d(this.params)}`}assemble(){return{type:\"sequence\",...this.params}}}class Ad extends $c{constructor(e){let t;if(super(null),e??={name:\"source\"},gc(e)||(t=e.format?{...f(e.format,[\"parse\"])}:{}),mc(e))this._data={values:e.values};else if(dc(e)){if(this._data={url:e.url},!t.type){let n=/(?:\\.([^.]+))?$/.exec(e.url)[1];p([\"json\",\"csv\",\"tsv\",\"dsv\",\"topojson\"],n)||(n=\"json\"),t.type=n}}else yc(e)?this._data={values:[{type:\"Sphere\"}]}:(pc(e)||gc(e))&&(this._data={});this._generator=gc(e),e.name&&(this._name=e.name),t&&!S(t)&&(this._data.format=t)}dependentFields(){return new Set}producedFields(){}get data(){return this._data}hasName(){return!!this._name}get isGenerator(){return this._generator}get dataName(){return this._name}set dataName(e){this._name=e}set parent(e){throw new Error(\"Source nodes have to be roots.\")}remove(){throw new Error(\"Source nodes are roots and cannot be removed.\")}hash(){throw new Error(\"Cannot hash sources\")}assemble(){return{name:this._name,...this._data,transform:[]}}}function Td(e){return e instanceof Ad||e instanceof Pd||e instanceof Nd}class jd{#e;constructor(){this.#e=!1}setModified(){this.#e=!0}get modifiedFlag(){return this.#e}}class Ed extends jd{getNodeDepths(e,t,n){n.set(e,t);for(const i of e.children)this.getNodeDepths(i,t+1,n);return n}optimize(e){const t=[...this.getNodeDepths(e,0,new Map).entries()].sort(((e,t)=>t[1]-e[1]));for(const e of t)this.run(e[0]);return this.modifiedFlag}}class Md extends jd{optimize(e){this.run(e);for(const t of e.children)this.optimize(t);return this.modifiedFlag}}class Rd extends Md{mergeNodes(e,t){const n=t.shift();for(const i of t)e.removeChild(i),i.parent=n,i.remove()}run(e){const t=e.children.map((e=>e.hash())),n={};for(let i=0;i<t.length;i++)void 0===n[t[i]]?n[t[i]]=[e.children[i]]:n[t[i]].push(e.children[i]);for(const t of D(n))n[t].length>1&&(this.setModified(),this.mergeNodes(e,n[t]))}}class Ld extends Md{constructor(e){super(),this.requiresSelectionId=e&&rf(e)}run(e){e instanceof _d&&(this.requiresSelectionId&&(Td(e.parent)||e.parent instanceof Sd||e.parent instanceof Cd)||(this.setModified(),e.remove()))}}class qd extends jd{optimize(e){return this.run(e,new Set),this.modifiedFlag}run(e,t){let n=new Set;e instanceof Dc&&(n=e.producedFields(),$(n,t)&&(this.setModified(),e.removeFormulas(t),0===e.producedFields.length&&e.remove()));for(const i of e.children)this.run(i,new Set([...t,...n]))}}class Ud extends Md{constructor(){super()}run(e){e instanceof wc&&!e.isRequired()&&(this.setModified(),e.remove())}}class Wd extends Ed{run(e){if(!(Td(e)||e.numChildren()>1))for(const t of e.children)if(t instanceof Cd)if(e instanceof Cd)this.setModified(),e.merge(t);else{if(k(e.producedFields(),t.dependentFields()))continue;this.setModified(),t.swapWithParent()}}}class Id extends Ed{run(e){const t=[...e.children],n=e.children.filter((e=>e instanceof Cd));if(e.numChildren()>1&&n.length>=1){const i={},r=new Set;for(const e of n){const t=e.parse;for(const e of D(t))e in i?i[e]!==t[e]&&r.add(e):i[e]=t[e]}for(const e of r)delete i[e];if(!S(i)){this.setModified();const n=new Cd(e,i);for(const r of t){if(r instanceof Cd)for(const e of D(i))delete r.parse[e];e.removeChild(r),r.parent=n,r instanceof Cd&&0===D(r.parse).length&&r.remove()}}}}}class Bd extends Ed{run(e){e instanceof wc||e.numChildren()>0||e instanceof Dd||e instanceof Ad||(this.setModified(),e.remove())}}class Vd extends Ed{run(e){const t=e.children.filter((e=>e instanceof Dc)),n=t.pop();for(const e of t)this.setModified(),n.merge(e)}}class Hd extends Ed{run(e){const t=e.children.filter((e=>e instanceof Sd)),n={};for(const e of t){const t=d(e.groupBy);t in n||(n[t]=[]),n[t].push(e)}for(const t of D(n)){const i=n[t];if(i.length>1){const t=i.pop();for(const n of i)t.merge(n)&&(e.removeChild(n),n.parent=t,n.remove(),this.setModified())"
-  , "}}}}class Gd extends Ed{constructor(e){super(),this.model=e}run(e){const t=!(Td(e)||e instanceof uf||e instanceof Cd||e instanceof _d),n=[],i=[];for(const r of e.children)r instanceof wd&&(t&&!k(e.producedFields(),r.dependentFields())?n.push(r):i.push(r));if(n.length>0){const t=n.pop();for(const e of n)t.merge(e,this.model.renameSignal.bind(this.model));this.setModified(),e instanceof wd?e.merge(t,this.model.renameSignal.bind(this.model)):t.swapWithParent()}if(i.length>1){const e=i.pop();for(const t of i)e.merge(t,this.model.renameSignal.bind(this.model));this.setModified()}}}class Yd extends Ed{run(e){const t=[...e.children];if(!g(t,(e=>e instanceof wc))||e.numChildren()<=1)return;const n=[];let i;for(const r of t)if(r instanceof wc){let t=r;for(;1===t.numChildren();){const[e]=t.children;if(!(e instanceof wc))break;t=e}n.push(...t.children),i?(e.removeChild(r),r.parent=i.parent,i.parent.removeChild(i),i.parent=t,this.setModified()):i=t}else n.push(r);if(n.length){this.setModified();for(const e of n)e.parent.removeChild(e),e.parent=i}}}class Xd extends $c{clone(){return new Xd(null,l(this.transform))}constructor(e,t){super(e),this.transform=t}addDimensions(e){this.transform.groupby=b(this.transform.groupby.concat(e),(e=>e))}dependentFields(){const e=new Set;return this.transform.groupby&&this.transform.groupby.forEach(e.add,e),this.transform.joinaggregate.map((e=>e.field)).filter((e=>void 0!==e)).forEach(e.add,e),e}producedFields(){return new Set(this.transform.joinaggregate.map(this.getDefaultName))}getDefaultName(e){return e.as??ma(e)}hash(){return`JoinAggregateTransform ${d(this.transform)}`}assemble(){const e=[],t=[],n=[];for(const i of this.transform.joinaggregate)t.push(i.op),n.push(this.getDefaultName(i)),e.push(void 0===i.field?null:i.field);const i=this.transform.groupby;return{type:\"joinaggregate\",as:n,ops:t,fields:e,...void 0!==i?{groupby:i}:{}}}}class Qd extends $c{clone(){return new Qd(null,{...this.filter})}constructor(e,t){super(e),this.filter=t}static make(e,t,n){const{config:i,markDef:r}=t,{marks:o,scales:a}=n;if(\"include-invalid-values\"===o&&\"include-invalid-values\"===a)return null;const s=t.reduceFieldDef(((e,n,o)=>{const a=Qt(o)&&t.getScaleComponent(o);if(a){const t=a.get(\"type\"),{aggregate:s}=n,l=go({scaleChannel:o,markDef:r,config:i,scaleType:t,isCountAggregate:cn(s)});\"show\"!==l&&\"always-valid\"!==l&&(e[n.field]=n)}return e}),{});return D(s).length?new Qd(e,s):null}dependentFields(){return new Set(D(this.filter))}producedFields(){return new Set}hash(){return`FilterInvalid ${d(this.filter)}`}assemble(){const e=D(this.filter).reduce(((e,t)=>{const n=this.filter[t],i=ma(n,{expr:\"datum\"});return null!==n&&(\"temporal\"===n.type?e.push(`(isDate(${i}) || (${Jd(i)}))`):\"quantitative\"===n.type&&e.push(Jd(i))),e}),[]);return e.length>0?{type:\"filter\",expr:e.join(\" && \")}:null}}function Jd(e){return`isValid(${e}) && isFinite(+${e})`}class Kd extends $c{clone(){return new Kd(null,l(this._stack))}constructor(e,t){super(e),this._stack=t}static makeFromTransform(e,n){const{stack:i,groupby:r,as:o,offset:a=\"zero\"}=n,s=[],l=[];if(void 0!==n.sort)for(const e of n.sort)s.push(e.field),l.push(U(e.order,\"ascending\"));const c={field:s,order:l};let u;return u=function(e){return t.isArray(e)&&e.every((e=>t.isString(e)))&&e.length>1}(o)?o:t.isString(o)?[o,`${o}_end`]:[`${n.stack}_start`,`${n.stack}_end`],new Kd(e,{dimensionFieldDefs:[],stackField:i,groupby:r,offset:a,sort:c,facetby:[],as:u})}static makeFromEncoding(e,n){const i=n.stack,{encoding:r}=n;if(!i)return null;const{groupbyChannels:o,fieldChannel:a,offset:s,impute:l}=i,c=o.map((e=>wa(r[e]))).filter((e=>!!e)),u=function(e){return e.stack.stackBy.reduce(((e,t)=>{const n=ma(t.fieldDef);return n&&e.push(n),e}),[])}(n),f=n.encoding.order;let d;if(t.isArray(f)||Zo(f))d=qn(f);else{const e=Xo(f)?f.sort:\"y\"===a?\"descending\":\"ascending\";d=u.reduce(((t,n)=>(t.field.includes(n)||(t.field.push(n),t.order.push(e)),t)),{field:[],order:[]})}return new Kd(e,{dimensionFieldDefs:c,stackField:n.vgField(a),facetby:[],stackby:u,sort:d,offset:s,impute:l,as:[n.vgField(a,{suffix:\"start\",forAs"
-  , ":!0}),n.vgField(a,{suffix:\"end\",forAs:!0})]})}get stack(){return this._stack}addDimensions(e){this._stack.facetby.push(...e)}dependentFields(){const e=new Set;return e.add(this._stack.stackField),this.getGroupbyFields().forEach(e.add,e),this._stack.facetby.forEach(e.add,e),this._stack.sort.field.forEach(e.add,e),e}producedFields(){return new Set(this._stack.as)}hash(){return`Stack ${d(this._stack)}`}getGroupbyFields(){const{dimensionFieldDefs:e,impute:t,groupby:n}=this._stack;return e.length>0?e.map((e=>e.bin?t?[ma(e,{binSuffix:\"mid\"})]:[ma(e,{}),ma(e,{binSuffix:\"end\"})]:[ma(e)])).flat():n??[]}assemble(){const e=[],{facetby:t,dimensionFieldDefs:n,stackField:i,stackby:r,sort:o,offset:a,impute:s,as:l}=this._stack;if(s)for(const o of n){const{bandPosition:n=.5,bin:a}=o;if(a){const t=ma(o,{expr:\"datum\"}),i=ma(o,{expr:\"datum\",binSuffix:\"end\"});e.push({type:\"formula\",expr:`${Jd(t)} ? ${n}*${t}+${1-n}*${i} : ${t}`,as:ma(o,{binSuffix:\"mid\",forAs:!0})})}e.push({type:\"impute\",field:i,groupby:[...r,...t],key:ma(o,{binSuffix:\"mid\"}),method:\"value\",value:0})}return e.push({type:\"stack\",groupby:[...this.getGroupbyFields(),...t],field:i,sort:o,as:l,offset:a}),e}}class Zd extends $c{clone(){return new Zd(null,l(this.transform))}constructor(e,t){super(e),this.transform=t}addDimensions(e){this.transform.groupby=b(this.transform.groupby.concat(e),(e=>e))}dependentFields(){const e=new Set;return(this.transform.groupby??[]).forEach(e.add,e),(this.transform.sort??[]).forEach((t=>e.add(t.field))),this.transform.window.map((e=>e.field)).filter((e=>void 0!==e)).forEach(e.add,e),e}producedFields(){return new Set(this.transform.window.map(this.getDefaultName))}getDefaultName(e){return e.as??ma(e)}hash(){return`WindowTransform ${d(this.transform)}`}assemble(){const e=[],t=[],n=[],i=[];for(const r of this.transform.window)t.push(r.op),n.push(this.getDefaultName(r)),i.push(void 0===r.param?null:r.param),e.push(void 0===r.field?null:r.field);const r=this.transform.frame,o=this.transform.groupby;if(r&&null===r[0]&&null===r[1]&&t.every((e=>sn(e))))return{type:\"joinaggregate\",as:n,ops:t,fields:e,...void 0!==o?{groupby:o}:{}};const a=[],s=[];if(void 0!==this.transform.sort)for(const e of this.transform.sort)a.push(e.field),s.push(e.order??\"ascending\");const l={field:a,order:s},c=this.transform.ignorePeers;return{type:\"window\",params:i,as:n,ops:t,fields:e,sort:l,...void 0!==c?{ignorePeers:c}:{},...void 0!==o?{groupby:o}:{},...void 0!==r?{frame:r}:{}}}}function em(e){if(e instanceof Dd)if(1!==e.numChildren()||e.children[0]instanceof wc){const n=e.model.component.data.main;tm(n);const i=(t=e,function e(n){if(!(n instanceof Dd)){const i=n.clone();if(i instanceof wc){const e=nm+i.getSource();i.setSource(e),t.model.component.data.outputNodes[e]=i}else(i instanceof Sd||i instanceof Kd||i instanceof Zd||i instanceof Xd)&&i.addDimensions(t.fields);for(const t of n.children.flatMap(e))t.parent=i;return[i]}return n.children.flatMap(e)}),r=e.children.map(i).flat();for(const e of r)e.parent=n}else{const t=e.children[0];(t instanceof Sd||t instanceof Kd||t instanceof Zd||t instanceof Xd)&&t.addDimensions(e.fields),t.swapWithParent(),em(e)}else e.children.map(em);var t}function tm(e){if(e instanceof wc&&e.type===bc.Main&&1===e.numChildren()){const t=e.children[0];t instanceof Dd||(t.swapWithParent(),tm(e))}}const nm=\"scale_\",im=5;function rm(e){for(const t of e){for(const e of t.children)if(e.parent!==t)return!1;if(!rm(t.children))return!1}return!0}function om(e,t){let n=!1;for(const i of t)n=e.optimize(i)||n;return n}function am(e,t,n){let i=e.sources,r=!1;return r=om(new Ud,i)||r,r=om(new Ld(t),i)||r,i=i.filter((e=>e.numChildren()>0)),r=om(new Bd,i)||r,i=i.filter((e=>e.numChildren()>0)),n||(r=om(new Wd,i)||r,r=om(new Gd(t),i)||r,r=om(new qd,i)||r,r=om(new Id,i)||r,r=om(new Hd,i)||r,r=om(new Vd,i)||r,r=om(new Rd,i)||r,r=om(new Yd,i)||r),e.sources=i,r}class sm{constructor(e){Object.defineProperty(this,\"signal\",{enumerable:!0,get:e})}static fromName(e,t){return new sm((()=>e(t)))}}function lm(e){qm(e)?function(e){const t=e.component.scales;for(const n of D(t)){const i=cm"
-  , "(e,n);if(t[n].setWithExplicit(\"domains\",i),mm(e,n),e.component.data.isFaceted){let t=e;for(;!Um(t)&&t.parent;)t=t.parent;if(\"shared\"===t.component.resolve.scale[n])for(const e of i.value)Sn(e)&&(e.data=nm+e.data.replace(nm,\"\"))}}}(e):function(e){for(const t of e.children)lm(t);const t=e.component.scales;for(const n of D(t)){let i,r=null;for(const t of e.children){const e=t.component.scales[n];if(e){i=void 0===i?e.getWithExplicit(\"domains\"):uc(i,e.getWithExplicit(\"domains\"),\"domains\",\"scale\",gm);const t=e.get(\"selectionExtent\");r&&t&&r.param!==t.param&&Si(Zn),r=t}}t[n].setWithExplicit(\"domains\",i),r&&t[n].set(\"selectionExtent\",r,!0)}}(e)}function cm(e,t){const n=e.getScaleComponent(t).get(\"type\"),{encoding:i}=e,r=function(e,t,n,i){if(\"unaggregated\"===e){const{valid:e,reason:i}=pm(t,n);if(!e)return void Si(i)}else if(void 0===e&&i.useUnaggregatedDomain){const{valid:e}=pm(t,n);if(e)return\"unaggregated\"}return e}(e.scaleDomain(t),e.typedFieldDef(t),n,e.config.scale);return r!==e.scaleDomain(t)&&(e.specifiedScales[t]={...e.specifiedScales[t],domain:r}),\"x\"===t&&ka(i.x2)?ka(i.x)?uc(fm(n,r,e,\"x\"),fm(n,r,e,\"x2\"),\"domain\",\"scale\",gm):fm(n,r,e,\"x2\"):\"y\"===t&&ka(i.y2)?ka(i.y)?uc(fm(n,r,e,\"y\"),fm(n,r,e,\"y2\"),\"domain\",\"scale\",gm):fm(n,r,e,\"y2\"):fm(n,r,e,t)}function um(e,t,n){const i=Ii(n)?.unit;return\"temporal\"===t||i?function(e,t,n){return e.map((e=>({signal:`{data: ${_a(e,{timeUnit:n,type:t})}}`})))}(e,t,i):[e]}function fm(e,n,i,r){const{encoding:o,markDef:a,mark:s,config:l,stack:c}=i,u=ka(o[r]),{type:f}=u,d=u.timeUnit,m=function(e){const{marks:t,scales:n}=xc(e);return t===n?bc.Main:\"include-invalid-values\"===n?bc.PreFilterInvalid:bc.PostFilterInvalid}({invalid:Mn(\"invalid\",a,l),isPath:Zr(s)});if(function(e){return J(e,\"unionWith\")}(n)){const t=fm(e,void 0,i,r);return ac([...um(n.unionWith,f,d),...t.value])}if(wn(n))return ac([n]);if(n&&\"unaggregated\"!==n&&!Or(n))return ac(um(n,f,d));if(c&&r===c.fieldChannel){if(\"normalize\"===c.offset)return sc([[0,1]]);const e=i.requestDataName(m);return sc([{data:e,field:i.vgField(r,{suffix:\"start\"})},{data:e,field:i.vgField(r,{suffix:\"end\"})}])}const g=Qt(r)&&Zo(u)?function(e,t,n){if(!kr(n))return;const i=e.fieldDef(t),r=i.sort;if(qo(r))return{op:\"min\",field:Ff(i,t),order:\"ascending\"};const{stack:o}=e,a=o?new Set([...o.groupbyFields,...o.stackBy.map((e=>e.fieldDef.field))]):void 0;if(Lo(r)){return dm(r,o&&!a.has(r.field))}if(function(e){return J(e,\"encoding\")}(r)){const{encoding:t,order:n}=r,i=e.fieldDef(t),{aggregate:s,field:l}=i,c=o&&!a.has(l);if(on(s)||an(s))return dm({field:ma(i),order:n},c);if(sn(s)||!s)return dm({op:s,field:l,order:n},c)}else{if(\"descending\"===r)return{op:\"min\",field:e.vgField(t),order:\"descending\"};if(p([\"ascending\",void 0],r))return!0}return}(i,r,e):void 0;if(ta(u)){return sc(um([u.datum],f,d))}const h=u;if(\"unaggregated\"===n){const{field:e}=u;return sc([{data:i.requestDataName(m),field:ma({field:e,aggregate:\"min\"})},{data:i.requestDataName(m),field:ma({field:e,aggregate:\"max\"})}])}if(mn(h.bin)){if(kr(e))return sc(\"bin-ordinal\"===e?[]:[{data:z(g)?i.requestDataName(m):i.requestDataName(bc.Raw),field:i.vgField(r,Na(h,r)?{binSuffix:\"range\"}:{}),sort:!0!==g&&t.isObject(g)?g:{field:i.vgField(r,{}),op:\"min\"}}]);{const{bin:e}=h;if(mn(e)){const t=xd(i,h.field,e);return sc([new sm((()=>{const e=i.getSignalName(t);return`[${e}.start, ${e}.stop]`}))])}return sc([{data:i.requestDataName(m),field:i.vgField(r,{})}])}}if(h.timeUnit&&p([\"time\",\"utc\"],e)){const e=o[at(r)];if(Yo(h,e,a,l)){const t=i.requestDataName(m),n=Ho({fieldDef:h,fieldDef2:e,markDef:a,config:l}),o=eo(s)&&.5!==n&&_t(r);return sc([{data:t,field:i.vgField(r,o?{suffix:Fc}:{})},{data:t,field:i.vgField(r,{suffix:o?Oc:\"end\"})}])}}return sc(g?[{data:z(g)?i.requestDataName(m):i.requestDataName(bc.Raw),field:i.vgField(r),sort:g}]:[{data:i.requestDataName(m),field:i.vgField(r)}])}function dm(e,t){const{op:n,field:i,order:r}=e;return{op:n??(t?\"sum\":Eo),...i?{field:M(i)}:{},...r?{order:r}:{}}}function mm(e,t){const n=e.component.scales[t],i=e.specifiedScales[t].domain,r=e.fieldDef(t)?.bin,o=Or(i)?i:void 0,a=gn(r)&&hn(r.extent)?r.e"
-  , "xtent:void 0;(o||a)&&n.set(\"selectionExtent\",o??a,!0)}function pm(e,n){const{aggregate:i,type:r}=e;return i?t.isString(i)&&!fn.has(i)?{valid:!1,reason:mi(i)}:\"quantitative\"===r&&\"log\"===n?{valid:!1,reason:pi(e)}:{valid:!0}:{valid:!1,reason:di(e)}}function gm(e,t,n,i){return e.explicit&&t.explicit&&Si(function(e,t,n,i){return`Conflicting ${t.toString()} property \"${e.toString()}\" (${Q(n)} and ${Q(i)}). Using the union of the two domains.`}(n,i,e.value,t.value)),{explicit:e.explicit,value:[...e.value,...t.value]}}function hm(e){const n=b(e.map((e=>{if(Sn(e)){const{sort:t,...n}=e;return n}return e})),d),i=b(e.map((e=>{if(Sn(e)){const t=e.sort;return void 0===t||z(t)||(\"op\"in t&&\"count\"===t.op&&delete t.field,\"ascending\"===t.order&&delete t.order),t}})).filter((e=>void 0!==e)),d);if(0===n.length)return;if(1===n.length){const n=e[0];if(Sn(n)&&i.length>0){let e=i[0];if(i.length>1){Si(yi);const n=i.filter((e=>t.isObject(e)&&\"op\"in e&&\"min\"!==e.op));e=!i.every((e=>t.isObject(e)&&\"op\"in e))||1!==n.length||n[0]}else if(t.isObject(e)&&\"field\"in e){const t=e.field;n.field===t&&(e=!e.order||{order:e.order})}return{...n,sort:e}}return n}const r=b(i.map((e=>z(e)||!(\"op\"in e)||t.isString(e.op)&&t.hasOwnProperty(rn,e.op)?e:(Si(function(e){return`Dropping sort property ${Q(e)} as unioned domains only support boolean or op \"count\", \"min\", and \"max\".`}(e)),!0))),d);let o;1===r.length?o=r[0]:r.length>1&&(Si(yi),o=!0);const a=b(e.map((e=>Sn(e)?e.data:null)),(e=>e));if(1===a.length&&null!==a[0]){return{data:a[0],fields:n.map((e=>e.field)),...o?{sort:o}:{}}}return{fields:n,...o?{sort:o}:{}}}function ym(e){if(Sn(e)&&t.isString(e.field))return e.field;if(function(e){return!t.isArray(e)&&J(e,\"fields\")&&!J(e,\"data\")}(e)){let n;for(const i of e.fields)if(Sn(i)&&t.isString(i.field))if(n){if(n!==i.field)return Si(\"Detected faceted independent scales that union domain of multiple fields from different data sources. We will use the first field. The result view size may be incorrect.\"),n}else n=i.field;return Si(\"Detected faceted independent scales that union domain of the same fields from different source. We will assume that this is the same field from a different fork of the same data source. However, if this is not the case, the result view size may be incorrect.\"),n}if(function(e){return!t.isArray(e)&&J(e,\"fields\")&&J(e,\"data\")}(e)){Si(\"Detected faceted independent scales that union domain of multiple fields from the same data source. We will use the first field. The result view size may be incorrect.\");const n=e.fields[0];return t.isString(n)?n:void 0}}function vm(e,t){const n=e.component.scales[t].get(\"domains\").map((t=>(Sn(t)&&(t.data=e.lookupDataSource(t.data)),t)));return hm(n)}function bm(e){return Im(e)||Wm(e)?e.children.reduce(((e,t)=>e.concat(bm(t))),xm(e)):xm(e)}function xm(e){return D(e.component.scales).reduce(((n,i)=>{const r=e.component.scales[i];if(r.merged)return n;const o=r.combine(),{name:a,type:s,selectionExtent:l,domains:c,range:u,reverse:f,...d}=o,m=function(e,n,i,r){if(_t(i)){if(kn(e))return{step:{signal:`${n}_step`}}}else if(t.isObject(e)&&Sn(e))return{...e,data:r.lookupDataSource(e.data)};return e}(o.range,a,i,e),p=vm(e,i),g=l?function(e,n,i,r){const o=df(e,n.param,n);return{signal:Sr(i.get(\"type\"))&&t.isArray(r)&&r[0]>r[1]?`isValid(${o}) && reverse(${o})`:o}}(e,l,r,p):null;return n.push({name:a,type:s,...p?{domain:p}:{},...g?{domainRaw:g}:{},range:m,...void 0!==f?{reverse:f}:{},...d}),n}),[])}class $m extends oc{merged=!1;constructor(e,t){super({},{name:e}),this.setWithExplicit(\"type\",t)}domainHasZero(){const e=this.get(\"type\");if(p([dr.LOG,dr.TIME,dr.UTC],e))return\"definitely-not\";const n=this.get(\"zero\");if(!0===n||void 0===n&&p([dr.LINEAR,dr.SQRT,dr.POW],e))return\"definitely\";const i=this.get(\"domains\");if(i.length>0){let e=!1,n=!1,r=!1;for(const o of i){if(t.isArray(o)){const i=o[0],r=o[o.length-1];if(t.isNumber(i)&&t.isNumber(r)){if(i<=0&&r>=0){e=!0;continue}n=!0;continue}}r=!0}if(e)return\"definitely\";if(n&&!r)return\"definitely-not\"}return\"maybe\"}}const wm=[\"range\",\"scheme\"];function km(e,n){const i=e.fieldDef(n);if(i?."
-  , "bin){const{bin:r,field:o}=i,a=st(n),s=e.getName(a);if(t.isObject(r)&&r.binned&&void 0!==r.step)return new sm((()=>{const t=e.scaleName(n),i=`(domain(\"${t}\")[1] - domain(\"${t}\")[0]) / ${r.step}`;return`${e.getSignalName(s)} / (${i})`}));if(mn(r)){const t=xd(e,o,r);return new sm((()=>{const n=e.getSignalName(t),i=`(${n}.stop - ${n}.start) / ${n}.step`;return`${e.getSignalName(s)} / (${i})`}))}}}function Sm(e,n){const i=n.specifiedScales[e],{size:r}=n,o=n.getScaleComponent(e).get(\"type\");for(const r of wm)if(void 0!==i[r]){const a=Er(o,r),s=Mr(e,r);if(a)if(s)Si(s);else switch(r){case\"range\":{const r=i.range;if(t.isArray(r)){if(_t(e))return ac(r.map((e=>{if(\"width\"===e||\"height\"===e){const t=n.getName(e),i=n.getSignalName.bind(n);return sm.fromName(i,t)}return e})))}else if(t.isObject(r))return ac({data:n.requestDataName(bc.Main),field:r.field,sort:{op:\"min\",field:n.vgField(e)}});return ac(r)}case\"scheme\":return ac(Dm(i[r]))}else Si(gi(o,r,e))}const a=e===te||\"xOffset\"===e?\"width\":\"height\",s=r[a];if(Rs(s))if(_t(e))if(kr(o)){const t=Om(s,n,e);if(t)return ac({step:t})}else Si(hi(a));else if(jt(e)){const t=e===oe?\"x\":\"y\";if(\"band\"===n.getScaleComponent(t).get(\"type\")){const e=zm(s,o);if(e)return ac(e)}}const{rangeMin:l,rangeMax:u}=i,f=function(e,n){const{size:i,config:r,mark:o,encoding:a}=n,{type:s}=ka(a[e]),l=n.getScaleComponent(e),u=l.get(\"type\"),{domain:f,domainMid:d}=n.specifiedScales[e];switch(e){case te:case ne:if(p([\"point\",\"band\"],u)){const t=Cm(e,i,r.view);if(Rs(t)){return{step:Om(t,n,e)}}}return Fm(e,n,u);case oe:case ae:return function(e,t,n){const i=e===oe?\"x\":\"y\",r=t.getScaleComponent(i);if(!r)return Fm(i,t,n,{center:!0});const o=r.get(\"type\"),a=t.scaleName(i),{markDef:s,config:l}=t;if(\"band\"===o){const e=Cm(i,t.size,t.config.view);if(Rs(e)){const t=zm(e,n);if(t)return t}return[0,{signal:`bandwidth('${a}')`}]}{const n=t.encoding[i];if(Zo(n)&&n.timeUnit){const e=Bi(n.timeUnit,(e=>`scale('${a}', ${e})`)),i=t.config.scale.bandWithNestedOffsetPaddingInner,r=Ho({fieldDef:n,markDef:s,config:l})-.5,o=0!==r?` + ${r}`:\"\";if(i){return[{signal:`${wn(i)?`${i.signal}/2`+o:`${i/2+r}`} * (${e})`},{signal:`${wn(i)?`(1 - ${i.signal}/2)`+o:`${1-i/2+r}`} * (${e})`}]}return[0,{signal:e}]}return c(`Cannot use ${e} scale if ${i} scale is not discrete.`)}}(e,n,u);case xe:{const a=function(e,t){switch(e){case\"bar\":case\"tick\":return t.scale.minBandSize;case\"line\":case\"trail\":case\"rule\":return t.scale.minStrokeWidth;case\"text\":return t.scale.minFontSize;case\"point\":case\"square\":case\"circle\":return t.scale.minSize}throw new Error(li(\"size\",e))}(o,r),s=function(e,n,i,r){const o={x:km(i,\"x\"),y:km(i,\"y\")};switch(e){case\"bar\":case\"tick\":{if(void 0!==r.scale.maxBandSize)return r.scale.maxBandSize;const e=Pm(n,o,r.view);return t.isNumber(e)?e-1:new sm((()=>`${e.signal} - 1`))}case\"line\":case\"trail\":case\"rule\":return r.scale.maxStrokeWidth;case\"text\":return r.scale.maxFontSize;case\"point\":case\"square\":case\"circle\":{if(r.scale.maxSize)return r.scale.maxSize;const e=Pm(n,o,r.view);return t.isNumber(e)?Math.pow(_m*e,2):new sm((()=>`pow(${_m} * ${e.signal}, 2)`))}}throw new Error(li(\"size\",e))}(o,i,n,r);return Fr(u)?function(e,t,n){const i=()=>{const i=An(t),r=An(e),o=`(${i} - ${r}) / (${n} - 1)`;return`sequence(${r}, ${i} + ${o}, ${o})`};return wn(t)?new sm(i):{signal:i()}}(a,s,function(e,n,i,r){switch(e){case\"quantile\":return n.scale.quantileCount;case\"quantize\":return n.scale.quantizeCount;case\"threshold\":return void 0!==i&&t.isArray(i)?i.length+1:(Si(function(e){return`Domain for ${e} is required for threshold scale.`}(r)),3)}}(u,r,f,e)):[a,s]}case ce:return[0,2*Math.PI];case $e:return[0,360];case se:return[0,new sm((()=>`min(${n.getSignalName(Um(n.parent)?\"child_width\":\"width\")},${n.getSignalName(Um(n.parent)?\"child_height\":\"height\")})/2`))];case ge:return{step:1e3/r.scale.framesPerSecond};case De:return[r.scale.minStrokeWidth,r.scale.maxStrokeWidth];case Fe:return[[1,0],[4,2],[2,1],[1,1],[1,2,4,2]];case be:return\"symbol\";case he:case ye:case ve:return\"ordinal\"===u?\"nominal\"===s?\"category\":\"ordinal\":void 0!==d?\"diverging\":\"rect\"===o||\"geoshape\"==="
-  , "o?\"heatmap\":\"ramp\";case we:case ke:case Se:return[r.scale.minOpacity,r.scale.maxOpacity]}}(e,n);return(void 0!==l||void 0!==u)&&Er(o,\"rangeMin\")&&t.isArray(f)&&2===f.length?ac([l??f[0],u??f[1]]):sc(f)}function Dm(e){return function(e){return!t.isString(e)&&J(e,\"name\")}(e)?{scheme:e.name,...f(e,[\"name\"])}:{scheme:e}}function Fm(e,t,n){let{center:i}=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};const r=st(e),o=t.getName(r),a=t.getSignalName.bind(t);return e===ne&&Sr(n)?i?[sm.fromName((e=>`${a(e)}/2`),o),sm.fromName((e=>`-${a(e)}/2`),o)]:[sm.fromName(a,o),0]:i?[sm.fromName((e=>`-${a(e)}/2`),o),sm.fromName((e=>`${a(e)}/2`),o)]:[0,sm.fromName(a,o)]}function Om(e,n,i){const{encoding:r}=n,o=n.getScaleComponent(i),a=ct(i),s=r[a];if(\"offset\"===Ms({step:e,offsetIsDiscrete:oa(s)&&ar(s.type)})&&Ba(r,a)){const i=n.getScaleComponent(a);let r=`domain('${n.scaleName(a)}').length`;if(\"band\"===i.get(\"type\")){r=`bandspace(${r}, ${i.get(\"paddingInner\")??i.get(\"padding\")??0}, ${i.get(\"paddingOuter\")??i.get(\"padding\")??0})`}const s=o.get(\"paddingInner\")??o.get(\"padding\");return{signal:`${e.step} * ${r} / (1-${l=s,wn(l)?l.signal:t.stringValue(l)})`}}return e.step;var l}function zm(e,t){if(\"offset\"===Ms({step:e,offsetIsDiscrete:kr(t)}))return{step:e.step}}function Cm(e,t,n){const i=e===te?\"width\":\"height\",r=t[i];return r||Is(n,i)}const _m=.95;function Pm(e,t,n){const i=Rs(e.width)?e.width.step:Ws(n,\"width\"),r=Rs(e.height)?e.height.step:Ws(n,\"height\");return t.x||t.y?new sm((()=>`min(${[t.x?t.x.signal:i,t.y?t.y.signal:r].join(\", \")})`)):Math.min(i,r)}function Nm(e,t){qm(e)?function(e,t){const n=e.component.scales,{config:i,encoding:r,markDef:o,specifiedScales:a}=e;for(const s of D(n)){const l=a[s],c=n[s],u=e.getScaleComponent(s),f=ka(r[s]),d=l[t],m=u.get(\"type\"),p=u.get(\"padding\"),g=u.get(\"paddingInner\"),h=Er(m,t),y=Mr(s,t);if(void 0!==d&&(h?y&&Si(y):Si(gi(m,t,s))),h&&void 0===y)if(void 0!==d){const e=f.timeUnit,n=f.type;switch(t){case\"domainMax\":case\"domainMin\":Di(l[t])||\"temporal\"===n||e?c.set(t,{signal:_a(l[t],{type:n,timeUnit:e})},!0):c.set(t,l[t],!0);break;default:c.copyKeyFromObject(t,l)}}else{const n=J(Am,t)?Am[t]({model:e,channel:s,fieldOrDatumDef:f,scaleType:m,scalePadding:p,scalePaddingInner:g,domain:l.domain,domainMin:l.domainMin,domainMax:l.domainMax,markDef:o,config:i,hasNestedOffsetScale:Va(r,s),hasSecondaryRangeChannel:!!r[at(s)]}):i.scale[t];void 0!==n&&c.set(t,n,!1)}}}(e,t):jm(e,t)}const Am={bins:e=>{let{model:t,fieldOrDatumDef:n}=e;return Zo(n)?function(e,t){const n=t.bin;if(mn(n)){const i=xd(e,t.field,n);return new sm((()=>e.getSignalName(i)))}if(pn(n)&&gn(n)&&void 0!==n.step)return{step:n.step};return}(t,n):void 0},interpolate:e=>{let{channel:t,fieldOrDatumDef:n}=e;return function(e,t){if(p([he,ye,ve],e)&&\"nominal\"!==t)return\"hcl\";return}(t,n.type)},nice:e=>{let{scaleType:n,channel:i,domain:r,domainMin:o,domainMax:a,fieldOrDatumDef:s}=e;return function(e,n,i,r,o,a){if(wa(a)?.bin||t.isArray(i)||null!=o||null!=r||p([dr.TIME,dr.UTC],e))return;return!!_t(n)||void 0}(n,i,r,o,a,s)},padding:e=>{let{channel:t,scaleType:n,fieldOrDatumDef:i,markDef:r,config:o}=e;return function(e,t,n,i,r,o){if(_t(e)){if(Dr(t)){if(void 0!==n.continuousPadding)return n.continuousPadding;const{type:t,orient:a}=r;if(\"bar\"===t&&(!Zo(i)||!i.bin&&!i.timeUnit)&&(\"vertical\"===a&&\"x\"===e||\"horizontal\"===a&&\"y\"===e))return o.continuousBandSize}if(t===dr.POINT)return n.pointPadding}return}(t,n,o.scale,i,r,o.bar)},paddingInner:e=>{let{scalePadding:t,channel:n,markDef:i,scaleType:r,config:o,hasNestedOffsetScale:a}=e;return function(e,t,n,i,r){let o=arguments.length>5&&void 0!==arguments[5]&&arguments[5];if(void 0!==e)return;if(_t(t)){const{bandPaddingInner:e,barBandPaddingInner:t,rectBandPaddingInner:i,tickBandPaddingInner:a,bandWithNestedOffsetPaddingInner:s}=r;return o?s:U(e,\"bar\"===n?t:\"tick\"===n?a:i)}if(jt(t)&&i===dr.BAND)return r.offsetBandPaddingInner;return}(t,n,i.type,r,o.scale,a)},paddingOuter:e=>{let{scalePadding:t,channel:n,scaleType:i,scalePaddingInner:r,config:o,hasNestedOffsetScale:a}=e;return function(e,t,n,i,r){let o=arguments.length>5&&v"
-  , "oid 0!==arguments[5]&&arguments[5];if(void 0!==e)return;if(_t(t)){const{bandPaddingOuter:e,bandWithNestedOffsetPaddingOuter:t}=r;if(o)return t;if(n===dr.BAND)return U(e,wn(i)?{signal:`${i.signal}/2`}:i/2)}else if(jt(t)){if(n===dr.POINT)return.5;if(n===dr.BAND)return r.offsetBandPaddingOuter}return}(t,n,i,r,o.scale,a)},reverse:e=>{let{fieldOrDatumDef:t,scaleType:n,channel:i,config:r}=e;return function(e,t,n,i){if(\"x\"===n&&void 0!==i.xReverse)return Sr(e)&&\"descending\"===t?wn(i.xReverse)?{signal:`!${i.xReverse.signal}`}:!i.xReverse:i.xReverse;if(Sr(e)&&\"descending\"===t)return!0;return}(n,Zo(t)?t.sort:void 0,i,r.scale)},zero:e=>{let{channel:n,fieldOrDatumDef:i,domain:r,markDef:o,scaleType:a,config:s,hasSecondaryRangeChannel:l}=e;return function(e,n,i,r,o,a,s){if(i&&\"unaggregated\"!==i&&Sr(o)){if(t.isArray(i)){const e=i[0],n=i[i.length-1];if(t.isNumber(e)&&e<=0&&t.isNumber(n)&&n>=0)return!0}return!1}if(\"size\"===e&&\"quantitative\"===n.type&&!Fr(o))return!0;if((!Zo(n)||!n.bin)&&p([...Ct,...Nt],e)){const{orient:t,type:n}=r;return(!p([\"bar\",\"area\",\"line\",\"trail\"],n)||!(\"horizontal\"===t&&\"y\"===e||\"vertical\"===t&&\"x\"===e))&&(!(!p([\"bar\",\"area\"],n)||s)||a?.zero)}return!1}(n,i,r,o,a,s.scale,l)}};function Tm(e){qm(e)?function(e){const t=e.component.scales;for(const n of Xt){const i=t[n];if(!i)continue;const r=Sm(n,e);i.setWithExplicit(\"range\",r)}}(e):jm(e,\"range\")}function jm(e,t){const n=e.component.scales;for(const n of e.children)\"range\"===t?Tm(n):Nm(n,t);for(const i of D(n)){let r;for(const n of e.children){const e=n.component.scales[i];if(e){r=uc(r,e.getWithExplicit(t),t,\"scale\",lc(((e,n)=>\"range\"===t&&e.step&&n.step?e.step-n.step:0)))}}n[i].setWithExplicit(t,r)}}function Em(e,t,n,i){const r=function(e,t,n,i){switch(t.type){case\"nominal\":case\"ordinal\":if(We(e)||\"discrete\"===tn(e))return\"shape\"===e&&\"ordinal\"===t.type&&Si(fi(e,\"ordinal\")),\"ordinal\";if(Mt(e))return\"band\";if(_t(e)||jt(e)){if(p([\"rect\",\"bar\",\"image\",\"rule\",\"tick\"],n.type))return\"band\";if(i)return\"band\"}else if(\"arc\"===n.type&&e in Pt)return\"band\";return lo(n[st(e)])||ca(t)&&t.axis?.tickBand?\"band\":\"point\";case\"temporal\":return We(e)?\"time\":\"discrete\"===tn(e)?(Si(fi(e,\"temporal\")),\"ordinal\"):Zo(t)&&t.timeUnit&&Ii(t.timeUnit).utc?\"utc\":Mt(e)?\"band\":\"time\";case\"quantitative\":return We(e)?Zo(t)&&mn(t.bin)?\"bin-ordinal\":\"linear\":\"discrete\"===tn(e)?(Si(fi(e,\"quantitative\")),\"ordinal\"):Mt(e)?\"band\":\"linear\";case\"geojson\":return}throw new Error(oi(t.type))}(t,n,i,arguments.length>4&&void 0!==arguments[4]&&arguments[4]),{type:o}=e;return Qt(t)?void 0!==o?function(e,t){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];if(!Qt(e))return!1;switch(e){case te:case ne:case oe:case ae:case ce:case se:return!!Dr(t)||\"band\"===t||\"point\"===t&&!n;case ge:return p([\"linear\",\"band\"],t);case xe:case De:case we:case ke:case Se:case $e:return Dr(t)||Fr(t)||p([\"band\",\"point\",\"ordinal\"],t);case he:case ye:case ve:return\"band\"!==t;case Fe:case be:return\"ordinal\"===t||Fr(t)}}(t,o)?Zo(n)&&(a=o,s=n.type,!(p([lr,ur],s)?void 0===a||kr(a):s===cr?p([dr.TIME,dr.UTC,void 0],a):s!==sr||br(a)||Fr(a)||void 0===a))?(Si(function(e,t){return`FieldDef does not work with \"${e}\" scale. We are using \"${t}\" scale instead.`}(o,r)),r):o:(Si(function(e,t,n){return`Channel \"${e}\" does not work with \"${t}\" scale. We are using \"${n}\" scale instead.`}(t,o,r)),r):r:null;var a,s}function Mm(e){qm(e)?e.component.scales=function(e){const{encoding:t,mark:n,markDef:i}=e,r={};for(const o of Xt){const a=ka(t[o]);if(a&&n===Kr&&o===be&&a.type===fr)continue;let s=a&&a.scale;if(a&&null!==s&&!1!==s){s??={};const n=Em(s,o,a,i,Va(t,o));r[o]=new $m(e.scaleName(`${o}`,!0),{value:n,explicit:s.type===n})}}return r}(e):e.component.scales=function(e){const t=e.component.scales={},n={},i=e.component.resolve;for(const t of e.children){Mm(t);for(const r of D(t.component.scales))if(i.scale[r]??=Yf(r,e),\"shared\"===i.scale[r]){const e=n[r],o=t.component.scales[r].getWithExplicit(\"type\");e?pr(e.value,o.value)?n[r]=uc(e,o,\"type\",\"scale\",Rm):(i.scale[r]=\"independent\",delete n[r]):n[r]=o}}for(const i of D(n)){const r=e.scaleName(i,!0),o=n[i]"
-  , ";t[i]=new $m(r,o);for(const t of e.children){const e=t.component.scales[i];e&&(t.renameScale(e.get(\"name\"),r),e.merged=!0)}}return t}(e)}const Rm=lc(((e,t)=>hr(e)-hr(t)));class Lm{constructor(){this.nameMap={}}rename(e,t){this.nameMap[e]=t}has(e){return void 0!==this.nameMap[e]}get(e){for(;this.nameMap[e]&&e!==this.nameMap[e];)e=this.nameMap[e];return e}}function qm(e){return\"unit\"===e?.type}function Um(e){return\"facet\"===e?.type}function Wm(e){return\"concat\"===e?.type}function Im(e){return\"layer\"===e?.type}class Bm{constructor(e,n,i,r,o,a,c){this.type=n,this.parent=i,this.config=o,this.parent=i,this.config=o,this.view=bn(c),this.name=e.name??r,this.title=$n(e.title)?{text:e.title}:e.title?bn(e.title):void 0,this.scaleNameMap=i?i.scaleNameMap:new Lm,this.projectionNameMap=i?i.projectionNameMap:new Lm,this.signalNameMap=i?i.signalNameMap:new Lm,this.data=e.data,this.description=e.description,this.transforms=(e.transform??[]).map((e=>Ol(e)?{filter:s(e.filter,rr)}:e)),this.layout=\"layer\"===n||\"unit\"===n?{}:function(e,n,i){const r=i[n],o={},{spacing:a,columns:s}=r;void 0!==a&&(o.spacing=a),void 0!==s&&(Io(e)&&!Uo(e.facet)||Ts(e))&&(o.columns=s),js(e)&&(o.columns=1);for(const n of qs)if(void 0!==e[n])if(\"spacing\"===n){const i=e[n];o[n]=t.isNumber(i)?i:{row:i.row??a,column:i.column??a}}else o[n]=e[n];return o}(e,n,o),this.component={data:{sources:i?i.component.data.sources:[],outputNodes:i?i.component.data.outputNodes:{},outputNodeRefCounts:i?i.component.data.outputNodeRefCounts:{},isFaceted:Io(e)||i?.component.data.isFaceted&&void 0===e.data},layoutSize:new oc,layoutHeaders:{row:{},column:{},facet:{}},mark:null,resolve:{scale:{},axis:{},legend:{},...a?l(a):{}},selection:null,scales:null,projection:null,axes:{},legends:{}}}get width(){return this.getSizeSignalRef(\"width\")}get height(){return this.getSizeSignalRef(\"height\")}parse(){this.parseScale(),this.parseLayoutSize(),this.renameTopLevelLayoutSizeSignal(),this.parseSelections(),this.parseProjection(),this.parseData(),this.parseAxesAndHeaders(),this.parseLegends(),this.parseMarkGroup()}parseScale(){!function(e){let{ignoreRange:t}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};Mm(e),lm(e);for(const t of jr)Nm(e,t);t||Tm(e)}(this)}parseProjection(){yd(this)}renameTopLevelLayoutSizeSignal(){\"width\"!==this.getName(\"width\")&&this.renameSignal(this.getName(\"width\"),\"width\"),\"height\"!==this.getName(\"height\")&&this.renameSignal(this.getName(\"height\"),\"height\")}parseLegends(){sd(this)}assembleEncodeFromView(e){const{style:t,...n}=e,i={};for(const e of D(n)){const t=n[e];void 0!==t&&(i[e]=Pn(t))}return i}assembleGroupEncodeEntry(e){let t={};return this.view&&(t=this.assembleEncodeFromView(this.view)),e||(this.description&&(t.description=Pn(this.description)),\"unit\"!==this.type&&\"layer\"!==this.type)?S(t)?void 0:t:{width:this.getSizeSignalRef(\"width\"),height:this.getSizeSignalRef(\"height\"),...t}}assembleLayout(){if(!this.layout)return;const{spacing:e,...t}=this.layout,{component:n,config:i}=this,r=function(e,t){const n={};for(const i of Be){const r=e[i];if(r?.facetFieldDef){const{titleAnchor:e,titleOrient:o}=Cf([\"titleAnchor\",\"titleOrient\"],r.facetFieldDef.header,t,i),a=Of(i,o),s=qf(e,a);void 0!==s&&(n[a]=s)}}return S(n)?void 0:n}(n.layoutHeaders,i);return{padding:e,...this.assembleDefaultLayout(),...t,...r?{titleBand:r}:{}}}assembleDefaultLayout(){return{}}assembleHeaderMarks(){const{layoutHeaders:e}=this.component;let t=[];for(const n of Be)e[n].title&&t.push(Nf(this,n));for(const e of _f)t=t.concat(jf(this,e));return t}assembleAxes(){return function(e,t){const{x:n=[],y:i=[]}=e;return[...n.map((e=>gf(e,\"grid\",t))),...i.map((e=>gf(e,\"grid\",t))),...n.map((e=>gf(e,\"main\",t))),...i.map((e=>gf(e,\"main\",t)))].filter((e=>e))}(this.component.axes,this.config)}assembleLegends(){return dd(this)}assembleProjections(){return md(this)}assembleTitle(){const{encoding:e,...t}=this.title??{},n={...xn(this.config.title).nonMarkTitleProperties,...t,...e?{encode:{update:e}}:{}};if(n.text)return p([\"unit\",\"layer\"],this.type)?p([\"middle\",void 0],n.anchor)&&(n.frame??=\"group\"):n.anchor??=\"start\","
-  , "S(n)?void 0:n}assembleGroup(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];const t={};e=e.concat(this.assembleSignals()),e.length>0&&(t.signals=e);const n=this.assembleLayout();n&&(t.layout=n),t.marks=[].concat(this.assembleHeaderMarks(),this.assembleMarks());const i=!this.parent||Um(this.parent)?bm(this):[];i.length>0&&(t.scales=i);const r=this.assembleAxes();r.length>0&&(t.axes=r);const o=this.assembleLegends();return o.length>0&&(t.legends=o),t}getName(e){return C((this.name?`${this.name}_`:\"\")+e)}getDataName(e){return this.getName(bc[e].toLowerCase())}requestDataName(e){const t=this.getDataName(e),n=this.component.data.outputNodeRefCounts;return n[t]=(n[t]||0)+1,t}getSizeSignalRef(e){if(Um(this.parent)){const t=At(Hf(e)),n=this.component.scales[t];if(n&&!n.merged){const e=n.get(\"type\"),i=n.get(\"range\");if(kr(e)&&kn(i)){const e=n.get(\"name\"),i=ym(vm(this,t));if(i){return{signal:Vf(e,n,ma({aggregate:\"distinct\",field:i},{expr:\"datum\"}))}}return Si(Xn(t)),null}}}return{signal:this.signalNameMap.get(this.getName(e))}}lookupDataSource(e){const t=this.component.data.outputNodes[e];return t?t.getSource():e}getSignalName(e){return this.signalNameMap.get(e)}renameSignal(e,t){this.signalNameMap.rename(e,t)}renameScale(e,t){this.scaleNameMap.rename(e,t)}renameProjection(e,t){this.projectionNameMap.rename(e,t)}scaleName(e,t){return t?this.getName(e):tt(e)&&Qt(e)&&this.component.scales[e]||this.scaleNameMap.has(this.getName(e))?this.scaleNameMap.get(this.getName(e)):void 0}projectionName(e){return e?this.getName(\"projection\"):this.component.projection&&!this.component.projection.merged||this.projectionNameMap.has(this.getName(\"projection\"))?this.projectionNameMap.get(this.getName(\"projection\")):void 0}getScaleComponent(e){if(!this.component.scales)throw new Error(\"getScaleComponent cannot be called before parseScale(). Make sure you have called parseScale or use parseUnitModelWithScale().\");const t=this.component.scales[e];return t&&!t.merged?t:this.parent?this.parent.getScaleComponent(e):void 0}getScaleType(e){const t=this.getScaleComponent(e);return t?t.get(\"type\"):void 0}getSelectionComponent(e,t){let n=this.component.selection[e];if(!n&&this.parent&&(n=this.parent.getSelectionComponent(e,t)),!n)throw new Error(function(e){return`Cannot find a selection named \"${e}\".`}(t));return n}hasAxisOrientSignalRef(){return this.component.axes.x?.some((e=>e.hasOrientSignalRef()))||this.component.axes.y?.some((e=>e.hasOrientSignalRef()))}}class Vm extends Bm{vgField(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const n=this.fieldDef(e);if(n)return ma(n,t)}reduceFieldDef(e,n){return function(e,n,i,r){return e?D(e).reduce(((i,o)=>{const a=e[o];return t.isArray(a)?a.reduce(((e,t)=>n.call(r,e,t,o)),i):n.call(r,i,a,o)}),i):i}(this.getMapping(),((t,n,i)=>{const r=wa(n);return r?e(t,r,i):t}),n)}forEachFieldDef(e,t){Qa(this.getMapping(),((t,n)=>{const i=wa(t);i&&e(i,n)}),t)}}class Hm extends $c{clone(){return new Hm(null,l(this.transform))}constructor(e,t){super(e),this.transform=t,this.transform=l(t);const n=this.transform.as??[void 0,void 0];this.transform.as=[n[0]??\"value\",n[1]??\"density\"];const i=this.transform.resolve??\"shared\";this.transform.resolve=i}dependentFields(){return new Set([this.transform.density,...this.transform.groupby??[]])}producedFields(){return new Set(this.transform.as)}hash(){return`DensityTransform ${d(this.transform)}`}assemble(){const{density:e,...t}=this.transform,n={type:\"kde\",field:e,...t};return n.resolve=this.transform.resolve,n}}class Gm extends $c{clone(){return new Gm(null,l(this.transform))}constructor(e,t){super(e),this.transform=t,this.transform=l(t)}dependentFields(){return new Set([this.transform.extent])}producedFields(){return new Set([])}hash(){return`ExtentTransform ${d(this.transform)}`}assemble(){const{extent:e,param:t}=this.transform;return{type:\"extent\",field:e,signal:t}}}class Ym extends $c{clone(){return new Ym(this.parent,l(this.transform))}constructor(e,t){super(e),this.transform=t,this.transform=l(t);const{flatten:n,as:i=[]}=this.transform;this.transform"
-  , ".as=n.map(((e,t)=>i[t]??e))}dependentFields(){return new Set(this.transform.flatten)}producedFields(){return new Set(this.transform.as)}hash(){return`FlattenTransform ${d(this.transform)}`}assemble(){const{flatten:e,as:t}=this.transform;return{type:\"flatten\",fields:e,as:t}}}class Xm extends $c{clone(){return new Xm(null,l(this.transform))}constructor(e,t){super(e),this.transform=t,this.transform=l(t);const n=this.transform.as??[void 0,void 0];this.transform.as=[n[0]??\"key\",n[1]??\"value\"]}dependentFields(){return new Set(this.transform.fold)}producedFields(){return new Set(this.transform.as)}hash(){return`FoldTransform ${d(this.transform)}`}assemble(){const{fold:e,as:t}=this.transform;return{type:\"fold\",fields:e,as:t}}}class Qm extends $c{clone(){return new Qm(null,l(this.fields),this.geojson,this.signal)}static parseAll(e,t){if(t.component.projection&&!t.component.projection.isFit)return e;let n=0;for(const i of[[de,fe],[pe,me]]){const r=i.map((e=>{const n=ka(t.encoding[e]);return Zo(n)?n.field:ta(n)?{expr:`${n.datum}`}:sa(n)?{expr:`${n.value}`}:void 0}));(r[0]||r[1])&&(e=new Qm(e,r,null,t.getName(\"geojson_\"+n++)))}if(t.channelHasField(be)){const i=t.typedFieldDef(be);i.type===fr&&(e=new Qm(e,null,i.field,t.getName(\"geojson_\"+n++)))}return e}constructor(e,t,n,i){super(e),this.fields=t,this.geojson=n,this.signal=i}dependentFields(){const e=(this.fields??[]).filter(t.isString);return new Set([...this.geojson?[this.geojson]:[],...e])}producedFields(){return new Set}hash(){return`GeoJSON ${this.geojson} ${this.signal} ${d(this.fields)}`}assemble(){return[...this.geojson?[{type:\"filter\",expr:`isValid(datum[\"${this.geojson}\"])`}]:[],{type:\"geojson\",...this.fields?{fields:this.fields}:{},...this.geojson?{geojson:this.geojson}:{},signal:this.signal}]}}class Jm extends $c{clone(){return new Jm(null,this.projection,l(this.fields),l(this.as))}constructor(e,t,n,i){super(e),this.projection=t,this.fields=n,this.as=i}static parseAll(e,t){if(!t.projectionName())return e;for(const n of[[de,fe],[pe,me]]){const i=n.map((e=>{const n=ka(t.encoding[e]);return Zo(n)?n.field:ta(n)?{expr:`${n.datum}`}:sa(n)?{expr:`${n.value}`}:void 0})),r=n[0]===pe?\"2\":\"\";(i[0]||i[1])&&(e=new Jm(e,t.projectionName(),i,[t.getName(`x${r}`),t.getName(`y${r}`)]))}return e}dependentFields(){return new Set(this.fields.filter(t.isString))}producedFields(){return new Set(this.as)}hash(){return`Geopoint ${this.projection} ${d(this.fields)} ${d(this.as)}`}assemble(){return{type:\"geopoint\",projection:this.projection,fields:this.fields,as:this.as}}}class Km extends $c{clone(){return new Km(null,l(this.transform))}constructor(e,t){super(e),this.transform=t}dependentFields(){return new Set([this.transform.impute,this.transform.key,...this.transform.groupby??[]])}producedFields(){return new Set([this.transform.impute])}processSequence(e){const{start:t=0,stop:n,step:i}=e;return{signal:`sequence(${[t,n,...i?[i]:[]].join(\",\")})`}}static makeFromTransform(e,t){return new Km(e,t)}static makeFromEncoding(e,t){const n=t.encoding,i=n.x,r=n.y;if(Zo(i)&&Zo(r)){const o=i.impute?i:r.impute?r:void 0;if(void 0===o)return;const a=i.impute?r:r.impute?i:void 0,{method:s,value:l,frame:c,keyvals:u}=o.impute,f=Ja(t.mark,n);return new Km(e,{impute:o.field,key:a.field,...s?{method:s}:{},...void 0!==l?{value:l}:{},...c?{frame:c}:{},...void 0!==u?{keyvals:u}:{},...f.length?{groupby:f}:{}})}return null}hash(){return`Impute ${d(this.transform)}`}assemble(){const{impute:e,key:t,keyvals:n,method:i,groupby:r,value:o,frame:a=[null,null]}=this.transform,s={type:\"impute\",field:e,key:t,...n?{keyvals:(l=n,J(l,\"stop\")?this.processSequence(n):n)}:{},method:\"value\",...r?{groupby:r}:{},value:i&&\"value\"!==i?null:o};var l;if(i&&\"value\"!==i){return[s,{type:\"window\",as:[`imputed_${e}_value`],ops:[i],fields:[e],frame:a,ignorePeers:!1,...r?{groupby:r}:{}},{type:\"formula\",expr:`datum.${e} === null ? datum.imputed_${e}_value : datum.${e}`,as:e}]}return[s]}}class Zm extends $c{clone(){return new Zm(null,l(this.transform))}constructor(e,t){super(e),this.transform=t,this.transform=l(t);const n=this.transform.as??[void 0,void"
-  , " 0];this.transform.as=[n[0]??t.on,n[1]??t.loess]}dependentFields(){return new Set([this.transform.loess,this.transform.on,...this.transform.groupby??[]])}producedFields(){return new Set(this.transform.as)}hash(){return`LoessTransform ${d(this.transform)}`}assemble(){const{loess:e,on:t,...n}=this.transform;return{type:\"loess\",x:t,y:e,...n}}}class ep extends $c{clone(){return new ep(null,l(this.transform),this.secondary)}constructor(e,t,n){super(e),this.transform=t,this.secondary=n}static make(e,t,n,i){const r=t.component.data.sources,{from:o}=n;let a=null;if(function(e){return J(e,\"data\")}(o)){let e=gp(o.data,r);e||(e=new Ad(o.data),r.push(e));const n=t.getName(`lookup_${i}`);a=new wc(e,n,bc.Lookup,t.component.data.outputNodeRefCounts),t.component.data.outputNodes[n]=a}else if(function(e){return J(e,\"param\")}(o)){const e=o.param;let i;n={as:e,...n};try{i=t.getSelectionComponent(C(e),e)}catch(t){throw new Error(function(e){return`Lookups can only be performed on selection parameters. \"${e}\" is a variable parameter.`}(e))}if(a=i.materialized,!a)throw new Error(function(e){return`Cannot define and lookup the \"${e}\" selection in the same view. Try moving the lookup into a second, layered view?`}(e))}return new ep(e,n,a.getSource())}dependentFields(){return new Set([this.transform.lookup])}producedFields(){return new Set(this.transform.as?t.array(this.transform.as):this.transform.from.fields)}hash(){return`Lookup ${d({transform:this.transform,secondary:this.secondary})}`}assemble(){let e;if(this.transform.from.fields)e={values:this.transform.from.fields,...this.transform.as?{as:t.array(this.transform.as)}:{}};else{let n=this.transform.as;t.isString(n)||(Si('If \"from.fields\" is not specified, \"as\" has to be a string that specifies the key to be used for the data from the secondary source.'),n=\"_lookup\"),e={as:[n]}}return{type:\"lookup\",from:this.secondary,key:this.transform.from.key,fields:[this.transform.lookup],...e,...this.transform.default?{default:this.transform.default}:{}}}}class tp extends $c{clone(){return new tp(null,l(this.transform))}constructor(e,t){super(e),this.transform=t,this.transform=l(t);const n=this.transform.as??[void 0,void 0];this.transform.as=[n[0]??\"prob\",n[1]??\"value\"]}dependentFields(){return new Set([this.transform.quantile,...this.transform.groupby??[]])}producedFields(){return new Set(this.transform.as)}hash(){return`QuantileTransform ${d(this.transform)}`}assemble(){const{quantile:e,...t}=this.transform;return{type:\"quantile\",field:e,...t}}}class np extends $c{clone(){return new np(null,l(this.transform))}constructor(e,t){super(e),this.transform=t,this.transform=l(t);const n=this.transform.as??[void 0,void 0];this.transform.as=[n[0]??t.on,n[1]??t.regression]}dependentFields(){return new Set([this.transform.regression,this.transform.on,...this.transform.groupby??[]])}producedFields(){return new Set(this.transform.as)}hash(){return`RegressionTransform ${d(this.transform)}`}assemble(){const{regression:e,on:t,...n}=this.transform;return{type:\"regression\",x:t,y:e,...n}}}class ip extends $c{clone(){return new ip(null,l(this.transform))}constructor(e,t){super(e),this.transform=t}addDimensions(e){this.transform.groupby=b((this.transform.groupby??[]).concat(e),(e=>e))}producedFields(){}dependentFields(){return new Set([this.transform.pivot,this.transform.value,...this.transform.groupby??[]])}hash(){return`PivotTransform ${d(this.transform)}`}assemble(){const{pivot:e,value:t,groupby:n,limit:i,op:r}=this.transform;return{type:\"pivot\",field:e,value:t,...void 0!==i?{limit:i}:{},...void 0!==r?{op:r}:{},...void 0!==n?{groupby:n}:{}}}}class rp extends $c{clone(){return new rp(null,l(this.transform))}constructor(e,t){super(e),this.transform=t}dependentFields(){return new Set}producedFields(){return new Set}hash(){return`SampleTransform ${d(this.transform)}`}assemble(){return{type:\"sample\",size:this.transform.sample}}}function op(e){let t=0;return function n(i,r){if(i instanceof Ad&&!i.isGenerator&&!dc(i.data)){e.push(r);r={name:null,source:r.name,transform:[]}}if(i instanceof Cd&&(i.parent instanceof Ad&&!r.source"
-  , "?(r.format={...r.format,parse:i.assembleFormatParse()},r.transform.push(...i.assembleTransforms(!0))):r.transform.push(...i.assembleTransforms())),i instanceof Dd)return r.name||(r.name=\"data_\"+t++),!r.source||r.transform.length>0?(e.push(r),i.data=r.name):i.data=r.source,void e.push(...i.assemble());if((i instanceof Pd||i instanceof Nd||i instanceof Qd||i instanceof uf||i instanceof Df||i instanceof Jm||i instanceof Sd||i instanceof ep||i instanceof Zd||i instanceof Xd||i instanceof Xm||i instanceof Ym||i instanceof Hm||i instanceof Zm||i instanceof tp||i instanceof np||i instanceof _d||i instanceof rp||i instanceof ip||i instanceof Gm)&&r.transform.push(i.assemble()),(i instanceof wd||i instanceof Dc||i instanceof Km||i instanceof Kd||i instanceof Qm)&&r.transform.push(...i.assemble()),i instanceof wc)if(r.source&&0===r.transform.length)i.setSource(r.source);else if(i.parent instanceof wc)i.setSource(r.name);else if(r.name||(r.name=\"data_\"+t++),i.setSource(r.name),1===i.numChildren()){e.push(r);r={name:null,source:r.name,transform:[]}}switch(i.numChildren()){case 0:i instanceof wc&&(!r.source||r.transform.length>0)&&e.push(r);break;case 1:n(i.children[0],r);break;default:{r.name||(r.name=\"data_\"+t++);let o=r.name;!r.source||r.transform.length>0?e.push(r):o=r.source;for(const e of i.children){n(e,{name:null,source:o,transform:[]})}break}}}}function ap(e){return\"top\"===e||\"left\"===e||wn(e)?\"header\":\"footer\"}function sp(e,n){const{facet:i,config:r,child:o,component:a}=e;if(e.channelHasField(n)){const s=i[n],l=zf(\"title\",null,r,n);let c=va(s,r,{allowDisabling:!0,includeDefault:void 0===l||!!l});o.component.layoutHeaders[n].title&&(c=t.isArray(c)?c.join(\", \"):c,c+=` / ${o.component.layoutHeaders[n].title}`,o.component.layoutHeaders[n].title=null);const u=zf(\"labelOrient\",s.header,r,n),f=null!==s.header&&U(s.header?.labels,r.header.labels,!0),d=p([\"bottom\",\"right\"],u)?\"footer\":\"header\";a.layoutHeaders[n]={title:null!==s.header?c:null,facetFieldDef:s,[d]:\"facet\"===n?[]:[lp(e,n,f)]}}}function lp(e,t,n){const i=\"row\"===t?\"height\":\"width\";return{labels:n,sizeSignal:e.child.component.layoutSize.get(i)?e.child.getSizeSignalRef(i):void 0,axes:[]}}function cp(e,t){const{child:n}=e;if(n.component.axes[t]){const{layoutHeaders:i,resolve:r}=e.component;if(r.axis[t]=Xf(r,t),\"shared\"===r.axis[t]){const r=\"x\"===t?\"column\":\"row\",o=i[r];for(const i of n.component.axes[t]){const t=ap(i.get(\"orient\"));o[t]??=[lp(e,r,!1)];const n=gf(i,\"main\",e.config,{header:!0});n&&o[t][0].axes.push(n),i.mainExtracted=!0}}}}function up(e){for(const t of e.children)t.parseLayoutSize()}function fp(e,t){const n=Hf(t),i=At(n),r=e.component.resolve,o=e.component.layoutSize;let a;for(const t of e.children){const o=t.component.layoutSize.getWithExplicit(n),s=r.scale[i]??Yf(i,e);if(\"independent\"===s&&\"step\"===o.value){a=void 0;break}if(a){if(\"independent\"===s&&a.value!==o.value){a=void 0;break}a=uc(a,o,n,\"\")}else a=o}if(a){for(const i of e.children)e.renameSignal(i.getName(n),e.getName(t)),i.component.layoutSize.set(n,\"merged\",!1);o.setWithExplicit(t,a)}else o.setWithExplicit(t,{explicit:!1,value:void 0})}function dp(e,t){const n=\"width\"===t?\"x\":\"y\",i=e.config,r=e.getScaleComponent(n);if(r){const e=r.get(\"type\"),n=r.get(\"range\");if(kr(e)){const e=Is(i.view,t);return kn(n)||Rs(e)?\"step\":e}return Us(i.view,t)}if(e.hasProjection||\"arc\"===e.mark)return Us(i.view,t);{const e=Is(i.view,t);return Rs(e)?e.step:e}}function mp(e,t,n){return ma(t,{suffix:`by_${ma(e)}`,...n})}class pp extends Vm{constructor(e,t,n,i){super(e,\"facet\",t,n,i,e.resolve),this.child=Up(e.spec,this,this.getName(\"child\"),void 0,i),this.children=[this.child],this.facet=this.initFacet(e.facet)}initFacet(e){if(!Uo(e))return{facet:this.initFacetFieldDef(e,\"facet\")};const t=D(e),n={};for(const i of t){if(![K,Z].includes(i)){Si(li(i,\"facet\"));break}const t=e[i];if(void 0===t.field){Si(si(t,i));break}n[i]=this.initFacetFieldDef(t,i)}return n}initFacetFieldDef(e,t){const n=Fa(e,t);return n.header?n.header=bn(n.header):null===n.header&&(n.header=null),n}channelHasField(e){return J(this.facet,e)}fieldDef(e){return "
-  , "this.facet[e]}parseData(){this.component.data=hp(this),this.child.parseData()}parseLayoutSize(){up(this)}parseSelections(){this.child.parseSelections(),this.component.selection=this.child.component.selection,Object.values(this.component.selection).some((e=>af(e)))&&ki(ti)}parseMarkGroup(){this.child.parseMarkGroup()}parseAxesAndHeaders(){this.child.parseAxesAndHeaders(),function(e){for(const t of Be)sp(e,t);cp(e,\"x\"),cp(e,\"y\")}(this)}assembleSelectionTopLevelSignals(e){return this.child.assembleSelectionTopLevelSignals(e)}assembleSignals(){return this.child.assembleSignals(),[]}assembleSelectionData(e){return this.child.assembleSelectionData(e)}getHeaderLayoutMixins(){const e={};for(const t of Be)for(const n of Pf){const i=this.component.layoutHeaders[t],r=i[n],{facetFieldDef:o}=i;if(o){const n=zf(\"titleOrient\",o.header,this.config,t);if([\"right\",\"bottom\"].includes(n)){const i=Of(t,n);e.titleAnchor??={},e.titleAnchor[i]=\"end\"}}if(r?.[0]){const r=\"row\"===t?\"height\":\"width\",o=\"header\"===n?\"headerBand\":\"footerBand\";\"facet\"===t||this.child.component.layoutSize.get(r)||(e[o]??={},e[o][t]=.5),i.title&&(e.offset??={},e.offset[\"row\"===t?\"rowTitle\":\"columnTitle\"]=10)}}return e}assembleDefaultLayout(){const{column:e,row:t}=this.facet,n=e?this.columnDistinctSignal():t?1:void 0;let i=\"all\";return(t||\"independent\"!==this.component.resolve.scale.x)&&(e||\"independent\"!==this.component.resolve.scale.y)||(i=\"none\"),{...this.getHeaderLayoutMixins(),...n?{columns:n}:{},bounds:\"full\",align:i}}assembleLayoutSignals(){return this.child.assembleLayoutSignals()}columnDistinctSignal(){if(!(this.parent&&this.parent instanceof pp)){return{signal:`length(data('${this.getName(\"column_domain\")}'))`}}}assembleGroupStyle(){}assembleGroup(e){return this.parent&&this.parent instanceof pp?{...this.channelHasField(\"column\")?{encode:{update:{columns:{field:ma(this.facet.column,{prefix:\"distinct\"})}}}}:{},...super.assembleGroup(e)}:super.assembleGroup(e)}getCardinalityAggregateForChild(){const e=[],t=[],n=[];if(this.child instanceof pp){if(this.child.channelHasField(\"column\")){const i=ma(this.child.facet.column);e.push(i),t.push(\"distinct\"),n.push(`distinct_${i}`)}}else for(const i of Ct){const r=this.child.component.scales[i];if(r&&!r.merged){const o=r.get(\"type\"),a=r.get(\"range\");if(kr(o)&&kn(a)){const r=ym(vm(this.child,i));r?(e.push(r),t.push(\"distinct\"),n.push(`distinct_${r}`)):Si(Xn(i))}}}return{fields:e,ops:t,as:n}}assembleFacet(){const{name:e,data:n}=this.component.data.facetRoot,{row:i,column:r}=this.facet,{fields:o,ops:a,as:s}=this.getCardinalityAggregateForChild(),l=[];for(const e of Be){const n=this.facet[e];if(n){l.push(ma(n));const{bin:c,sort:u}=n;if(mn(c)&&l.push(ma(n,{binSuffix:\"end\"})),Lo(u)){const{field:e,op:t=Eo}=u,l=mp(n,u);i&&r?(o.push(l),a.push(\"max\"),s.push(l)):(o.push(e),a.push(t),s.push(l))}else if(t.isArray(u)){const t=Ff(n,e);o.push(t),a.push(\"max\"),s.push(t)}}}const c=!!i&&!!r;return{name:e,data:n,groupby:l,...c||o.length>0?{aggregate:{...c?{cross:c}:{},...o.length?{fields:o,ops:a,as:s}:{}}}:{}}}facetSortFields(e){const{facet:n}=this,i=n[e];return i?Lo(i.sort)?[mp(i,i.sort,{expr:\"datum\"})]:t.isArray(i.sort)?[Ff(i,e,{expr:\"datum\"})]:[ma(i,{expr:\"datum\"})]:[]}facetSortOrder(e){const{facet:n}=this,i=n[e];if(i){const{sort:e}=i;return[(Lo(e)?e.order:!t.isArray(e)&&e)||\"ascending\"]}return[]}assembleLabelTitle(){const{facet:e,config:t}=this;if(e.facet)return Mf(e.facet,\"facet\",t);const n={row:[\"top\",\"bottom\"],column:[\"left\",\"right\"]};for(const i of _f)if(e[i]){const r=zf(\"labelOrient\",e[i]?.header,t,i);if(n[i].includes(r))return Mf(e[i],i,t)}}assembleMarks(){const{child:e}=this,t=function(e){const t=[],n=op(t);for(const t of e.children)n(t,{source:e.name,name:null,transform:[]});return t}(this.component.data.facetRoot),n=e.assembleGroupEncodeEntry(!1),i=this.assembleLabelTitle()||e.assembleTitle(),r=e.assembleGroupStyle();return[{name:this.getName(\"cell\"),type:\"group\",...i?{title:i}:{},...r?{style:r}:{},from:{facet:this.assembleFacet()},sort:{field:Be.map((e=>this.facetSortFields(e))).flat(),order:Be.map((e=>this.facetSortOrder(e))).flat"
-  , "()},...t.length>0?{data:t}:{},...n?{encode:{update:n}}:{},...e.assembleGroup(Gc(this,[]))}]}getMapping(){return this.facet}}function gp(e,t){for(const n of t){const t=n.data;if(e.name&&n.hasName()&&e.name!==n.dataName)continue;const i=e.format?.mesh,r=t.format?.feature;if(i&&r)continue;const o=e.format?.feature;if((o||r)&&o!==r)continue;const a=t.format?.mesh;if(!i&&!a||i===a)if(mc(e)&&mc(t)){if(X(e.values,t.values))return n}else if(dc(e)&&dc(t)){if(e.url===t.url)return n}else if(pc(e)&&e.name===n.dataName)return n}return null}function hp(e){let t=function(e,t){if(e.data||!e.parent){if(null===e.data){const e=new Ad({values:[]});return t.push(e),e}const n=gp(e.data,t);if(n)return gc(e.data)||(n.data.format=y({},e.data.format,n.data.format)),!n.hasName()&&e.data.name&&(n.dataName=e.data.name),n;{const n=new Ad(e.data);return t.push(n),n}}return e.parent.component.data.facetRoot?e.parent.component.data.facetRoot:e.parent.component.data.main}(e,e.component.data.sources);const{outputNodes:n,outputNodeRefCounts:i}=e.component.data,r=e.data,o=!(r&&(gc(r)||dc(r)||mc(r)))&&e.parent?e.parent.component.data.ancestorParse.clone():new fc;gc(r)?(hc(r)?t=new Nd(t,r.sequence):vc(r)&&(t=new Pd(t,r.graticule)),o.parseNothing=!0):null===r?.format?.parse&&(o.parseNothing=!0),t=Cd.makeExplicit(t,e,o)??t,t=new _d(t);const a=e.parent&&Im(e.parent);(qm(e)||Um(e))&&a&&(t=wd.makeFromEncoding(t,e)??t),e.transforms.length>0&&(t=function(e,t,n){let i=0;for(const r of t.transforms){let o,a;if(Rl(r))a=e=new Df(e,r),o=\"derived\";else if(Ol(r)){const i=Od(r);a=e=Cd.makeWithAncestors(e,{},i,n)??e,e=new uf(e,t,r.filter)}else if(Ll(r))a=e=wd.makeFromTransform(e,r,t),o=\"number\";else if(Ul(r))o=\"date\",void 0===n.getWithExplicit(r.field).value&&(e=new Cd(e,{[r.field]:o}),n.set(r.field,o,!1)),a=e=Dc.makeFromTransform(e,r);else if(Wl(r))a=e=Sd.makeFromTransform(e,r),o=\"number\",rf(t)&&(e=new _d(e));else if(zl(r))a=e=ep.make(e,t,r,i++),o=\"derived\";else if(jl(r))a=e=new Zd(e,r),o=\"number\";else if(El(r))a=e=new Xd(e,r),o=\"number\";else if(Il(r))a=e=Kd.makeFromTransform(e,r),o=\"derived\";else if(Bl(r))a=e=new Xm(e,r),o=\"derived\";else if(Vl(r))a=e=new Gm(e,r),o=\"derived\";else if(Ml(r))a=e=new Ym(e,r),o=\"derived\";else if(Cl(r))a=e=new ip(e,r),o=\"derived\";else if(Tl(r))e=new rp(e,r);else if(ql(r))a=e=Km.makeFromTransform(e,r),o=\"derived\";else if(_l(r))a=e=new Hm(e,r),o=\"derived\";else if(Pl(r))a=e=new tp(e,r),o=\"derived\";else if(Nl(r))a=e=new np(e,r),o=\"derived\";else{if(!Al(r)){Si(`Ignoring an invalid transform: ${Q(r)}.`);continue}a=e=new Zm(e,r),o=\"derived\"}if(a&&void 0!==o)for(const e of a.producedFields()??[])n.set(e,o,!1)}return e}(t,e,o));const s=function(e){const t={};if(qm(e)&&e.component.selection)for(const n of D(e.component.selection)){const i=e.component.selection[n];for(const e of i.project.items)!e.channel&&q(e.field)>1&&(t[e.field]=\"flatten\")}return t}(e),l=zd(e);t=Cd.makeWithAncestors(t,{},{...s,...l},o)??t,qm(e)&&(t=Qm.parseAll(t,e),t=Jm.parseAll(t,e)),(qm(e)||Um(e))&&(a||(t=wd.makeFromEncoding(t,e)??t),t=Dc.makeFromEncoding(t,e)??t,t=Df.parseAllForSortIndex(t,e));const c=t=yp(bc.Raw,e,t);if(qm(e)){const n=Sd.makeFromEncoding(t,e);n&&(t=n,rf(e)&&(t=new _d(t))),t=Km.makeFromEncoding(t,e)??t,t=Kd.makeFromEncoding(t,e)??t}let u,f;if(qm(e)){const{markDef:n,mark:i,config:r}=e,o=En(\"invalid\",n,r),{marks:a,scales:s}=f=xc({invalid:o,isPath:Zr(i)});a!==s&&\"include-invalid-values\"===s&&(u=t=yp(bc.PreFilterInvalid,e,t)),\"exclude-invalid-values\"===a&&(t=Qd.make(t,e,f)??t)}const d=t=yp(bc.Main,e,t);let m;if(qm(e)&&f){const{marks:n,scales:i}=f;\"include-invalid-values\"===n&&\"exclude-invalid-values\"===i&&(t=Qd.make(t,e,f)??t,m=t=yp(bc.PostFilterInvalid,e,t))}qm(e)&&function(e,t){for(const[n,i]of O(e.component.selection??{})){const r=e.getName(`lookup_${n}`);e.component.data.outputNodes[r]=i.materialized=new wc(new uf(t,e,{param:n}),r,bc.Lookup,e.component.data.outputNodeRefCounts)}}(e,d);let p=null;if(Um(e)){const i=e.getName(\"facet\");t=function(e,t){const{row:n,column:i}=t;if(n&&i){let t=null;for(const r of[n,i])if(Lo(r.sort)){const{field:n,op:i=Eo}=r.sort;e=t=new Xd(e,{"
-  , "joinaggregate:[{op:i,field:n,as:mp(r,r.sort,{forAs:!0})}],groupby:[ma(r)]})}return t}return null}(t,e.facet)??t,p=new Dd(t,e,i,d.getSource()),n[i]=p}return{...e.component.data,outputNodes:n,outputNodeRefCounts:i,raw:c,main:d,facetRoot:p,ancestorParse:o,preFilterInvalid:u,postFilterInvalid:m}}function yp(e,t,n){const{outputNodes:i,outputNodeRefCounts:r}=t.component.data,o=t.getDataName(e),a=new wc(n,o,e,r);return i[o]=a,a}class vp extends Bm{constructor(e,t,n,i){super(e,\"concat\",t,n,i,e.resolve),\"shared\"!==e.resolve?.axis?.x&&\"shared\"!==e.resolve?.axis?.y||Si(\"Axes cannot be shared in concatenated or repeated views yet (https://github.com/vega/vega-lite/issues/2415).\"),this.children=this.getChildren(e).map(((e,t)=>Up(e,this,this.getName(`concat_${t}`),void 0,i)))}parseData(){this.component.data=hp(this);for(const e of this.children)e.parseData()}parseSelections(){this.component.selection={};for(const e of this.children){e.parseSelections();for(const t of D(e.component.selection))this.component.selection[t]=e.component.selection[t]}Object.values(this.component.selection).some((e=>af(e)))&&ki(ti)}parseMarkGroup(){for(const e of this.children)e.parseMarkGroup()}parseAxesAndHeaders(){for(const e of this.children)e.parseAxesAndHeaders()}getChildren(e){return js(e)?e.vconcat:Es(e)?e.hconcat:e.concat}parseLayoutSize(){!function(e){up(e);const t=1===e.layout.columns?\"width\":\"childWidth\",n=void 0===e.layout.columns?\"height\":\"childHeight\";fp(e,t),fp(e,n)}(this)}parseAxisGroup(){return null}assembleSelectionTopLevelSignals(e){return this.children.reduce(((e,t)=>t.assembleSelectionTopLevelSignals(e)),e)}assembleSignals(){return this.children.forEach((e=>e.assembleSignals())),[]}assembleLayoutSignals(){const e=Wf(this);for(const t of this.children)e.push(...t.assembleLayoutSignals());return e}assembleSelectionData(e){return this.children.reduce(((e,t)=>t.assembleSelectionData(e)),e)}assembleMarks(){return this.children.map((e=>{const t=e.assembleTitle(),n=e.assembleGroupStyle(),i=e.assembleGroupEncodeEntry(!1);return{type:\"group\",name:e.getName(\"group\"),...t?{title:t}:{},...n?{style:n}:{},...i?{encode:{update:i}}:{},...e.assembleGroup()}}))}assembleGroupStyle(){}assembleDefaultLayout(){const e=this.layout.columns;return{...null!=e?{columns:e}:{},bounds:\"full\",align:\"each\"}}}const bp={disable:1,gridScale:1,scale:1,...Ma,labelExpr:1,encode:1},xp=D(bp);class $p extends oc{constructor(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];super(),this.explicit=e,this.implicit=t,this.mainExtracted=n}clone(){return new $p(l(this.explicit),l(this.implicit),this.mainExtracted)}hasAxisPart(e){return\"axis\"===e||(\"grid\"===e||\"title\"===e?!!this.get(e):!(!1===(t=this.get(e))||null===t));var t}hasOrientSignalRef(){return wn(this.explicit.orient)}}const wp={bottom:\"top\",top:\"bottom\",left:\"right\",right:\"left\"};function kp(e,t){if(!e)return t.map((e=>e.clone()));{if(e.length!==t.length)return;const n=e.length;for(let i=0;i<n;i++){const n=e[i],r=t[i];if(!!n!=!!r)return;if(n&&r){const t=n.getWithExplicit(\"orient\"),o=r.getWithExplicit(\"orient\");if(t.explicit&&o.explicit&&t.value!==o.value)return;e[i]=Sp(n,r)}}}return e}function Sp(e,t){for(const n of xp){const i=uc(e.getWithExplicit(n),t.getWithExplicit(n),n,\"axis\",((e,t)=>{switch(n){case\"title\":return In(e,t);case\"gridScale\":return{explicit:e.explicit,value:U(e.value,t.value)}}return cc(e,t,n,\"axis\")}));e.setWithExplicit(n,i)}return e}function Dp(e,t,n,i,r){if(\"disable\"===t)return void 0!==n;switch(n=n||{},t){case\"titleAngle\":case\"labelAngle\":return e===(wn(n.labelAngle)?n.labelAngle:H(n.labelAngle));case\"values\":return!!n.values;case\"encode\":return!!n.encoding||!!n.labelAngle;case\"title\":if(e===Sf(i,r))return!0}return e===n[t]}const Fp=new Set([\"grid\",\"translate\",\"format\",\"formatType\",\"orient\",\"labelExpr\",\"tickCount\",\"position\",\"tickMinStep\"]);function Op(e,t){let n=t.axis(e);const i=new $p,r=ka(t.encoding[e]),{mark:o,config:a}=t,s=n?.orient||a[\"x\"===e?\"axisX\":\"axisY\"]?.orien"
-  , "t||a.axis?.orient||function(e){return\"x\"===e?\"bottom\":\"left\"}(e),l=t.getScaleComponent(e).get(\"type\"),c=function(e,t,n,i){const r=\"band\"===t?[\"axisDiscrete\",\"axisBand\"]:\"point\"===t?[\"axisDiscrete\",\"axisPoint\"]:br(t)?[\"axisQuantitative\"]:\"time\"===t||\"utc\"===t?[\"axisTemporal\"]:[],o=\"x\"===e?\"axisX\":\"axisY\",a=wn(n)?\"axisOrient\":`axis${N(n)}`,s=[...r,...r.map((e=>o+e.substr(4)))],l=[\"axis\",a,o];return{vlOnlyAxisConfig:yf(s,i,e,n),vgAxisConfig:yf(l,i,e,n),axisConfigStyle:vf([...l,...s],i)}}(e,l,s,t.config),u=void 0!==n?!n:bf(\"disable\",a.style,n?.style,c).configValue;if(i.set(\"disable\",u,void 0!==n),u)return i;n=n||{};const f=function(e,t,n,i,r){const o=t?.labelAngle;if(void 0!==o)return wn(o)?o:H(o);{const{configValue:o}=bf(\"labelAngle\",i,t?.style,r);return void 0!==o?H(o):n!==te||!p([ur,lr],e.type)||Zo(e)&&e.timeUnit?void 0:270}}(r,n,e,a.style,c),d=Po(n.formatType,r,l),m=_o(r,r.type,n.format,n.formatType,a,!0),g={fieldOrDatumDef:r,axis:n,channel:e,model:t,scaleType:l,orient:s,labelAngle:f,format:m,formatType:d,mark:o,config:a};for(const r of xp){const o=r in xf?xf[r](g):La(r)?n[r]:void 0,s=void 0!==o,l=Dp(o,r,n,t,e);if(s&&l)i.set(r,o,l);else{const{configValue:e,configFrom:t}=La(r)&&\"values\"!==r?bf(r,a.style,n.style,c):{},u=void 0!==e;s&&!u?i.set(r,o,l):(\"vgAxisConfig\"!==t||Fp.has(r)&&u||Ta(e)||wn(e))&&i.set(r,e,!1)}}const h=n.encoding??{},y=ja.reduce(((n,r)=>{if(!i.hasAxisPart(r))return n;const o=Gf(h[r]??{},t),a=\"labels\"===r?function(e,t,n){const{encoding:i,config:r}=e,o=ka(i[t])??ka(i[at(t)]),a=e.axis(t)||{},{format:s,formatType:l}=a;if(So(l))return{text:Co({fieldOrDatumDef:o,field:\"datum.value\",format:s,formatType:l,config:r}),...n};if(void 0===s&&void 0===l&&r.customFormatTypes){if(\"quantitative\"===ea(o)){if(ca(o)&&\"normalize\"===o.stack&&r.normalizedNumberFormatType)return{text:Co({fieldOrDatumDef:o,field:\"datum.value\",format:r.normalizedNumberFormat,formatType:r.normalizedNumberFormatType,config:r}),...n};if(r.numberFormatType)return{text:Co({fieldOrDatumDef:o,field:\"datum.value\",format:r.numberFormat,formatType:r.numberFormatType,config:r}),...n}}if(\"temporal\"===ea(o)&&r.timeFormatType&&Zo(o)&&!o.timeUnit)return{text:Co({fieldOrDatumDef:o,field:\"datum.value\",format:r.timeFormat,formatType:r.timeFormatType,config:r}),...n}}return n}(t,e,o):o;return void 0===a||S(a)||(n[r]={update:a}),n}),{});return S(y)||i.set(\"encode\",y,!!n.encoding||void 0!==n.labelAngle),i}function zp(e,t){const{config:n}=e;return{...Pu(e,{align:\"ignore\",baseline:\"ignore\",color:\"include\",size:\"include\",orient:\"ignore\",theta:\"ignore\"}),...yu(\"x\",e,{defaultPos:\"mid\"}),...yu(\"y\",e,{defaultPos:\"mid\"}),...mu(\"size\",e),...mu(\"angle\",e),...Cp(e,n,t)}}function Cp(e,t,n){return n?{shape:{value:n}}:mu(\"shape\",e)}const _p={vgMark:\"rule\",encodeEntry:e=>{const{markDef:t}=e,n=t.orient;return e.encoding.x||e.encoding.y||e.encoding.latitude||e.encoding.longitude?{...Pu(e,{align:\"ignore\",baseline:\"ignore\",color:\"include\",orient:\"ignore\",size:\"ignore\",theta:\"ignore\"}),...ku(\"x\",e,{defaultPos:\"horizontal\"===n?\"zeroOrMax\":\"mid\",defaultPos2:\"zeroOrMin\",range:\"vertical\"!==n}),...ku(\"y\",e,{defaultPos:\"vertical\"===n?\"zeroOrMax\":\"mid\",defaultPos2:\"zeroOrMin\",range:\"horizontal\"!==n}),...mu(\"size\",e,{vgChannel:\"strokeWidth\"})}:{}}};function Pp(e,t,n){if(void 0===En(\"align\",e,n))return\"center\"}function Np(e,t,n){if(void 0===En(\"baseline\",e,n))return\"middle\"}const Ap={vgMark:\"rect\",encodeEntry:e=>{const{config:t,markDef:n}=e,i=n.orient,r=\"horizontal\"===i?\"x\":\"y\",o=\"horizontal\"===i?\"y\":\"x\",a=\"horizontal\"===i?\"height\":\"width\";return{...Pu(e,{align:\"ignore\",baseline:\"ignore\",color:\"include\",orient:\"ignore\",size:\"ignore\",theta:\"ignore\"}),...Fu(e,r),...yu(o,e,{defaultPos:\"mid\",vgChannel:\"y\"===o?\"yc\":\"xc\"}),[a]:Pn(En(\"thickness\",n,t))}}},Tp={arc:{vgMark:\"arc\",encodeEntry:e=>({...Pu(e,{align:\"ignore\",baseline:\"ignore\",color:\"include\",size:\"ignore\",orient:\"ignore\",theta:\"ignore\"}),...yu(\"x\",e,{defaultPos:\"mid\"}),...yu(\"y\",e,{defaultPos:\"mid\"}),...Fu(e,\"radius\"),...Fu(e,\"theta\")})},area:{vgMark:\"area\",encodeEntry:e=>({...Pu(e,{align:\"ignore\",baseline:\"ignore\",color:\"include\",orient:\"inclu"
-  , "de\",size:\"ignore\",theta:\"ignore\"}),...ku(\"x\",e,{defaultPos:\"zeroOrMin\",defaultPos2:\"zeroOrMin\",range:\"horizontal\"===e.markDef.orient}),...ku(\"y\",e,{defaultPos:\"zeroOrMin\",defaultPos2:\"zeroOrMin\",range:\"vertical\"===e.markDef.orient}),...Tu(e)})},bar:{vgMark:\"rect\",encodeEntry:e=>({...Pu(e,{align:\"ignore\",baseline:\"ignore\",color:\"include\",orient:\"ignore\",size:\"ignore\",theta:\"ignore\"}),...Fu(e,\"x\"),...Fu(e,\"y\")})},circle:{vgMark:\"symbol\",encodeEntry:e=>zp(e,\"circle\")},geoshape:{vgMark:\"shape\",encodeEntry:e=>({...Pu(e,{align:\"ignore\",baseline:\"ignore\",color:\"include\",size:\"ignore\",orient:\"ignore\",theta:\"ignore\"})}),postEncodingTransform:e=>{const{encoding:t}=e,n=t.shape;return[{type:\"geoshape\",projection:e.projectionName(),...n&&Zo(n)&&n.type===fr?{field:ma(n,{expr:\"datum\"})}:{}}]}},image:{vgMark:\"image\",encodeEntry:e=>({...Pu(e,{align:\"ignore\",baseline:\"ignore\",color:\"ignore\",orient:\"ignore\",size:\"ignore\",theta:\"ignore\"}),...Fu(e,\"x\"),...Fu(e,\"y\"),...ou(e,\"url\")})},line:{vgMark:\"line\",encodeEntry:e=>({...Pu(e,{align:\"ignore\",baseline:\"ignore\",color:\"include\",size:\"ignore\",orient:\"ignore\",theta:\"ignore\"}),...yu(\"x\",e,{defaultPos:\"mid\"}),...yu(\"y\",e,{defaultPos:\"mid\"}),...mu(\"size\",e,{vgChannel:\"strokeWidth\"}),...Tu(e)})},point:{vgMark:\"symbol\",encodeEntry:e=>zp(e)},rect:{vgMark:\"rect\",encodeEntry:e=>({...Pu(e,{align:\"ignore\",baseline:\"ignore\",color:\"include\",orient:\"ignore\",size:\"ignore\",theta:\"ignore\"}),...Fu(e,\"x\"),...Fu(e,\"y\")})},rule:_p,square:{vgMark:\"symbol\",encodeEntry:e=>zp(e,\"square\")},text:{vgMark:\"text\",encodeEntry:e=>{const{config:t,encoding:n}=e;return{...Pu(e,{align:\"include\",baseline:\"include\",color:\"include\",size:\"ignore\",orient:\"ignore\",theta:\"include\"}),...yu(\"x\",e,{defaultPos:\"mid\"}),...yu(\"y\",e,{defaultPos:\"mid\"}),...ou(e),...mu(\"size\",e,{vgChannel:\"fontSize\"}),...mu(\"angle\",e),...ju(\"align\",Pp(e.markDef,n,t)),...ju(\"baseline\",Np(e.markDef,n,t)),...yu(\"radius\",e,{defaultPos:null}),...yu(\"theta\",e,{defaultPos:null})}}},tick:Ap,trail:{vgMark:\"trail\",encodeEntry:e=>({...Pu(e,{align:\"ignore\",baseline:\"ignore\",color:\"include\",size:\"include\",orient:\"ignore\",theta:\"ignore\"}),...yu(\"x\",e,{defaultPos:\"mid\"}),...yu(\"y\",e,{defaultPos:\"mid\"}),...mu(\"size\",e),...Tu(e)})}};function jp(e){if(p([Ir,qr,Xr],e.mark)){const t=Ja(e.mark,e.encoding);if(t.length>0)return function(e,t){return[{name:e.getName(\"pathgroup\"),type:\"group\",from:{facet:{name:Ep+e.requestDataName(bc.Main),data:e.requestDataName(bc.Main),groupby:t}},encode:{update:{width:{field:{group:\"width\"}},height:{field:{group:\"height\"}}}},marks:Rp(e,{fromPrefix:Ep})}]}(e,t)}else if(e.mark===Ur){const t=On.some((t=>En(t,e.markDef,e.config)));if(e.stack&&!e.fieldDef(\"size\")&&t)return function(e){const[t]=Rp(e,{fromPrefix:Mp}),n=e.scaleName(e.stack.fieldChannel),i=function(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return e.vgField(e.stack.fieldChannel,t)},r=(e,t)=>`${e}(${[i({prefix:\"min\",suffix:\"start\",expr:t}),i({prefix:\"max\",suffix:\"start\",expr:t}),i({prefix:\"min\",suffix:\"end\",expr:t}),i({prefix:\"max\",suffix:\"end\",expr:t})].map((e=>`scale('${n}',${e})`)).join(\",\")})`;let o,a;\"x\"===e.stack.fieldChannel?(o={...u(t.encode.update,[\"y\",\"yc\",\"y2\",\"height\",...On]),x:{signal:r(\"min\",\"datum\")},x2:{signal:r(\"max\",\"datum\")},clip:{value:!0}},a={x:{field:{group:\"x\"},mult:-1},height:{field:{group:\"height\"}}},t.encode.update={...f(t.encode.update,[\"y\",\"yc\",\"y2\"]),height:{field:{group:\"height\"}}}):(o={...u(t.encode.update,[\"x\",\"xc\",\"x2\",\"width\"]),y:{signal:r(\"min\",\"datum\")},y2:{signal:r(\"max\",\"datum\")},clip:{value:!0}},a={y:{field:{group:\"y\"},mult:-1},width:{field:{group:\"width\"}}},t.encode.update={...f(t.encode.update,[\"x\",\"xc\",\"x2\"]),width:{field:{group:\"width\"}}});for(const n of On){const i=Mn(n,e.markDef,e.config);t.encode.update[n]?(o[n]=t.encode.update[n],delete t.encode.update[n]):i&&(o[n]=Pn(i)),i&&(t.encode.update[n]={value:0})}const s=[];if(e.stack.groupbyChannels?.length>0)for(const t of e.stack.groupbyChannels){const n=e.fieldDef(t),i=ma(n);i&&s.push(i),(n?.bin||n?.timeUnit)&&s.push(ma(n,{binSuffix:\"end\"}))}o=[\"stroke\",\"strokeWidth\",\"strokeJoin\""
-  , ",\"strokeCap\",\"strokeDash\",\"strokeDashOffset\",\"strokeMiterLimit\",\"strokeOpacity\"].reduce(((n,i)=>{if(t.encode.update[i])return{...n,[i]:t.encode.update[i]};{const t=Mn(i,e.markDef,e.config);return void 0!==t?{...n,[i]:Pn(t)}:n}}),o),o.stroke&&(o.strokeForeground={value:!0},o.strokeOffset={value:0});return[{type:\"group\",from:{facet:{data:e.requestDataName(bc.Main),name:Mp+e.requestDataName(bc.Main),groupby:s,aggregate:{fields:[i({suffix:\"start\"}),i({suffix:\"start\"}),i({suffix:\"end\"}),i({suffix:\"end\"})],ops:[\"min\",\"max\",\"min\",\"max\"]}}},encode:{update:o},marks:[{type:\"group\",encode:{update:a},marks:[t]}]}]}(e)}return Rp(e)}const Ep=\"faceted_path_\";const Mp=\"stack_group_\";function Rp(e){let n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{fromPrefix:\"\"};const{mark:i,markDef:r,encoding:o,config:a}=e,s=U(r.clip,function(e){const t=e.getScaleComponent(\"x\"),n=e.getScaleComponent(\"y\");return!(!t?.get(\"selectionExtent\")&&!n?.get(\"selectionExtent\"))||void 0}(e),function(e){const t=e.component.projection;return!(!t||t.isFit)||void 0}(e)),l=jn(r),c=o.key,u=function(e){const{encoding:n,stack:i,mark:r,markDef:o,config:a}=e,s=n.order;if(!(!t.isArray(s)&&sa(s)&&m(s.value)||!s&&m(En(\"order\",o,a)))){if((t.isArray(s)||Zo(s))&&!i)return qn(s,{expr:\"datum\"});if(Zr(r)){const e=\"horizontal\"===o.orient?\"y\":\"x\";if(Zo(n[e]))return{field:e}}}}(e),f=function(e){if(!e.component.selection)return null;const t=D(e.component.selection).length;let n=t,i=e.parent;for(;i&&0===n;)n=D(i.component.selection).length,i=i.parent;return n?{interactive:t>0||\"geoshape\"===e.mark||!!e.encoding.tooltip||!!e.markDef.tooltip}:null}(e),d=En(\"aria\",r,a),p=Tp[i].postEncodingTransform?Tp[i].postEncodingTransform(e):null;return[{name:e.getName(\"marks\"),type:Tp[i].vgMark,...s?{clip:s}:{},...l?{style:l}:{},...c?{key:c.field}:{},...u?{sort:u}:{},...f||{},...!1===d?{aria:d}:{},from:{data:n.fromPrefix+e.requestDataName(bc.Main)},encode:{update:Tp[i].encodeEntry(e)},...p?{transform:p}:{}}]}class Lp extends Vm{specifiedScales={};specifiedAxes={};specifiedLegends={};specifiedProjection={};selection=[];children=[];constructor(e,n,i){let r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},o=arguments.length>4?arguments[4]:void 0;super(e,\"unit\",n,i,o,void 0,Ls(e)?e.view:void 0);const a=no(e.mark)?{...e.mark}:{type:e.mark},s=a.type;void 0===a.filled&&(a.filled=function(e,t,n){let{graticule:i}=n;if(i)return!1;const r=Mn(\"filled\",e,t),o=e.type;return U(r,o!==Br&&o!==Ir&&o!==Hr)}(a,o,{graticule:e.data&&vc(e.data)}));const l=this.encoding=function(e,n,i,r){const o={};for(const t of D(e))tt(t)||Si(`${a=t}-encoding is dropped as ${a} is not a valid encoding channel.`);var a;for(let a of ft){if(!e[a])continue;const s=e[a];if(jt(a)){const e=ut(a),t=o[e];if(Zo(t)&&or(t.type)&&Zo(s)&&!t.timeUnit){Si(ri(e));continue}}if(\"angle\"!==a||\"arc\"!==n||e.theta||(Si(\"Arc marks uses theta channel rather than angle, replacing angle with theta.\"),a=ce),Ya(e,a,n)){if(a===xe&&\"line\"===n){const t=wa(e[a]);if(t?.aggregate){Si(\"Line marks cannot encode size with a non-groupby field. You may want to use trail marks instead.\");continue}}if(a===he&&(i?\"fill\"in e:\"stroke\"in e))Si(ai(\"encoding\",{fill:\"fill\"in e,stroke:\"stroke\"in e}));else if(a===Ce||a===ze&&!t.isArray(s)&&!sa(s)||a===Pe&&t.isArray(s)){if(s){if(a===ze){const t=e[a];if(Xo(t)){o[a]=t;continue}}o[a]=t.array(s).reduce(((e,t)=>(Zo(t)?e.push(Fa(t,a)):Si(si(t,a)),e)),[])}}else{if(a===Pe&&null===s)o[a]=null;else if(!(Zo(s)||ta(s)||sa(s)||Qo(s)||wn(s))){Si(si(s,a));continue}o[a]=Sa(s,a,r)}}else Si(li(a,n))}return o}(e.encoding||{},s,a.filled,o);this.markDef=fl(a,l,o),this.size=function(e){let{encoding:t,size:n}=e;for(const e of Ct){const i=st(e);Rs(n[i])&&na(t[e])&&(delete n[i],Si(hi(i)))}return n}({encoding:l,size:Ls(e)?{...r,...e.width?{width:e.width}:{},...e.height?{height:e.height}:{}}:r}),this.stack=ul(this.markDef,l),this.specifiedScales=this.initScales(s,l),this.specifiedAxes=this.initAxes(l),this.specifiedLegends=this.initLegends(l),this.specifiedProjection=e.projection,this.selection=(e.params??[]).filter((e=>Ns(e)))}get hasProjection(){co"
-  , "nst{encoding:e}=this,t=this.mark===Kr,n=e&&qe.some((t=>oa(e[t])));return t||n}scaleDomain(e){const t=this.specifiedScales[e];return t?t.domain:void 0}axis(e){return this.specifiedAxes[e]}legend(e){return this.specifiedLegends[e]}initScales(e,t){return Xt.reduce(((e,n)=>{const i=ka(t[n]);return i&&(e[n]=this.initScale(i.scale??{})),e}),{})}initScale(e){const{domain:n,range:i}=e,r=bn(e);return t.isArray(n)&&(r.domain=n.map(Cn)),t.isArray(i)&&(r.range=i.map(Cn)),r}initAxes(e){return Ct.reduce(((t,n)=>{const i=e[n];if(oa(i)||n===te&&oa(e.x2)||n===ne&&oa(e.y2)){const e=oa(i)?i.axis:void 0;t[n]=e?this.initAxis({...e}):e}return t}),{})}initAxis(e){const t=D(e),n={};for(const i of t){const t=e[i];n[i]=Ta(t)?zn(t):Cn(t)}return n}initLegends(e){return Gt.reduce(((t,n)=>{const i=ka(e[n]);if(i&&function(e){switch(e){case he:case ye:case ve:case xe:case be:case we:case De:case Fe:return!0;case ke:case Se:case $e:case ge:return!1}}(n)){const e=i.legend;t[n]=e?bn(e):e}return t}),{})}parseData(){this.component.data=hp(this)}parseLayoutSize(){!function(e){const{size:t,component:n}=e;for(const i of Ct){const r=st(i);if(t[r]){const e=t[r];n.layoutSize.set(r,Rs(e)?\"step\":e,!0)}else{const t=dp(e,r);n.layoutSize.set(r,t,!1)}}}(this)}parseSelections(){this.component.selection=function(e,n){const i={},r=e.config.selection;if(!n||!n.length)return i;let o=0;for(const a of n){const n=C(a.name),s=a.select,c=t.isString(s)?s:s.type,u=t.isObject(s)?l(s):{type:c},f=r[c];for(const e in f)\"fields\"!==e&&\"encodings\"!==e&&(\"mark\"===e&&(u.mark={...f.mark,...u.mark}),void 0!==u[e]&&!0!==u[e]||(u[e]=l(f[e]??u[e])));const d=i[n]={...u,name:n,type:c,init:a.value,bind:a.bind,events:t.isString(u.on)?t.parseSelector(u.on,\"scope\"):t.array(l(u.on))};if(af(d)&&(o++,o>1)){delete i[n];continue}const m=l(a);for(const t of tf)t.defined(d)&&t.parse&&t.parse(e,d,m)}return o>1&&Si(\"Multiple timer selections in one unit spec are not supported. Ignoring all but the first.\"),i}(this,this.selection)}parseMarkGroup(){this.component.mark=jp(this)}parseAxesAndHeaders(){var e;this.component.axes=(e=this,Ct.reduce(((t,n)=>(e.component.scales[n]&&(t[n]=[Op(n,e)]),t)),{}))}assembleSelectionTopLevelSignals(e){return function(e,n){let i=!1;for(const r of F(e.component.selection??{})){const o=r.name,a=t.stringValue(o+Ju);if(0===n.filter((e=>e.name===o)).length){const e=\"global\"===r.resolve?\"union\":r.resolve,i=\"point\"===r.type?\", true, true)\":\")\";n.push({name:r.name,update:`${ef}(${a}, ${t.stringValue(e)}${i}`})}i=!0;for(const t of tf)t.defined(r)&&t.topLevelSignals&&(n=t.topLevelSignals(e,r,n))}i&&0===n.filter((e=>\"unit\"===e.name)).length&&n.unshift({name:\"unit\",value:{},on:[{events:\"pointermove\",update:\"isTuple(group()) ? group() : unit\"}]});return Xc(n)}(this,e)}assembleSignals(){return[...hf(this),...Hc(this,[])]}assembleSelectionData(e){return function(e,t){const n=[],i=[],r=nf(e,{escape:!1});for(const o of F(e.component.selection??{})){const a={name:o.name+Ju};if(o.project.hasSelectionId&&(a.transform=[{type:\"collect\",sort:{field:zs}}]),o.init){const e=o.project.items.map(Bc);a.values=o.project.hasSelectionId?o.init.map((e=>({unit:r,[zs]:Vc(e,!1)[0]}))):o.init.map((t=>({unit:r,fields:e,values:Vc(t,!1)})))}if([...n,...t].filter((e=>e.name===o.name+Ju)).length||n.push(a),af(o)&&t.length){const n=e.lookupDataSource(e.getDataName(bc.Main)),r=t.find((e=>e.name===n)),o=r.transform.find((e=>\"filter\"===e.type&&e.expr.includes(\"vlSelectionTest\")));if(o){r.transform=r.transform.filter((e=>e!==o));const e={name:r.name+Tc,source:r.name,transform:[o]};i.push(e)}}}return n.concat(t,i)}(this,e)}assembleLayout(){return null}assembleLayoutSignals(){return Wf(this)}correctDataNames=e=>(e.from?.data&&(e.from.data=this.lookupDataSource(e.from.data),\"time\"in this.encoding&&(e.from.data=e.from.data+Tc)),e.from?.facet?.data&&(e.from.facet.data=this.lookupDataSource(e.from.facet.data)),e);assembleMarks(){let e=this.component.mark??[];return this.parent&&Im(this.parent)||(e=Yc(this,e)),e.map(this.correctDataNames)}assembleGroupStyle(){const{style:e}=this.view||{};return void 0!==e?e:this.encoding.x||this.enc"
-  , "oding.y?\"cell\":\"view\"}getMapping(){return this.encoding}get mark(){return this.markDef.type}channelHasField(e){return Ia(this.encoding,e)}fieldDef(e){return wa(this.encoding[e])}typedFieldDef(e){const t=this.fieldDef(e);return aa(t)?t:null}}class qp extends Bm{constructor(e,t,n,i,r){super(e,\"layer\",t,n,r,e.resolve,e.view);const o={...i,...e.width?{width:e.width}:{},...e.height?{height:e.height}:{}};this.children=e.layer.map(((e,t)=>{if(il(e))return new qp(e,this,this.getName(`layer_${t}`),o,r);if(Ua(e))return new Lp(e,this,this.getName(`layer_${t}`),o,r);throw new Error(Bn(e))}))}parseData(){this.component.data=hp(this);for(const e of this.children)e.parseData()}parseLayoutSize(){var e;up(e=this),fp(e,\"width\"),fp(e,\"height\")}parseSelections(){this.component.selection={};for(const e of this.children){e.parseSelections();for(const t of D(e.component.selection))this.component.selection[t]=e.component.selection[t]}Object.values(this.component.selection).some((e=>af(e)))&&ki(ti)}parseMarkGroup(){for(const e of this.children)e.parseMarkGroup()}parseAxesAndHeaders(){!function(e){const{axes:t,resolve:n}=e.component,i={top:0,bottom:0,right:0,left:0};for(const i of e.children){i.parseAxesAndHeaders();for(const r of D(i.component.axes))n.axis[r]=Xf(e.component.resolve,r),\"shared\"===n.axis[r]&&(t[r]=kp(t[r],i.component.axes[r]),t[r]||(n.axis[r]=\"independent\",delete t[r]))}for(const r of Ct){for(const o of e.children)if(o.component.axes[r]){if(\"independent\"===n.axis[r]){t[r]=(t[r]??[]).concat(o.component.axes[r]);for(const e of o.component.axes[r]){const{value:t,explicit:n}=e.getWithExplicit(\"orient\");if(!wn(t)){if(i[t]>0&&!n){const n=wp[t];i[t]>i[n]&&e.set(\"orient\",n,!1)}i[t]++}}}delete o.component.axes[r]}if(\"independent\"===n.axis[r]&&t[r]&&t[r].length>1)for(const[e,n]of(t[r]||[]).entries())e>0&&n.get(\"grid\")&&!n.explicit.grid&&(n.implicit.grid=!1)}}(this)}assembleSelectionTopLevelSignals(e){return this.children.reduce(((e,t)=>t.assembleSelectionTopLevelSignals(e)),e)}assembleSignals(){return this.children.reduce(((e,t)=>e.concat(t.assembleSignals())),hf(this))}assembleLayoutSignals(){return this.children.reduce(((e,t)=>e.concat(t.assembleLayoutSignals())),Wf(this))}assembleSelectionData(e){return this.children.reduce(((e,t)=>t.assembleSelectionData(e)),e)}assembleGroupStyle(){const e=new Set;for(const n of this.children)for(const i of t.array(n.assembleGroupStyle()))e.add(i);const n=Array.from(e);return n.length>1?n:1===n.length?n[0]:void 0}assembleTitle(){let e=super.assembleTitle();if(e)return e;for(const t of this.children)if(e=t.assembleTitle(),e)return e}assembleLayout(){return null}assembleMarks(){return function(e,t){for(const n of e.children)qm(n)&&(t=Yc(n,t));return t}(this,this.children.flatMap((e=>e.assembleMarks())))}assembleLegends(){return this.children.reduce(((e,t)=>e.concat(t.assembleLegends())),dd(this))}}function Up(e,t,n,i,r){if(Io(e))return new pp(e,t,n,r);if(il(e))return new qp(e,t,n,i,r);if(Ua(e))return new Lp(e,t,n,i,r);if(function(e){return js(e)||Es(e)||Ts(e)}(e))return new vp(e,t,n,r);throw new Error(Bn(e))}const Wp=n;e.accessPathDepth=q,e.accessPathWithDatum=A,e.accessWithDatumToUnescapedPath=j,e.compile=function(e){let n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};var i;n.logger&&(i=n.logger,wi=i),n.fieldTitle&&ya(n.fieldTitle);try{const i=Js(t.mergeConfig(n.config,e.config)),r=Kl(e,i),o=Up(r,null,\"\",void 0,i);o.parse(),function(e,t){rm(e.sources);let n=0,i=0;for(let i=0;i<im&&am(e,t,!0);i++)n++;e.sources.map(em);for(let n=0;n<im&&am(e,t,!1);n++)i++;rm(e.sources),Math.max(n,i)===im&&Si(`Maximum optimization runs(${im}) reached.`)}(o.component.data,o);const a=function(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i=arguments.length>3?arguments[3]:void 0;const r=e.config?tl(e.config):void 0,o=function(e,t){const n=[],i=op(n);let r=0;for(const t of e.sources){t.hasName()||(t.dataName=\"source_\"+r++);const e=t.assemble();i(t,e)}for(const e of n)0===e.transform.length&&delete e.transform;let o=0;for(const[e,t]of n.entries())0!==(t.transform??[]).length||t.source||n.splice"
-  , "(o++,0,n.splice(e,1)[0]);for(const t of n)for(const n of t.transform??[])\"lookup\"===n.type&&(n.from=e.outputNodes[n.from].getSource());for(const e of n)e.name in t&&(e.values=t[e.name]);return n}(e.component.data,n),a=e.assembleSelectionData(o),s=e.assembleProjections(),l=e.assembleTitle(),c=e.assembleGroupStyle(),u=e.assembleGroupEncodeEntry(!0);let f=e.assembleLayoutSignals();f=f.filter((e=>\"width\"!==e.name&&\"height\"!==e.name||void 0===e.value||(t[e.name]=+e.value,!1)));const{params:d,...m}=t;return{$schema:\"https://vega.github.io/schema/vega/v5.json\",...e.description?{description:e.description}:{},...m,...l?{title:l}:{},...c?{style:c}:{},...u?{encode:{update:u}}:{},data:a,...s.length>0?{projections:s}:{},...e.assembleGroup([...f,...e.assembleSelectionTopLevelSignals([]),...As(d)]),...r?{config:r}:{},...i?{usermeta:i}:{}}}(o,function(e,n,i,r){const o=r.component.layoutSize.get(\"width\"),a=r.component.layoutSize.get(\"height\");void 0===n?(n={type:\"pad\"},r.hasAxisOrientSignalRef()&&(n.resize=!0)):t.isString(n)&&(n={type:n});if(o&&a&&(s=n.type,[\"fit\",\"fit-x\",\"fit-y\"].includes(s)))if(\"step\"===o&&\"step\"===a)Si(Yn()),n.type=\"pad\";else if(\"step\"===o||\"step\"===a){const e=\"step\"===o?\"width\":\"height\";Si(Yn(At(e)));const t=\"width\"===e?\"height\":\"width\";n.type=function(e){return e?`fit-${At(e)}`:\"fit\"}(t)}var s;return{...1===D(n).length&&n.type?\"pad\"===n.type?{}:{autosize:n.type}:{autosize:n},...rc(i,!1),...rc(e,!0)}}(e,r.autosize,i,o),e.datasets,e.usermeta);return{spec:a,normalized:r}}finally{n.logger&&(wi=$i),n.fieldTitle&&ya(ga)}},e.contains=p,e.deepEqual=X,e.deleteNestedProperty=P,e.duplicate=l,e.entries=O,e.every=h,e.fieldIntersection=k,e.flatAccessWithDatum=T,e.getFirstDefined=U,e.hasIntersection=$,e.hasProperty=J,e.hash=d,e.internalField=B,e.isBoolean=z,e.isEmpty=S,e.isEqual=function(e,t){const n=D(e),i=D(t);if(n.length!==i.length)return!1;for(const i of n)if(e[i]!==t[i])return!1;return!0},e.isInternalField=V,e.isNullOrFalse=m,e.isNumeric=G,e.keys=D,e.logicalExpr=_,e.mergeDeep=y,e.never=c,e.normalize=Kl,e.normalizeAngle=H,e.omit=f,e.pick=u,e.prefixGenerator=w,e.removePathFromField=L,e.replaceAll=R,e.replacePathInField=M,e.resetIdCounter=function(){W=42},e.setEqual=x,e.some=g,e.stringify=Q,e.titleCase=N,e.unique=b,e.uniqueId=I,e.vals=F,e.varName=C,e.version=Wp}));\n//# sourceMappingURL=vega-lite.min.js.map\n"
-  ]
-
-vegaEmbedJS :: Text
-vegaEmbedJS = T.concat
-  [ "!function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=t(require(\"vega\"),require(\"vega-lite\")):\"function\"==typeof define&&define.amd?define([\"vega\",\"vega-lite\"],t):(e=\"undefined\"!=typeof globalThis?globalThis:e||self).vegaEmbed=t(e.vega,e.vegaLite)}(this,(function(e,t){\"use strict\";function n(e){var t=Object.create(null);return e&&Object.keys(e).forEach((function(n){if(\"default\"!==n){var r=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(t,n,r.get?r:{enumerable:!0,get:function(){return e[n]}})}})),t.default=e,Object.freeze(t)}var r,i=n(e),o=n(t),a=(r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},r(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),s=Object.prototype.hasOwnProperty;function l(e,t){return s.call(e,t)}function c(e){if(Array.isArray(e)){for(var t=new Array(e.length),n=0;n<t.length;n++)t[n]=\"\"+n;return t}if(Object.keys)return Object.keys(e);var r=[];for(var i in e)l(e,i)&&r.push(i);return r}function f(e){switch(typeof e){case\"object\":return JSON.parse(JSON.stringify(e));case\"undefined\":return null;default:return e}}function d(e){for(var t,n=0,r=e.length;n<r;){if(!((t=e.charCodeAt(n))>=48&&t<=57))return!1;n++}return!0}function p(e){return-1===e.indexOf(\"/\")&&-1===e.indexOf(\"~\")?e:e.replace(/~/g,\"~0\").replace(/\\//g,\"~1\")}function u(e){return e.replace(/~1/g,\"/\").replace(/~0/g,\"~\")}function h(e){if(void 0===e)return!0;if(e)if(Array.isArray(e)){for(var t=0,n=e.length;t<n;t++)if(h(e[t]))return!0}else if(\"object\"==typeof e)for(var r=c(e),i=r.length,o=0;o<i;o++)if(h(e[r[o]]))return!0;return!1}function g(e,t){var n=[e];for(var r in t){var i=\"object\"==typeof t[r]?JSON.stringify(t[r],null,2):t[r];void 0!==i&&n.push(r+\": \"+i)}return n.join(\"\\n\")}var m=function(e){function t(t,n,r,i,o){var a=this.constructor,s=e.call(this,g(t,{name:n,index:r,operation:i,tree:o}))||this;return s.name=n,s.index=r,s.operation=i,s.tree=o,Object.setPrototypeOf(s,a.prototype),s.message=g(t,{name:n,index:r,operation:i,tree:o}),s}return a(t,e),t}(Error),E=m,v=f,b={add:function(e,t,n){return e[t]=this.value,{newDocument:n}},remove:function(e,t,n){var r=e[t];return delete e[t],{newDocument:n,removed:r}},replace:function(e,t,n){var r=e[t];return e[t]=this.value,{newDocument:n,removed:r}},move:function(e,t,n){var r=w(n,this.path);r&&(r=f(r));var i=O(n,{op:\"remove\",path:this.from}).removed;return O(n,{op:\"add\",path:this.path,value:i}),{newDocument:n,removed:r}},copy:function(e,t,n){var r=w(n,this.from);return O(n,{op:\"add\",path:this.path,value:f(r)}),{newDocument:n}},test:function(e,t,n){return{newDocument:n,test:L(e[t],this.value)}},_get:function(e,t,n){return this.value=e[t],{newDocument:n}}},y={add:function(e,t,n){return d(t)?e.splice(t,0,this.value):e[t]=this.value,{newDocument:n,index:t}},remove:function(e,t,n){return{newDocument:n,removed:e.splice(t,1)[0]}},replace:function(e,t,n){var r=e[t];return e[t]=this.value,{newDocument:n,removed:r}},move:b.move,copy:b.copy,test:b.test,_get:b._get};function w(e,t){if(\"\"==t)return e;var n={op:\"_get\",path:t};return O(e,n),n.value}function O(e,t,n,r,i,o){if(void 0===n&&(n=!1),void 0===r&&(r=!0),void 0===i&&(i=!0),void 0===o&&(o=0),n&&(\"function\"==typeof n?n(t,0,e,t.path):I(t,0)),\"\"===t.path){var a={newDocument:e};if(\"add\"===t.op)return a.newDocument=t.value,a;if(\"replace\"===t.op)return a.newDocument=t.value,a.removed=e,a;if(\"move\"===t.op||\"copy\"===t.op)return a.newDocument=w(e,t.from),\"move\"===t.op&&(a.removed=e),a;if(\"test\"===t.op){if(a.test=L(e,t.value),!1===a.test)throw new E(\"Test operation failed\",\"TEST_OPERATION_FAILED\",o,t,e);return a.newDocument=e,a}if(\"remove\"===t.op)return a.removed=e,a.newDocument=null,a;if(\"_get\"===t.op)return t.value=e,a;if(n)throw new E(\"Operation `op` property is not one of operations defined in RFC-6902\",\"OPERATION_OP_INVALID\",o,t,e);return a}r||(e=f(e));var s=(t.path||\"\").split(\"/\"),l=e,c=1,p=s.length,h=void 0,g=void 0,m=void 0;for(m=\""
-  , "function\"==typeof n?n:I;;){if((g=s[c])&&-1!=g.indexOf(\"~\")&&(g=u(g)),i&&(\"__proto__\"==g||\"prototype\"==g&&c>0&&\"constructor\"==s[c-1]))throw new TypeError(\"JSON-Patch: modifying `__proto__` or `constructor/prototype` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README\");if(n&&void 0===h&&(void 0===l[g]?h=s.slice(0,c).join(\"/\"):c==p-1&&(h=t.path),void 0!==h&&m(t,0,e,h)),c++,Array.isArray(l)){if(\"-\"===g)g=l.length;else{if(n&&!d(g))throw new E(\"Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index\",\"OPERATION_PATH_ILLEGAL_ARRAY_INDEX\",o,t,e);d(g)&&(g=~~g)}if(c>=p){if(n&&\"add\"===t.op&&g>l.length)throw new E(\"The specified index MUST NOT be greater than the number of elements in the array\",\"OPERATION_VALUE_OUT_OF_BOUNDS\",o,t,e);if(!1===(a=y[t.op].call(t,l,g,e)).test)throw new E(\"Test operation failed\",\"TEST_OPERATION_FAILED\",o,t,e);return a}}else if(c>=p){if(!1===(a=b[t.op].call(t,l,g,e)).test)throw new E(\"Test operation failed\",\"TEST_OPERATION_FAILED\",o,t,e);return a}if(l=l[g],n&&c<p&&(!l||\"object\"!=typeof l))throw new E(\"Cannot perform operation at the desired path\",\"OPERATION_PATH_UNRESOLVABLE\",o,t,e)}}function A(e,t,n,r,i){if(void 0===r&&(r=!0),void 0===i&&(i=!0),n&&!Array.isArray(t))throw new E(\"Patch sequence must be an array\",\"SEQUENCE_NOT_AN_ARRAY\");r||(e=f(e));for(var o=new Array(t.length),a=0,s=t.length;a<s;a++)o[a]=O(e,t[a],n,!0,i,a),e=o[a].newDocument;return o.newDocument=e,o}function I(e,t,n,r){if(\"object\"!=typeof e||null===e||Array.isArray(e))throw new E(\"Operation is not an object\",\"OPERATION_NOT_AN_OBJECT\",t,e,n);if(!b[e.op])throw new E(\"Operation `op` property is not one of operations defined in RFC-6902\",\"OPERATION_OP_INVALID\",t,e,n);if(\"string\"!=typeof e.path)throw new E(\"Operation `path` property is not a string\",\"OPERATION_PATH_INVALID\",t,e,n);if(0!==e.path.indexOf(\"/\")&&e.path.length>0)throw new E('Operation `path` property must start with \"/\"',\"OPERATION_PATH_INVALID\",t,e,n);if((\"move\"===e.op||\"copy\"===e.op)&&\"string\"!=typeof e.from)throw new E(\"Operation `from` property is not present (applicable in `move` and `copy` operations)\",\"OPERATION_FROM_REQUIRED\",t,e,n);if((\"add\"===e.op||\"replace\"===e.op||\"test\"===e.op)&&void 0===e.value)throw new E(\"Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)\",\"OPERATION_VALUE_REQUIRED\",t,e,n);if((\"add\"===e.op||\"replace\"===e.op||\"test\"===e.op)&&h(e.value))throw new E(\"Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)\",\"OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED\",t,e,n);if(n)if(\"add\"==e.op){var i=e.path.split(\"/\").length,o=r.split(\"/\").length;if(i!==o+1&&i!==o)throw new E(\"Cannot perform an `add` operation at the desired path\",\"OPERATION_PATH_CANNOT_ADD\",t,e,n)}else if(\"replace\"===e.op||\"remove\"===e.op||\"_get\"===e.op){if(e.path!==r)throw new E(\"Cannot perform the operation at a path that does not exist\",\"OPERATION_PATH_UNRESOLVABLE\",t,e,n)}else if(\"move\"===e.op||\"copy\"===e.op){var a=x([{op:\"_get\",path:e.from,value:void 0}],n);if(a&&\"OPERATION_PATH_UNRESOLVABLE\"===a.name)throw new E(\"Cannot perform the operation from a path that does not exist\",\"OPERATION_FROM_UNRESOLVABLE\",t,e,n)}}function x(e,t,n){try{if(!Array.isArray(e))throw new E(\"Patch sequence must be an array\",\"SEQUENCE_NOT_AN_ARRAY\");if(t)A(f(t),f(e),n||!0);else{n=n||I;for(var r=0;r<e.length;r++)n(e[r],r,t,void 0)}}catch(e){if(e instanceof E)return e;throw e}}function L(e,t){if(e===t)return!0;if(e&&t&&\"object\"==typeof e&&\"object\"==typeof t){var n,r,i,o=Array.isArray(e),a=Array.isArray(t);if(o&&a){if((r=e.length)!=t.length)return!1;for(n=r;0!=n--;)if(!L(e[n],t[n]))return!1;return!0}if(o!=a)return!1;var s=Object.keys(e);if((r=s.length)!==Object.keys(t).length)return!1;for(n=r;0!=n--;)if(!t.hasOwnProperty(s[n]))return!1;for(n=r;0!=n--;)if(!L(e[i=s[n]],t[i]))return!1;return!0}return e!=e&&t!=t}var N=Object.freeze({__proto__:null,JsonPatchEr"
-  , "ror:E,_areEquals:L,applyOperation:O,applyPatch:A,applyReducer:function(e,t,n){var r=O(e,t);if(!1===r.test)throw new E(\"Test operation failed\",\"TEST_OPERATION_FAILED\",n,t,e);return r.newDocument},deepClone:v,getValueByPointer:w,validate:x,validator:I}),$=new WeakMap,R=function(e){this.observers=new Map,this.obj=e},T=function(e,t){this.callback=e,this.observer=t};\n/*!\n     * https://github.com/Starcounter-Jack/JSON-Patch\n     * (c) 2017-2021 Joachim Wester\n     * MIT license\n     */function S(e,t){void 0===t&&(t=!1);var n=$.get(e.object);C(n.value,e.object,e.patches,\"\",t),e.patches.length&&A(n.value,e.patches);var r=e.patches;return r.length>0&&(e.patches=[],e.callback&&e.callback(r)),r}function C(e,t,n,r,i){if(t!==e){\"function\"==typeof t.toJSON&&(t=t.toJSON());for(var o=c(t),a=c(e),s=!1,d=a.length-1;d>=0;d--){var u=e[g=a[d]];if(!l(t,g)||void 0===t[g]&&void 0!==u&&!1===Array.isArray(t))Array.isArray(e)===Array.isArray(t)?(i&&n.push({op:\"test\",path:r+\"/\"+p(g),value:f(u)}),n.push({op:\"remove\",path:r+\"/\"+p(g)}),s=!0):(i&&n.push({op:\"test\",path:r,value:e}),n.push({op:\"replace\",path:r,value:t}));else{var h=t[g];\"object\"==typeof u&&null!=u&&\"object\"==typeof h&&null!=h&&Array.isArray(u)===Array.isArray(h)?C(u,h,n,r+\"/\"+p(g),i):u!==h&&(i&&n.push({op:\"test\",path:r+\"/\"+p(g),value:f(u)}),n.push({op:\"replace\",path:r+\"/\"+p(g),value:f(h)}))}}if(s||o.length!=a.length)for(d=0;d<o.length;d++){var g;l(e,g=o[d])||void 0===t[g]||n.push({op:\"add\",path:r+\"/\"+p(g),value:f(t[g])})}}}var D=Object.freeze({__proto__:null,compare:function(e,t,n){void 0===n&&(n=!1);var r=[];return C(e,t,r,\"\",n),r},generate:S,observe:function(e,t){var n,r=function(e){return $.get(e)}(e);if(r){var i=function(e,t){return e.observers.get(t)}(r,t);n=i&&i.observer}else r=new R(e),$.set(e,r);if(n)return n;if(n={},r.value=f(e),t){n.callback=t,n.next=null;var o=function(){S(n)},a=function(){clearTimeout(n.next),n.next=setTimeout(o)};\"undefined\"!=typeof window&&(window.addEventListener(\"mouseup\",a),window.addEventListener(\"keyup\",a),window.addEventListener(\"mousedown\",a),window.addEventListener(\"keydown\",a),window.addEventListener(\"change\",a))}return n.patches=[],n.object=e,n.unobserve=function(){S(n),clearTimeout(n.next),function(e,t){e.observers.delete(t.callback)}(r,n),\"undefined\"!=typeof window&&(window.removeEventListener(\"mouseup\",a),window.removeEventListener(\"keyup\",a),window.removeEventListener(\"mousedown\",a),window.removeEventListener(\"keydown\",a),window.removeEventListener(\"change\",a))},r.observers.set(t,new T(t,n)),n},unobserve:function(e,t){t.unobserve()}});Object.assign({},N,D,{JsonPatchError:m,deepClone:f,escapePathComponent:p,unescapePathComponent:u});const F=/(\"(?:[^\\\\\"]|\\\\.)*\")|[:,]/g;function k(e,t={}){const n=JSON.stringify([1],void 0,void 0===t.indent?2:t.indent).slice(2,-3),r=\"\"===n?1/0:void 0===t.maxLength?80:t.maxLength;let{replacer:i}=t;return function e(t,o,a){t&&\"function\"==typeof t.toJSON&&(t=t.toJSON());const s=JSON.stringify(t,i);if(void 0===s)return s;const l=r-o.length-a;if(s.length<=l){const e=s.replace(F,((e,t)=>t||`${e} `));if(e.length<=l)return e}if(null!=i&&(t=JSON.parse(s),i=void 0),\"object\"==typeof t&&null!==t){const r=o+n,i=[];let a,s,l=0;if(Array.isArray(t)){a=\"[\",s=\"]\";const{length:n}=t;for(;l<n;l++)i.push(e(t[l],r,l===n-1?0:1)||\"null\")}else{a=\"{\",s=\"}\";const n=Object.keys(t),{length:o}=n;for(;l<o;l++){const a=n[l],s=`${JSON.stringify(a)}: `,c=e(t[a],r,s.length+(l===o-1?0:1));void 0!==c&&i.push(s+c)}}if(i.length>0)return[a,n+i.join(`,\\n${r}`),s].join(`\\n${o}`)}return s}(e,\"\",0)}function _(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,\"default\")?e.default:e}var P,M,z,j;function U(){if(j)return z;j=1;const e=Object.freeze({loose:!0}),t=Object.freeze({});return z=n=>n?\"object\"!=typeof n?e:n:t}var B,G,W,X,V,H,Y,q,J,Q,Z,K,ee,te,ne,re,ie,oe,ae,se,le,ce,fe,de,pe,ue,he,ge,me,Ee,ve,be={exports:{}};function ye(){if(G)return B;G=1;const e=Number.MAX_SAFE_INTEGER||9007199254740991;return B={MAX_LENGTH:256,MAX_SAFE_COMPONENT_LENGTH:16,MAX_SAFE_BUILD_LENGTH:250,MAX_SAFE_INTEGER:e,RELEASE_TYPES:[\"major\",\"premajor\",\"minor\",\"preminor"
-  , "\",\"patch\",\"prepatch\",\"prerelease\"],SEMVER_SPEC_VERSION:\"2.0.0\",FLAG_INCLUDE_PRERELEASE:1,FLAG_LOOSE:2}}function we(){if(X)return W;X=1;const e=\"object\"==typeof process&&process.env&&process.env.NODE_DEBUG&&/\\bsemver\\b/i.test(process.env.NODE_DEBUG)?(...e)=>console.error(\"SEMVER\",...e):()=>{};return W=e}function Oe(){return V||(V=1,function(e,t){const{MAX_SAFE_COMPONENT_LENGTH:n,MAX_SAFE_BUILD_LENGTH:r,MAX_LENGTH:i}=ye(),o=we(),a=(t=e.exports={}).re=[],s=t.safeRe=[],l=t.src=[],c=t.t={};let f=0;const d=\"[a-zA-Z0-9-]\",p=[[\"\\\\s\",1],[\"\\\\d\",i],[d,r]],u=(e,t,n)=>{const r=(e=>{for(const[t,n]of p)e=e.split(`${t}*`).join(`${t}{0,${n}}`).split(`${t}+`).join(`${t}{1,${n}}`);return e})(t),i=f++;o(e,i,t),c[e]=i,l[i]=t,a[i]=new RegExp(t,n?\"g\":void 0),s[i]=new RegExp(r,n?\"g\":void 0)};u(\"NUMERICIDENTIFIER\",\"0|[1-9]\\\\d*\"),u(\"NUMERICIDENTIFIERLOOSE\",\"\\\\d+\"),u(\"NONNUMERICIDENTIFIER\",`\\\\d*[a-zA-Z-]${d}*`),u(\"MAINVERSION\",`(${l[c.NUMERICIDENTIFIER]})\\\\.(${l[c.NUMERICIDENTIFIER]})\\\\.(${l[c.NUMERICIDENTIFIER]})`),u(\"MAINVERSIONLOOSE\",`(${l[c.NUMERICIDENTIFIERLOOSE]})\\\\.(${l[c.NUMERICIDENTIFIERLOOSE]})\\\\.(${l[c.NUMERICIDENTIFIERLOOSE]})`),u(\"PRERELEASEIDENTIFIER\",`(?:${l[c.NUMERICIDENTIFIER]}|${l[c.NONNUMERICIDENTIFIER]})`),u(\"PRERELEASEIDENTIFIERLOOSE\",`(?:${l[c.NUMERICIDENTIFIERLOOSE]}|${l[c.NONNUMERICIDENTIFIER]})`),u(\"PRERELEASE\",`(?:-(${l[c.PRERELEASEIDENTIFIER]}(?:\\\\.${l[c.PRERELEASEIDENTIFIER]})*))`),u(\"PRERELEASELOOSE\",`(?:-?(${l[c.PRERELEASEIDENTIFIERLOOSE]}(?:\\\\.${l[c.PRERELEASEIDENTIFIERLOOSE]})*))`),u(\"BUILDIDENTIFIER\",`${d}+`),u(\"BUILD\",`(?:\\\\+(${l[c.BUILDIDENTIFIER]}(?:\\\\.${l[c.BUILDIDENTIFIER]})*))`),u(\"FULLPLAIN\",`v?${l[c.MAINVERSION]}${l[c.PRERELEASE]}?${l[c.BUILD]}?`),u(\"FULL\",`^${l[c.FULLPLAIN]}$`),u(\"LOOSEPLAIN\",`[v=\\\\s]*${l[c.MAINVERSIONLOOSE]}${l[c.PRERELEASELOOSE]}?${l[c.BUILD]}?`),u(\"LOOSE\",`^${l[c.LOOSEPLAIN]}$`),u(\"GTLT\",\"((?:<|>)?=?)\"),u(\"XRANGEIDENTIFIERLOOSE\",`${l[c.NUMERICIDENTIFIERLOOSE]}|x|X|\\\\*`),u(\"XRANGEIDENTIFIER\",`${l[c.NUMERICIDENTIFIER]}|x|X|\\\\*`),u(\"XRANGEPLAIN\",`[v=\\\\s]*(${l[c.XRANGEIDENTIFIER]})(?:\\\\.(${l[c.XRANGEIDENTIFIER]})(?:\\\\.(${l[c.XRANGEIDENTIFIER]})(?:${l[c.PRERELEASE]})?${l[c.BUILD]}?)?)?`),u(\"XRANGEPLAINLOOSE\",`[v=\\\\s]*(${l[c.XRANGEIDENTIFIERLOOSE]})(?:\\\\.(${l[c.XRANGEIDENTIFIERLOOSE]})(?:\\\\.(${l[c.XRANGEIDENTIFIERLOOSE]})(?:${l[c.PRERELEASELOOSE]})?${l[c.BUILD]}?)?)?`),u(\"XRANGE\",`^${l[c.GTLT]}\\\\s*${l[c.XRANGEPLAIN]}$`),u(\"XRANGELOOSE\",`^${l[c.GTLT]}\\\\s*${l[c.XRANGEPLAINLOOSE]}$`),u(\"COERCEPLAIN\",`(^|[^\\\\d])(\\\\d{1,${n}})(?:\\\\.(\\\\d{1,${n}}))?(?:\\\\.(\\\\d{1,${n}}))?`),u(\"COERCE\",`${l[c.COERCEPLAIN]}(?:$|[^\\\\d])`),u(\"COERCEFULL\",l[c.COERCEPLAIN]+`(?:${l[c.PRERELEASE]})?`+`(?:${l[c.BUILD]})?(?:$|[^\\\\d])`),u(\"COERCERTL\",l[c.COERCE],!0),u(\"COERCERTLFULL\",l[c.COERCEFULL],!0),u(\"LONETILDE\",\"(?:~>?)\"),u(\"TILDETRIM\",`(\\\\s*)${l[c.LONETILDE]}\\\\s+`,!0),t.tildeTrimReplace=\"$1~\",u(\"TILDE\",`^${l[c.LONETILDE]}${l[c.XRANGEPLAIN]}$`),u(\"TILDELOOSE\",`^${l[c.LONETILDE]}${l[c.XRANGEPLAINLOOSE]}$`),u(\"LONECARET\",\"(?:\\\\^)\"),u(\"CARETTRIM\",`(\\\\s*)${l[c.LONECARET]}\\\\s+`,!0),t.caretTrimReplace=\"$1^\",u(\"CARET\",`^${l[c.LONECARET]}${l[c.XRANGEPLAIN]}$`),u(\"CARETLOOSE\",`^${l[c.LONECARET]}${l[c.XRANGEPLAINLOOSE]}$`),u(\"COMPARATORLOOSE\",`^${l[c.GTLT]}\\\\s*(${l[c.LOOSEPLAIN]})$|^$`),u(\"COMPARATOR\",`^${l[c.GTLT]}\\\\s*(${l[c.FULLPLAIN]})$|^$`),u(\"COMPARATORTRIM\",`(\\\\s*)${l[c.GTLT]}\\\\s*(${l[c.LOOSEPLAIN]}|${l[c.XRANGEPLAIN]})`,!0),t.comparatorTrimReplace=\"$1$2$3\",u(\"HYPHENRANGE\",`^\\\\s*(${l[c.XRANGEPLAIN]})\\\\s+-\\\\s+(${l[c.XRANGEPLAIN]})\\\\s*$`),u(\"HYPHENRANGELOOSE\",`^\\\\s*(${l[c.XRANGEPLAINLOOSE]})\\\\s+-\\\\s+(${l[c.XRANGEPLAINLOOSE]})\\\\s*$`),u(\"STAR\",\"(<|>)?=?\\\\s*\\\\*\"),u(\"GTE0\",\"^\\\\s*>=\\\\s*0\\\\.0\\\\.0\\\\s*$\"),u(\"GTE0PRE\",\"^\\\\s*>=\\\\s*0\\\\.0\\\\.0-0\\\\s*$\")}(be,be.exports)),be.exports}function Ae(){if(J)return q;J=1;const e=we(),{MAX_LENGTH:t,MAX_SAFE_INTEGER:n}=ye(),{safeRe:r,t:i}=Oe(),o=U(),{compareIdentifiers:a}=function(){if(Y)return H;Y=1;const e=/^[0-9]+$/,t=(t,n)=>{const r=e.test(t),i=e.test(n);return r&&i&&(t=+t,n=+n),t===n?0:r&&!i?-1:i&&!r?1:t<n?-1:1};return H={compareIdentifiers:t,rcompareIdentifiers:(e,n)=>t(n,e)}}();class s{constructor"
-  , "(a,l){if(l=o(l),a instanceof s){if(a.loose===!!l.loose&&a.includePrerelease===!!l.includePrerelease)return a;a=a.version}else if(\"string\"!=typeof a)throw new TypeError(`Invalid version. Must be a string. Got type \"${typeof a}\".`);if(a.length>t)throw new TypeError(`version is longer than ${t} characters`);e(\"SemVer\",a,l),this.options=l,this.loose=!!l.loose,this.includePrerelease=!!l.includePrerelease;const c=a.trim().match(l.loose?r[i.LOOSE]:r[i.FULL]);if(!c)throw new TypeError(`Invalid Version: ${a}`);if(this.raw=a,this.major=+c[1],this.minor=+c[2],this.patch=+c[3],this.major>n||this.major<0)throw new TypeError(\"Invalid major version\");if(this.minor>n||this.minor<0)throw new TypeError(\"Invalid minor version\");if(this.patch>n||this.patch<0)throw new TypeError(\"Invalid patch version\");c[4]?this.prerelease=c[4].split(\".\").map((e=>{if(/^[0-9]+$/.test(e)){const t=+e;if(t>=0&&t<n)return t}return e})):this.prerelease=[],this.build=c[5]?c[5].split(\".\"):[],this.format()}format(){return this.version=`${this.major}.${this.minor}.${this.patch}`,this.prerelease.length&&(this.version+=`-${this.prerelease.join(\".\")}`),this.version}toString(){return this.version}compare(t){if(e(\"SemVer.compare\",this.version,this.options,t),!(t instanceof s)){if(\"string\"==typeof t&&t===this.version)return 0;t=new s(t,this.options)}return t.version===this.version?0:this.compareMain(t)||this.comparePre(t)}compareMain(e){return e instanceof s||(e=new s(e,this.options)),a(this.major,e.major)||a(this.minor,e.minor)||a(this.patch,e.patch)}comparePre(t){if(t instanceof s||(t=new s(t,this.options)),this.prerelease.length&&!t.prerelease.length)return-1;if(!this.prerelease.length&&t.prerelease.length)return 1;if(!this.prerelease.length&&!t.prerelease.length)return 0;let n=0;do{const r=this.prerelease[n],i=t.prerelease[n];if(e(\"prerelease compare\",n,r,i),void 0===r&&void 0===i)return 0;if(void 0===i)return 1;if(void 0===r)return-1;if(r!==i)return a(r,i)}while(++n)}compareBuild(t){t instanceof s||(t=new s(t,this.options));let n=0;do{const r=this.build[n],i=t.build[n];if(e(\"build compare\",n,r,i),void 0===r&&void 0===i)return 0;if(void 0===i)return 1;if(void 0===r)return-1;if(r!==i)return a(r,i)}while(++n)}inc(e,t,n){switch(e){case\"premajor\":this.prerelease.length=0,this.patch=0,this.minor=0,this.major++,this.inc(\"pre\",t,n);break;case\"preminor\":this.prerelease.length=0,this.patch=0,this.minor++,this.inc(\"pre\",t,n);break;case\"prepatch\":this.prerelease.length=0,this.inc(\"patch\",t,n),this.inc(\"pre\",t,n);break;case\"prerelease\":0===this.prerelease.length&&this.inc(\"patch\",t,n),this.inc(\"pre\",t,n);break;case\"major\":0===this.minor&&0===this.patch&&0!==this.prerelease.length||this.major++,this.minor=0,this.patch=0,this.prerelease=[];break;case\"minor\":0===this.patch&&0!==this.prerelease.length||this.minor++,this.patch=0,this.prerelease=[];break;case\"patch\":0===this.prerelease.length&&this.patch++,this.prerelease=[];break;case\"pre\":{const e=Number(n)?1:0;if(!t&&!1===n)throw new Error(\"invalid increment argument: identifier is empty\");if(0===this.prerelease.length)this.prerelease=[e];else{let r=this.prerelease.length;for(;--r>=0;)\"number\"==typeof this.prerelease[r]&&(this.prerelease[r]++,r=-2);if(-1===r){if(t===this.prerelease.join(\".\")&&!1===n)throw new Error(\"invalid increment argument: identifier already exists\");this.prerelease.push(e)}}if(t){let r=[t,e];!1===n&&(r=[t]),0===a(this.prerelease[0],t)?isNaN(this.prerelease[1])&&(this.prerelease=r):this.prerelease=r}break}default:throw new Error(`invalid increment argument: ${e}`)}return this.raw=this.format(),this.build.length&&(this.raw+=`+${this.build.join(\".\")}`),this}}return q=s}function Ie(){if(Z)return Q;Z=1;const e=Ae();return Q=(t,n,r)=>new e(t,r).compare(new e(n,r))}function xe(){if(pe)return de;pe=1;const e=function(){if(ee)return K;ee=1;const e=Ie();return K=(t,n,r)=>0===e(t,n,r)}(),t=function(){if(ne)return te;ne=1;const e=Ie();return te=(t,n,r)=>0!==e(t,n,r)}(),n=function(){if(ie)return re;ie=1;const e=Ie();return re=(t,n,r)=>e(t,n,r)>0}(),r=function(){if(ae)return oe;ae=1;const e=Ie();return oe=(t,n,r)=>e(t,n,r)>=0"
-  , "}(),i=function(){if(le)return se;le=1;const e=Ie();return se=(t,n,r)=>e(t,n,r)<0}(),o=function(){if(fe)return ce;fe=1;const e=Ie();return ce=(t,n,r)=>e(t,n,r)<=0}();return de=(a,s,l,c)=>{switch(s){case\"===\":return\"object\"==typeof a&&(a=a.version),\"object\"==typeof l&&(l=l.version),a===l;case\"!==\":return\"object\"==typeof a&&(a=a.version),\"object\"==typeof l&&(l=l.version),a!==l;case\"\":case\"=\":case\"==\":return e(a,l,c);case\"!=\":return t(a,l,c);case\">\":return n(a,l,c);case\">=\":return r(a,l,c);case\"<\":return i(a,l,c);case\"<=\":return o(a,l,c);default:throw new TypeError(`Invalid operator: ${s}`)}}}function Le(){if(me)return ge;me=1;const e=/\\s+/g;class t{constructor(n,o){if(o=r(o),n instanceof t)return n.loose===!!o.loose&&n.includePrerelease===!!o.includePrerelease?n:new t(n.raw,o);if(n instanceof i)return this.raw=n.value,this.set=[[n]],this.formatted=void 0,this;if(this.options=o,this.loose=!!o.loose,this.includePrerelease=!!o.includePrerelease,this.raw=n.trim().replace(e,\" \"),this.set=this.raw.split(\"||\").map((e=>this.parseRange(e.trim()))).filter((e=>e.length)),!this.set.length)throw new TypeError(`Invalid SemVer Range: ${this.raw}`);if(this.set.length>1){const e=this.set[0];if(this.set=this.set.filter((e=>!h(e[0]))),0===this.set.length)this.set=[e];else if(this.set.length>1)for(const e of this.set)if(1===e.length&&g(e[0])){this.set=[e];break}}this.formatted=void 0}get range(){if(void 0===this.formatted){this.formatted=\"\";for(let e=0;e<this.set.length;e++){e>0&&(this.formatted+=\"||\");const t=this.set[e];for(let e=0;e<t.length;e++)e>0&&(this.formatted+=\" \"),this.formatted+=t[e].toString().trim()}}return this.formatted}format(){return this.range}toString(){return this.range}parseRange(e){const t=((this.options.includePrerelease&&p)|(this.options.loose&&u))+\":\"+e,r=n.get(t);if(r)return r;const a=this.options.loose,g=a?s[l.HYPHENRANGELOOSE]:s[l.HYPHENRANGE];e=e.replace(g,N(this.options.includePrerelease)),o(\"hyphen replace\",e),e=e.replace(s[l.COMPARATORTRIM],c),o(\"comparator trim\",e),e=e.replace(s[l.TILDETRIM],f),o(\"tilde trim\",e),e=e.replace(s[l.CARETTRIM],d),o(\"caret trim\",e);let m=e.split(\" \").map((e=>E(e,this.options))).join(\" \").split(/\\s+/).map((e=>L(e,this.options)));a&&(m=m.filter((e=>(o(\"loose invalid filter\",e,this.options),!!e.match(s[l.COMPARATORLOOSE]))))),o(\"range list\",m);const v=new Map,b=m.map((e=>new i(e,this.options)));for(const e of b){if(h(e))return[e];v.set(e.value,e)}v.size>1&&v.has(\"\")&&v.delete(\"\");const y=[...v.values()];return n.set(t,y),y}intersects(e,n){if(!(e instanceof t))throw new TypeError(\"a Range is required\");return this.set.some((t=>m(t,n)&&e.set.some((e=>m(e,n)&&t.every((t=>e.every((e=>t.intersects(e,n)))))))))}test(e){if(!e)return!1;if(\"string\"==typeof e)try{e=new a(e,this.options)}catch(e){return!1}for(let t=0;t<this.set.length;t++)if($(this.set[t],e,this.options))return!0;return!1}}ge=t;const n=new(M?P:(M=1,P=class{constructor(){this.max=1e3,this.map=new Map}get(e){const t=this.map.get(e);return void 0===t?void 0:(this.map.delete(e),this.map.set(e,t),t)}delete(e){return this.map.delete(e)}set(e,t){if(!this.delete(e)&&void 0!==t){if(this.map.size>=this.max){const e=this.map.keys().next().value;this.delete(e)}this.map.set(e,t)}return this}})),r=U(),i=function(){if(he)return ue;he=1;const e=Symbol(\"SemVer ANY\");class t{static get ANY(){return e}constructor(r,i){if(i=n(i),r instanceof t){if(r.loose===!!i.loose)return r;r=r.value}r=r.trim().split(/\\s+/).join(\" \"),a(\"comparator\",r,i),this.options=i,this.loose=!!i.loose,this.parse(r),this.semver===e?this.value=\"\":this.value=this.operator+this.semver.version,a(\"comp\",this)}parse(t){const n=this.options.loose?r[i.COMPARATORLOOSE]:r[i.COMPARATOR],o=t.match(n);if(!o)throw new TypeError(`Invalid comparator: ${t}`);this.operator=void 0!==o[1]?o[1]:\"\",\"=\"===this.operator&&(this.operator=\"\"),o[2]?this.semver=new s(o[2],this.options.loose):this.semver=e}toString(){return this.value}test(t){if(a(\"Comparator.test\",t,this.options.loose),this.semver===e||t===e)return!0;if(\"string\"==typeof t)try{t=new s(t,this.options)}catch(e){return!1}return o(t,this.operat"
-  , "or,this.semver,this.options)}intersects(e,r){if(!(e instanceof t))throw new TypeError(\"a Comparator is required\");return\"\"===this.operator?\"\"===this.value||new l(e.value,r).test(this.value):\"\"===e.operator?\"\"===e.value||new l(this.value,r).test(e.semver):!((r=n(r)).includePrerelease&&(\"<0.0.0-0\"===this.value||\"<0.0.0-0\"===e.value)||!r.includePrerelease&&(this.value.startsWith(\"<0.0.0\")||e.value.startsWith(\"<0.0.0\"))||(!this.operator.startsWith(\">\")||!e.operator.startsWith(\">\"))&&(!this.operator.startsWith(\"<\")||!e.operator.startsWith(\"<\"))&&(this.semver.version!==e.semver.version||!this.operator.includes(\"=\")||!e.operator.includes(\"=\"))&&!(o(this.semver,\"<\",e.semver,r)&&this.operator.startsWith(\">\")&&e.operator.startsWith(\"<\"))&&!(o(this.semver,\">\",e.semver,r)&&this.operator.startsWith(\"<\")&&e.operator.startsWith(\">\")))}}ue=t;const n=U(),{safeRe:r,t:i}=Oe(),o=xe(),a=we(),s=Ae(),l=Le();return ue}(),o=we(),a=Ae(),{safeRe:s,t:l,comparatorTrimReplace:c,tildeTrimReplace:f,caretTrimReplace:d}=Oe(),{FLAG_INCLUDE_PRERELEASE:p,FLAG_LOOSE:u}=ye(),h=e=>\"<0.0.0-0\"===e.value,g=e=>\"\"===e.value,m=(e,t)=>{let n=!0;const r=e.slice();let i=r.pop();for(;n&&r.length;)n=r.every((e=>i.intersects(e,t))),i=r.pop();return n},E=(e,t)=>(o(\"comp\",e,t),e=w(e,t),o(\"caret\",e),e=b(e,t),o(\"tildes\",e),e=A(e,t),o(\"xrange\",e),e=x(e,t),o(\"stars\",e),e),v=e=>!e||\"x\"===e.toLowerCase()||\"*\"===e,b=(e,t)=>e.trim().split(/\\s+/).map((e=>y(e,t))).join(\" \"),y=(e,t)=>{const n=t.loose?s[l.TILDELOOSE]:s[l.TILDE];return e.replace(n,((t,n,r,i,a)=>{let s;return o(\"tilde\",e,t,n,r,i,a),v(n)?s=\"\":v(r)?s=`>=${n}.0.0 <${+n+1}.0.0-0`:v(i)?s=`>=${n}.${r}.0 <${n}.${+r+1}.0-0`:a?(o(\"replaceTilde pr\",a),s=`>=${n}.${r}.${i}-${a} <${n}.${+r+1}.0-0`):s=`>=${n}.${r}.${i} <${n}.${+r+1}.0-0`,o(\"tilde return\",s),s}))},w=(e,t)=>e.trim().split(/\\s+/).map((e=>O(e,t))).join(\" \"),O=(e,t)=>{o(\"caret\",e,t);const n=t.loose?s[l.CARETLOOSE]:s[l.CARET],r=t.includePrerelease?\"-0\":\"\";return e.replace(n,((t,n,i,a,s)=>{let l;return o(\"caret\",e,t,n,i,a,s),v(n)?l=\"\":v(i)?l=`>=${n}.0.0${r} <${+n+1}.0.0-0`:v(a)?l=\"0\"===n?`>=${n}.${i}.0${r} <${n}.${+i+1}.0-0`:`>=${n}.${i}.0${r} <${+n+1}.0.0-0`:s?(o(\"replaceCaret pr\",s),l=\"0\"===n?\"0\"===i?`>=${n}.${i}.${a}-${s} <${n}.${i}.${+a+1}-0`:`>=${n}.${i}.${a}-${s} <${n}.${+i+1}.0-0`:`>=${n}.${i}.${a}-${s} <${+n+1}.0.0-0`):(o(\"no pr\"),l=\"0\"===n?\"0\"===i?`>=${n}.${i}.${a}${r} <${n}.${i}.${+a+1}-0`:`>=${n}.${i}.${a}${r} <${n}.${+i+1}.0-0`:`>=${n}.${i}.${a} <${+n+1}.0.0-0`),o(\"caret return\",l),l}))},A=(e,t)=>(o(\"replaceXRanges\",e,t),e.split(/\\s+/).map((e=>I(e,t))).join(\" \")),I=(e,t)=>{e=e.trim();const n=t.loose?s[l.XRANGELOOSE]:s[l.XRANGE];return e.replace(n,((n,r,i,a,s,l)=>{o(\"xRange\",e,n,r,i,a,s,l);const c=v(i),f=c||v(a),d=f||v(s),p=d;return\"=\"===r&&p&&(r=\"\"),l=t.includePrerelease?\"-0\":\"\",c?n=\">\"===r||\"<\"===r?\"<0.0.0-0\":\"*\":r&&p?(f&&(a=0),s=0,\">\"===r?(r=\">=\",f?(i=+i+1,a=0,s=0):(a=+a+1,s=0)):\"<=\"===r&&(r=\"<\",f?i=+i+1:a=+a+1),\"<\"===r&&(l=\"-0\"),n=`${r+i}.${a}.${s}${l}`):f?n=`>=${i}.0.0${l} <${+i+1}.0.0-0`:d&&(n=`>=${i}.${a}.0${l} <${i}.${+a+1}.0-0`),o(\"xRange return\",n),n}))},x=(e,t)=>(o(\"replaceStars\",e,t),e.trim().replace(s[l.STAR],\"\")),L=(e,t)=>(o(\"replaceGTE0\",e,t),e.trim().replace(s[t.includePrerelease?l.GTE0PRE:l.GTE0],\"\")),N=e=>(t,n,r,i,o,a,s,l,c,f,d,p)=>`${n=v(r)?\"\":v(i)?`>=${r}.0.0${e?\"-0\":\"\"}`:v(o)?`>=${r}.${i}.0${e?\"-0\":\"\"}`:a?`>=${n}`:`>=${n}${e?\"-0\":\"\"}`} ${l=v(c)?\"\":v(f)?`<${+c+1}.0.0-0`:v(d)?`<${c}.${+f+1}.0-0`:p?`<=${c}.${f}.${d}-${p}`:e?`<${c}.${f}.${+d+1}-0`:`<=${l}`}`.trim(),$=(e,t,n)=>{for(let n=0;n<e.length;n++)if(!e[n].test(t))return!1;if(t.prerelease.length&&!n.includePrerelease){for(let n=0;n<e.length;n++)if(o(e[n].semver),e[n].semver!==i.ANY&&e[n].semver.prerelease.length>0){const r=e[n].semver;if(r.major===t.major&&r.minor===t.minor&&r.patch===t.patch)return!0}return!1}return!0};return ge}var Ne=function(){if(ve)return Ee;ve=1;const e=Le();return Ee=(t,n,r)=>{try{n=new e(n,r)}catch(e){return!1}return n.test(t)},Ee}(),$e=_(Ne);var Re={NaN:NaN,E:Math.E,LN2:Math.LN2,LN10:Math.LN10,LOG2E:Math.LOG2E,LOG10E:Math.LOG10E,PI:Math.PI,SQRT1_2:Math.SQRT1_2,SQRT2"
-  , ":Math.SQRT2,MIN_VALUE:Number.MIN_VALUE,MAX_VALUE:Number.MAX_VALUE},Te={\"*\":(e,t)=>e*t,\"+\":(e,t)=>e+t,\"-\":(e,t)=>e-t,\"/\":(e,t)=>e/t,\"%\":(e,t)=>e%t,\">\":(e,t)=>e>t,\"<\":(e,t)=>e<t,\"<=\":(e,t)=>e<=t,\">=\":(e,t)=>e>=t,\"==\":(e,t)=>e==t,\"!=\":(e,t)=>e!=t,\"===\":(e,t)=>e===t,\"!==\":(e,t)=>e!==t,\"&\":(e,t)=>e&t,\"|\":(e,t)=>e|t,\"^\":(e,t)=>e^t,\"<<\":(e,t)=>e<<t,\">>\":(e,t)=>e>>t,\">>>\":(e,t)=>e>>>t},Se={\"+\":e=>+e,\"-\":e=>-e,\"~\":e=>~e,\"!\":e=>!e};const Ce=Array.prototype.slice,De=(e,t,n)=>{const r=n?n(t[0]):t[0];return r[e].apply(r,Ce.call(t,1))};var Fe={isNaN:Number.isNaN,isFinite:Number.isFinite,abs:Math.abs,acos:Math.acos,asin:Math.asin,atan:Math.atan,atan2:Math.atan2,ceil:Math.ceil,cos:Math.cos,exp:Math.exp,floor:Math.floor,log:Math.log,max:Math.max,min:Math.min,pow:Math.pow,random:Math.random,round:Math.round,sin:Math.sin,sqrt:Math.sqrt,tan:Math.tan,clamp:(e,t,n)=>Math.max(t,Math.min(n,e)),now:Date.now,utc:Date.UTC,datetime:(e,t,n,r,i,o,a)=>new Date(e,t||0,null!=n?n:1,r||0,i||0,o||0,a||0),date:e=>new Date(e).getDate(),day:e=>new Date(e).getDay(),year:e=>new Date(e).getFullYear(),month:e=>new Date(e).getMonth(),hours:e=>new Date(e).getHours(),minutes:e=>new Date(e).getMinutes(),seconds:e=>new Date(e).getSeconds(),milliseconds:e=>new Date(e).getMilliseconds(),time:e=>new Date(e).getTime(),timezoneoffset:e=>new Date(e).getTimezoneOffset(),utcdate:e=>new Date(e).getUTCDate(),utcday:e=>new Date(e).getUTCDay(),utcyear:e=>new Date(e).getUTCFullYear(),utcmonth:e=>new Date(e).getUTCMonth(),utchours:e=>new Date(e).getUTCHours(),utcminutes:e=>new Date(e).getUTCMinutes(),utcseconds:e=>new Date(e).getUTCSeconds(),utcmilliseconds:e=>new Date(e).getUTCMilliseconds(),length:e=>e.length,join:function(){return De(\"join\",arguments)},indexof:function(){return De(\"indexOf\",arguments)},lastindexof:function(){return De(\"lastIndexOf\",arguments)},slice:function(){return De(\"slice\",arguments)},reverse:e=>e.slice().reverse(),parseFloat:parseFloat,parseInt:parseInt,upper:e=>String(e).toUpperCase(),lower:e=>String(e).toLowerCase(),substring:function(){return De(\"substring\",arguments,String)},split:function(){return De(\"split\",arguments,String)},replace:function(){return De(\"replace\",arguments,String)},trim:e=>String(e).trim(),regexp:RegExp,test:(e,t)=>RegExp(e).test(t)};const ke=[\"view\",\"item\",\"group\",\"xy\",\"x\",\"y\"],_e=new Set([Function,eval,setTimeout,setInterval]);\"function\"==typeof setImmediate&&_e.add(setImmediate);const Pe={Literal:(e,t)=>t.value,Identifier:(e,t)=>{const n=t.name;return e.memberDepth>0?n:\"datum\"===n?e.datum:\"event\"===n?e.event:\"item\"===n?e.item:Re[n]||e.params[\"$\"+n]},MemberExpression:(e,t)=>{const n=!t.computed,r=e(t.object);n&&(e.memberDepth+=1);const i=e(t.property);if(n&&(e.memberDepth-=1),!_e.has(r[i]))return r[i];console.error(`Prevented interpretation of member \"${i}\" which could lead to insecure code execution`)},CallExpression:(e,t)=>{const n=t.arguments;let r=t.callee.name;return r.startsWith(\"_\")&&(r=r.slice(1)),\"if\"===r?e(n[0])?e(n[1]):e(n[2]):(e.fn[r]||Fe[r]).apply(e.fn,n.map(e))},ArrayExpression:(e,t)=>t.elements.map(e),BinaryExpression:(e,t)=>Te[t.operator](e(t.left),e(t.right)),UnaryExpression:(e,t)=>Se[t.operator](e(t.argument)),ConditionalExpression:(e,t)=>e(t.test)?e(t.consequent):e(t.alternate),LogicalExpression:(e,t)=>\"&&\"===t.operator?e(t.left)&&e(t.right):e(t.left)||e(t.right),ObjectExpression:(e,t)=>t.properties.reduce(((t,n)=>{e.memberDepth+=1;const r=e(n.key);return e.memberDepth-=1,_e.has(e(n.value))?console.error(`Prevented interpretation of property \"${r}\" which could lead to insecure code execution`):t[r]=e(n.value),t}),{})};function Me(e,t,n,r,i,o){const a=e=>Pe[e.type](a,e);return a.memberDepth=0,a.fn=Object.create(t),a.params=n,a.datum=r,a.event=i,a.item=o,ke.forEach((e=>a.fn[e]=function(){return i.vega[e](...arguments)})),a(e)}var ze={operator(e,t){const n=t.ast,r=e.functions;return e=>Me(n,r,e)},parameter(e,t){const n=t.ast,r=e.functions;return(e,t)=>Me(n,r,t,e)},event(e,t){const n=t.ast,r=e.functions;return e=>Me(n,r,void 0,void 0,e)},handler(e,t){const n=t.ast,r=e.functions;return(e,t)=>{const i=t.item&&t.item.d"
-  , "atum;return Me(n,r,e,i,t)}},encode(e,t){const{marktype:n,channels:r}=t,i=e.functions,o=\"group\"===n||\"image\"===n||\"rect\"===n;return(e,t)=>{const a=e.datum;let s,l=0;for(const n in r)s=Me(r[n].ast,i,t,a,void 0,e),e[n]!==s&&(e[n]=s,l=1);return\"rule\"!==n&&function(e,t,n){let r;t.x2&&(t.x?(n&&e.x>e.x2&&(r=e.x,e.x=e.x2,e.x2=r),e.width=e.x2-e.x):e.x=e.x2-(e.width||0)),t.xc&&(e.x=e.xc-(e.width||0)/2),t.y2&&(t.y?(n&&e.y>e.y2&&(r=e.y,e.y=e.y2,e.y2=r),e.height=e.y2-e.y):e.y=e.y2-(e.height||0)),t.yc&&(e.y=e.yc-(e.height||0)/2)}(e,r,o),l}}};function je(e){const[t,n]=/schema\\/([\\w-]+)\\/([\\w\\.\\-]+)\\.json$/g.exec(e).slice(1,3);return{library:t,version:n}}var Ue=\"2.15.0\";const Be=\"#fff\",Ge=\"#888\",We={background:\"#333\",view:{stroke:Ge},title:{color:Be,subtitleColor:Be},style:{\"guide-label\":{fill:Be},\"guide-title\":{fill:Be}},axis:{domainColor:Be,gridColor:Ge,tickColor:Be}},Xe=\"#4572a7\",Ve={background:\"#fff\",arc:{fill:Xe},area:{fill:Xe},line:{stroke:Xe,strokeWidth:2},path:{stroke:Xe},rect:{fill:Xe},shape:{stroke:Xe},symbol:{fill:Xe,strokeWidth:1.5,size:50},axis:{bandPosition:.5,grid:!0,gridColor:\"#000000\",gridOpacity:1,gridWidth:.5,labelPadding:10,tickSize:5,tickWidth:.5},axisBand:{grid:!1,tickExtra:!0},legend:{labelBaseline:\"middle\",labelFontSize:11,symbolSize:50,symbolType:\"square\"},range:{category:[\"#4572a7\",\"#aa4643\",\"#8aa453\",\"#71598e\",\"#4598ae\",\"#d98445\",\"#94aace\",\"#d09393\",\"#b9cc98\",\"#a99cbc\"]}},He=\"#30a2da\",Ye=\"#cbcbcb\",qe=\"#f0f0f0\",Je=\"#333\",Qe={arc:{fill:He},area:{fill:He},axis:{domainColor:Ye,grid:!0,gridColor:Ye,gridWidth:1,labelColor:\"#999\",labelFontSize:10,titleColor:\"#333\",tickColor:Ye,tickSize:10,titleFontSize:14,titlePadding:10,labelPadding:4},axisBand:{grid:!1},background:qe,group:{fill:qe},legend:{labelColor:Je,labelFontSize:11,padding:1,symbolSize:30,symbolType:\"square\",titleColor:Je,titleFontSize:14,titlePadding:10},line:{stroke:He,strokeWidth:2},path:{stroke:He,strokeWidth:.5},rect:{fill:He},range:{category:[\"#30a2da\",\"#fc4f30\",\"#e5ae38\",\"#6d904f\",\"#8b8b8b\",\"#b96db8\",\"#ff9e27\",\"#56cc60\",\"#52d2ca\",\"#52689e\",\"#545454\",\"#9fe4f8\"],diverging:[\"#cc0020\",\"#e77866\",\"#f6e7e1\",\"#d6e8ed\",\"#91bfd9\",\"#1d78b5\"],heatmap:[\"#d6e8ed\",\"#cee0e5\",\"#91bfd9\",\"#549cc6\",\"#1d78b5\"]},point:{filled:!0,shape:\"circle\"},shape:{stroke:He},bar:{binSpacing:2,fill:He,stroke:null},title:{anchor:\"start\",fontSize:24,fontWeight:600,offset:20}},Ze=\"#000\",Ke={group:{fill:\"#e5e5e5\"},arc:{fill:Ze},area:{fill:Ze},line:{stroke:Ze},path:{stroke:Ze},rect:{fill:Ze},shape:{stroke:Ze},symbol:{fill:Ze,size:40},axis:{domain:!1,grid:!0,gridColor:\"#FFFFFF\",gridOpacity:1,labelColor:\"#7F7F7F\",labelPadding:4,tickColor:\"#7F7F7F\",tickSize:5.67,titleFontSize:16,titleFontWeight:\"normal\"},legend:{labelBaseline:\"middle\",labelFontSize:11,symbolSize:40},range:{category:[\"#000000\",\"#7F7F7F\",\"#1A1A1A\",\"#999999\",\"#333333\",\"#B0B0B0\",\"#4D4D4D\",\"#C9C9C9\",\"#666666\",\"#DCDCDC\"]}},et=\"Benton Gothic, sans-serif\",tt=\"#82c6df\",nt=\"Benton Gothic Bold, sans-serif\",rt=\"normal\",it={\"category-6\":[\"#ec8431\",\"#829eb1\",\"#c89d29\",\"#3580b1\",\"#adc839\",\"#ab7fb4\"],\"fire-7\":[\"#fbf2c7\",\"#f9e39c\",\"#f8d36e\",\"#f4bb6a\",\"#e68a4f\",\"#d15a40\",\"#ab4232\"],\"fireandice-6\":[\"#e68a4f\",\"#f4bb6a\",\"#f9e39c\",\"#dadfe2\",\"#a6b7c6\",\"#849eae\"],\"ice-7\":[\"#edefee\",\"#dadfe2\",\"#c4ccd2\",\"#a6b7c6\",\"#849eae\",\"#607785\",\"#47525d\"]},ot={background:\"#ffffff\",title:{anchor:\"start\",color:\"#000000\",font:nt,fontSize:22,fontWeight:\"normal\"},arc:{fill:tt},area:{fill:tt},line:{stroke:tt,strokeWidth:2},path:{stroke:tt},rect:{fill:tt},shape:{stroke:tt},symbol:{fill:tt,size:30},axis:{labelFont:et,labelFontSize:11.5,labelFontWeight:\"normal\",titleFont:nt,titleFontSize:13,titleFontWeight:rt},axisX:{labelAngle:0,labelPadding:4,tickSize:3},axisY:{labelBaseline:\"middle\",maxExtent:45,minExtent:45,tickSize:2,titleAlign:\"left\",titleAngle:0,titleX:-45,titleY:-11},legend:{labelFont:et,labelFontSize:11.5,symbolType:\"square\",titleFont:nt,titleFontSize:13,titleFontWeight:rt},range:{category:it[\"category-6\"],diverging:it[\"fireandice-6\"],heatmap:it[\"fire-7\"],ordinal:it[\"fire-7\"],ramp:it[\"fire-7\"]}},at=\"#ab5787\",st=\"#979797\",lt={background:\"#f9f9f9\",arc:{fill:at},area:{"
-  , "fill:at},line:{stroke:at},path:{stroke:at},rect:{fill:at},shape:{stroke:at},symbol:{fill:at,size:30},axis:{domainColor:st,domainWidth:.5,gridWidth:.2,labelColor:st,tickColor:st,tickWidth:.2,titleColor:st},axisBand:{grid:!1},axisX:{grid:!0,tickSize:10},axisY:{domain:!1,grid:!0,tickSize:0},legend:{labelFontSize:11,padding:1,symbolSize:30,symbolType:\"square\"},range:{category:[\"#ab5787\",\"#51b2e5\",\"#703c5c\",\"#168dd9\",\"#d190b6\",\"#00609f\",\"#d365ba\",\"#154866\",\"#666666\",\"#c4c4c4\"]}},ct=\"#3e5c69\",ft={background:\"#fff\",arc:{fill:ct},area:{fill:ct},line:{stroke:ct},path:{stroke:ct},rect:{fill:ct},shape:{stroke:ct},symbol:{fill:ct},axis:{domainWidth:.5,grid:!0,labelPadding:2,tickSize:5,tickWidth:.5,titleFontWeight:\"normal\"},axisBand:{grid:!1},axisX:{gridWidth:.2},axisY:{gridDash:[3],gridWidth:.4},legend:{labelFontSize:11,padding:1,symbolType:\"square\"},range:{category:[\"#3e5c69\",\"#6793a6\",\"#182429\",\"#0570b0\",\"#3690c0\",\"#74a9cf\",\"#a6bddb\",\"#e2ddf2\"]}},dt=\"#1696d2\",pt=\"#000000\",ut=\"Lato\",ht=\"Lato\",gt={\"main-colors\":[\"#1696d2\",\"#d2d2d2\",\"#000000\",\"#fdbf11\",\"#ec008b\",\"#55b748\",\"#5c5859\",\"#db2b27\"],\"shades-blue\":[\"#CFE8F3\",\"#A2D4EC\",\"#73BFE2\",\"#46ABDB\",\"#1696D2\",\"#12719E\",\"#0A4C6A\",\"#062635\"],\"shades-gray\":[\"#F5F5F5\",\"#ECECEC\",\"#E3E3E3\",\"#DCDBDB\",\"#D2D2D2\",\"#9D9D9D\",\"#696969\",\"#353535\"],\"shades-yellow\":[\"#FFF2CF\",\"#FCE39E\",\"#FDD870\",\"#FCCB41\",\"#FDBF11\",\"#E88E2D\",\"#CA5800\",\"#843215\"],\"shades-magenta\":[\"#F5CBDF\",\"#EB99C2\",\"#E46AA7\",\"#E54096\",\"#EC008B\",\"#AF1F6B\",\"#761548\",\"#351123\"],\"shades-green\":[\"#DCEDD9\",\"#BCDEB4\",\"#98CF90\",\"#78C26D\",\"#55B748\",\"#408941\",\"#2C5C2D\",\"#1A2E19\"],\"shades-black\":[\"#D5D5D4\",\"#ADABAC\",\"#848081\",\"#5C5859\",\"#332D2F\",\"#262223\",\"#1A1717\",\"#0E0C0D\"],\"shades-red\":[\"#F8D5D4\",\"#F1AAA9\",\"#E9807D\",\"#E25552\",\"#DB2B27\",\"#A4201D\",\"#6E1614\",\"#370B0A\"],\"one-group\":[\"#1696d2\",\"#000000\"],\"two-groups-cat-1\":[\"#1696d2\",\"#000000\"],\"two-groups-cat-2\":[\"#1696d2\",\"#fdbf11\"],\"two-groups-cat-3\":[\"#1696d2\",\"#db2b27\"],\"two-groups-seq\":[\"#a2d4ec\",\"#1696d2\"],\"three-groups-cat\":[\"#1696d2\",\"#fdbf11\",\"#000000\"],\"three-groups-seq\":[\"#a2d4ec\",\"#1696d2\",\"#0a4c6a\"],\"four-groups-cat-1\":[\"#000000\",\"#d2d2d2\",\"#fdbf11\",\"#1696d2\"],\"four-groups-cat-2\":[\"#1696d2\",\"#ec0008b\",\"#fdbf11\",\"#5c5859\"],\"four-groups-seq\":[\"#cfe8f3\",\"#73bf42\",\"#1696d2\",\"#0a4c6a\"],\"five-groups-cat-1\":[\"#1696d2\",\"#fdbf11\",\"#d2d2d2\",\"#ec008b\",\"#000000\"],\"five-groups-cat-2\":[\"#1696d2\",\"#0a4c6a\",\"#d2d2d2\",\"#fdbf11\",\"#332d2f\"],\"five-groups-seq\":[\"#cfe8f3\",\"#73bf42\",\"#1696d2\",\"#0a4c6a\",\"#000000\"],\"six-groups-cat-1\":[\"#1696d2\",\"#ec008b\",\"#fdbf11\",\"#000000\",\"#d2d2d2\",\"#55b748\"],\"six-groups-cat-2\":[\"#1696d2\",\"#d2d2d2\",\"#ec008b\",\"#fdbf11\",\"#332d2f\",\"#0a4c6a\"],\"six-groups-seq\":[\"#cfe8f3\",\"#a2d4ec\",\"#73bfe2\",\"#46abdb\",\"#1696d2\",\"#12719e\"],\"diverging-colors\":[\"#ca5800\",\"#fdbf11\",\"#fdd870\",\"#fff2cf\",\"#cfe8f3\",\"#73bfe2\",\"#1696d2\",\"#0a4c6a\"]},mt={background:\"#FFFFFF\",title:{anchor:\"start\",fontSize:18,font:ut},axisX:{domain:!0,domainColor:pt,domainWidth:1,grid:!1,labelFontSize:12,labelFont:ht,labelAngle:0,tickColor:pt,tickSize:5,titleFontSize:12,titlePadding:10,titleFont:ut},axisY:{domain:!1,domainWidth:1,grid:!0,gridColor:\"#DEDDDD\",gridWidth:1,labelFontSize:12,labelFont:ht,labelPadding:8,ticks:!1,titleFontSize:12,titlePadding:10,titleFont:ut,titleAngle:0,titleY:-10,titleX:18},legend:{labelFontSize:12,labelFont:ht,symbolSize:100,titleFontSize:12,titlePadding:10,titleFont:ut,orient:\"right\",offset:10},view:{stroke:\"transparent\"},range:{category:gt[\"six-groups-cat-1\"],diverging:gt[\"diverging-colors\"],heatmap:gt[\"diverging-colors\"],ordinal:gt[\"six-groups-seq\"],ramp:gt[\"shades-blue\"]},area:{fill:dt},rect:{fill:dt},line:{color:dt,stroke:dt,strokeWidth:5},trail:{color:dt,stroke:dt,strokeWidth:0,size:1},path:{stroke:dt,strokeWidth:.5},point:{filled:!0},text:{font:\"Lato\",color:dt,fontSize:11,align:\"center\",fontWeight:400,size:11},style:{bar:{fill:dt,stroke:null}},arc:{fill:dt},shape:{stroke:dt},symbol:{fill:dt,size:30}},Et=\"#3366CC\",vt=\"#ccc\",bt=\"Arial, sans-serif\",yt={arc:{fill:Et},area:{fill:Et},path:{stroke:Et},rect:{fill:Et},shape:{stroke:Et},symbol:{stroke:Et},circle:{fill:Et},background:\"#fff\",padding:{"
-  , "top:10,right:10,bottom:10,left:10},style:{\"guide-label\":{font:bt,fontSize:12},\"guide-title\":{font:bt,fontSize:12},\"group-title\":{font:bt,fontSize:12}},title:{font:bt,fontSize:14,fontWeight:\"bold\",dy:-3,anchor:\"start\"},axis:{gridColor:vt,tickColor:vt,domain:!1,grid:!0},range:{category:[\"#4285F4\",\"#DB4437\",\"#F4B400\",\"#0F9D58\",\"#AB47BC\",\"#00ACC1\",\"#FF7043\",\"#9E9D24\",\"#5C6BC0\",\"#F06292\",\"#00796B\",\"#C2185B\"],heatmap:[\"#c6dafc\",\"#5e97f6\",\"#2a56c6\"]}},wt=e=>e*(1/3+1),Ot=wt(9),At=wt(10),It=wt(12),xt=\"Segoe UI\",Lt=\"wf_standard-font, helvetica, arial, sans-serif\",Nt=\"#252423\",$t=\"#605E5C\",Rt=\"transparent\",Tt=\"#118DFF\",St=\"#DEEFFF\",Ct=[St,Tt],Dt={view:{stroke:Rt},background:Rt,font:xt,header:{titleFont:Lt,titleFontSize:It,titleColor:Nt,labelFont:xt,labelFontSize:At,labelColor:$t},axis:{ticks:!1,grid:!1,domain:!1,labelColor:$t,labelFontSize:Ot,titleFont:Lt,titleColor:Nt,titleFontSize:It,titleFontWeight:\"normal\"},axisQuantitative:{tickCount:3,grid:!0,gridColor:\"#C8C6C4\",gridDash:[1,5],labelFlush:!1},axisBand:{tickExtra:!0},axisX:{labelPadding:5},axisY:{labelPadding:10},bar:{fill:Tt},line:{stroke:Tt,strokeWidth:3,strokeCap:\"round\",strokeJoin:\"round\"},text:{font:xt,fontSize:Ot,fill:$t},arc:{fill:Tt},area:{fill:Tt,line:!0,opacity:.6},path:{stroke:Tt},rect:{fill:Tt},point:{fill:Tt,filled:!0,size:75},shape:{stroke:Tt},symbol:{fill:Tt,strokeWidth:1.5,size:50},legend:{titleFont:xt,titleFontWeight:\"bold\",titleColor:$t,labelFont:xt,labelFontSize:At,labelColor:$t,symbolType:\"circle\",symbolSize:75},range:{category:[Tt,\"#12239E\",\"#E66C37\",\"#6B007B\",\"#E044A7\",\"#744EC2\",\"#D9B300\",\"#D64550\"],diverging:Ct,heatmap:Ct,ordinal:[St,\"#c7e4ff\",\"#b0d9ff\",\"#9aceff\",\"#83c3ff\",\"#6cb9ff\",\"#55aeff\",\"#3fa3ff\",\"#2898ff\",Tt]}},Ft='IBM Plex Sans,system-ui,-apple-system,BlinkMacSystemFont,\".sfnstext-regular\",sans-serif',kt={textPrimary:{g90:\"#f4f4f4\",g100:\"#f4f4f4\",white:\"#161616\",g10:\"#161616\"},textSecondary:{g90:\"#c6c6c6\",g100:\"#c6c6c6\",white:\"#525252\",g10:\"#525252\"},layerAccent01:{white:\"#e0e0e0\",g10:\"#e0e0e0\",g90:\"#525252\",g100:\"#393939\"},gridBg:{white:\"#ffffff\",g10:\"#ffffff\",g90:\"#161616\",g100:\"#161616\"}},_t=[\"#8a3ffc\",\"#33b1ff\",\"#007d79\",\"#ff7eb6\",\"#fa4d56\",\"#fff1f1\",\"#6fdc8c\",\"#4589ff\",\"#d12771\",\"#d2a106\",\"#08bdba\",\"#bae6ff\",\"#ba4e00\",\"#d4bbff\"],Pt=[\"#6929c4\",\"#1192e8\",\"#005d5d\",\"#9f1853\",\"#fa4d56\",\"#570408\",\"#198038\",\"#002d9c\",\"#ee538b\",\"#b28600\",\"#009d9a\",\"#012749\",\"#8a3800\",\"#a56eff\"];function Mt({theme:e,background:t}){const n=[\"white\",\"g10\"].includes(e)?\"light\":\"dark\",r=kt.gridBg[e],i=kt.textPrimary[e],o=kt.textSecondary[e],a=\"dark\"===n?_t:Pt,s=\"dark\"===n?\"#d4bbff\":\"#6929c4\";return{background:t,arc:{fill:s},area:{fill:s},path:{stroke:s},rect:{fill:s},shape:{stroke:s},symbol:{stroke:s},circle:{fill:s},view:{fill:r,stroke:r},group:{fill:r},title:{color:i,anchor:\"start\",dy:-15,fontSize:16,font:Ft,fontWeight:600},axis:{labelColor:o,labelFontSize:12,labelFont:'IBM Plex Sans Condensed, system-ui, -apple-system, BlinkMacSystemFont, \".SFNSText-Regular\", sans-serif',labelFontWeight:400,titleColor:i,titleFontWeight:600,titleFontSize:12,grid:!0,gridColor:kt.layerAccent01[e],labelAngle:0},axisX:{titlePadding:10},axisY:{titlePadding:2.5},style:{\"guide-label\":{font:Ft,fill:o,fontWeight:400},\"guide-title\":{font:Ft,fill:o,fontWeight:400}},range:{category:a,diverging:[\"#750e13\",\"#a2191f\",\"#da1e28\",\"#fa4d56\",\"#ff8389\",\"#ffb3b8\",\"#ffd7d9\",\"#fff1f1\",\"#e5f6ff\",\"#bae6ff\",\"#82cfff\",\"#33b1ff\",\"#1192e8\",\"#0072c3\",\"#00539a\",\"#003a6d\"],heatmap:[\"#f6f2ff\",\"#e8daff\",\"#d4bbff\",\"#be95ff\",\"#a56eff\",\"#8a3ffc\",\"#6929c4\",\"#491d8b\",\"#31135e\",\"#1c0f30\"]}}}const zt=Mt({theme:\"white\",background:\"#ffffff\"}),jt=Mt({theme:\"g10\",background:\"#f4f4f4\"}),Ut=Mt({theme:\"g90\",background:\"#262626\"}),Bt=Mt({theme:\"g100\",background:\"#161616\"}),Gt=Ue;var Wt=Object.freeze({__proto__:null,carbong10:jt,carbong100:Bt,carbong90:Ut,carbonwhite:zt,dark:We,excel:Ve,fivethirtyeight:Qe,ggplot2:Ke,googlecharts:yt,latimes:ot,powerbi:Dt,quartz:lt,urbaninstitute:mt,version:Gt,vox:ft});function Xt(e,t,n){return e.fields=t||[],e.fname=n,e}const Vt=e=>function(t){return t[e]},Ht=e=>{const t=e.length;return function(n){for("
-  , "let r=0;r<t;++r)n=n[e[r]];return n}};function Yt(e){throw Error(e)}!function(e){const t=function(e){const t=[],n=e.length;let r,i,o,a=null,s=0,l=\"\";function c(){t.push(l+e.substring(r,i)),l=\"\",r=i+1}for(e+=\"\",r=i=0;i<n;++i)if(o=e[i],\"\\\\\"===o)l+=e.substring(r,i++),r=i;else if(o===a)c(),a=null,s=-1;else{if(a)continue;r===s&&'\"'===o||r===s&&\"'\"===o?(r=i+1,a=o):\".\"!==o||s?\"[\"===o?(i>r&&c(),s=r=i+1):\"]\"===o&&(s||Yt(\"Access path missing open bracket: \"+e),s>0&&c(),s=0,r=i+1):i>r?c():r=i+1}return s&&Yt(\"Access path missing closing bracket: \"+e),a&&Yt(\"Access path missing closing quote: \"+e),i>r&&(i++,c()),t}(e);e=1===t.length?t[0]:e,Xt(function(e){return 1===e.length?Vt(e[0]):Ht(e)}(t),[e],e)}(\"id\"),Xt((e=>e),[],\"identity\"),Xt((()=>0),[],\"zero\"),Xt((()=>1),[],\"one\"),Xt((()=>!0),[],\"true\"),Xt((()=>!1),[],\"false\");var qt=Array.isArray;function Jt(e){return e===Object(e)}function Qt(e,t){return JSON.stringify(e,function(e){const t=[];return function(n,r){if(\"object\"!=typeof r||null===r)return r;const i=t.indexOf(this)+1;return t.length=i,t.length>e?\"[Object]\":t.indexOf(r)>=0?\"[Circular]\":(t.push(r),r)}}(t))}var Zt=\"#vg-tooltip-element {\\n  visibility: hidden;\\n  padding: 8px;\\n  position: fixed;\\n  z-index: 1000;\\n  font-family: sans-serif;\\n  font-size: 11px;\\n  border-radius: 3px;\\n  box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);\\n  /* The default theme is the light theme. */\\n  background-color: rgba(255, 255, 255, 0.95);\\n  border: 1px solid #d9d9d9;\\n  color: black;\\n}\\n#vg-tooltip-element.visible {\\n  visibility: visible;\\n}\\n#vg-tooltip-element h2 {\\n  margin-top: 0;\\n  margin-bottom: 10px;\\n  font-size: 13px;\\n}\\n#vg-tooltip-element table {\\n  border-spacing: 0;\\n}\\n#vg-tooltip-element table tr {\\n  border: none;\\n}\\n#vg-tooltip-element table tr td {\\n  overflow: hidden;\\n  text-overflow: ellipsis;\\n  padding-top: 2px;\\n  padding-bottom: 2px;\\n}\\n#vg-tooltip-element table tr td.key {\\n  color: #808080;\\n  max-width: 150px;\\n  text-align: right;\\n  padding-right: 4px;\\n}\\n#vg-tooltip-element table tr td.value {\\n  display: block;\\n  max-width: 300px;\\n  max-height: 7em;\\n  text-align: left;\\n}\\n#vg-tooltip-element.dark-theme {\\n  background-color: rgba(32, 32, 32, 0.9);\\n  border: 1px solid #f5f5f5;\\n  color: white;\\n}\\n#vg-tooltip-element.dark-theme td.key {\\n  color: #bfbfbf;\\n}\\n\";const Kt=\"vg-tooltip-element\",en={offsetX:10,offsetY:10,id:Kt,styleId:\"vega-tooltip-style\",theme:\"light\",disableDefaultStyle:!1,sanitize:function(e){return String(e).replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\")},maxDepth:2,formatTooltip:function(e,t,n,r){if(qt(e))return`[${e.map((e=>t(\"string\"==typeof e?e:Qt(e,n)))).join(\", \")}]`;if(Jt(e)){let i=\"\";const{title:o,image:a,...s}=e;o&&(i+=`<h2>${t(o)}</h2>`),a&&(i+=`<img src=\"${new URL(t(a),r||location.href).href}\">`);const l=Object.keys(s);if(l.length>0){i+=\"<table>\";for(const e of l){let r=s[e];void 0!==r&&(Jt(r)&&(r=Qt(r,n)),i+=`<tr><td class=\"key\">${t(e)}</td><td class=\"value\">${t(r)}</td></tr>`)}i+=\"</table>\"}return i||\"{}\"}return t(e)},baseURL:\"\",anchor:\"cursor\",position:[\"top\",\"bottom\",\"left\",\"right\",\"top-left\",\"top-right\",\"bottom-left\",\"bottom-right\"]};function tn(e,t,{offsetX:n,offsetY:r}){const i=nn({x1:e.clientX,x2:e.clientX,y1:e.clientY,y2:e.clientY},t,n,r),o=[\"bottom-right\",\"bottom-left\",\"top-right\",\"top-left\"];for(const e of o)if(rn(i[e],t))return i[e];return i[\"top-left\"]}function nn(e,t,n,r){const i=(e.x1+e.x2)/2,o=(e.y1+e.y2)/2,a=e.x1-t.width-n,s=i-t.width/2,l=e.x2+n,c=e.y1-t.height-r,f=o-t.height/2,d=e.y2+r;return{top:{x:s,y:c},bottom:{x:s,y:d},left:{x:a,y:f},right:{x:l,y:f},\"top-left\":{x:a,y:c},\"top-right\":{x:l,y:c},\"bottom-left\":{x:a,y:d},\"bottom-right\":{x:l,y:d}}}function rn(e,t){return e.x>=0&&e.y>=0&&e.x+t.width<=window.innerWidth&&e.y+t.height<=window.innerHeight}function on(e,t,n){return e.clientX>=t.x&&e.clientX<=t.x+n.width&&e.clientY>=t.y&&e.clientY<=t.y+n.height}class an{constructor(e){this.options={...en,...e};const t=this.options.id;if(this.el=null,this.call=this.tooltipHandler.bind(this),!this.options.disableDefaultStyle&&!document.getElementById(this.options.styleId)){co"
-  , "nst e=document.createElement(\"style\");e.setAttribute(\"id\",this.options.styleId),e.innerHTML=function(e){if(!/^[A-Za-z]+[-:.\\w]*$/.test(e))throw new Error(\"Invalid HTML ID\");return Zt.toString().replace(Kt,e)}(t);const n=document.head;n.childNodes.length>0?n.insertBefore(e,n.childNodes[0]):n.appendChild(e)}}tooltipHandler(e,t,n,r){if(this.el=document.getElementById(this.options.id),!this.el){this.el=document.createElement(\"div\"),this.el.setAttribute(\"id\",this.options.id),this.el.classList.add(\"vg-tooltip\");(document.fullscreenElement??document.body).appendChild(this.el)}if(null==r||\"\"===r)return void this.el.classList.remove(\"visible\",`${this.options.theme}-theme`);this.el.innerHTML=this.options.formatTooltip(r,this.options.sanitize,this.options.maxDepth,this.options.baseURL),this.el.classList.add(\"visible\",`${this.options.theme}-theme`);const{x:i,y:o}=\"mark\"===this.options.anchor?function(e,t,n,r,i){const{position:o,offsetX:a,offsetY:s}=i,l=function(e,t,n){const r=n.isVoronoi?n.datum.bounds:n.bounds;let i=e.left+t[0]+r.x1,o=e.top+t[1]+r.y1,a=n;for(;a.mark.group;)a=a.mark.group,i+=a.x??0,o+=a.y??0;return{x1:i,x2:i+(r.x2-r.x1),y1:o,y2:o+(r.y2-r.y1)}}(e._el.getBoundingClientRect(),e._origin,n),c=nn(l,r,a,s),f=Array.isArray(o)?o:[o];for(const e of f)if(rn(c[e],r)&&!on(t,c[e],r))return c[e];return tn(t,r,i)}(e,t,n,this.el.getBoundingClientRect(),this.options):tn(t,this.el.getBoundingClientRect(),this.options);this.el.style.top=`${o}px`,this.el.style.left=`${i}px`}}var sn='.vega-embed {\\n  position: relative;\\n  display: inline-block;\\n  box-sizing: border-box;\\n}\\n.vega-embed.has-actions {\\n  padding-right: 38px;\\n}\\n.vega-embed details:not([open]) > :not(summary) {\\n  display: none !important;\\n}\\n.vega-embed summary {\\n  list-style: none;\\n  position: absolute;\\n  top: 0;\\n  right: 0;\\n  padding: 6px;\\n  z-index: 1000;\\n  background: white;\\n  box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1);\\n  color: #1b1e23;\\n  border: 1px solid #aaa;\\n  border-radius: 999px;\\n  opacity: 0.2;\\n  transition: opacity 0.4s ease-in;\\n  cursor: pointer;\\n  line-height: 0px;\\n}\\n.vega-embed summary::-webkit-details-marker {\\n  display: none;\\n}\\n.vega-embed summary:active {\\n  box-shadow: #aaa 0px 0px 0px 1px inset;\\n}\\n.vega-embed summary svg {\\n  width: 14px;\\n  height: 14px;\\n}\\n.vega-embed details[open] summary {\\n  opacity: 0.7;\\n}\\n.vega-embed:hover summary, .vega-embed:focus-within summary {\\n  opacity: 1 !important;\\n  transition: opacity 0.2s ease;\\n}\\n.vega-embed .vega-actions {\\n  position: absolute;\\n  z-index: 1001;\\n  top: 35px;\\n  right: -9px;\\n  display: flex;\\n  flex-direction: column;\\n  padding-bottom: 8px;\\n  padding-top: 8px;\\n  border-radius: 4px;\\n  box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.2);\\n  border: 1px solid #d9d9d9;\\n  background: white;\\n  animation-duration: 0.15s;\\n  animation-name: scale-in;\\n  animation-timing-function: cubic-bezier(0.2, 0, 0.13, 1.5);\\n  text-align: left;\\n}\\n.vega-embed .vega-actions a {\\n  padding: 8px 16px;\\n  font-family: sans-serif;\\n  font-size: 14px;\\n  font-weight: 600;\\n  white-space: nowrap;\\n  color: #434a56;\\n  text-decoration: none;\\n}\\n.vega-embed .vega-actions a:hover, .vega-embed .vega-actions a:focus {\\n  background-color: #f7f7f9;\\n  color: black;\\n}\\n.vega-embed .vega-actions::before, .vega-embed .vega-actions::after {\\n  content: \"\";\\n  display: inline-block;\\n  position: absolute;\\n}\\n.vega-embed .vega-actions::before {\\n  left: auto;\\n  right: 14px;\\n  top: -16px;\\n  border: 8px solid rgba(0, 0, 0, 0);\\n  border-bottom-color: #d9d9d9;\\n}\\n.vega-embed .vega-actions::after {\\n  left: auto;\\n  right: 15px;\\n  top: -14px;\\n  border: 7px solid rgba(0, 0, 0, 0);\\n  border-bottom-color: #fff;\\n}\\n.vega-embed .chart-wrapper.fit-x {\\n  width: 100%;\\n}\\n.vega-embed .chart-wrapper.fit-y {\\n  height: 100%;\\n}\\n\\n.vega-embed-wrapper {\\n  max-width: 100%;\\n  overflow: auto;\\n  padding-right: 14px;\\n}\\n\\n@keyframes scale-in {\\n  from {\\n    opacity: 0;\\n    transform: scale(0.6);\\n  }\\n  to {\\n    opacity: 1;\\n    transform: scale(1);\\n  }\\n}\\n';function ln(e,...t){for(const n of t)cn(e,n);r"
-  , "eturn e}function cn(t,n){for(const r of Object.keys(n))e.writeConfig(t,r,n[r],!0)}const fn=\"6.29.0\",dn=i;let pn=o;const un=\"undefined\"!=typeof window?window:void 0;void 0===pn&&un?.vl?.compile&&(pn=un.vl);const hn={export:{svg:!0,png:!0},source:!0,compiled:!0,editor:!0},gn={CLICK_TO_VIEW_ACTIONS:\"Click to view actions\",COMPILED_ACTION:\"View Compiled Vega\",EDITOR_ACTION:\"Open in Vega Editor\",PNG_ACTION:\"Save as PNG\",SOURCE_ACTION:\"View Source\",SVG_ACTION:\"Save as SVG\"},mn={vega:\"Vega\",\"vega-lite\":\"Vega-Lite\"},En={vega:dn.version,\"vega-lite\":pn?pn.version:\"not available\"},vn={vega:e=>e,\"vega-lite\":(e,t)=>pn.compile(e,{config:t}).spec},bn='\\n<svg viewBox=\"0 0 16 16\" fill=\"currentColor\" stroke=\"none\" stroke-width=\"1\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\\n  <circle r=\"2\" cy=\"8\" cx=\"2\"></circle>\\n  <circle r=\"2\" cy=\"8\" cx=\"8\"></circle>\\n  <circle r=\"2\" cy=\"8\" cx=\"14\"></circle>\\n</svg>',yn=\"chart-wrapper\";function wn(e,t,n,r){const i=`<html><head>${t}</head><body><pre><code class=\"json\">`,o=`</code></pre>${n}</body></html>`,a=window.open(\"\");a.document.write(i+e+o),a.document.title=`${mn[r]} JSON Source`}function On(e){return!(!e||!(\"load\"in e))}function An(e){return On(e)?e:dn.loader(e)}async function In(t,n,r={}){let i,o;e.isString(n)?(o=An(r.loader),i=JSON.parse(await o.load(n))):i=n;const a=function(t){const n=t.usermeta?.embedOptions??{};return e.isString(n.defaultStyle)&&(n.defaultStyle=!1),n}(i),s=a.loader;o&&!s||(o=An(r.loader??s));const l=await xn(a,o),c=await xn(r,o),f={...ln(c,l),config:e.mergeConfig(c.config??{},l.config??{})};return await async function(t,n,r={},i){const o=r.theme?e.mergeConfig(Wt[r.theme],r.config??{}):r.config,a=e.isBoolean(r.actions)?r.actions:ln({},hn,r.actions??{}),s={...gn,...r.i18n},l=r.renderer??\"canvas\",c=r.logLevel??dn.Warn,f=r.downloadFileName??\"visualization\",d=\"string\"==typeof t?document.querySelector(t):t;if(!d)throw new Error(`${t} does not exist`);if(!1!==r.defaultStyle){const e=\"vega-embed-style\",{root:t,rootContainer:n}=function(e){const t=e.getRootNode?e.getRootNode():document;return t instanceof ShadowRoot?{root:t,rootContainer:t}:{root:document,rootContainer:document.head??document.body}}(d);if(!t.getElementById(e)){const t=document.createElement(\"style\");t.id=e,t.innerHTML=void 0===r.defaultStyle||!0===r.defaultStyle?sn.toString():r.defaultStyle,n.appendChild(t)}}const p=function(e,t){if(e.$schema){const n=je(e.$schema);t&&t!==n.library&&console.warn(`The given visualization spec is written in ${mn[n.library]}, but mode argument sets ${mn[t]??t}.`);const r=n.library;return $e(En[r],`^${n.version.slice(1)}`)||console.warn(`The input spec uses ${mn[r]} ${n.version}, but the current version of ${mn[r]} is v${En[r]}.`),r}return\"mark\"in e||\"encoding\"in e||\"layer\"in e||\"hconcat\"in e||\"vconcat\"in e||\"facet\"in e||\"repeat\"in e?\"vega-lite\":\"marks\"in e||\"signals\"in e||\"scales\"in e||\"axes\"in e?\"vega\":t??\"vega\"}(n,r.mode);let u=vn[p](n,o);if(\"vega-lite\"===p&&u.$schema){const e=je(u.$schema);$e(En.vega,`^${e.version.slice(1)}`)||console.warn(`The compiled spec uses Vega ${e.version}, but current version is v${En.vega}.`)}d.classList.add(\"vega-embed\"),a&&d.classList.add(\"has-actions\");d.innerHTML=\"\";let h=d;if(a){const e=document.createElement(\"div\");e.classList.add(yn),d.appendChild(e),h=e}const g=r.patch;g&&(u=g instanceof Function?g(u):A(u,g,!0,!1).newDocument);r.formatLocale&&dn.formatLocale(r.formatLocale);r.timeFormatLocale&&dn.timeFormatLocale(r.timeFormatLocale);if(r.expressionFunctions)for(const e in r.expressionFunctions){const t=r.expressionFunctions[e];\"fn\"in t?dn.expressionFunction(e,t.fn,t.visitor):t instanceof Function&&dn.expressionFunction(e,t)}const{ast:m}=r,E=dn.parse(u,\"vega-lite\"===p?{}:o,{ast:m}),v=new(r.viewClass||dn.View)(E,{loader:i,logLevel:c,renderer:l,...m?{expr:dn.expressionInterpreter??r.expr??ze}:{}});if(v.addSignalListener(\"autosize\",((e,t)=>{const{type:n}=t;\"fit-x\"==n?(h.classList.add(\"fit-x\"),h.classList.remove(\"fit-y\")):\"fit-y\"==n?(h.classList.remove(\"fit-x\"),h.classList.add(\"fit-y\")):\"fit\"==n?h.classList.add(\"fit-x\",\"fit-y\"):h.classList.remove(\""
-  , "fit-x\",\"fit-y\")})),!1!==r.tooltip){const{loader:e,tooltip:t}=r,n=e&&!On(e)?e?.baseURL:void 0,i=\"function\"==typeof t?t:new an({baseURL:n,...!0===t?{}:t}).call;v.tooltip(i)}let b,{hover:y}=r;void 0===y&&(y=\"vega\"===p);if(y){const{hoverSet:e,updateSet:t}=\"boolean\"==typeof y?{}:y;v.hover(e,t)}r&&(null!=r.width&&v.width(r.width),null!=r.height&&v.height(r.height),null!=r.padding&&v.padding(r.padding));if(await v.initialize(h,r.bind).runAsync(),!1!==a){let t=d;if(!1!==r.defaultStyle||r.forceActionsMenu){const e=document.createElement(\"details\");e.title=s.CLICK_TO_VIEW_ACTIONS,d.append(e),t=e;const n=document.createElement(\"summary\");n.innerHTML=bn,e.append(n),b=t=>{e.contains(t.target)||e.removeAttribute(\"open\")},document.addEventListener(\"click\",b)}const i=document.createElement(\"div\");if(t.append(i),i.classList.add(\"vega-actions\"),!0===a||!1!==a.export)for(const t of[\"svg\",\"png\"])if(!0===a||!0===a.export||a.export[t]){const n=s[`${t.toUpperCase()}_ACTION`],o=document.createElement(\"a\"),a=e.isObject(r.scaleFactor)?r.scaleFactor[t]:r.scaleFactor;o.text=n,o.href=\"#\",o.target=\"_blank\",o.download=`${f}.${t}`,o.addEventListener(\"mousedown\",(async function(e){e.preventDefault();const n=await v.toImageURL(t,a);this.href=n})),i.append(o)}if(!0===a||!1!==a.source){const e=document.createElement(\"a\");e.text=s.SOURCE_ACTION,e.href=\"#\",e.addEventListener(\"click\",(function(e){wn(k(n),r.sourceHeader??\"\",r.sourceFooter??\"\",p),e.preventDefault()})),i.append(e)}if(\"vega-lite\"===p&&(!0===a||!1!==a.compiled)){const e=document.createElement(\"a\");e.text=s.COMPILED_ACTION,e.href=\"#\",e.addEventListener(\"click\",(function(e){wn(k(u),r.sourceHeader??\"\",r.sourceFooter??\"\",\"vega\"),e.preventDefault()})),i.append(e)}if(!0===a||!1!==a.editor){const e=r.editorUrl??\"https://vega.github.io/editor/\",t=document.createElement(\"a\");t.text=s.EDITOR_ACTION,t.href=\"#\",t.addEventListener(\"click\",(function(t){!function(e,t,n){const r=e.open(t),{origin:i}=new URL(t);let o=40;e.addEventListener(\"message\",(function t(n){n.source===r&&(o=0,e.removeEventListener(\"message\",t,!1))}),!1),setTimeout((function e(){o<=0||(r.postMessage(n,i),setTimeout(e,250),o-=1)}),250)}(window,e,{config:o,mode:g?\"vega\":p,renderer:l,spec:k(g?u:n)}),t.preventDefault()})),i.append(t)}}function w(){b&&document.removeEventListener(\"click\",b),v.finalize()}return{view:v,spec:n,vgSpec:u,finalize:w,embedOptions:r}}(t,i,f,o)}async function xn(t,n){const r=e.isString(t.config)?JSON.parse(await n.load(t.config)):t.config??{},i=e.isString(t.patch)?JSON.parse(await n.load(t.patch)):t.patch;return{...t,...i?{patch:i}:{},...r?{config:r}:{}}}async function Ln(e,t={}){const n=document.createElement(\"div\");n.classList.add(\"vega-embed-wrapper\");const r=document.createElement(\"div\");n.appendChild(r);const i=!0===t.actions||!1===t.actions?t.actions:{export:!0,source:!1,compiled:!0,editor:!0,...t.actions},o=await In(r,e,{actions:i,...t});return n.value=o.view,n}const Nn=(...t)=>{return t.length>1&&(e.isString(t[0])&&!((n=t[0]).startsWith(\"http://\")||n.startsWith(\"https://\")||n.startsWith(\"//\"))||t[0]instanceof HTMLElement||3===t.length)?In(t[0],t[1],t[2]):Ln(t[0],t[1]);var n};return Nn.vegaLite=pn,Nn.vl=pn,Nn.container=Ln,Nn.embed=In,Nn.vega=dn,Nn.default=In,Nn.version=fn,Nn}));\n//# sourceMappingURL=vega-embed.min.js.map\n"
-  ]
-
diff --git a/src/Hanalyze/Viz/Bar.hs b/src/Hanalyze/Viz/Bar.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/Bar.hs
+++ /dev/null
@@ -1,203 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.Bar
--- Description : 棒グラフ (縦・横・積み上げ・グループ化) の可視化
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- | Bar-chart visualizations.
---
---   * 'barChart'      — vertical bar chart by category.
---   * 'barChartH'     — horizontal bar chart (handy for long labels).
---   * 'stackedBar'    — stacked bar chart.
---   * 'groupedBar'    — grouped bar chart.
---   * 'barChartFile'  — write to HTML / PNG / SVG.
-module Hanalyze.Viz.Bar
-  ( barChart
-  , barChartH
-  , stackedBar
-  , groupedBar
-  , barChartFile
-    -- * 130: PlotData ベースの汎用 spec API
-  , barSpec
-  ) where
-
-import Data.Text (Text)
-import qualified Data.Vector as V
-import Graphics.Vega.VegaLite
-
-import Hanalyze.Viz.Core     (PlotConfig (..), OutputFormat, writeSpec)
-import Hanalyze.Viz.PlotData (PlotData, numericColumn, textColumn)
-
--- ---------------------------------------------------------------------------
--- 縦棒グラフ (カテゴリ → 数値)
--- ---------------------------------------------------------------------------
-
--- | A simple vertical bar chart.
---
--- @
--- barChart cfg "Month" "Sales"
---   ["Jan","Feb","Mar"] [120,95,140]
--- @
-barChart :: PlotConfig
-         -> Text     -- ^ X-axis label.
-         -> Text     -- ^ Y-axis label.
-         -> [Text]   -- ^ Categories.
-         -> [Double] -- ^ Per-category values.
-         -> VegaLite
-barChart cfg xLabel yLabel cats vals =
-  toVegaLite
-    [ title (plotTitle cfg) []
-    , dataFromColumns []
-        . dataColumn xLabel (Strings cats)
-        . dataColumn yLabel (Numbers vals)
-        $ []
-    , mark Bar [MColor "#4C72B0", MOpacity 0.85]
-    , encoding
-        . position X [ PName xLabel, PmType Nominal
-                     , PAxis [AxTitle xLabel, AxLabelAngle (-30)]
-                     , PSort [] ]
-        . position Y [ PName yLabel, PmType Quantitative
-                     , PAxis [AxTitle yLabel] ]
-        $ []
-    , widthStep 40
-    , height (plotHeight cfg)
-    ]
-
--- ---------------------------------------------------------------------------
--- 水平棒グラフ
--- ---------------------------------------------------------------------------
-
--- | A horizontal bar chart. Best when labels are long or for ranking
--- displays.
---
--- @
--- barChartH cfg "Country" "GDP" countries gdps
--- @
-barChartH :: PlotConfig
-          -> Text     -- ^ Y-axis (category) label.
-          -> Text     -- ^ X-axis (value) label.
-          -> [Text]   -- ^ Categories.
-          -> [Double] -- ^ Per-category values.
-          -> VegaLite
-barChartH cfg yLabel xLabel cats vals =
-  toVegaLite
-    [ title (plotTitle cfg) []
-    , dataFromColumns []
-        . dataColumn yLabel (Strings cats)
-        . dataColumn xLabel (Numbers vals)
-        $ []
-    , mark Bar [MColor "#4C72B0", MOpacity 0.85]
-    , encoding
-        . position Y [ PName yLabel, PmType Nominal
-                     , PAxis [AxTitle yLabel]
-                     , PSort [Descending] ]
-        . position X [ PName xLabel, PmType Quantitative
-                     , PAxis [AxTitle xLabel] ]
-        $ []
-    , width (plotWidth cfg)
-    , heightStep 24
-    ]
-
--- ---------------------------------------------------------------------------
--- 積み上げ棒グラフ
--- ---------------------------------------------------------------------------
-
--- | Stacked bar chart: each category shows its breakdown by group.
---
--- @
--- stackedBar cfg "Quarter" "Revenue" "Product"
---   ["Q1","Q1","Q1","Q2","Q2","Q2"]  -- x 軸カテゴリ (繰り返しOK)
---   [100, 80, 60, 120, 90, 70]       -- 値
---   ["A",  "B", "C", "A", "B", "C"] -- 色分けグループ
--- @
-stackedBar :: PlotConfig -> Text -> Text -> Text
-           -> [Text] -> [Double] -> [Text]
-           -> VegaLite
-stackedBar cfg xLabel yLabel colorLabel xCats vals colorCats =
-  toVegaLite
-    [ title (plotTitle cfg) []
-    , dataFromColumns []
-        . dataColumn xLabel    (Strings xCats)
-        . dataColumn yLabel    (Numbers vals)
-        . dataColumn colorLabel (Strings colorCats)
-        $ []
-    , mark Bar []
-    , encoding
-        . position X [ PName xLabel, PmType Nominal
-                     , PAxis [AxTitle xLabel, AxLabelAngle (-30)]
-                     , PSort [] ]
-        . position Y [ PName yLabel, PmType Quantitative
-                     , PAxis [AxTitle yLabel]
-                     , PStack StZero ]
-        . color [ MName colorLabel, MmType Nominal
-                , MScale [SScheme "tableau10" []] ]
-        $ []
-    , widthStep 50
-    , height (plotHeight cfg)
-    ]
-
--- ---------------------------------------------------------------------------
--- グループ別棒グラフ
--- ---------------------------------------------------------------------------
-
--- | Grouped bar chart (side-by-side comparison).
---
--- @
--- groupedBar cfg "Method" "ESS" "Case"
---   ["MH","HMC","NUTS","MH","HMC","NUTS"]  -- x 軸
---   [120, 900, 1800, 80, 1200, 1900]       -- 値
---   ["Easy","Easy","Easy","Hard","Hard","Hard"]  -- グループ
--- @
-groupedBar :: PlotConfig -> Text -> Text -> Text
-           -> [Text] -> [Double] -> [Text]
-           -> VegaLite
-groupedBar cfg xLabel yLabel groupLabel xCats vals groupCats =
-  toVegaLite
-    [ title (plotTitle cfg) []
-    , dataFromColumns []
-        . dataColumn xLabel     (Strings xCats)
-        . dataColumn yLabel     (Numbers vals)
-        . dataColumn groupLabel (Strings groupCats)
-        $ []
-    , mark Bar []
-    , encoding
-        . position X [ PName groupLabel, PmType Nominal
-                     , PAxis [AxTitle ""]
-                     , PScale [SPaddingInner 0.1] ]
-        . position Y [ PName yLabel, PmType Quantitative
-                     , PAxis [AxTitle yLabel] ]
-        . color [ MName groupLabel, MmType Nominal
-                , MScale [SScheme "tableau10" []] ]
-        . column [ FName xLabel, FmType Nominal
-                 , FHeader [HTitle xLabel, HLabelAngle (-30)] ]
-        $ []
-    , height (plotHeight cfg)
-    ]
-
--- ---------------------------------------------------------------------------
--- ファイル書き出し
--- ---------------------------------------------------------------------------
-
--- | Write a bar-chart spec to disk in the given output format.
-barChartFile :: OutputFormat -> FilePath -> VegaLite -> IO ()
-barChartFile = writeSpec
-
--- ---------------------------------------------------------------------------
--- 130: PlotData ベースの汎用 spec API
--- ---------------------------------------------------------------------------
-
--- | Build a Vega-Lite bar chart spec from a 'PlotData' source.
---
--- The category column must live in @pdText@ and the value column in
--- @pdNumeric@. Returns 'barChart' empty-data spec if either is missing.
-barSpec
-  :: PlotConfig
-  -> Text          -- ^ category column (text)
-  -> Text          -- ^ value column (numeric)
-  -> PlotData
-  -> VegaLite
-barSpec cfg catCol valCol pd =
-  let cats = maybe [] V.toList (textColumn    catCol pd)
-      vals = maybe [] V.toList (numericColumn valCol pd)
-  in barChart cfg catCol valCol cats vals
diff --git a/src/Hanalyze/Viz/Core.hs b/src/Hanalyze/Viz/Core.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/Core.hs
+++ /dev/null
@@ -1,104 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.Core
--- Description : 全 Viz.* モジュール共有の I/O ヘルパ (writeSpec / openInBrowser / vlJson 等)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- | Core visualization I/O helpers shared by every @Viz.*@ module.
---
--- Owns 'OutputFormat', 'writeSpec' (HTML / PNG / SVG via @vl-convert@
--- subprocess; HTML is the always-available fallback), 'openInBrowser',
--- and the JSON serialiser 'vlJson' used by downstream consumers
--- (HPotfire) to ship Vega-Lite specs over the wire.
---
--- Plot configuration ('PlotConfig' / 'defaultConfig') lives in
--- 'Hanalyze.Viz.PlotConfig' since 2026-05-14 and is re-exported here for
--- backwards compatibility.
-module Hanalyze.Viz.Core
-  ( -- * Plot configuration (re-exported from "Hanalyze.Viz.PlotConfig")
-    PlotConfig (..)
-  , defaultConfig
-    -- * Spec I/O
-  , openInBrowser
-  , OutputFormat (..)
-  , parseFormat
-  , writeSpec
-    -- * Spec serialisation
-  , vlJson
-  ) where
-
-import Control.Exception (SomeException, try)
-import Data.Aeson (encode)
-import Data.ByteString.Lazy (toStrict)
-import Data.Text (Text)
-import Data.Text.Encoding (decodeUtf8)
-import qualified Data.Text.IO as TIO
-import Graphics.Vega.VegaLite (VegaLite, toHtmlFile, fromVL)
-import System.FilePath (replaceExtension)
-import System.Info (os)
-import System.IO (hFlush, hClose, hPutStrLn, stderr)
-import System.IO.Temp (withSystemTempFile)
-import System.Process (callCommand, callProcess)
-
-import Hanalyze.Viz.PlotConfig (PlotConfig (..), defaultConfig)
-
--- | Serialise a 'VegaLite' spec to its canonical JSON 'Text'. Convenient
--- for downstream consumers (e.g. HPotfire's @/api/viz@) that need to
--- ship the spec over the wire instead of writing to disk.
---
--- Equivalent to @decodeUtf8 . toStrict . encode . fromVL@; provided here
--- so every @Viz.*@ module can re-export a single canonical spelling.
-vlJson :: VegaLite -> Text
-vlJson = decodeUtf8 . toStrict . encode . fromVL
-
--- | Output format for generated plots.
-data OutputFormat = HTML | PNG | SVG deriving (Show, Eq)
-
--- | Parse an 'OutputFormat' name (@\"html\"@ / @\"png\"@ / @\"svg\"@).
-parseFormat :: String -> Either String OutputFormat
-parseFormat "html" = Right HTML
-parseFormat "png"  = Right PNG
-parseFormat "svg"  = Right SVG
-parseFormat s      = Left ("Unknown format '" ++ s ++ "'. Use: html | png | svg")
-
--- | Write a Vega-Lite spec in the requested format. PNG and SVG are
--- produced by piping the JSON through the @vl-convert@ CLI.
-writeSpec :: OutputFormat -> FilePath -> VegaLite -> IO ()
-writeSpec HTML path spec = toHtmlFile path spec
-writeSpec fmt  path spec = do
-  result <- try (writeViaVlConvert fmt path spec) :: IO (Either SomeException ())
-  case result of
-    Right _ -> return ()
-    Left err -> do
-      hPutStrLn stderr $ "Warning: vl-convert failed (" ++ show err ++ "). Writing HTML instead."
-      toHtmlFile (replaceExtension path "html") spec
-
--- | Convert a Vega-Lite spec to PNG / SVG via @vl-convert@.
--- Writes the spec to a temporary JSON file, invokes @vl-convert@, and
--- removes the temporary file.
-writeViaVlConvert :: OutputFormat -> FilePath -> VegaLite -> IO ()
-writeViaVlConvert fmt outPath spec = do
-  let json   = decodeUtf8 . toStrict . encode . fromVL $ spec
-      subcmd = case fmt of
-        PNG -> "vl2png"
-        SVG -> "vl2svg"
-        HTML -> "vl2html"
-  withSystemTempFile "vl-spec-.json" $ \tmpPath tmpH -> do
-    TIO.hPutStr tmpH json
-    hFlush tmpH
-    hClose tmpH
-    callProcess "vl-convert" [subcmd, "-i", tmpPath, "-o", outPath]
-
--- | Open a file in the platform's default browser.
-openInBrowser :: FilePath -> IO ()
-openInBrowser path = do
-  result <- try (callCommand cmd) :: IO (Either SomeException ())
-  case result of
-    Right _  -> return ()
-    Left err -> putStrLn $ "Note: could not open browser (" ++ show err ++ ")"
-  where
-    cmd = case os of
-      "darwin"  -> "open "     ++ path
-      "mingw32" -> "start "    ++ path
-      _         -> "xdg-open " ++ path
diff --git a/src/Hanalyze/Viz/GP.hs b/src/Hanalyze/Viz/GP.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/GP.hs
+++ /dev/null
@@ -1,101 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.GP
--- Description : ガウス過程回帰結果 (訓練データ・事後平均・信用区間) の可視化
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- | Visualization of Gaussian-process regression results.
---
--- Plots training data (scatter), the posterior mean (curve), and a 95 %
--- credible band.
-module Hanalyze.Viz.GP
-  ( gpPlot
-  , gpPlotFile
-  ) where
-
-import Hanalyze.Model.GP     (GPResult (..))
-import Hanalyze.Viz.Core     (PlotConfig (..), OutputFormat, writeSpec)
-import Data.Text    (Text)
-import Graphics.Vega.VegaLite
-
--- | GP 予測プロットを構築する。
---
--- 描画要素:
---   - 散布点: 訓練データ (trainData)
---   - 青い曲線: 事後平均
---   - 青い帯: 平均 ± 2σ (≈95% 信用区間)
-gpPlot
-  :: PlotConfig
-  -> Text              -- ^ x 軸の列名ラベル
-  -> Text              -- ^ y 軸の列名ラベル
-  -> [(Double, Double)] -- ^ 訓練データ (x, y)
-  -> GPResult
-  -> VegaLite
-gpPlot cfg xCol yCol trainData res =
-  toVegaLite
-    [ title (plotTitle cfg) []
-    , layer [bandLayer, meanLayer, pointLayer]
-    , width  (plotWidth  cfg)
-    , height (plotHeight cfg)
-    ]
-  where
-    (trnX, trnY) = unzip trainData
-    testXs = gpTestX  res
-    means  = gpMean   res
-    lowers = gpLower  res
-    uppers = gpUpper  res
-
-    -- 訓練点
-    pointLayer = asSpec
-      [ dataFromColumns []
-          . dataColumn xCol (Numbers trnX)
-          . dataColumn yCol (Numbers trnY)
-          $ []
-      , mark Point [MTooltip TTEncoding, MColor "black", MOpacity 0.8, MSize 40]
-      , encoding
-          . position X [PName xCol, PmType Quantitative, PAxis [AxTitle xCol]]
-          . position Y [PName yCol, PmType Quantitative, PAxis [AxTitle yCol]]
-          $ []
-      ]
-
-    -- 事後平均曲線
-    meanLayer = asSpec
-      [ dataFromColumns []
-          . dataColumn xCol   (Numbers testXs)
-          . dataColumn "mean" (Numbers means)
-          $ []
-      , mark Line [MColor "steelblue", MStrokeWidth 2.5]
-      , encoding
-          . position X [PName xCol,   PmType Quantitative]
-          . position Y [PName "mean", PmType Quantitative, PAxis [AxTitle yCol]]
-          $ []
-      ]
-
-    -- 95% 信用区間バンド
-    bandLayer = asSpec
-      [ dataFromColumns []
-          . dataColumn xCol    (Numbers testXs)
-          . dataColumn "lower" (Numbers lowers)
-          . dataColumn "upper" (Numbers uppers)
-          $ []
-      , mark Area [MOpacity 0.2, MColor "steelblue"]
-      , encoding
-          . position X  [PName xCol,    PmType Quantitative]
-          . position Y  [PName "lower", PmType Quantitative]
-          . position Y2 [PName "upper"]
-          $ []
-      ]
-
--- | ファイルに書き出す。
-gpPlotFile
-  :: OutputFormat
-  -> FilePath
-  -> PlotConfig
-  -> Text
-  -> Text
-  -> [(Double, Double)]
-  -> GPResult
-  -> IO ()
-gpPlotFile fmt path cfg xCol yCol trainData res =
-  writeSpec fmt path (gpPlot cfg xCol yCol trainData res)
diff --git a/src/Hanalyze/Viz/GPReport.hs b/src/Hanalyze/Viz/GPReport.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/GPReport.hs
+++ /dev/null
@@ -1,748 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.GPReport
--- Description : GP 回帰用の統合 HTML レポート (データ特性・モデル比較・回帰結果・対話予測・付録)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- | Comprehensive HTML report for GP regression.
---
--- Bundles data characteristics, model comparison, regression results,
--- interactive prediction and an appendix into a single file. Sliders for
--- the predictor variables let JavaScript update predictions and credible
--- intervals in real time.
---
--- @
--- let fits = [ makeGPFit "RBF"       RBF      optRBF  trainX trainY testX
---            , makeGPFit "Matérn5/2" Matern52 optM52  trainX trainY testX
---            ]
--- writeGPReport "report.html" (defaultGPReportConfig "My GP") trainData fits
--- @
-module Hanalyze.Viz.GPReport
-  ( GPReportConfig (..)
-  , defaultGPReportConfig
-  , GPModelFit (..)
-  , makeGPFit
-  , writeGPReport
-  ) where
-
-import Data.Aeson (encode)
-import Data.ByteString.Lazy (toStrict)
-import Data.List (sortBy)
-import Data.Ord (comparing, Down (..))
-import Data.Text (Text)
-import qualified Data.Text    as T
-import qualified Data.Text.IO as TIO
-import Data.Text.Encoding (decodeUtf8)
-import Graphics.Vega.VegaLite (fromVL)
-import Numeric (showFFloat)
-
-import Hanalyze.Model.GP
-import Hanalyze.Viz.Assets (vegaJS, vegaLiteJS, vegaEmbedJS)
-import Hanalyze.Viz.Core  (PlotConfig (..))
-import Hanalyze.Viz.GP    (gpPlot)
-
--- ---------------------------------------------------------------------------
--- Public types
--- ---------------------------------------------------------------------------
-
-data GPReportConfig = GPReportConfig
-  { gpReportTitle :: Text   -- ^ レポートタイトル
-  , gpXLabel      :: Text   -- ^ X 軸ラベル
-  , gpYLabel      :: Text   -- ^ Y 軸ラベル
-  } deriving (Show)
-
-defaultGPReportConfig :: Text -> GPReportConfig
-defaultGPReportConfig t = GPReportConfig t "x" "y"
-
--- | 1つのカーネルに対するフィット結果。
-data GPModelFit = GPModelFit
-  { fLabel    :: Text        -- ^ 表示ラベル (例: "RBF")
-  , fKernel   :: Kernel
-  , fParams   :: GPParams
-  , fResult   :: GPResult
-  , fLML      :: Double      -- ^ 対数周辺尤度
-  , fPredData :: GPPredData  -- ^ JS 対話予測用データ
-  } deriving (Show)
-
--- | フィット結果を計算してまとめる。
-makeGPFit
-  :: Text          -- ^ ラベル
-  -> Kernel
-  -> GPParams      -- ^ 最適化済みハイパーパラメータ
-  -> [Double]      -- ^ 訓練 X
-  -> [Double]      -- ^ 訓練 Y
-  -> [Double]      -- ^ テスト X (予測グリッド)
-  -> GPModelFit
-makeGPFit lbl ker params trainX trainY testX =
-  let model    = GPModel ker params
-      res      = fitGP model trainX trainY testX
-      lml      = logMarginalLikelihood trainX trainY ker params
-      predData = gpPredData model trainX trainY
-  in GPModelFit lbl ker params res lml predData
-
--- ---------------------------------------------------------------------------
--- Entry point
--- ---------------------------------------------------------------------------
-
-writeGPReport
-  :: FilePath
-  -> GPReportConfig
-  -> [(Double, Double)]  -- ^ 訓練データ (x, y)
-  -> [GPModelFit]
-  -> IO ()
-writeGPReport path cfg trainData fits =
-  TIO.writeFile path (buildHtml cfg trainData sortedFits)
-  where
-    sortedFits = sortBy (comparing (Down . fLML)) fits
-
--- ---------------------------------------------------------------------------
--- HTML builder
--- ---------------------------------------------------------------------------
-
-buildHtml :: GPReportConfig -> [(Double, Double)] -> [GPModelFit] -> Text
-buildHtml cfg trainData fits = T.unlines $
-  [ "<!DOCTYPE html>"
-  , "<html lang=\"ja\">"
-  , "<head>"
-  , "  <meta charset=\"utf-8\">"
-  , "  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
-  , "  <title>" <> gpReportTitle cfg <> "</title>"
-  , "  <script>" <> vegaJS      <> "</script>"
-  , "  <script>" <> vegaLiteJS  <> "</script>"
-  , "  <script>" <> vegaEmbedJS <> "</script>"
-  , "  <style>"
-  , css
-  , "  </style>"
-  , "</head>"
-  , "<body>"
-  , navBar cfg fits
-  , "<main>"
-  , dataSummarySection cfg trainData
-  , modelComparisonSection fits
-  , regressionSection cfg trainData fits
-  , predictionSection cfg trainData fits
-  , appendixSection fits
-  , "</main>"
-  , "<script>"
-  , vegaEmbedScript fits
-  , tabScript
-  , predictionScript cfg trainData fits
-  , smoothScrollScript
-  , "</script>"
-  , "</body>"
-  , "</html>"
-  ]
-
--- ---------------------------------------------------------------------------
--- CSS
--- ---------------------------------------------------------------------------
-
-css :: Text
-css = T.unlines
-  [ "* { box-sizing: border-box; margin: 0; padding: 0; }"
-  , "body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; color: #333; line-height: 1.6; }"
-  , "nav { position: sticky; top: 0; z-index: 100; background: #1a3a5c;"
-  , "      padding: 10px 28px; display: flex; gap: 20px; align-items: center;"
-  , "      box-shadow: 0 2px 6px rgba(0,0,0,.25); }"
-  , "nav h1 { color: #ecf0f1; font-size: 1em; font-weight: 600; flex: 1; }"
-  , ".nav-link { color: #9ab; text-decoration: none; font-size: .82em; white-space: nowrap; }"
-  , ".nav-link:hover { color: #fff; }"
-  , "main { max-width: 1100px; margin: 0 auto; padding: 32px 20px; }"
-  , "section { background: white; border-radius: 12px; padding: 26px 28px;"
-  , "          margin-bottom: 28px; box-shadow: 0 2px 10px rgba(0,0,0,.07); }"
-  , "h2 { font-size: 1.05em; font-weight: 700; color: #1a3a5c; margin-bottom: 18px;"
-  , "     border-bottom: 2px solid #e4e9f0; padding-bottom: 8px;"
-  , "     display: flex; align-items: center; gap: 8px; }"
-  , "h3 { font-size: .95em; font-weight: 600; color: #2c5; margin: 18px 0 10px; }"
-  , ".sec-icon { font-size: 1.1em; }"
-  , ".stat-grid { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 20px; }"
-  , ".stat-box { background: #f7f9fc; border: 1px solid #e4e9f0; border-radius: 10px;"
-  , "            padding: 14px 20px; min-width: 120px; text-align: center; }"
-  , ".stat-box .lbl { font-size: .72em; color: #888; text-transform: uppercase;"
-  , "                 letter-spacing: .05em; margin-bottom: 4px; }"
-  , ".stat-box .val { font-size: 1.35em; font-weight: 700; color: #1a3a5c; }"
-  , ".stat-box.highlight { background: #e8f4e8; border-color: #4caf50; }"
-  , ".stat-box.highlight .val { color: #2e7d32; }"
-  , "table { width: 100%; border-collapse: collapse; font-size: .88em; }"
-  , "thead tr { background: #f0f4f8; }"
-  , "th { padding: 9px 14px; text-align: right; font-weight: 600; color: #444; }"
-  , "th:first-child { text-align: left; }"
-  , "td { padding: 8px 14px; border-bottom: 1px solid #f0f2f5; text-align: right; font-family: monospace; }"
-  , "td:first-child { text-align: left; font-family: inherit; font-weight: 500; }"
-  , "tr:last-child td { border-bottom: none; }"
-  , "tr.best-row td { background: #f0faf0; font-weight: 600; }"
-  , ".vl-wrap { overflow-x: auto; }"
-  , ".tab-bar { display: flex; gap: 6px; margin-bottom: 18px; flex-wrap: wrap; }"
-  , ".tab-btn { padding: 7px 18px; border: 1.5px solid #c0ccd8; border-radius: 20px;"
-  , "           background: white; color: #555; cursor: pointer; font-size: .88em;"
-  , "           transition: all .15s; }"
-  , ".tab-btn:hover { border-color: #1a3a5c; color: #1a3a5c; }"
-  , ".tab-btn.active { background: #1a3a5c; color: white; border-color: #1a3a5c; }"
-  , ".tab-content { display: none; }"
-  , ".tab-content.active { display: block; }"
-  , ".predict-controls { background: #f7f9fc; border-radius: 10px; padding: 20px 24px; margin-bottom: 20px; }"
-  , ".slider-row { display: flex; align-items: center; gap: 16px; margin-bottom: 14px; flex-wrap: wrap; }"
-  , ".slider-row label { font-size: .9em; color: #555; min-width: 80px; }"
-  , "input[type=range] { flex: 1; min-width: 200px; accent-color: #1a3a5c; }"
-  , "input[type=number] { width: 110px; padding: 6px 10px; border: 1.5px solid #c0ccd8;"
-  , "                     border-radius: 6px; font-size: .9em; }"
-  , "select { padding: 7px 12px; border: 1.5px solid #c0ccd8; border-radius: 6px;"
-  , "         font-size: .88em; background: white; }"
-  , ".predict-output { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 6px; }"
-  , ".pred-box { flex: 1; min-width: 160px; background: white; border: 1.5px solid #e4e9f0;"
-  , "            border-radius: 10px; padding: 14px 18px; text-align: center; }"
-  , ".pred-box .plbl { font-size: .75em; color: #888; text-transform: uppercase; letter-spacing: .05em; }"
-  , ".pred-box .pval { font-size: 1.4em; font-weight: 700; color: #1a3a5c; margin: 4px 0; }"
-  , ".pred-box .psub { font-size: .78em; color: #888; }"
-  , ".pred-box.mean-box { border-color: #1a3a5c; }"
-  , ".pred-box.mean-box .pval { color: #1a3a5c; }"
-  , ".appendix-block { background: #f7f9fc; border-left: 4px solid #1a3a5c;"
-  , "                  padding: 14px 18px; margin: 14px 0; border-radius: 0 8px 8px 0; }"
-  , ".appendix-block h4 { font-size: .9em; font-weight: 700; color: #1a3a5c; margin-bottom: 6px; }"
-  , ".appendix-block p, .appendix-block li { font-size: .88em; color: #444; margin-bottom: 4px; }"
-  , "code { background: #f0f2f5; padding: 2px 6px; border-radius: 4px; font-size: .9em; }"
-  , ".formula { background: #f7f9fc; border: 1px solid #e4e9f0; border-radius: 8px;"
-  , "           padding: 12px 16px; margin: 10px 0; font-family: monospace; font-size: .88em; color: #333; }"
-  , ".kernel-badge { display: inline-block; padding: 2px 10px; border-radius: 12px;"
-  , "                font-size: .78em; font-weight: 600; background: #e8f0fe; color: #1a3a5c; }"
-  , ".best-badge { background: #e8f4e8; color: #2e7d32; margin-left: 6px; }"
-  ]
-
--- ---------------------------------------------------------------------------
--- Nav bar
--- ---------------------------------------------------------------------------
-
-navBar :: GPReportConfig -> [GPModelFit] -> Text
-navBar cfg _ = T.unlines
-  [ "<nav>"
-  , "  <h1>&#128202; " <> gpReportTitle cfg <> "</h1>"
-  , "  <a class=\"nav-link\" href=\"#sec-data\">データ</a>"
-  , "  <a class=\"nav-link\" href=\"#sec-models\">モデル比較</a>"
-  , "  <a class=\"nav-link\" href=\"#sec-results\">回帰結果</a>"
-  , "  <a class=\"nav-link\" href=\"#sec-predict\">予測</a>"
-  , "  <a class=\"nav-link\" href=\"#sec-appendix\">付録</a>"
-  , "</nav>"
-  ]
-
--- ---------------------------------------------------------------------------
--- Section 1: Data Summary
--- ---------------------------------------------------------------------------
-
-dataSummarySection :: GPReportConfig -> [(Double, Double)] -> Text
-dataSummarySection cfg trainData = T.unlines $
-  [ "<section id=\"sec-data\">"
-  , "  <h2><span class=\"sec-icon\">&#128202;</span> 1. データの特性</h2>"
-  , "  <div class=\"stat-grid\">"
-  , statBox "N (観測数)" (T.pack (show n)) False
-  , statBox "X 最小値"  (fmt4 xMin) False
-  , statBox "X 最大値"  (fmt4 xMax) False
-  , statBox "X 平均"    (fmt4 xMean) False
-  , statBox "X 標準偏差" (fmt4 xStd) False
-  , statBox "Y 最小値"  (fmt4 yMin) False
-  , statBox "Y 最大値"  (fmt4 yMax) False
-  , statBox "Y 平均"    (fmt4 yMean) False
-  , statBox "Y 標準偏差" (fmt4 yStd) False
-  , "  </div>"
-  , "  <div class=\"vl-wrap\"><div id=\"vl-data\"></div></div>"
-  , "  <script>window.__vlData = " <> scatterSpecJson cfg trainData <> ";</script>"
-  , "</section>"
-  ]
-  where
-    (xs, ys) = unzip trainData
-    n     = length xs
-    xMin  = minimum xs;  xMax  = maximum xs
-    yMin  = minimum ys;  yMax  = maximum ys
-    xMean = sum xs / fromIntegral n
-    yMean = sum ys / fromIntegral n
-    xStd  = sqrt (sum (map (\x -> (x - xMean)^(2::Int)) xs) / fromIntegral n)
-    yStd  = sqrt (sum (map (\y -> (y - yMean)^(2::Int)) ys) / fromIntegral n)
-
--- 訓練データだけの散布図 Vega-Lite JSON
-scatterSpecJson :: GPReportConfig -> [(Double, Double)] -> Text
-scatterSpecJson cfg trainData =
-  let (xs, ys) = unzip trainData
-      xl = gpXLabel cfg
-      yl = gpYLabel cfg
-      spec = toVegaLitePure
-               [ ("$schema", "\"https://vega.github.io/schema/vega-lite/v5.json\"")
-               , ("title",   "\"Training Data\"")
-               , ("width",   "600")
-               , ("height",  "240")
-               , ("data",    mkDataJson xl yl xs ys)
-               , ("mark",    "{\"type\":\"point\",\"tooltip\":true,\"size\":50,\"color\":\"#1a3a5c\"}")
-               , ("encoding", mkEncJson xl yl)
-               ]
-  in spec
-
--- 簡易 Vega-Lite JSON ビルダー（hvega を使わない生JSONアプローチ）
-toVegaLitePure :: [(Text, Text)] -> Text
-toVegaLitePure pairs = "{" <> T.intercalate "," (map kv pairs) <> "}"
-  where kv (k, v) = "\"" <> k <> "\":" <> v
-
-mkDataJson :: Text -> Text -> [Double] -> [Double] -> Text
-mkDataJson xl yl xs ys =
-  let rows = zipWith mkRow xs ys
-      mkRow x y = "{\"" <> xl <> "\":" <> fmtJS x <> ",\"" <> yl <> "\":" <> fmtJS y <> "}"
-  in "{\"values\":[" <> T.intercalate "," rows <> "]}"
-
-mkEncJson :: Text -> Text -> Text
-mkEncJson xl yl = T.unlines
-  [ "{"
-  , "  \"x\": {\"field\": \"" <> xl <> "\", \"type\": \"quantitative\","
-  , "          \"axis\": {\"title\": \"" <> xl <> "\"}},"
-  , "  \"y\": {\"field\": \"" <> yl <> "\", \"type\": \"quantitative\","
-  , "          \"axis\": {\"title\": \"" <> yl <> "\"}}"
-  , "}"
-  ]
-
--- ---------------------------------------------------------------------------
--- Section 2: Model Comparison
--- ---------------------------------------------------------------------------
-
-modelComparisonSection :: [GPModelFit] -> Text
-modelComparisonSection fits = T.unlines
-  [ "<section id=\"sec-models\">"
-  , "  <h2><span class=\"sec-icon\">&#9878;</span> 2. モデル比較</h2>"
-  , "  <p style=\"font-size:.88em;color:#666;margin-bottom:14px\">"
-  , "    対数周辺尤度 (LML) が高いほどデータへの適合が良い。ハイパーパラメータは自動最適化済み。"
-  , "  </p>"
-  , "  <table>"
-  , "    <thead><tr>"
-  , "      <th>カーネル</th>"
-  , "      <th>ℓ (長さスケール)</th>"
-  , "      <th>σ_f (シグナル)</th>"
-  , "      <th>σ_n (ノイズ)</th>"
-  , "      <th>p (周期)</th>"
-  , "      <th>LML ↑</th>"
-  , "      <th>順位</th>"
-  , "    </tr></thead>"
-  , "    <tbody>"
-  , T.concat (zipWith (modelRow bestLML) [1..] fits)
-  , "    </tbody>"
-  , "  </table>"
-  , "  <p style=\"margin-top:12px;font-size:.82em;color:#888\">"
-  , "    LML = 対数周辺尤度 log p(y | X, θ)。モデル複雑度へのペナルティを含む。"
-  , "  </p>"
-  , "</section>"
-  ]
-  where
-    bestLML = maximum (map fLML fits)
-
-    modelRow best rank fit =
-      let isBest = fLML fit == best
-          rowCls = if isBest then " class=\"best-row\"" else ""
-          hasPeriod = fKernel fit == Periodic
-          pCell = if hasPeriod
-                  then td (fmt4 (gpPeriod (fParams fit)))
-                  else td "—"
-          badge = if isBest
-                  then " <span class=\"kernel-badge best-badge\">&#11088; Best</span>"
-                  else ""
-      in T.unlines
-           [ "      <tr" <> rowCls <> ">"
-           , "        <td>" <> fLabel fit <> badge <> "</td>"
-           , td (fmt4 (gpLengthScale (fParams fit)))
-           , td (fmt4 (sqrt (gpSignalVar (fParams fit))))
-           , td (fmt6 (sqrt (gpNoiseVar (fParams fit))))
-           , pCell
-           , td (fmt2 (fLML fit))
-           , td ("#" <> T.pack (show (rank :: Int)))
-           , "      </tr>"
-           ]
-
-td :: Text -> Text
-td v = "        <td>" <> v <> "</td>"
-
--- ---------------------------------------------------------------------------
--- Section 3: Regression Results
--- ---------------------------------------------------------------------------
-
-regressionSection :: GPReportConfig -> [(Double, Double)] -> [GPModelFit] -> Text
-regressionSection cfg trainData fits = T.unlines $
-  [ "<section id=\"sec-results\">"
-  , "  <h2><span class=\"sec-icon\">&#128200;</span> 3. 回帰結果</h2>"
-  , "  <p style=\"font-size:.88em;color:#666;margin-bottom:14px\">"
-  , "    青い帯 = 平均 ± 2σ (≈95% 信用区間)。黒点 = 訓練データ。"
-  , "  </p>"
-  , "  <div class=\"tab-bar\">"
-  ] ++
-  zipWith (tabBtn fits) [0..] fits ++
-  [ "  </div>" ] ++
-  concatMap (tabContent cfg trainData) (zip [0..] fits) ++
-  [ "</section>" ]
-
-tabBtn :: [GPModelFit] -> Int -> GPModelFit -> Text
-tabBtn fits i fit =
-  let bestLML = maximum (map fLML fits)
-      star    = if fLML fit == bestLML then " &#11088;" else ""
-      active  = if i == 0 then " active" else ""
-  in "  <button class=\"tab-btn" <> active <> "\" onclick=\"showTab(" <> T.pack (show i) <> ")\">"
-     <> fLabel fit <> star <> "</button>"
-
-tabContent :: GPReportConfig -> [(Double, Double)] -> (Int, GPModelFit) -> [Text]
-tabContent cfg trainData (i, fit) =
-  let active = if i == 0 then " active" else ""
-      xl  = gpXLabel cfg
-      yl  = gpYLabel cfg
-      pCfg = PlotConfig
-               { plotTitle  = fLabel fit <> " — GP Regression"
-               , plotWidth  = 700
-               , plotHeight = 320
-               }
-      spec = gpPlot pCfg xl yl trainData (fResult fit)
-      json = decodeUtf8 . toStrict . encode . fromVL $ spec
-      divId = "vl-fit-" <> T.pack (show i)
-  in [ "  <div id=\"tab-" <> T.pack (show i) <> "\" class=\"tab-content" <> active <> "\">"
-     , "    <div class=\"vl-wrap\"><div id=\"" <> divId <> "\"></div></div>"
-     , "    <script>window.__vlFit" <> T.pack (show i) <> " = " <> json <> ";</script>"
-     , "    " <> fitParamSummary fit
-     , "  </div>"
-     ]
-
-fitParamSummary :: GPModelFit -> Text
-fitParamSummary fit = T.unlines
-  [ "    <div style=\"margin-top:16px;background:#f7f9fc;border-radius:8px;padding:12px 16px;"
-  , "         display:flex;gap:20px;flex-wrap:wrap;font-size:.85em;\">"
-  , "      <span><b>カーネル:</b> " <> fLabel fit <> "</span>"
-  , "      <span><b>ℓ =</b> " <> fmt4 (gpLengthScale (fParams fit)) <> "</span>"
-  , "      <span><b>σ_f =</b> " <> fmt4 (sqrt (gpSignalVar (fParams fit))) <> "</span>"
-  , "      <span><b>σ_n =</b> " <> fmt6 (sqrt (gpNoiseVar (fParams fit))) <> "</span>"
-  , if fKernel fit == Periodic
-      then "      <span><b>p =</b> " <> fmt4 (gpPeriod (fParams fit)) <> "</span>"
-      else ""
-  , "      <span style=\"margin-left:auto;color:#888\"><b>LML =</b> " <> fmt2 (fLML fit) <> "</span>"
-  , "    </div>"
-  ]
-
--- ---------------------------------------------------------------------------
--- Section 4: Interactive Prediction
--- ---------------------------------------------------------------------------
-
-predictionSection :: GPReportConfig -> [(Double, Double)] -> [GPModelFit] -> Text
-predictionSection cfg trainData fits =
-  let (xs, _) = unzip trainData
-      xMin = minimum xs
-      xMax = maximum xs
-      xMid = (xMin + xMax) / 2
-  in T.unlines
-  [ "<section id=\"sec-predict\">"
-  , "  <h2><span class=\"sec-icon\">&#127919;</span> 4. 対話的予測</h2>"
-  , "  <p style=\"font-size:.88em;color:#666;margin-bottom:18px\">"
-  , "    スライダーまたは入力欄で説明変数 x の値を変えると、選択モデルの予測値をリアルタイムで計算します。"
-  , "  </p>"
-  , "  <div class=\"predict-controls\">"
-  , "    <div class=\"slider-row\">"
-  , "      <label>モデル:</label>"
-  , "      <select id=\"pred-kernel\" onchange=\"updatePrediction()\">"
-  , T.concat (zipWith modelOption [0..] fits)
-  , "      </select>"
-  , "    </div>"
-  , "    <div class=\"slider-row\">"
-  , "      <label>" <> gpXLabel cfg <> " 値:</label>"
-  , "      <input type=\"range\" id=\"x-slider\""
-  , "             min=\"" <> fmtJS xMin <> "\" max=\"" <> fmtJS xMax <> "\""
-  , "             step=\"" <> fmtJS ((xMax - xMin) / 500) <> "\""
-  , "             value=\"" <> fmtJS xMid <> "\""
-  , "             oninput=\"syncXFromSlider()\">"
-  , "      <input type=\"number\" id=\"x-num\""
-  , "             min=\"" <> fmtJS xMin <> "\" max=\"" <> fmtJS xMax <> "\""
-  , "             step=\"" <> fmtJS ((xMax - xMin) / 500) <> "\""
-  , "             value=\"" <> fmtJS xMid <> "\""
-  , "             onchange=\"syncXFromInput()\">"
-  , "    </div>"
-  , "    <div class=\"slider-row\">"
-  , "      <label>現在の " <> gpXLabel cfg <> ":</label>"
-  , "      <span id=\"x-current\" style=\"font-size:1.1em;font-weight:700;color:#1a3a5c\">"
-  , "        " <> fmtJS xMid
-  , "      </span>"
-  , "    </div>"
-  , "  </div>"
-  , "  <div class=\"predict-output\">"
-  , "    <div class=\"pred-box mean-box\">"
-  , "      <div class=\"plbl\">予測値 (事後平均)</div>"
-  , "      <div class=\"pval\" id=\"pred-mean\">—</div>"
-  , "      <div class=\"psub\">" <> gpYLabel cfg <> "</div>"
-  , "    </div>"
-  , "    <div class=\"pred-box\">"
-  , "      <div class=\"plbl\">標準偏差 (σ)</div>"
-  , "      <div class=\"pval\" id=\"pred-std\">—</div>"
-  , "      <div class=\"psub\">事後不確実性</div>"
-  , "    </div>"
-  , "    <div class=\"pred-box\">"
-  , "      <div class=\"plbl\">95% 信用区間 下限</div>"
-  , "      <div class=\"pval\" id=\"pred-lo\">—</div>"
-  , "      <div class=\"psub\">平均 − 2σ</div>"
-  , "    </div>"
-  , "    <div class=\"pred-box\">"
-  , "      <div class=\"plbl\">95% 信用区間 上限</div>"
-  , "      <div class=\"pval\" id=\"pred-hi\">—</div>"
-  , "      <div class=\"psub\">平均 + 2σ</div>"
-  , "    </div>"
-  , "  </div>"
-  , "</section>"
-  ]
-
-modelOption :: Int -> GPModelFit -> Text
-modelOption i fit =
-  "      <option value=\"" <> T.pack (show i) <> "\">"
-  <> fLabel fit <> " (LML=" <> fmt2 (fLML fit) <> ")"
-  <> "</option>\n"
-
--- ---------------------------------------------------------------------------
--- Section 5: Appendix
--- ---------------------------------------------------------------------------
-
-appendixSection :: [GPModelFit] -> Text
-appendixSection fits = T.unlines
-  [ "<section id=\"sec-appendix\">"
-  , "  <h2><span class=\"sec-icon\">&#128218;</span> 付録: GP 回帰の原理</h2>"
-  , appendixGP
-  , appendixKernels fits
-  , appendixHyperparams
-  , appendixLML
-  , "</section>"
-  ]
-
-appendixGP :: Text
-appendixGP = T.unlines
-  [ "  <div class=\"appendix-block\">"
-  , "    <h4>ガウス過程 (Gaussian Process) とは</h4>"
-  , "    <p>ガウス過程は関数に対する確率分布です。有限個の点での関数値が常に多変量正規分布に従うとき、"
-  , "    その関数の分布をガウス過程と呼びます。</p>"
-  , "    <p>平均関数 m(x) と共分散関数 (カーネル) k(x, x') によって定義されます:</p>"
-  , "    <div class=\"formula\">f(x) ~ GP( m(x), k(x, x') )</div>"
-  , "    <p>訓練データ (X, y) を条件付けることで事後分布が計算できます:</p>"
-  , "    <div class=\"formula\">"
-  , "    事後平均:   μ(x*) = K(x*, X) · [K(X,X) + σ²_n I]⁻¹ · y<br>"
-  , "    事後分散:   σ²(x*) = k(x*, x*) − K(x*, X) · [K(X,X) + σ²_n I]⁻¹ · K(X, x*)"
-  , "    </div>"
-  , "    <p>この実装では hmatrix (LAPACK/BLAS) でコレスキー分解を行い数値的安定性を確保しています。</p>"
-  , "  </div>"
-  ]
-
-appendixKernels :: [GPModelFit] -> Text
-appendixKernels fits = T.unlines $
-  [ "  <div class=\"appendix-block\">"
-  , "    <h4>使用したカーネル関数</h4>"
-  ] ++
-  concatMap kernelDesc usedKernels ++
-  [ "  </div>" ]
-  where
-    usedKernels = map fKernel fits
-
-    kernelDesc RBF =
-      [ "    <p><b>RBF (Squared Exponential / 二乗指数カーネル)</b></p>"
-      , "    <div class=\"formula\">k(x, x') = σ²_f · exp( −(x−x')² / (2ℓ²) )</div>"
-      , "    <p>無限回微分可能な滑らかな関数をモデル化します。最も広く使われるカーネル。</p>"
-      ]
-    kernelDesc Matern52 =
-      [ "    <p><b>Matérn 5/2 カーネル</b></p>"
-      , "    <div class=\"formula\">k(x, x') = σ²_f · (1 + √5·r/ℓ + 5r²/(3ℓ²)) · exp(−√5·r/ℓ) &nbsp; (r = |x−x'|)</div>"
-      , "    <p>RBF より少し荒れた関数に対応。物理・気象・機械学習でよく使われます。</p>"
-      ]
-    kernelDesc Periodic =
-      [ "    <p><b>Periodic カーネル</b></p>"
-      , "    <div class=\"formula\">k(x, x') = σ²_f · exp( −2 sin²(π|x−x'|/p) / ℓ² )</div>"
-      , "    <p>周期 p の周期的パターンを持つ関数をモデル化します。</p>"
-      ]
-    kernelDesc Linear =
-      [ "    <p><b>Linear (内積) カーネル</b></p>"
-      , "    <div class=\"formula\">k(x, x') = σ²_f · (x·x')</div>"
-      , "    <p>線形な関数をモデル化する非定常カーネル。</p>"
-      ]
-    kernelDesc (Poly d) =
-      [ "    <p><b>Polynomial カーネル (次数 " <> T.pack (show d) <> ")</b></p>"
-      , "    <div class=\"formula\">k(x, x') = (γ·(x·x') + 1)^" <> T.pack (show d) <> " &nbsp; (γ = 1/(2ℓ²))</div>"
-      , "    <p>多項式の決定境界をモデル化する非定常カーネル。</p>"
-      ]
-
-appendixHyperparams :: Text
-appendixHyperparams = T.unlines
-  [ "  <div class=\"appendix-block\">"
-  , "    <h4>ハイパーパラメータの意味</h4>"
-  , "    <table>"
-  , "      <thead><tr><th>パラメータ</th><th style=\"text-align:left\">意味</th><th>影響</th></tr></thead>"
-  , "      <tbody>"
-  , "        <tr><td>ℓ (長さスケール)</td><td style=\"text-align:left\">関数の「滑らかさの範囲」</td><td style=\"text-align:left\">大きい → 広範囲で相関、小さい → 局所的</td></tr>"
-  , "        <tr><td>σ_f (シグナル標準偏差)</td><td style=\"text-align:left\">関数値の変動幅</td><td style=\"text-align:left\">大きい → 振れ幅が大きい関数</td></tr>"
-  , "        <tr><td>σ_n (ノイズ標準偏差)</td><td style=\"text-align:left\">観測ノイズの大きさ</td><td style=\"text-align:left\">小さい → 補間、大きい → 平滑化</td></tr>"
-  , "        <tr><td>p (周期、Periodicのみ)</td><td style=\"text-align:left\">パターンの繰り返し周期</td><td style=\"text-align:left\">データの周期に合わせて設定</td></tr>"
-  , "      </tbody>"
-  , "    </table>"
-  , "  </div>"
-  ]
-
-appendixLML :: Text
-appendixLML = T.unlines
-  [ "  <div class=\"appendix-block\">"
-  , "    <h4>対数周辺尤度 (Log Marginal Likelihood, LML) によるモデル選択</h4>"
-  , "    <div class=\"formula\">"
-  , "    log p(y | X, θ) = −½ yᵀ K⁻¹_y y − ½ log|K_y| − n/2 · log(2π)"
-  , "    </div>"
-  , "    <p>LML はデータへの当てはまり (第1項) とモデル複雑度ペナルティ (第2項) のバランスを自動的に取ります。</p>"
-  , "    <p>この実装では log-space で数値勾配上昇法 (400ステップ) によりハイパーパラメータを最適化しています。</p>"
-  , "  </div>"
-  ]
-
--- ---------------------------------------------------------------------------
--- JavaScript
--- ---------------------------------------------------------------------------
-
--- Vega-Lite の embed 呼び出し
-vegaEmbedScript :: [GPModelFit] -> Text
-vegaEmbedScript fits = T.unlines $
-  [ "vegaEmbed('#vl-data', window.__vlData, {renderer:'canvas',actions:false}).catch(console.error);" ] ++
-  [ "vegaEmbed('#vl-fit-" <> T.pack (show i) <> "', window.__vlFit" <> T.pack (show i)
-    <> ", {renderer:'canvas',actions:false}).catch(console.error);"
-  | i <- [0 .. length fits - 1]
-  ]
-
--- タブ切り替え
-tabScript :: Text
-tabScript = T.unlines
-  [ "function showTab(idx) {"
-  , "  document.querySelectorAll('.tab-content').forEach((el,i) => {"
-  , "    el.classList.toggle('active', i === idx);"
-  , "  });"
-  , "  document.querySelectorAll('.tab-btn').forEach((el,i) => {"
-  , "    el.classList.toggle('active', i === idx);"
-  , "  });"
-  , "}"
-  ]
-
--- 対話予測 JS
-predictionScript :: GPReportConfig -> [(Double, Double)] -> [GPModelFit] -> Text
-predictionScript _cfg _trainData fits = T.unlines $
-  [ "// ---- GP prediction data ----"
-  , "const gpModels = " <> jsModelsArray fits <> ";"
-  , ""
-  , "// カーネル評価関数"
-  , "function kernelEval(ker, p, x1, x2) {"
-  , "  if (ker === 'rbf') {"
-  , "    const d = x1 - x2, l = p.ell;"
-  , "    return p.sf2 * Math.exp(-(d*d) / (2*l*l));"
-  , "  } else if (ker === 'matern52') {"
-  , "    const d = Math.abs(x1 - x2), l = p.ell;"
-  , "    const s = Math.sqrt(5) * d / l;"
-  , "    return p.sf2 * (1 + s + s*s/3) * Math.exp(-s);"
-  , "  } else { // periodic"
-  , "    const d = Math.abs(x1 - x2);"
-  , "    const s = Math.sin(Math.PI * d / p.period);"
-  , "    return p.sf2 * Math.exp(-2 * s*s / (p.ell * p.ell));"
-  , "  }"
-  , "}"
-  , ""
-  , "// GP 事後予測"
-  , "function gpPredict(modelIdx, xStar) {"
-  , "  const m = gpModels[modelIdx];"
-  , "  const kStar = m.trainX.map(xi => kernelEval(m.kernel, m.params, xi, xStar));"
-  , "  const mean  = kStar.reduce((s, k, i) => s + k * m.alpha[i], 0);"
-  , "  const v     = m.kyInv.map(row => row.reduce((s, v, j) => s + v * kStar[j], 0));"
-  , "  const kss   = kernelEval(m.kernel, m.params, xStar, xStar);"
-  , "  const variance = Math.max(0, kss - kStar.reduce((s, k, i) => s + k * v[i], 0));"
-  , "  return { mean, std: Math.sqrt(variance) };"
-  , "}"
-  , ""
-  , "function updatePrediction() {"
-  , "  const xStar = parseFloat(document.getElementById('x-slider').value);"
-  , "  const midx  = parseInt(document.getElementById('pred-kernel').value);"
-  , "  const { mean, std } = gpPredict(midx, xStar);"
-  , "  document.getElementById('x-current').textContent = xStar.toFixed(5);"
-  , "  document.getElementById('pred-mean').textContent = mean.toFixed(5);"
-  , "  document.getElementById('pred-std').textContent  = std.toFixed(5);"
-  , "  document.getElementById('pred-lo').textContent   = (mean - 2*std).toFixed(5);"
-  , "  document.getElementById('pred-hi').textContent   = (mean + 2*std).toFixed(5);"
-  , "}"
-  , ""
-  , "function syncXFromSlider() {"
-  , "  const v = document.getElementById('x-slider').value;"
-  , "  document.getElementById('x-num').value = parseFloat(v).toFixed(6);"
-  , "  updatePrediction();"
-  , "}"
-  , ""
-  , "function syncXFromInput() {"
-  , "  const v = parseFloat(document.getElementById('x-num').value);"
-  , "  document.getElementById('x-slider').value = v;"
-  , "  updatePrediction();"
-  , "}"
-  , ""
-  , "updatePrediction();"
-  ]
-
-smoothScrollScript :: Text
-smoothScrollScript = T.unlines
-  [ "document.querySelectorAll('.nav-link').forEach(a => {"
-  , "  a.addEventListener('click', e => {"
-  , "    e.preventDefault();"
-  , "    const target = document.querySelector(a.getAttribute('href'));"
-  , "    if (target) target.scrollIntoView({ behavior: 'smooth' });"
-  , "  });"
-  , "});"
-  ]
-
--- ---------------------------------------------------------------------------
--- JS data serialisation
--- ---------------------------------------------------------------------------
-
-jsModelsArray :: [GPModelFit] -> Text
-jsModelsArray fits = "[" <> T.intercalate "," (map jsModel fits) <> "]"
-
-jsModel :: GPModelFit -> Text
-jsModel fit = T.unlines
-  [ "{"
-  , "  kernel: '" <> jsKernelId (fKernel fit) <> "',"
-  , "  params: " <> jsParams (fKernel fit) (fParams fit) <> ","
-  , "  trainX: " <> jsDoubleArray (pdTrainX (fPredData fit)) <> ","
-  , "  alpha:  " <> jsDoubleArray (pdAlpha  (fPredData fit)) <> ","
-  , "  kyInv:  " <> jsMatrix      (pdKyInv  (fPredData fit))
-  , "}"
-  ]
-
-jsKernelId :: Kernel -> Text
-jsKernelId RBF      = "rbf"
-jsKernelId Matern52 = "matern52"
-jsKernelId Periodic = "periodic"
-jsKernelId Linear   = "linear"
-jsKernelId (Poly _) = "poly"
-
-jsParams :: Kernel -> GPParams -> Text
-jsParams ker p = "{ell:" <> fmtJS (gpLengthScale p)
-              <> ",sf2:"  <> fmtJS (gpSignalVar   p)
-              <> ",sn2:"  <> fmtJS (gpNoiseVar    p)
-              <> if ker == Periodic then ",period:" <> fmtJS (gpPeriod p) else ""
-              <> "}"
-
-jsDoubleArray :: [Double] -> Text
-jsDoubleArray xs = "[" <> T.intercalate "," (map fmtJS xs) <> "]"
-
-jsMatrix :: [[Double]] -> Text
-jsMatrix rows = "[" <> T.intercalate "," (map jsDoubleArray rows) <> "]"
-
--- ---------------------------------------------------------------------------
--- Formatting helpers
--- ---------------------------------------------------------------------------
-
--- | Double を JavaScript 数値リテラルに変換 (10桁精度)。
-fmtJS :: Double -> Text
-fmtJS v
-  | isNaN v      = "0"
-  | isInfinite v = if v > 0 then "1e308" else "-1e308"
-  | otherwise    = T.pack (showFFloat (Just 10) v "")
-
-fmt2 :: Double -> Text
-fmt2 v = T.pack (showFFloat (Just 2) v "")
-
-fmt4 :: Double -> Text
-fmt4 v = T.pack (showFFloat (Just 4) v "")
-
-fmt6 :: Double -> Text
-fmt6 v = T.pack (showFFloat (Just 6) v "")
-
-statBox :: Text -> Text -> Bool -> Text
-statBox lbl val highlight = T.unlines
-  [ "    <div class=\"stat-box" <> (if highlight then " highlight" else "") <> "\">"
-  , "      <div class=\"lbl\">" <> lbl <> "</div>"
-  , "      <div class=\"val\">" <> val <> "</div>"
-  , "    </div>"
-  ]
diff --git a/src/Hanalyze/Viz/Histogram.hs b/src/Hanalyze/Viz/Histogram.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/Histogram.hs
+++ /dev/null
@@ -1,160 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.Histogram
--- Description : ヒストグラム描画 (基本ヒストグラム + 理論分布密度の重ね描き)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- | Histogram plotting.
---
--- 'histogramPlot' renders a basic histogram; 'histogramWithDensity'
--- overlays a fitted theoretical PDF (or PMF for discrete distributions).
--- 'histogramPlotFile' writes to HTML / PNG / SVG.
-module Hanalyze.Viz.Histogram
-  ( histogramPlot
-  , histogramPlotFile
-  , histogramWithDensity
-  , histogramWithDensityFile
-    -- * 130: PlotData ベースの汎用 spec API
-  , histSpec
-  ) where
-
-import Hanalyze.Stat.Distribution (Distribution, isContinuous, supportRange, distributionName)
-import qualified Hanalyze.Stat.Distribution as Dist
-import Hanalyze.Viz.Core        (PlotConfig (..), OutputFormat, writeSpec)
-import Hanalyze.Viz.PlotData    (PlotData, numericColumn)
-
-import Data.Text (Text)
-import qualified Data.Vector as V
-import Graphics.Vega.VegaLite
-
--- ---------------------------------------------------------------------------
--- Pure histogram
--- ---------------------------------------------------------------------------
-
-histogramPlot :: PlotConfig -> Text -> [Double] -> Maybe Int -> VegaLite
-histogramPlot cfg xCol vals mBins =
-  toVegaLite
-    [ title (plotTitle cfg) []
-    , dataFromColumns [] . dataColumn xCol (Numbers vals) $ []
-    , mark Bar []
-    , encoding
-        . position X [ PName xCol, PmType Quantitative
-                     , PBin [Step (binStepVal mBins vals)]
-                     , PAxis [AxTitle xCol] ]
-        . position Y [ PAggregate Count, PmType Quantitative
-                     , PAxis [AxTitle "Count"] ]
-        $ []
-    , width  (plotWidth  cfg)
-    , height (plotHeight cfg)
-    ]
-
-histogramPlotFile :: OutputFormat -> FilePath -> PlotConfig -> Text -> [Double] -> Maybe Int -> IO ()
-histogramPlotFile fmt path cfg xCol vals mBins =
-  writeSpec fmt path (histogramPlot cfg xCol vals mBins)
-
--- ---------------------------------------------------------------------------
--- Histogram + PDF/PMF overlay
--- ---------------------------------------------------------------------------
-
--- | Histogram with theoretical PDF/PMF overlaid.
--- Y-axis is Count; the PDF curve is scaled by (n × binStep) so both align.
-histogramWithDensity
-  :: PlotConfig
-  -> Text           -- x axis label
-  -> [Double]       -- observed data
-  -> Maybe Int      -- bin count (Nothing → Sturges' rule)
-  -> Distribution
-  -> VegaLite
-histogramWithDensity cfg xCol vals mBins dist =
-  toVegaLite
-    [ title (plotTitle cfg)
-        [ TSubtitle (distributionName dist)
-        , TSubtitleFontSize 11, TSubtitleColor "#555" ]
-    , layer [histLayer, curveLayer]
-    , width  (plotWidth  cfg)
-    , height (plotHeight cfg)
-    ]
-  where
-    n     = length vals
-    step  = binStepVal mBins vals
-    scale = fromIntegral n * step   -- PDF → Count scaling factor
-
-    (xLo, xHi) = supportRange dist
-    nGrid = 300 :: Int
-
-    histLayer = asSpec
-      [ dataFromColumns [] . dataColumn xCol (Numbers vals) $ []
-      , mark Bar [MOpacity 0.55, MColor "#4C72B0"]
-      , encoding
-          . position X [ PName xCol, PmType Quantitative
-                       , PBin [Step step]
-                       , PAxis [AxTitle xCol] ]
-          . position Y [ PAggregate Count, PmType Quantitative
-                       , PAxis [AxTitle "Count"] ]
-          $ []
-      ]
-
-    curveLayer = asSpec
-      [ dataFromColumns []
-          . dataColumn "x"     (Numbers gridX)
-          . dataColumn "count" (Numbers gridY)
-          $ []
-      , mark (if isContinuous dist then Line else Point)
-          [MColor "#DD4444", MStrokeWidth 2.0, MPoint (PMMarker [])]
-      , encoding
-          . position X [PName "x",     PmType Quantitative]
-          . position Y [PName "count", PmType Quantitative]
-          $ []
-      ]
-
-    (gridX, gridY) = unzip (scaledGrid dist xLo xHi nGrid scale)
-
--- | (x, pdf(x) * scale) for the overlay curve.
-scaledGrid :: Distribution -> Double -> Double -> Int -> Double -> [(Double, Double)]
-scaledGrid dist xLo xHi nPts scale
-  | isContinuous dist =
-      [ let x = xLo + fromIntegral i * (xHi - xLo) / fromIntegral (nPts - 1)
-        in (x, Dist.density dist x * scale)
-      | i <- [0 .. nPts - 1] ]
-  | otherwise =
-      [ (fromIntegral k, Dist.density dist (fromIntegral k) * scale)
-      | k <- [round xLo .. round xHi :: Int] ]
-
-histogramWithDensityFile
-  :: OutputFormat -> FilePath -> PlotConfig -> Text -> [Double] -> Maybe Int -> Distribution -> IO ()
-histogramWithDensityFile fmt path cfg xCol vals mBins dist =
-  writeSpec fmt path (histogramWithDensity cfg xCol vals mBins dist)
-
--- ---------------------------------------------------------------------------
--- Bin helpers
--- ---------------------------------------------------------------------------
-
-sturgesBins :: [Double] -> Int
-sturgesBins xs = max 5 (ceiling (logBase 2 (fromIntegral (length xs) :: Double)) + 1)
-
-binStepVal :: Maybe Int -> [Double] -> Double
-binStepVal _ [] = 1
-binStepVal mBins xs =
-  let lo   = minimum xs
-      hi   = maximum xs
-      bins = maybe (sturgesBins xs) id mBins
-  in (hi - lo) / fromIntegral bins
-
--- ---------------------------------------------------------------------------
--- 130: PlotData ベースの汎用 spec API
--- ---------------------------------------------------------------------------
-
--- | Build a Vega-Lite histogram spec from a 'PlotData' source.
---
--- @maxBins@ overrides Sturges' rule when provided. Returns an empty
--- (zero-row) spec if the column is missing from @pdNumeric@.
-histSpec
-  :: PlotConfig
-  -> Text          -- ^ numeric column name
-  -> Maybe Int     -- ^ max bin count (Nothing = Sturges)
-  -> PlotData
-  -> VegaLite
-histSpec cfg col mBins pd =
-  let vals = maybe [] V.toList (numericColumn col pd)
-  in histogramPlot cfg col vals mBins
diff --git a/src/Hanalyze/Viz/MCMC.hs b/src/Hanalyze/Viz/MCMC.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/MCMC.hs
+++ /dev/null
@@ -1,846 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.MCMC
--- Description : Vega-Lite ベースの MCMC 診断プロット (トレース・事後密度・自己相関・forest/energy 等)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- | MCMC diagnostic plots (built on Vega-Lite).
---
--- Provides single-chain and multi-chain variants. Posterior densities
--- are drawn with kernel density estimation (KDE).
-module Hanalyze.Viz.MCMC
-  ( -- * 単一チェーン
-    tracePlot,       tracePlotFile
-  , tracePlotHDI,    tracePlotHDIFile
-  , posteriorPlot,   posteriorPlotFile
-  , autocorrPlot,    autocorrPlotFile
-  , pairScatter,     pairScatterFile
-  , mcmcDiagnostics, mcmcDiagnosticsFile
-    -- * Multi-chain panels (PyMC style)
-  , multiTracePlot,        multiTracePlotFile
-  , mcmcDiagnosticsMulti,  mcmcDiagnosticsMultiFile
-    -- * Forest plot (cross-parameter posterior comparison)
-  , forestPlot, forestPlotFile
-    -- * Energy plot (NUTS BFMI diagnostic)
-  , energyPlot, energyPlotFile
-    -- * Rank plot (multi-chain convergence diagnostic)
-  , rankPlot, rankPlotFile
-    -- * Posterior predictive check (PyMC @pp_check@ analogue)
-  , ppcPlot, ppcPlotFile
-    -- * Divergence overlay (visualize NUTS divergent transitions)
-  , pairScatterDiv, pairScatterDivFile
-    -- * Posterior summary table (@az.summary@ analogue)
-  , SummaryRow (..)
-  , posteriorSummary
-  , posteriorSummaryHtml
-  , posteriorSummaryFile
-  , printPosteriorSummary
-  ) where
-
-import Data.Text (Text)
-import qualified Data.Text as T
-import Graphics.Vega.VegaLite
-
-import Hanalyze.MCMC.Core  (Chain (..), chainVals)
-import Hanalyze.Stat.MCMC    (autocorr, hdi, kde, bfmi, rankHist)
-import Hanalyze.Stat.Summary (SummaryRow (..), posteriorSummary)
-import Data.List   (sortBy)
-import Data.Maybe (fromMaybe)
-import Text.Printf (printf)
-import qualified Data.Text.IO as TIO
-import Hanalyze.Viz.Core   (PlotConfig (..), OutputFormat, writeSpec)
-
--- ---------------------------------------------------------------------------
--- Trace plot (単一チェーン)
--- ---------------------------------------------------------------------------
-
--- | Trace plot for one or more parameters of a single chain. Each
--- parameter gets its own vertical panel.
-tracePlot :: PlotConfig -> [Text] -> Chain -> VegaLite
-tracePlot cfg names chain = toVegaLite
-  [ title (plotTitle cfg) []
-  , vConcat (map tracePanel names)
-  ]
-  where
-    n = length (chainSamples chain)
-    tracePanel pname =
-      let vals = chainVals pname chain
-      in asSpec
-          [ dataFromColumns []
-              . dataColumn "iter"  (Numbers (map fromIntegral [1 .. n]))
-              . dataColumn "value" (Numbers vals)
-              $ []
-          , mark Line [MColor "#4C72B0", MStrokeWidth 1.0, MOpacity 0.7]
-          , encoding
-              . position X [ PName "iter",  PmType Quantitative
-                           , PAxis [AxTitle "Iteration"] ]
-              . position Y [ PName "value", PmType Quantitative
-                           , PAxis [AxTitle pname] ]
-              $ []
-          , width  (plotWidth cfg)
-          , height 90
-          ]
-
-tracePlotFile :: OutputFormat -> FilePath -> PlotConfig -> [Text] -> Chain -> IO ()
-tracePlotFile fmt path cfg names chain =
-  writeSpec fmt path (tracePlot cfg names chain)
-
--- | Trace plot with the HDI band overlaid (e.g. @level = 0.94@).
--- 上下の HDI 境界を赤い水平ルールで描画し、内側を半透明赤で塗りつぶす。
--- バーンイン後サンプルから HDI を計算し、視覚的に「事後分布の質量がどこに
--- 集中しているか」をトレースと一緒に確認できる。
-tracePlotHDI :: PlotConfig -> Double -> [Text] -> Chain -> VegaLite
-tracePlotHDI cfg level names chain = toVegaLite
-  [ title (plotTitle cfg) []
-  , vConcat (map tracePanel names)
-  ]
-  where
-    n = length (chainSamples chain)
-    tracePanel pname =
-      let vals     = chainVals pname chain
-          (lo, hi) = hdi level vals
-      in asSpec
-          [ layer
-              [ -- HDI 帯 (rect)
-                asSpec
-                  [ dataFromColumns []
-                      . dataColumn "lo" (Numbers [lo])
-                      . dataColumn "hi" (Numbers [hi])
-                      $ []
-                  , mark Rect [MColor "#DD4444", MOpacity 0.12]
-                  , encoding
-                      . position Y  [PName "lo", PmType Quantitative]
-                      . position Y2 [PName "hi"]
-                      $ []
-                  ]
-              , -- HDI 上限 / 下限ライン
-                asSpec
-                  [ dataFromColumns []
-                      . dataColumn "y" (Numbers [lo, hi])
-                      $ []
-                  , mark Rule [MColor "#DD4444", MStrokeWidth 1.5,
-                               MStrokeDash [3, 3]]
-                  , encoding
-                      . position Y [PName "y", PmType Quantitative]
-                      $ []
-                  ]
-              , -- トレース本体
-                asSpec
-                  [ dataFromColumns []
-                      . dataColumn "iter"  (Numbers (map fromIntegral [1 .. n]))
-                      . dataColumn "value" (Numbers vals)
-                      $ []
-                  , mark Line [MColor "#4C72B0", MStrokeWidth 1.0, MOpacity 0.7]
-                  , encoding
-                      . position X [ PName "iter",  PmType Quantitative
-                                   , PAxis [AxTitle "Iteration"] ]
-                      . position Y [ PName "value", PmType Quantitative
-                                   , PAxis [AxTitle pname] ]
-                      $ []
-                  ]
-              ]
-          , width  (plotWidth cfg)
-          , height 90
-          ]
-
-tracePlotHDIFile :: OutputFormat -> FilePath -> PlotConfig
-                 -> Double -> [Text] -> Chain -> IO ()
-tracePlotHDIFile fmt path cfg level names chain =
-  writeSpec fmt path (tracePlotHDI cfg level names chain)
-
--- ---------------------------------------------------------------------------
--- Multi-chain trace plot
--- ---------------------------------------------------------------------------
-
--- | Multi-chain trace plot. Each chain is overlaid with its own color.
-multiTracePlot :: PlotConfig -> [Text] -> [Chain] -> VegaLite
-multiTracePlot cfg names chains = toVegaLite
-  [ title (plotTitle cfg) []
-  , vConcat (map (mkMultiTracePanel' (plotWidth cfg) 90) names)
-  ]
-  where
-    mkMultiTracePanel' w h pname = mkMultiTracePanel pname w h chains
-
-multiTracePlotFile :: OutputFormat -> FilePath -> PlotConfig -> [Text] -> [Chain] -> IO ()
-multiTracePlotFile fmt path cfg names chains =
-  writeSpec fmt path (multiTracePlot cfg names chains)
-
--- ---------------------------------------------------------------------------
--- Posterior KDE plot (単一チェーン)
--- ---------------------------------------------------------------------------
-
--- | Posterior density plot per parameter (KDE-based).
-posteriorPlot :: PlotConfig -> [Text] -> Chain -> VegaLite
-posteriorPlot cfg names chain = toVegaLite
-  [ title (plotTitle cfg) []
-  , vConcat (map (\n -> mkKdePanel n (plotWidth cfg) 110 chain) names)
-  ]
-
-posteriorPlotFile :: OutputFormat -> FilePath -> PlotConfig -> [Text] -> Chain -> IO ()
-posteriorPlotFile fmt path cfg names chain =
-  writeSpec fmt path (posteriorPlot cfg names chain)
-
--- ---------------------------------------------------------------------------
--- Autocorrelation plot
--- ---------------------------------------------------------------------------
-
--- | Per-parameter autocorrelation plot up to a given maximum lag.
-autocorrPlot :: PlotConfig -> Int -> [Text] -> Chain -> VegaLite
-autocorrPlot cfg maxLag names chain = toVegaLite
-  [ title (plotTitle cfg) []
-  , vConcat (map acfPanel names)
-  ]
-  where
-    acfPanel pname =
-      let acData         = autocorr maxLag (chainVals pname chain)
-          (lags, acVals) = unzip acData
-      in asSpec
-          [ dataFromColumns []
-              . dataColumn "lag" (Numbers (map fromIntegral lags))
-              . dataColumn "acf" (Numbers acVals)
-              $ []
-          , mark Bar [MColor "#4C72B0", MOpacity 0.8]
-          , encoding
-              . position X [ PName "lag", PmType Quantitative
-                           , PAxis [AxTitle "Lag"] ]
-              . position Y [ PName "acf", PmType Quantitative
-                           , PScale [SDomain (DNumbers [-1, 1])]
-                           , PAxis [AxTitle pname] ]
-              $ []
-          , width  (plotWidth cfg)
-          , height 80
-          ]
-
-autocorrPlotFile :: OutputFormat -> FilePath -> PlotConfig -> Int -> [Text] -> Chain -> IO ()
-autocorrPlotFile fmt path cfg maxLag names chain =
-  writeSpec fmt path (autocorrPlot cfg maxLag names chain)
-
--- ---------------------------------------------------------------------------
--- Pair scatter
--- ---------------------------------------------------------------------------
-
--- | Bivariate posterior scatter for two parameters of a chain.
-pairScatter :: PlotConfig -> Text -> Text -> Chain -> VegaLite
-pairScatter cfg xName yName chain = toVegaLite
-  [ title (plotTitle cfg) []
-  , dataFromColumns []
-      . dataColumn xName (Numbers (chainVals xName chain))
-      . dataColumn yName (Numbers (chainVals yName chain))
-      $ []
-  , mark Point [MOpacity 0.25, MSize 15, MColor "#4C72B0"]
-  , encoding
-      . position X [PName xName, PmType Quantitative]
-      . position Y [PName yName, PmType Quantitative]
-      $ []
-  , width  (plotWidth  cfg)
-  , height (plotHeight cfg)
-  ]
-
-pairScatterFile :: OutputFormat -> FilePath -> PlotConfig -> Text -> Text -> Chain -> IO ()
-pairScatterFile fmt path cfg xName yName chain =
-  writeSpec fmt path (pairScatter cfg xName yName chain)
-
--- ---------------------------------------------------------------------------
--- Combined PyMC-style: [KDE | trace]  (単一チェーン)
--- ---------------------------------------------------------------------------
-
--- | PyMC-style combined diagnostics (KDE + trace) for one chain.
-mcmcDiagnostics :: PlotConfig -> [Text] -> Chain -> VegaLite
-mcmcDiagnostics cfg names chain = toVegaLite
-  [ title (plotTitle cfg) []
-  , vConcat (map rowFor names)
-  ]
-  where
-    n = length (chainSamples chain)
-    rowFor pname = asSpec
-      [ hConcat [ mkKdePanel   pname 220 80 chain
-                , mkTracePanel pname 420 80 n chain ] ]
-
-mcmcDiagnosticsFile :: OutputFormat -> FilePath -> PlotConfig -> [Text] -> Chain -> IO ()
-mcmcDiagnosticsFile fmt path cfg names chain =
-  writeSpec fmt path (mcmcDiagnostics cfg names chain)
-
--- ---------------------------------------------------------------------------
--- Combined PyMC-style: [KDE | multi-trace]  (多チェーン)
--- ---------------------------------------------------------------------------
-
--- | PyMC-style combined diagnostics for multiple chains.
--- 左: 全チェーン合算の KDE。右: チェーン別色分けトレース。
-mcmcDiagnosticsMulti :: PlotConfig -> [Text] -> [Chain] -> VegaLite
-mcmcDiagnosticsMulti cfg names chains = toVegaLite
-  [ title (plotTitle cfg) []
-  , vConcat (map rowFor names)
-  ]
-  where
-    combined pname = concatMap (chainVals pname) chains
-    rowFor pname = asSpec
-      [ hConcat
-          [ mkKdePanelFrom pname 220 80 (combined pname)
-          , mkMultiTracePanel pname 420 80 chains
-          ]
-      ]
-
-mcmcDiagnosticsMultiFile :: OutputFormat -> FilePath -> PlotConfig -> [Text] -> [Chain] -> IO ()
-mcmcDiagnosticsMultiFile fmt path cfg names chains =
-  writeSpec fmt path (mcmcDiagnosticsMulti cfg names chains)
-
--- ---------------------------------------------------------------------------
--- 内部: KDE パネル
--- ---------------------------------------------------------------------------
-
--- | KDE density plot with a 94 % HDI rule overlay.
-mkKdePanel :: Text -> Double -> Double -> Chain -> VLSpec
-mkKdePanel pname w h chain =
-  mkKdePanelFrom pname w h (chainVals pname chain)
-
-mkKdePanelFrom :: Text -> Double -> Double -> [Double] -> VLSpec
-mkKdePanelFrom pname w h vals =
-  let kdeData      = kde 200 vals
-      (xs, ys)     = unzip kdeData
-      (lo, hi)     = hdi 0.94 vals
-  in asSpec
-      [ layer
-          [ asSpec  -- KDE filled area
-              [ dataFromColumns []
-                  . dataColumn "x" (Numbers xs)
-                  . dataColumn "y" (Numbers ys)
-                  $ []
-              , mark Area [MColor "#4C72B0", MOpacity 0.3]
-              , encoding
-                  . position X [ PName "x", PmType Quantitative
-                               , PAxis [AxTitle pname] ]
-                  . position Y [ PName "y", PmType Quantitative
-                               , PAxis [AxTitle "Density", AxGrid False] ]
-                  $ []
-              ]
-          , asSpec  -- KDE line
-              [ dataFromColumns []
-                  . dataColumn "x" (Numbers xs)
-                  . dataColumn "y" (Numbers ys)
-                  $ []
-              , mark Line [MColor "#4C72B0", MStrokeWidth 2.0]
-              , encoding
-                  . position X [PName "x", PmType Quantitative]
-                  . position Y [PName "y", PmType Quantitative]
-                  $ []
-              ]
-          , asSpec  -- 94% HDI span (rule at bottom)
-              [ dataFromColumns []
-                  . dataColumn "lo" (Numbers [lo])
-                  . dataColumn "hi" (Numbers [hi])
-                  $ []
-              , mark Rule [MColor "#DD4444", MStrokeWidth 3.5]
-              , encoding
-                  . position X  [PName "lo", PmType Quantitative]
-                  . position X2 [PName "hi"]
-                  $ []
-              ]
-          ]
-      , width w, height h
-      ]
-
--- ---------------------------------------------------------------------------
--- 内部: トレースパネル (単一チェーン)
--- ---------------------------------------------------------------------------
-
-mkTracePanel :: Text -> Double -> Double -> Int -> Chain -> VLSpec
-mkTracePanel pname w h n chain =
-  let vals = chainVals pname chain
-  in asSpec
-      [ dataFromColumns []
-          . dataColumn "iter"  (Numbers (map fromIntegral [1 .. n]))
-          . dataColumn "value" (Numbers vals)
-          $ []
-      , mark Line [MColor "#4C72B0", MStrokeWidth 1.0, MOpacity 0.7]
-      , encoding
-          . position X [ PName "iter",  PmType Quantitative
-                       , PAxis [AxTitle "Iteration"] ]
-          . position Y [ PName "value", PmType Quantitative
-                       , PAxis [AxTitle ""] ]
-          $ []
-      , width w, height h
-      ]
-
--- ---------------------------------------------------------------------------
--- 内部: 多チェーントレースパネル
--- ---------------------------------------------------------------------------
-
-mkMultiTracePanel :: Text -> Double -> Double -> [Chain] -> VLSpec
-mkMultiTracePanel pname w h chains =
-  let (iters, values, chainIds) = unzip3
-        [ (fromIntegral i :: Double, v, T.pack (show c))
-        | (c, ch) <- zip [1 :: Int ..] chains
-        , (i, v)  <- zip [1 :: Int ..] (chainVals pname ch)
-        ]
-  in asSpec
-      [ dataFromColumns []
-          . dataColumn "iter"  (Numbers  iters)
-          . dataColumn "value" (Numbers  values)
-          . dataColumn "chain" (Strings  chainIds)
-          $ []
-      , mark Line [MStrokeWidth 1.0, MOpacity 0.7]
-      , encoding
-          . position X [ PName "iter",  PmType Quantitative
-                       , PAxis [AxTitle "Iteration"] ]
-          . position Y [ PName "value", PmType Quantitative
-                       , PAxis [AxTitle ""] ]
-          . color [ MName "chain", MmType Nominal
-                  , MScale [SScheme "tableau10" []]
-                  , MLegend [LTitle "Chain"] ]
-          $ []
-      , width w, height h
-      ]
-
--- ---------------------------------------------------------------------------
--- Forest plot (パラメータ事後を 1 つの図に並べて比較)
--- ---------------------------------------------------------------------------
-
--- | Forest plot: per-parameter posterior mean with a 95 % credible
--- interval, stacked horizontally.
---
--- ArviZ の @plot_forest@ 相当。複数モデル/複数チェーンの比較や、
--- 階層モデルでグループ別パラメータを並べて見るのに便利。
---
--- 単一チェーンの場合は @[chain]@ に 1 要素入れて呼ぶ。
-forestPlot
-  :: PlotConfig
-  -> [Text]      -- ^ 表示するパラメータ名 (上から下に並ぶ)
-  -> [Chain]     -- ^ 1 つ以上のチェーン (複数あれば色分け)
-  -> VegaLite
-forestPlot cfg params chains = toVegaLite
-  [ title (plotTitle cfg) []
-  , dataFromColumns []
-      . dataColumn "param" (Strings params')
-      . dataColumn "chain" (Strings chainIds)
-      . dataColumn "mean"  (Numbers means)
-      . dataColumn "lo"    (Numbers loQs)
-      . dataColumn "hi"    (Numbers hiQs)
-      $ []
-  , layer
-      [ -- 信用区間の横線
-        asSpec
-          [ mark Rule [MStrokeWidth 2, MOpacity 0.7]
-          , encoding
-              . position Y [ PName "param", PmType Nominal
-                           , PAxis [AxTitle "Parameter", AxLabelFontSize 11] ]
-              . position X  [ PName "lo", PmType Quantitative
-                            , PAxis [AxTitle "Posterior 95% CI"] ]
-              . position X2 [ PName "hi" ]
-              . color [ MName "chain", MmType Nominal
-                      , MScale [SScheme "tableau10" []]
-                      , MLegend [LTitle "Chain"] ]
-              $ []
-          ]
-        -- 事後平均ドット
-      , asSpec
-          [ mark Circle [MSize 80, MOpacity 0.95]
-          , encoding
-              . position Y [ PName "param", PmType Nominal ]
-              . position X [ PName "mean", PmType Quantitative ]
-              . color [ MName "chain", MmType Nominal
-                      , MScale [SScheme "tableau10" []] ]
-              $ []
-          ]
-      ]
-  , width (plotWidth cfg)
-  , height (max 200 (fromIntegral (length params * 30) :: Double))
-  ]
-  where
-    cs = zip [1 :: Int ..] chains
-    -- 各 (param, chain) の組について 1 行
-    rows =
-      [ (p, T.pack (show ci), m, l, h)
-      | (ci, ch) <- cs
-      , p        <- params
-      , let xs = chainVals p ch
-      , not (null xs)
-      , let n   = length xs
-            sxs = sortAsc xs
-            mu  = sum xs / fromIntegral n
-            qAt q = sxs !! min (n - 1) (max 0 (floor (q * fromIntegral n) :: Int))
-            (l, h) = (qAt 0.025, qAt 0.975)
-            m  = mu
-      ]
-    params'   = [p | (p,_,_,_,_) <- rows]
-    chainIds  = [c | (_,c,_,_,_) <- rows]
-    means     = [m | (_,_,m,_,_) <- rows]
-    loQs      = [l | (_,_,_,l,_) <- rows]
-    hiQs      = [h | (_,_,_,_,h) <- rows]
-
-    sortAsc :: [Double] -> [Double]
-    sortAsc = qs
-      where
-        qs []     = []
-        qs (p:xs) = qs [x | x <- xs, x <= p] ++ [p] ++ qs [x | x <- xs, x > p]
-
-forestPlotFile
-  :: OutputFormat -> FilePath -> PlotConfig -> [Text] -> [Chain] -> IO ()
-forestPlotFile fmt path cfg params chains =
-  writeSpec fmt path (forestPlot cfg params chains)
-
--- ---------------------------------------------------------------------------
--- Energy plot (NUTS の BFMI 診断)
--- ---------------------------------------------------------------------------
-
--- | PyMC-style energy plot for HMC / NUTS chains.
---
--- 2 本の KDE を重ね描き:
---
---   * Marginal energy E_n         — 事後分布から見た energy の分布
---   * Energy transition |E_n − E_{n−1}| を中心化した分布 (= π_E)
---
--- 両者がよく重なるなら良好。乖離が大きい (= BFMI が低い) と
--- 運動量再サンプリングがエネルギー方向の探索を取りこぼしている可能性。
---
--- 'chainEnergy' が空のチェーン (MH/Gibbs 由来) では空の図になる。
-energyPlot :: PlotConfig -> Chain -> VegaLite
-energyPlot cfg chain =
-  let es     = chainEnergy chain
-      mu     = if null es then 0 else sum es / fromIntegral (length es)
-      eMar   = map (\e -> e - mu) es                    -- 中心化エネルギー
-      eTrans = zipWith (-) (drop 1 es) es               -- ΔE_n
-      bfmiV  = fromMaybe (0/0) (bfmi es)
-      sub    = T.pack (printf "BFMI = %.3f" bfmiV)
-      kdeMar = kde 200 eMar
-      kdeTr  = kde 200 eTrans
-      (xM, yM) = unzip kdeMar
-      (xT, yT) = unzip kdeTr
-  in toVegaLite
-      [ title (plotTitle cfg <> " — " <> sub) []
-      , layer
-          [ asSpec
-              [ dataFromColumns []
-                  . dataColumn "x" (Numbers xM)
-                  . dataColumn "y" (Numbers yM)
-                  . dataColumn "kind" (Strings (replicate (length xM) "marginal E (centered)"))
-                  $ []
-              , mark Area [MOpacity 0.35]
-              , encoding
-                  . position X [PName "x", PmType Quantitative,
-                                PAxis [AxTitle "Energy"]]
-                  . position Y [PName "y", PmType Quantitative,
-                                PAxis [AxTitle "Density"]]
-                  . color [ MName "kind", MmType Nominal
-                          , MScale [SScheme "tableau10" []]
-                          , MLegend [LTitle ""] ]
-                  $ []
-              ]
-          , asSpec
-              [ dataFromColumns []
-                  . dataColumn "x" (Numbers xT)
-                  . dataColumn "y" (Numbers yT)
-                  . dataColumn "kind" (Strings (replicate (length xT) "transition ΔE"))
-                  $ []
-              , mark Area [MOpacity 0.35]
-              , encoding
-                  . position X [PName "x", PmType Quantitative]
-                  . position Y [PName "y", PmType Quantitative]
-                  . color [ MName "kind", MmType Nominal
-                          , MScale [SScheme "tableau10" []]
-                          , MLegend [LTitle ""] ]
-                  $ []
-              ]
-          ]
-      , width  (plotWidth cfg)
-      , height (plotHeight cfg)
-      ]
-
-energyPlotFile :: OutputFormat -> FilePath -> PlotConfig -> Chain -> IO ()
-energyPlotFile fmt path cfg chain =
-  writeSpec fmt path (energyPlot cfg chain)
-
--- ---------------------------------------------------------------------------
--- Posterior summary table  (az.summary 相当)
--- ---------------------------------------------------------------------------
-
--- SummaryRow / posteriorSummary は Hanalyze.Stat.Summary に移管 (Phase H6)。
--- 後方互換のため Hanalyze.Viz.MCMC からも export 経由で参照可能。
-
--- | Standalone HTML table summarizing the posterior.
-posteriorSummaryHtml :: Text -> [SummaryRow] -> Text
-posteriorSummaryHtml title rows =
-  let multi      = any (\r -> case srRhat r of Just _ -> True; _ -> False) rows
-      rhatHeader = if multi then "<th>R-hat</th>" else ""
-      cell t     = "<td>" <> t <> "</td>"
-      fmt v      = T.pack (printf "%.4f" v)
-      essCell e  = cell (T.pack (show (round e :: Int)))
-      rhatCell r = case r of
-        Nothing -> if multi then "<td>—</td>" else ""
-        Just v  -> "<td style=\"color:" <>
-                   (if v < 1.01 then "#2a9d2a" else "#cc2222") <>
-                   "\">" <> fmt v <> "</td>"
-      row r = T.unlines
-        [ "    <tr>"
-        , "      " <> cell (srName r)
-        , "      " <> cell (fmt (srMean r))
-        , "      " <> cell (fmt (srSD r))
-        , "      " <> cell (fmt (srHdiLo r))
-        , "      " <> cell (fmt (srHdiHi r))
-        , "      " <> essCell (srEssV r)
-        , "      " <> rhatCell (srRhat r)
-        , "    </tr>"
-        ]
-      header = T.unlines
-        [ "    <tr>"
-        , "      <th>Parameter</th><th>Mean</th><th>SD</th>"
-        , "      <th>HDI 3%</th><th>HDI 97%</th><th>ESS (bulk)</th>" <> rhatHeader
-        , "    </tr>"
-        ]
-  in T.unlines
-       [ "<!DOCTYPE html>"
-       , "<html><head><meta charset=\"utf-8\"><title>" <> title <> "</title>"
-       , "<style>"
-       , "body{font-family:sans-serif;max-width:900px;margin:2em auto;padding:0 1em;}"
-       , "table{border-collapse:collapse;width:100%;}"
-       , "th,td{padding:.4em .8em;border-bottom:1px solid #ddd;text-align:right;}"
-       , "th:first-child,td:first-child{text-align:left;}"
-       , "th{background:#f3f3f3;}"
-       , "tr:hover{background:#fafafa;}"
-       , "h2{border-bottom:2px solid #333;padding-bottom:.3em;}"
-       , "</style></head><body>"
-       , "<h2>" <> title <> "</h2>"
-       , "<table>"
-       , "  <thead>" <> header <> "  </thead>"
-       , "  <tbody>"
-       , T.concat (map row rows)
-       , "  </tbody>"
-       , "</table>"
-       , "</body></html>"
-       ]
-
--- | Write the posterior summary to a standalone HTML file.
-posteriorSummaryFile :: FilePath -> Text -> [Text] -> [Chain] -> IO ()
-posteriorSummaryFile path title params chains =
-  TIO.writeFile path
-    (posteriorSummaryHtml title (posteriorSummary params chains))
-
--- | Print the posterior summary to the console as a table.
-printPosteriorSummary :: [Text] -> [Chain] -> IO ()
-printPosteriorSummary params chains = do
-  let rows  = posteriorSummary params chains
-      multi = any (\r -> case srRhat r of Just _ -> True; _ -> False) rows
-      hdr | multi     =
-              printf "%-12s  %10s  %10s  %10s  %10s  %8s  %6s\n"
-                     ("Parameter" :: String) ("mean" :: String) ("sd" :: String)
-                     ("hdi_3%" :: String) ("hdi_97%" :: String)
-                     ("ess_bulk" :: String) ("r_hat" :: String)
-          | otherwise =
-              printf "%-12s  %10s  %10s  %10s  %10s  %8s\n"
-                     ("Parameter" :: String) ("mean" :: String) ("sd" :: String)
-                     ("hdi_3%" :: String) ("hdi_97%" :: String)
-                     ("ess_bulk" :: String)
-      pr r
-        | multi =
-            let rh = case srRhat r of Just v -> printf "%.3f" v; Nothing -> "—" :: String
-            in printf "%-12s  %10.4f  %10.4f  %10.4f  %10.4f  %8d  %6s\n"
-                  (T.unpack (srName r)) (srMean r) (srSD r)
-                  (srHdiLo r) (srHdiHi r) (round (srEssV r) :: Int) rh
-        | otherwise =
-            printf "%-12s  %10.4f  %10.4f  %10.4f  %10.4f  %8d\n"
-                  (T.unpack (srName r)) (srMean r) (srSD r)
-                  (srHdiLo r) (srHdiHi r) (round (srEssV r) :: Int)
-  hdr
-  putStrLn (replicate (if multi then 81 else 74) '-')
-  mapM_ pr rows
-
--- ---------------------------------------------------------------------------
--- Rank plot (多チェーン収束診断)
--- ---------------------------------------------------------------------------
-
--- | Rank plot (analogous to PyMC's @plot_rank@). Proposed by Vehtari et al. (2021)
--- 多チェーンの収束診断: 全チェーンを混ぜた順位を各チェーン内で
--- ヒストグラムにすると、収束時はチェーンごとに一様分布に近づく。
---
--- 引数 nBins は順位ヒストグラムのビン数 (典型値: 20)。
-rankPlot :: PlotConfig -> Int -> [Text] -> [Chain] -> VegaLite
-rankPlot cfg nBins names chains = toVegaLite
-  [ title (plotTitle cfg) []
-  , vConcat (map panel names)
-  ]
-  where
-    nChains = length chains
-    panel pname =
-      let perChain  = map (chainVals pname) chains
-          -- rank 正規化ヒストグラムは Stat.MCMC.rankHist に一元化 (Plot 経路と共有)。
-          hists     = rankHist nBins perChain          -- [chain][bin] (chain は 0-based)
-          triples   = [ (cid, b, c)
-                      | (cid, cnts) <- zip [(1 :: Int) ..] hists   -- 表示は 1-based chain
-                      , (b, c)      <- zip [(0 :: Int) ..] cnts ]
-          xs        = [ fromIntegral b :: Double | (_, b, _) <- triples ]
-          ys        = [ fromIntegral c :: Double | (_, _, c) <- triples ]
-          chainIds  = [ T.pack (show cid)          | (cid, _, _) <- triples ]
-      in asSpec
-          [ dataFromColumns []
-              . dataColumn "bin"   (Numbers xs)
-              . dataColumn "count" (Numbers ys)
-              . dataColumn "chain" (Strings chainIds)
-              $ []
-          , mark Bar [MOpacity 0.7]
-          , encoding
-              . position X [ PName "bin",   PmType Ordinal
-                           , PAxis [AxTitle "Rank bin"] ]
-              . position Y [ PName "count", PmType Quantitative
-                           , PAxis [AxTitle pname] ]
-              . color [ MName "chain", MmType Nominal
-                      , MScale [SScheme "tableau10" []]
-                      , MLegend [LTitle "Chain"] ]
-              . column [ FName "chain", FmType Nominal,
-                         FHeader [HTitle ("chain (" <> pname <> ")")] ]
-              $ []
-          , width (max 60 (plotWidth cfg / fromIntegral nChains))
-          , height 100
-          ]
-
-rankPlotFile :: OutputFormat -> FilePath -> PlotConfig
-             -> Int -> [Text] -> [Chain] -> IO ()
-rankPlotFile fmt path cfg nBins names chains =
-  writeSpec fmt path (rankPlot cfg nBins names chains)
-
--- ---------------------------------------------------------------------------
--- Posterior predictive check (pp_check 相当)
--- ---------------------------------------------------------------------------
-
--- | Posterior-predictive check: overlay the KDE of the observations on
--- the KDEs returned by @posteriorPredictive@
--- K 件の予測サンプル KDE をスパゲッティ式に重ね描き、平均予測 KDE を太線で
--- 重ねる。観測 (青) と予測の中心線 (オレンジ) が一致しなければモデル誤指定。
---
--- 引数:
---   * @observed@ — 元観測 y のリスト
---   * @predDraws@ — @posteriorPredictive@ 出力の各サンプル (各要素は元データと
---                   同じ長さの y_rep)
---   * @nOverlay@ — 描画する個別予測ドローの本数 (典型 50)
-ppcPlot :: PlotConfig -> [Double] -> [[Double]] -> Int -> VegaLite
-ppcPlot cfg observed predDraws nOverlay =
-  let nDraws        = length predDraws
-      step          = max 1 (nDraws `div` max 1 nOverlay)
-      thinned       = [ predDraws !! i
-                      | i <- [0, step .. nDraws - 1]
-                      , i < nDraws ]
-      -- 個別ドローごとの KDE (id, x, y)
-      drawSpecs     = concat
-        [ [ (i :: Int, x, y) | (x, y) <- kde 100 d ]
-        | (i, d) <- zip [0 ..] thinned
-        , length d >= 2 ]
-      drawIds       = [ T.pack (show i)            | (i, _, _) <- drawSpecs ]
-      drawXs        = [ x                          | (_, x, _) <- drawSpecs ]
-      drawYs        = [ y                          | (_, _, y) <- drawSpecs ]
-      -- 全予測平均の KDE
-      flatPred      = concat predDraws
-      meanKde       = kde 200 flatPred
-      (mxs, mys)    = unzip meanKde
-      -- 観測の KDE
-      obsKde        = kde 200 observed
-      (oxs, oys)    = unzip obsKde
-  in toVegaLite
-      [ title (plotTitle cfg) []
-      , layer
-          [ -- スパゲッティ (個別予測ドロー)
-            asSpec
-              [ dataFromColumns []
-                  . dataColumn "x"  (Numbers drawXs)
-                  . dataColumn "y"  (Numbers drawYs)
-                  . dataColumn "id" (Strings drawIds)
-                  $ []
-              , mark Line [MColor "#FF8C42", MOpacity 0.15, MStrokeWidth 0.8]
-              , encoding
-                  . position X [PName "x", PmType Quantitative,
-                                PAxis [AxTitle "y"]]
-                  . position Y [PName "y", PmType Quantitative,
-                                PAxis [AxTitle "Density"]]
-                  . detail [DName "id", DmType Nominal]
-                  $ []
-              ]
-          , -- 予測平均
-            asSpec
-              [ dataFromColumns []
-                  . dataColumn "x" (Numbers mxs)
-                  . dataColumn "y" (Numbers mys)
-                  $ []
-              , mark Line [MColor "#FF8C42", MStrokeWidth 2.5]
-              , encoding
-                  . position X [PName "x", PmType Quantitative]
-                  . position Y [PName "y", PmType Quantitative]
-                  $ []
-              ]
-          , -- 観測 KDE
-            asSpec
-              [ dataFromColumns []
-                  . dataColumn "x" (Numbers oxs)
-                  . dataColumn "y" (Numbers oys)
-                  $ []
-              , mark Line [MColor "#1F77B4", MStrokeWidth 3.0]
-              , encoding
-                  . position X [PName "x", PmType Quantitative]
-                  . position Y [PName "y", PmType Quantitative]
-                  $ []
-              ]
-          ]
-      , width  (plotWidth cfg)
-      , height (plotHeight cfg)
-      ]
-
-ppcPlotFile :: OutputFormat -> FilePath -> PlotConfig
-            -> [Double] -> [[Double]] -> Int -> IO ()
-ppcPlotFile fmt path cfg observed predDraws nOverlay =
-  writeSpec fmt path (ppcPlot cfg observed predDraws nOverlay)
-
--- ---------------------------------------------------------------------------
--- Divergence overlay (NUTS divergent transitions の可視化)
--- ---------------------------------------------------------------------------
-
--- | Pair scatter overlaid with divergent iterations as red X markers.
---
--- 引数:
---   * @xName@, @yName@: ペア散布の軸となる latent パラメタ名
---   * @divIdx@        : divergent 反復の 0-origin index 列 (バーンイン後)。
---                       将来 NUTS が `chainDivergences` を返したらそれを渡す。
---                       Phase F5 では空リストや手動指定で動作確認できる。
---
--- パラメタ空間で divergent が局所化していれば、その付近の事後分布が
--- 病的 (高曲率) であることを示し、reparameterization の検討材料になる。
-pairScatterDiv :: PlotConfig -> Text -> Text -> Chain -> [Int] -> VegaLite
-pairScatterDiv cfg xName yName chain divIdx =
-  let xs       = chainVals xName chain
-      ys       = chainVals yName chain
-      n        = min (length xs) (length ys)
-      validIdx = [ i | i <- divIdx, i >= 0, i < n ]
-      divXs    = [ xs !! i | i <- validIdx ]
-      divYs    = [ ys !! i | i <- validIdx ]
-  in toVegaLite
-      [ title (plotTitle cfg) []
-      , layer
-          [ asSpec  -- 通常の散布
-              [ dataFromColumns []
-                  . dataColumn xName (Numbers xs)
-                  . dataColumn yName (Numbers ys)
-                  $ []
-              , mark Point [MOpacity 0.25, MSize 15, MColor "#4C72B0"]
-              , encoding
-                  . position X [PName xName, PmType Quantitative]
-                  . position Y [PName yName, PmType Quantitative]
-                  $ []
-              ]
-          , asSpec  -- divergent な点を赤 X で重ねる
-              [ dataFromColumns []
-                  . dataColumn xName (Numbers divXs)
-                  . dataColumn yName (Numbers divYs)
-                  $ []
-              , mark Point [ MShape SymCross, MSize 80
-                           , MColor "#DD2222", MStrokeWidth 2.0
-                           , MOpacity 0.9 ]
-              , encoding
-                  . position X [PName xName, PmType Quantitative]
-                  . position Y [PName yName, PmType Quantitative]
-                  $ []
-              ]
-          ]
-      , width  (plotWidth  cfg)
-      , height (plotHeight cfg)
-      ]
-
-pairScatterDivFile :: OutputFormat -> FilePath -> PlotConfig
-                   -> Text -> Text -> Chain -> [Int] -> IO ()
-pairScatterDivFile fmt path cfg xName yName chain divIdx =
-  writeSpec fmt path (pairScatterDiv cfg xName yName chain divIdx)
diff --git a/src/Hanalyze/Viz/ModelGraph.hs b/src/Hanalyze/Viz/ModelGraph.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/ModelGraph.hs
+++ /dev/null
@@ -1,200 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.ModelGraph
--- Description : モデル DAG の Mermaid.js 可視化 (ModelGraph → HTML)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- | Mermaid.js visualization of model DAGs.
---
--- Renders the 'ModelGraph' that 'Hanalyze.Model.HBM.buildModelGraph' derives
--- automatically from a polymorphic model into an HTML file (displayed
--- in the browser via the Mermaid CDN).
---
--- == 3 ルートの選び方 (= hgg Phase 2 で 3 ルート併存方針確立)
---
--- 同じ 'Hanalyze.Model.HBM.ModelGraph' を可視化する 3 種類のルート:
---
--- +---------------+--------------------------------------------------+---------------------+-----------------------+
--- | ルート        | 場所                                             | 出力 / 描画依存     | 推奨用途              |
--- +===============+==================================================+=====================+=======================+
--- | __本 module__ | 'renderModelGraph' (= Mermaid HTML)              | .html + CDN script  | GitHub README、 ノート |
--- +---------------+--------------------------------------------------+---------------------+-----------------------+
--- | Graphviz DOT  | "Hanalyze.Viz.ModelGraphDot".renderModelGraphDot | .dot + dot CLI 別途 | graphviz 連携、 加工  |
--- +---------------+--------------------------------------------------+---------------------+-----------------------+
--- | hgg  | @Hgg.Plot.Bridge.Analyze.renderModelGraphSVG@| .svg (依存ゼロ)     | production、 offline  |
--- |               | (= @hgg-analyze-bridge@ package)        |                     |                       |
--- +---------------+--------------------------------------------------+---------------------+-----------------------+
---
--- 3 ルートとも同じ 'Hanalyze.Model.HBM.ModelGraph' 構造 (= node / edge / plate)
--- を表現する。 visual layout は実装ごとに異なる。 本ルート (= Mermaid) の利点:
---
---   * GitHub / GitLab README で render される (= 添付画像不要、 文字列で済む)
---   * ノート系 tool (= Notion 等) に貼り付けやすい
---   * 軽量 (= .html 1 ファイル、 ~5KB)
---
--- 弱点 (= 上記表の他ルートで補える):
---
---   * ブラウザ + ネット必須 (= offline 不可)
---   * production アプリ組込みには不向き (= hgg ルート推奨)
---   * 高度な layout (= graphviz dot 流) には不向き (= ModelGraphDot 推奨)
---
--- __本 module は撤廃されません__。 OSS 利用者の既存ワークフローを尊重して 3 ルート併存。
-module Hanalyze.Viz.ModelGraph
-  ( renderModelGraph
-  , buildMermaid
-  ) where
-
-import Data.Text (Text)
-import qualified Data.Text    as T
-import qualified Data.Text.IO as TIO
-import qualified Data.Set as Set
-import qualified Data.Map.Strict as Map
-import Data.List (groupBy, sortOn)
-import Data.Function (on)
-
-import Hanalyze.Model.HBM (ModelGraph (..), Node (..), NodeKind (..))
-
--- ---------------------------------------------------------------------------
--- Public API
--- ---------------------------------------------------------------------------
-
--- | Render a model graph to an HTML file (Mermaid is loaded from CDN).
-renderModelGraph :: FilePath -> Text -> ModelGraph -> IO ()
-renderModelGraph path title_ mg = TIO.writeFile path (buildHtml title_ mg)
-
--- ---------------------------------------------------------------------------
--- HTML wrapper
--- ---------------------------------------------------------------------------
-
-buildHtml :: Text -> ModelGraph -> Text
-buildHtml title_ mg = T.unlines
-  [ "<!DOCTYPE html>"
-  , "<html><head>"
-  , "  <meta charset=\"utf-8\">"
-  , "  <title>" <> title_ <> "</title>"
-  , "  <script src=\"https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js\"></script>"
-  , "  <style>"
-  , "    body { font-family: sans-serif; padding: 30px; background: #f5f5f5; margin: 0; }"
-  , "    h1   { color: #333; font-size: 1.3em; margin-bottom: 20px; }"
-  , "    .wrap { background: white; padding: 30px; border-radius: 10px;"
-  , "            box-shadow: 0 2px 10px rgba(0,0,0,.12); display: inline-block;"
-  , "            min-width: 300px; }"
-  , "    .legend { margin-top: 16px; font-size: .85em; color: #555; }"
-  , "    .legend span { display: inline-block; width: 12px; height: 12px;"
-  , "                   border-radius: 2px; margin-right: 4px; vertical-align: middle; }"
-  , "  </style>"
-  , "</head><body>"
-  , "  <h1>" <> title_ <> "</h1>"
-  , "  <div class=\"wrap\">"
-  , "    <div class=\"mermaid\">"
-  , buildMermaid mg
-  , "    </div>"
-  , "    <div class=\"legend\">"
-  , "      <span style=\"background:#4C72B0\"></span>latent &nbsp;&nbsp;"
-  , "      <span style=\"background:#DD8844\"></span>observed"
-  , "    </div>"
-  , "  </div>"
-  , "  <script>mermaid.initialize({ startOnLoad: true, theme: 'default' });</script>"
-  , "</body></html>"
-  ]
-
--- ---------------------------------------------------------------------------
--- Mermaid diagram
--- ---------------------------------------------------------------------------
-
--- | Build the Mermaid @flowchart TD@ source for a 'ModelGraph'.
--- Phase 40: plate に属するノードは @subgraph plate_<name>["<name> × N"]@
--- で囲まれる。 nested plate も入れ子で出力。
-buildMermaid :: ModelGraph -> Text
-buildMermaid mg = T.unlines $
-  [ "flowchart TD" ] ++
-  renderNodesGrouped 1 [] (mgNodes mg) (mgPlates mg) ++
-  [ "" ] ++
-  map mkEdgeLine (mgEdges mg) ++
-  [ "" ] ++
-  [ "    classDef latent   fill:#4C72B0,color:#fff,stroke:#2a5080,stroke-width:1.5px" ] ++
-  [ "    classDef observed fill:#DD8844,color:#fff,stroke:#b06020,stroke-width:1.5px" ] ++
-  classAssignLines mg
-
--- | nodePlates のスタックに沿って nodes をグルーピングし、 nested
--- subgraph を出力する。 'depth' はインデント用。 'curPath' は現在
--- 出力中の plate path (外→内)。
-renderNodesGrouped :: Int -> [Text] -> [Node] -> Map.Map Text Int -> [Text]
-renderNodesGrouped depth curPath ns plateSizes =
-  let ind = T.replicate (depth * 4) " "
-      -- 現位置に属する (= nodePlates == curPath) ノードを直接出力
-      hereNodes = [n | n <- ns, nodePlates n == curPath]
-      -- このスコープより内側 (= nodePlates が curPath で始まる かつ より長い) を集める
-      innerNodes = [n | n <- ns, isStrictPrefix curPath (nodePlates n)]
-      -- innerNodes を「curPath の直後の plate 名」 でグループ化
-      keyOf n = (nodePlates n) !! length curPath
-      sortedInner = sortOn keyOf innerNodes
-      grouped = groupBy ((==) `on` keyOf) sortedInner
-      hereLines = map (\n -> ind <> mkNodeLine n) hereNodes
-      innerLines = concatMap (renderPlateGroup depth curPath plateSizes) grouped
-  in hereLines ++ innerLines
-
-renderPlateGroup :: Int -> [Text] -> Map.Map Text Int -> [Node] -> [Text]
-renderPlateGroup _ _ _ [] = []
-renderPlateGroup depth curPath plateSizes ns@(n0:_) =
-  let plateName = (nodePlates n0) !! length curPath
-      sz        = Map.findWithDefault 0 plateName plateSizes
-      ind       = T.replicate (depth * 4) " "
-      header    = ind <> "subgraph plate_" <> sanitize plateName
-                <> "[\"" <> plateName <> " × " <> T.pack (show sz) <> "\"]"
-      footer    = ind <> "end"
-      inner     = renderNodesGrouped (depth + 1) (curPath ++ [plateName])
-                                     ns plateSizes
-  in [header] ++ inner ++ [footer]
-
-isStrictPrefix :: Eq a => [a] -> [a] -> Bool
-isStrictPrefix prefix xs =
-  length prefix < length xs && take (length prefix) xs == prefix
-
-sanitize :: Text -> Text
-sanitize = T.map (\c -> if c `elem` (" -.+*/" :: String) then '_' else c)
-
-mkNodeLine :: Node -> Text
-mkNodeLine n = "    " <> nid <> shapeOpen <> escaped <> shapeClose
-  where
-    nid     = nodeId (nodeName n)
-    label   = case nodeKind n of
-      LatentN     -> nodeName n <> "\\n" <> nodeDist n <>
-                     (if Set.null (nodeDeps n)
-                       then ""
-                       else " (deps: " <> T.intercalate "," (Set.toList (nodeDeps n)) <> ")")
-      ObservedN k -> nodeName n <> "\\n" <> nodeDist n
-                  <> "  (n=" <> T.pack (show k) <> ")"
-      -- Phase 60.4: DeterministicN は従来非網羅 (deterministic を含むモデルで
-      -- crash・ModelGraphDot の Phase 59.2 と同類) だったのを同時修正。
-      DeterministicN -> nodeName n <> "\\n" <> nodeDist n
-      DataN k -> nodeName n <> "\\n(n=" <> T.pack (show k) <> ")"
-    escaped = T.replace "\"" "&quot;" label
-    (shapeOpen, shapeClose) = case nodeKind n of
-      LatentN     -> ("[\"",  "\"]")
-      ObservedN _ -> ("([\"", "\"])")
-      DeterministicN -> ("[\"", "\"]")
-      DataN _     -> ("(\"", "\")")
-
-mkEdgeLine :: (Text, Text) -> Text
-mkEdgeLine (from, to) = "    " <> nodeId from <> " --> " <> nodeId to
-
-classAssignLines :: ModelGraph -> [Text]
-classAssignLines mg =
-  let latentIds   = [ nodeId (nodeName n) | n <- mgNodes mg, isLatent n ]
-      observedIds = [ nodeId (nodeName n) | n <- mgNodes mg, not (isLatent n) ]
-      assign cls ids
-        | null ids  = []
-        | otherwise = [ "    class " <> T.intercalate "," ids <> " " <> cls ]
-  in assign "latent" latentIds ++ assign "observed" observedIds
-
--- ---------------------------------------------------------------------------
--- Helpers
--- ---------------------------------------------------------------------------
-
-nodeId :: Text -> Text
-nodeId = T.map (\c -> if c `elem` (" -.+*/" :: String) then '_' else c)
-
-isLatent :: Node -> Bool
-isLatent n = case nodeKind n of { LatentN -> True; _ -> False }
diff --git a/src/Hanalyze/Viz/ModelGraphDot.hs b/src/Hanalyze/Viz/ModelGraphDot.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/ModelGraphDot.hs
+++ /dev/null
@@ -1,166 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.ModelGraphDot
--- Description : モデル DAG の Graphviz DOT 出力 (PyMC model_to_graphviz 同等の plate 描画)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- | Graphviz DOT 出力 (Phase 40-A3、 PyMC @pm.model_to_graphviz@ 同等の plate 描画)。
---
--- 'Hanalyze.Model.HBM.buildModelGraph' が出す 'ModelGraph' を DOT
--- ソースに変換する。 plate は @subgraph cluster_<name>@ + @label="<name> × N"@
--- (右下サイズ数字) で囲まれ、 PyMC 流の角丸長方形描画になる。
---
--- 使い方:
---
--- > let g = HBM.buildModelGraph m
--- > let dot = renderModelGraphDot g
--- > T.writeFile "model.dot" dot
--- > -- graphviz CLI で PNG / SVG 化:
--- > -- $ dot -Tpng model.dot -o model.png
---
--- == 3 ルートの選び方 (= hgg Phase 2 で 3 ルート併存方針確立)
---
--- 同じ 'Hanalyze.Model.HBM.ModelGraph' を可視化する 3 種類のルート:
---
--- +---------------+--------------------------------------------------+---------------------+-----------------------+
--- | ルート        | 場所                                             | 出力 / 描画依存     | 推奨用途              |
--- +===============+==================================================+=====================+=======================+
--- | Mermaid HTML  | "Hanalyze.Viz.ModelGraph".renderModelGraph       | .html + CDN script  | GitHub README、 ノート |
--- +---------------+--------------------------------------------------+---------------------+-----------------------+
--- | __本 module__ | 'renderModelGraphDot' (= Graphviz DOT)           | .dot + dot CLI 別途 | graphviz 連携、 加工  |
--- +---------------+--------------------------------------------------+---------------------+-----------------------+
--- | hgg  | @Hgg.Plot.Bridge.Analyze.renderModelGraphSVG@| .svg (依存ゼロ)     | production、 offline  |
--- |               | (= @hgg-analyze-bridge@ package)        |                     |                       |
--- +---------------+--------------------------------------------------+---------------------+-----------------------+
---
--- 3 ルートとも同じ 'Hanalyze.Model.HBM.ModelGraph' 構造 (= node / edge / plate)
--- を表現する。 visual layout は実装ごとに異なる。 本ルート (= Graphviz DOT) の利点:
---
---   * graphviz dot の高品質 layout (= Sugiyama framework 本家、 数十年の蓄積)
---   * @-Tpng@ @-Tsvg@ @-Tpdf@ @-Tps@ 等 多 format 出力
---   * @rank=same@ @constraint=false@ @cluster@ 等 dot 固有 directive で細かい制御
---   * 既存 graphviz エコシステム (= xdot、 gephi 等) と連携
---
--- 弱点 (= 上記表の他ルートで補える):
---
---   * @dot@ CLI が install 済必須 (= production 配布で外部依存)
---   * 出力は .dot text 中間ファイル (= 描画は別 step、 pipeline 化必要)
---
--- __本 module は撤廃されません__。 OSS 利用者の既存ワークフローを尊重して 3 ルート併存。
-module Hanalyze.Viz.ModelGraphDot
-  ( renderModelGraphDot
-  , writeModelGraphDot
-  ) where
-
-import Data.Text (Text)
-import qualified Data.Text    as T
-import qualified Data.Text.IO as TIO
-import qualified Data.Set as Set
-import qualified Data.Map.Strict as Map
-import Data.List (groupBy, sortOn)
-import Data.Function (on)
-
-import Hanalyze.Model.HBM (ModelGraph (..), Node (..), NodeKind (..))
-
--- ---------------------------------------------------------------------------
--- Public API
--- ---------------------------------------------------------------------------
-
--- | 'ModelGraph' を Graphviz DOT 形式の 'Text' に変換する。
-renderModelGraphDot :: ModelGraph -> Text
-renderModelGraphDot mg = T.unlines $
-  [ "digraph G {"
-  , "    rankdir=TB;"
-  , "    node [fontname=\"sans-serif\", fontsize=10];"
-  , "    edge [arrowsize=0.7];"
-  , ""
-  ] ++
-  renderNodesGrouped 1 [] (mgNodes mg) (mgPlates mg) ++
-  [ "" ] ++
-  map mkEdgeLine (mgEdges mg) ++
-  [ "}" ]
-
--- | DOT をファイルに書き出す利便 helper。
-writeModelGraphDot :: FilePath -> ModelGraph -> IO ()
-writeModelGraphDot path mg = TIO.writeFile path (renderModelGraphDot mg)
-
--- ---------------------------------------------------------------------------
--- Node grouping by plate (nested cluster)
--- ---------------------------------------------------------------------------
-
-renderNodesGrouped :: Int -> [Text] -> [Node] -> Map.Map Text Int -> [Text]
-renderNodesGrouped depth curPath ns plateSizes =
-  let ind = T.replicate (depth * 4) " "
-      hereNodes = [n | n <- ns, nodePlates n == curPath]
-      innerNodes = [n | n <- ns, isStrictPrefix curPath (nodePlates n)]
-      keyOf n = (nodePlates n) !! length curPath
-      sortedInner = sortOn keyOf innerNodes
-      grouped = groupBy ((==) `on` keyOf) sortedInner
-      hereLines = map (\n -> ind <> mkNodeLine n) hereNodes
-      innerLines = concatMap (renderPlateGroup depth curPath plateSizes) grouped
-  in hereLines ++ innerLines
-
-renderPlateGroup :: Int -> [Text] -> Map.Map Text Int -> [Node] -> [Text]
-renderPlateGroup _ _ _ [] = []
-renderPlateGroup depth curPath plateSizes ns@(n0:_) =
-  let plateName = (nodePlates n0) !! length curPath
-      sz        = Map.findWithDefault 0 plateName plateSizes
-      ind       = T.replicate (depth * 4) " "
-      header    = ind <> "subgraph cluster_" <> sanitize plateName <> " {"
-      label     = ind <> "    label=\"" <> plateName <> " × "
-                  <> T.pack (show sz) <> "\";"
-      style     = ind <> "    style=\"rounded\";"
-      labelloc  = ind <> "    labelloc=\"b\";"  -- 下に表示 (PyMC 流)
-      footer    = ind <> "}"
-      inner     = renderNodesGrouped (depth + 1) (curPath ++ [plateName])
-                                     ns plateSizes
-  in [header, label, style, labelloc] ++ inner ++ [footer]
-
-isStrictPrefix :: Eq a => [a] -> [a] -> Bool
-isStrictPrefix prefix xs =
-  length prefix < length xs && take (length prefix) xs == prefix
-
--- ---------------------------------------------------------------------------
--- Node / Edge rendering
--- ---------------------------------------------------------------------------
-
-mkNodeLine :: Node -> Text
-mkNodeLine n =
-  let nid     = nodeId (nodeName n)
-      label   = case nodeKind n of
-        LatentN        -> nodeName n <> "\\n" <> nodeDist n
-        ObservedN k    -> nodeName n <> "\\n" <> nodeDist n
-                          <> "\\n(n=" <> T.pack (show k) <> ")"
-        DeterministicN -> nodeName n <> "\\n" <> nodeDist n
-        -- Phase 60.4: データ slot は名前 + 長さのみ (分布を持たない)
-        DataN k        -> nodeName n <> "\\n(n=" <> T.pack (show k) <> ")"
-      escaped = T.replace "\"" "&quot;" label
-      attrs = case nodeKind n of
-        -- 潜在: 楕円・白塗り
-        LatentN        -> "label=\"" <> escaped <> "\", shape=ellipse"
-        -- 観測: 楕円・灰色塗り (PyMC 流)
-        ObservedN _    -> "label=\"" <> escaped <> "\", shape=ellipse, "
-                          <> "style=filled, fillcolor=lightgray"
-        -- 決定的変換: 四角・白塗り (PyMC の Deterministic 流)
-        DeterministicN -> "label=\"" <> escaped <> "\", shape=box"
-        -- データ slot (pm.Data 相当): 角丸四角・灰塗り (PyMC ConstantData 流)
-        DataN _        -> "label=\"" <> escaped <> "\", shape=box, "
-                          <> "style=\"rounded,filled\", fillcolor=lightgray"
-  in nid <> " [" <> attrs <> "];"
-
-mkEdgeLine :: (Text, Text) -> Text
-mkEdgeLine (from, to) = "    " <> nodeId from <> " -> " <> nodeId to <> ";"
-
--- ---------------------------------------------------------------------------
--- Helpers
--- ---------------------------------------------------------------------------
-
-nodeId :: Text -> Text
-nodeId = T.map (\c -> if c `elem` (" -.+*/" :: String) then '_' else c)
-
-sanitize :: Text -> Text
-sanitize = nodeId
-
-_unused :: Set.Set Text -> Set.Set Text
-_unused = id
diff --git a/src/Hanalyze/Viz/Pareto.hs b/src/Hanalyze/Viz/Pareto.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/Pareto.hs
+++ /dev/null
@@ -1,278 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.Pareto
--- Description : パレートフロント可視化 (散布・pairs・parallel coordinates・hypervolume 推移・比較)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- | Pareto-front visualizations (130 規約: PlotData ベース).
---
--- 2026-05-14 (130 リクエスト) — 旧版は @[Solution]@ を直接受けていたが、
--- HPotfire の Vega-Lite 移行で全 Viz モジュールを @PlotConfig -> ... ->
--- PlotData -> VegaLite@ で揃える方針になり、Pareto も他と同じ規約に
--- 統一した。@[Solution]@ → 'PlotData' の変換は 'solutionsToPlotData'
--- を経由する。
---
---   * 'paretoScatter'       — two-objective scatter, optional highlight column.
---   * 'paretoPair'          — pairs scatter matrix for ≥ 3 objectives.
---   * 'parallelCoordinates' — multi-objective parallel coordinates.
---   * 'hypervolumeHistory'  — hypervolume convergence trace (gen vs hv).
---   * 'paretoCompare'       — overlay two fronts (e.g. estimated vs true).
-module Hanalyze.Viz.Pareto
-  ( -- * 130: PlotData ベース API
-    paretoScatter
-  , paretoScatterFile
-  , paretoPair
-  , paretoPairFile
-  , parallelCoordinates
-  , parallelCoordinatesFile
-  , hypervolumeHistory
-  , hypervolumeHistoryFile
-  , paretoCompare
-  , paretoCompareFile
-    -- * 変換ヘルパ
-  , solutionsToPlotData
-  ) where
-
-import           Data.Map.Strict      (Map)
-import qualified Data.Map.Strict      as Map
-import           Data.Text            (Text)
-import qualified Data.Text            as T
-import qualified Data.Vector          as V
-import           Graphics.Vega.VegaLite
-
-import           Hanalyze.Optim.NSGA  (Solution (..))
-import           Hanalyze.Viz.Core    (PlotConfig (..), OutputFormat, writeSpec)
-import           Hanalyze.Viz.PlotData
-                  (PlotData (..), numericColumn, textColumn, fromMixedColumns)
-
--- ---------------------------------------------------------------------------
--- 変換ヘルパ
--- ---------------------------------------------------------------------------
-
--- | Convert a list of NSGA-II 'Solution' values to a 'PlotData' with one
--- numeric column per objective. @objLabels@ provides the column names
--- (length must match each solution's @solObjectives@); shorter
--- 'solObjectives' lists are padded with @0@.
---
--- This is the canonical bridge from optimisation results to Pareto
--- visualisations under the new 130 規約.
-solutionsToPlotData :: [Text] -> [Solution] -> PlotData
-solutionsToPlotData objLabels sols =
-  let m       = length objLabels
-      pad o   = take m (o ++ Prelude.repeat 0)
-      cols    = [ ( lab
-                  , V.fromList [ pad (solObjectives s) !! j | s <- sols ]
-                  )
-                | (j, lab) <- zip [0 :: Int ..] objLabels
-                ]
-  in fromMixedColumns cols []
-
--- 内部ヘルパ: 取り出し失敗時は空ベクタ
-numCol :: Text -> PlotData -> [Double]
-numCol n pd = maybe [] V.toList (numericColumn n pd)
-
-txtCol :: Text -> PlotData -> [Text]
-txtCol n pd = maybe [] V.toList (textColumn n pd)
-
--- ---------------------------------------------------------------------------
--- 2 目的の散布図
--- ---------------------------------------------------------------------------
-
--- | Pareto-front scatter plot for a two-objective problem on a single
--- 'PlotData'. The optional third argument is the name of a text column
--- in @pdText@ carrying a categorical highlight (e.g. @"front"@ /
--- @"all"@); when supplied, points are coloured by that column. Without
--- it, all points share a single colour.
-paretoScatter :: PlotConfig
-              -> (Text, Text)   -- ^ (xCol, yCol)
-              -> Maybe Text     -- ^ optional highlight column (text)
-              -> PlotData
-              -> VegaLite
-paretoScatter cfg (xCol, yCol) mHilite pd =
-  let xs = numCol xCol pd
-      ys = numCol yCol pd
-      addHi cols = case mHilite of
-        Just c  -> dataColumn c (Strings (txtCol c pd)) cols
-        Nothing -> cols
-      addColorEnc encs = case mHilite of
-        Just c  -> color [MName c, MmType Nominal] encs
-        Nothing -> encs
-  in toVegaLite
-      [ title (plotTitle cfg) []
-      , dataFromColumns []
-          . dataColumn xCol (Numbers xs)
-          . dataColumn yCol (Numbers ys)
-          . addHi
-          $ []
-      , mark Point [MOpacity 0.7, MSize 50]
-      , encoding
-          . position X [PName xCol, PmType Quantitative, PAxis [AxTitle xCol]]
-          . position Y [PName yCol, PmType Quantitative, PAxis [AxTitle yCol]]
-          . addColorEnc
-          $ []
-      , width  (plotWidth  cfg)
-      , height (plotHeight cfg)
-      ]
-
-paretoScatterFile :: OutputFormat -> FilePath -> PlotConfig
-                  -> (Text, Text) -> Maybe Text -> PlotData -> IO ()
-paretoScatterFile fmt path cfg cols mHi pd =
-  writeSpec fmt path (paretoScatter cfg cols mHi pd)
-
--- ---------------------------------------------------------------------------
--- ペア散布行列 (3+ 目的)
--- ---------------------------------------------------------------------------
-
--- | For 3+ objectives, lay out all pairwise 2D scatter plots in a
--- grid. Diagonal cells are omitted; only the upper triangle is drawn.
--- All @objCols@ must be present in @pdNumeric@.
-paretoPair :: PlotConfig -> [Text] -> PlotData -> VegaLite
-paretoPair cfg objCols pd =
-  let m       = length objCols
-      colVec  = Map.fromList [ (c, numCol c pd) | c <- objCols ] :: Map Text [Double]
-      lookupV c = Map.findWithDefault [] c colVec
-      panel i j =
-        let xC = objCols !! i
-            yC = objCols !! j
-        in asSpec
-             [ dataFromColumns []
-                 . dataColumn xC (Numbers (lookupV xC))
-                 . dataColumn yC (Numbers (lookupV yC))
-                 $ []
-             , mark Point [MOpacity 0.7, MSize 35, MColor "#DD2222"]
-             , encoding
-                 . position X [PName xC, PmType Quantitative]
-                 . position Y [PName yC, PmType Quantitative]
-                 $ []
-             , width  200
-             , height 200
-             ]
-      rowsAt i = [panel i j | j <- [i + 1 .. m - 1]]
-      gridRows = [asSpec [hConcat (rowsAt i)] | i <- [0 .. m - 2]]
-  in toVegaLite
-       [ title (plotTitle cfg) []
-       , vConcat gridRows
-       ]
-
-paretoPairFile :: OutputFormat -> FilePath -> PlotConfig
-               -> [Text] -> PlotData -> IO ()
-paretoPairFile fmt path cfg labels pd =
-  writeSpec fmt path (paretoPair cfg labels pd)
-
--- ---------------------------------------------------------------------------
--- 並行座標プロット
--- ---------------------------------------------------------------------------
-
--- | Multi-objective parallel-coordinates plot. One line per row in
--- 'PlotData'; objectives are spread along the @x@ axis. The @id@
--- column is synthesised from row index.
-parallelCoordinates :: PlotConfig
-                    -> [Text]    -- ^ objective column names (numeric)
-                    -> PlotData
-                    -> VegaLite
-parallelCoordinates cfg labels pd =
-  let n      = pdLength pd
-      idsRow = [ T.pack (show (i :: Int)) | i <- [0 .. n - 1] ]
-      rows   = [ (idsRow !! i, lab, numCol lab pd !! i)
-               | i   <- [0 .. n - 1]
-               , lab <- labels
-               , let xs = numCol lab pd
-               , length xs > i
-               ]
-      ids  = [ a | (a, _, _) <- rows ]
-      objs = [ b | (_, b, _) <- rows ]
-      vals = [ c | (_, _, c) <- rows ]
-  in toVegaLite
-      [ title (plotTitle cfg) []
-      , dataFromColumns []
-          . dataColumn "id"  (Strings ids)
-          . dataColumn "obj" (Strings objs)
-          . dataColumn "val" (Numbers vals)
-          $ []
-      , mark Line [MOpacity 0.3, MStrokeWidth 1.0]
-      , encoding
-          . position X [PName "obj", PmType Nominal, PAxis [AxTitle "Objective"]]
-          . position Y [PName "val", PmType Quantitative, PAxis [AxTitle "Value"]]
-          . detail   [DName "id", DmType Nominal]
-          . color    [MName "id", MmType Nominal,
-                      MLegend [], MScale [SScheme "tableau10" []]]
-          $ []
-      , width  (plotWidth  cfg)
-      , height (plotHeight cfg)
-      ]
-
-parallelCoordinatesFile :: OutputFormat -> FilePath -> PlotConfig
-                        -> [Text] -> PlotData -> IO ()
-parallelCoordinatesFile fmt path cfg labels pd =
-  writeSpec fmt path (parallelCoordinates cfg labels pd)
-
--- ---------------------------------------------------------------------------
--- HV 収束履歴
--- ---------------------------------------------------------------------------
-
--- | Convergence plot: per-generation hypervolume. @genCol@ and @hvCol@
--- must both live in @pdNumeric@.
-hypervolumeHistory :: PlotConfig
-                   -> Text     -- ^ generation column name
-                   -> Text     -- ^ hypervolume column name
-                   -> PlotData
-                   -> VegaLite
-hypervolumeHistory cfg genCol hvCol pd =
-  toVegaLite
-    [ title (plotTitle cfg) []
-    , dataFromColumns []
-        . dataColumn genCol (Numbers (numCol genCol pd))
-        . dataColumn hvCol  (Numbers (numCol hvCol  pd))
-        $ []
-    , mark Line [MColor "#1F77B4", MStrokeWidth 2.5]
-    , encoding
-        . position X [PName genCol, PmType Quantitative, PAxis [AxTitle "Generation"]]
-        . position Y [PName hvCol,  PmType Quantitative, PAxis [AxTitle "Hypervolume"]]
-        $ []
-    , width  (plotWidth  cfg)
-    , height (plotHeight cfg)
-    ]
-
-hypervolumeHistoryFile :: OutputFormat -> FilePath -> PlotConfig
-                       -> Text -> Text -> PlotData -> IO ()
-hypervolumeHistoryFile fmt path cfg genCol hvCol pd =
-  writeSpec fmt path (hypervolumeHistory cfg genCol hvCol pd)
-
--- ---------------------------------------------------------------------------
--- 推定 vs 真 front の比較 (2D)
--- ---------------------------------------------------------------------------
-
--- | Overlay two 2D fronts (typically estimated red over true grey
--- dashed). The @groupCol@ (text) splits 'PlotData' into the two layers
--- by category; the first distinct value gets the line style, the
--- second gets the points. If @groupCol@ has fewer than two distinct
--- values, falls back to a single point series.
-paretoCompare :: PlotConfig
-              -> (Text, Text)   -- ^ (xCol, yCol)
-              -> Text           -- ^ group column (text; e.g. "true"/"estimated")
-              -> PlotData
-              -> VegaLite
-paretoCompare cfg (xCol, yCol) gCol pd =
-  toVegaLite
-    [ title (plotTitle cfg) []
-    , dataFromColumns []
-        . dataColumn xCol (Numbers (numCol xCol pd))
-        . dataColumn yCol (Numbers (numCol yCol pd))
-        . dataColumn gCol (Strings (txtCol gCol pd))
-        $ []
-    , mark Point [MOpacity 0.85, MSize 60]
-    , encoding
-        . position X [PName xCol, PmType Quantitative, PAxis [AxTitle xCol]]
-        . position Y [PName yCol, PmType Quantitative, PAxis [AxTitle yCol]]
-        . color    [MName gCol, MmType Nominal,
-                    MScale [SScheme "set1" []]]
-        $ []
-    , width  (plotWidth  cfg)
-    , height (plotHeight cfg)
-    ]
-
-paretoCompareFile :: OutputFormat -> FilePath -> PlotConfig
-                  -> (Text, Text) -> Text -> PlotData -> IO ()
-paretoCompareFile fmt path cfg cols gCol pd =
-  writeSpec fmt path (paretoCompare cfg cols gCol pd)
diff --git a/src/Hanalyze/Viz/PlotConfig.hs b/src/Hanalyze/Viz/PlotConfig.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/PlotConfig.hs
+++ /dev/null
@@ -1,56 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.PlotConfig
--- Description : 全 Viz.* モジュール共有のプロット設定 (PlotConfig / defaultConfig) を定義
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- | Common plot configuration shared by every @Hanalyze.Viz.*@ module.
---
--- 'Hanalyze.Viz.Core' originally hosted 'PlotConfig' as a 3-field record
--- (title / width / height) so that the @writeSpec@ / @openInBrowser@
--- helpers had access to the basic geometry. As more downstream consumers
--- (notably HPotfire's Vega-Lite migration) needed colour scheme, facet
--- columns, legend placement, etc., the responsibility outgrew @Viz.Core@.
---
--- This module owns the canonical 'PlotConfig' definition and the
--- 'defaultConfig' constructor; @Viz.Core@ re-exports both for backwards
--- compatibility.
-module Hanalyze.Viz.PlotConfig
-  ( PlotConfig (..)
-  , defaultConfig
-  ) where
-
-import Data.Text (Text)
-
--- | Common plot configuration. Existing required fields ('plotTitle' /
--- 'plotWidth' / 'plotHeight') are kept as-is for the in-tree call sites
--- that pre-date this module. New optional fields default to 'Nothing'
--- via 'defaultConfig' so adding fields here does not break callers that
--- only update the title or dimensions.
-data PlotConfig = PlotConfig
-  { plotTitle       :: Text
-    -- ^ Plot title (mandatory; pass an empty string for an untitled chart).
-  , plotWidth       :: Double
-    -- ^ Plot width in pixels.
-  , plotHeight      :: Double
-    -- ^ Plot height in pixels.
-  , plotColorScheme :: Maybe Text
-    -- ^ Vega-Lite colour scheme name (e.g. @"viridis"@, @"category10"@).
-  , plotFacetColumn :: Maybe Text
-    -- ^ Column name to facet on (small multiples).
-  , plotLegendPos   :: Maybe Text
-    -- ^ Legend position (@"right"@ / @"bottom"@ / @"none"@ etc.).
-  } deriving (Show)
-
--- | Default 600 × 400 'PlotConfig' with the given title; all optional
--- fields are 'Nothing'.
-defaultConfig :: Text -> PlotConfig
-defaultConfig t = PlotConfig
-  { plotTitle       = t
-  , plotWidth       = 600
-  , plotHeight      = 400
-  , plotColorScheme = Nothing
-  , plotFacetColumn = Nothing
-  , plotLegendPos   = Nothing
-  }
diff --git a/src/Hanalyze/Viz/PlotData.hs b/src/Hanalyze/Viz/PlotData.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/PlotData.hs
+++ /dev/null
@@ -1,124 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.PlotData
--- Description : プロットデータの入力元非依存な中間表現 (PlotData / ToPlotData)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- | Source-agnostic intermediate representation for plot data.
---
--- HPotfire and other downstream consumers want to feed data from a
--- variety of backends — Hackage @dataframe@, Parquet/Arrow, a SQL/REST
--- store, or in-memory @[Double]@ lists — into the same @*Spec@ functions
--- in @Hanalyze.Viz.*@. To avoid a hard dependency from @Viz@ on
--- @dataframe@ (and a future ripple if/when DB-backed sources land),
--- @Viz.*@ accepts only 'PlotData', and adapter modules ('toPlotData')
--- handle conversion at the boundary.
---
--- 'PlotData' is intentionally a /concrete/ record (not an opaque newtype
--- around a type class) so that:
---
---   * @Vega-Lite@ JSON serialisation can iterate columns directly;
---   * unit tests can construct fixtures without an instance dance;
---   * future backends only need a one-shot @toPlotData@ extraction.
---
--- The 'ToPlotData' class lets callers stay polymorphic when the source
--- type is uniform across a call site.
-module Hanalyze.Viz.PlotData
-  ( -- * Concrete intermediate type
-    PlotData (..)
-  , emptyPlotData
-  , plotDataLength
-  , plotDataColumns
-  , numericColumn
-  , textColumn
-    -- * Construction helpers
-  , fromNumericColumns
-  , fromMixedColumns
-    -- * Polymorphic boundary
-  , ToPlotData (..)
-  ) where
-
-import           Data.Map.Strict (Map)
-import qualified Data.Map.Strict as Map
-import           Data.Text       (Text)
-import qualified Data.Vector     as V
-
--- | A row-aligned, column-oriented snapshot of plot input.
---
--- Each column in @pdNumeric@ / @pdText@ MUST have the same length
--- (@pdLength@); 'fromNumericColumns' / 'fromMixedColumns' enforce this.
--- Columns may live in either the numeric or text map but not both.
-data PlotData = PlotData
-  { pdNumeric :: !(Map Text (V.Vector Double))
-    -- ^ Numeric columns keyed by column name.
-  , pdText    :: !(Map Text (V.Vector Text))
-    -- ^ Text / categorical columns keyed by column name.
-  , pdLength  :: !Int
-    -- ^ Row count (invariant: equals every column's 'V.length').
-  } deriving (Show)
-
--- | An empty 'PlotData' with zero rows.
-emptyPlotData :: PlotData
-emptyPlotData = PlotData Map.empty Map.empty 0
-
--- | Row count.
-plotDataLength :: PlotData -> Int
-plotDataLength = pdLength
-
--- | All column names (numeric + text), preserving 'Map' order
--- (alphabetical).
-plotDataColumns :: PlotData -> [Text]
-plotDataColumns pd = Map.keys (pdNumeric pd) ++ Map.keys (pdText pd)
-
--- | Look up a numeric column by name.
-numericColumn :: Text -> PlotData -> Maybe (V.Vector Double)
-numericColumn n = Map.lookup n . pdNumeric
-
--- | Look up a text column by name.
-textColumn :: Text -> PlotData -> Maybe (V.Vector Text)
-textColumn n = Map.lookup n . pdText
-
--- | Build a 'PlotData' from a list of numeric columns. All columns must
--- have the same length; an empty input yields 'emptyPlotData'.
-fromNumericColumns :: [(Text, V.Vector Double)] -> PlotData
-fromNumericColumns []   = emptyPlotData
-fromNumericColumns cols =
-  let n = V.length (snd (head cols))
-  in if all ((== n) . V.length . snd) cols
-       then PlotData
-              { pdNumeric = Map.fromList cols
-              , pdText    = Map.empty
-              , pdLength  = n
-              }
-       else error "Hanalyze.Viz.PlotData.fromNumericColumns: \
-                   \column lengths disagree"
-
--- | Build a 'PlotData' from a mix of numeric and text columns. All
--- columns must have the same length.
-fromMixedColumns
-  :: [(Text, V.Vector Double)]
-  -> [(Text, V.Vector Text)]
-  -> PlotData
-fromMixedColumns numCols txtCols =
-  let allLens =  map (V.length . snd) numCols
-              ++ map (V.length . snd) txtCols
-  in case allLens of
-       []       -> emptyPlotData
-       (n : ns) ->
-         if all (== n) ns
-           then PlotData
-                  { pdNumeric = Map.fromList numCols
-                  , pdText    = Map.fromList txtCols
-                  , pdLength  = n
-                  }
-           else error "Hanalyze.Viz.PlotData.fromMixedColumns: \
-                       \column lengths disagree"
-
--- | Adapter type class: anything that can be projected to 'PlotData'
--- (Hackage @dataframe@, future SQL row source, ...). Adapters live
--- alongside the source type to keep @Viz@ free of source dependencies;
--- e.g. @Hanalyze.Viz.PlotData.DataFrame@ provides the @ToPlotData@
--- instance for @DataFrame@.
-class ToPlotData a where
-  toPlotData :: a -> PlotData
diff --git a/src/Hanalyze/Viz/PlotData/DataFrame.hs b/src/Hanalyze/Viz/PlotData/DataFrame.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/PlotData/DataFrame.hs
+++ /dev/null
@@ -1,50 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.PlotData.DataFrame
--- Description : Hackage dataframe 用の ToPlotData インスタンス (数値/テキスト列を PlotData に変換)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -Wno-orphans #-}
--- | 'ToPlotData' instance for Hackage @dataframe@.
---
--- Kept in its own module so 'Hanalyze.Viz.PlotData' itself does not
--- depend on @dataframe@; future backends (Parquet streaming, SQL, ...)
--- can ship analogous adapter modules without touching the core.
---
--- The instance copies all numeric columns into @pdNumeric@ and all
--- (parseable) text columns into @pdText@. Columns that do not match
--- either projection are dropped silently — Vega specs only ever
--- reference columns by name, so missing columns surface as runtime
--- errors at the spec level rather than at conversion time.
-module Hanalyze.Viz.PlotData.DataFrame
-  ( -- Re-exports
-    PlotData
-  , ToPlotData (..)
-  ) where
-
-import qualified Data.Map.Strict          as Map
-import           Data.Text                (Text)
-import qualified Data.Vector              as V
-import qualified DataFrame.Internal.DataFrame  as DX
-
-import           Hanalyze.DataIO.Convert  (getDoubleVec, getTextVec)
-import           Hanalyze.Viz.PlotData    (PlotData (..), ToPlotData (..),
-                                           emptyPlotData)
-
-instance ToPlotData DX.DataFrame where
-  toPlotData df = case DX.columnNames df of
-    [] -> emptyPlotData
-    cs ->
-      let pickNumeric n = (\v -> (n, v)) <$> getDoubleVec n df
-          pickText    n = (\v -> (n, v)) <$> getTextVec   n df
-          numCols       = [ c | Just c <- map pickNumeric cs ]
-          txtCols       = [ c | Just c <- map pickText    cs ]
-          rowLen        = maximum
-                            (0 :  map (V.length . snd) numCols
-                               ++ map (V.length . snd) txtCols)
-      in PlotData
-           { pdNumeric = Map.fromList numCols
-           , pdText    = Map.fromList txtCols
-           , pdLength  = rowLen
-           }
diff --git a/src/Hanalyze/Viz/Report.hs b/src/Hanalyze/Viz/Report.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/Report.hs
+++ /dev/null
@@ -1,379 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.Report
--- Description : MCMC 結果の統合 HTML レポート (モデル DAG・事後要約表・診断プロット・pairs 散布図)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- | Integrated HTML report for MCMC results.
---
--- Bundles the model graph (Mermaid DAG), posterior summary table,
--- diagnostic plots, autocorrelation, and pairs scatter plots into a
--- single navigable page.
---
--- @
--- let report = (defaultReport "My Model" chain names)
---                { reportGraph = Just graph
---                , reportPairs = [("mu", "tau")]
---                }
--- renderReport "report.html" report
--- @
-module Hanalyze.Viz.Report
-  ( MCMCReport (..)
-  , defaultReport
-  , renderReport
-  ) where
-
-import Data.Aeson (encode)
-import Data.ByteString.Lazy (toStrict)
-import Data.Text (Text)
-import qualified Data.Text    as T
-import qualified Data.Text.IO as TIO
-import Data.Text.Encoding (decodeUtf8)
-import Graphics.Vega.VegaLite (fromVL)
-
-import Hanalyze.Model.HBM        (ModelGraph)
-import Hanalyze.MCMC.Core        (Chain (..), chainVals, posteriorMean, posteriorSD, posteriorQuantile)
-import Hanalyze.Stat.Summary     (SummaryRow (..), posteriorSummary)
-import Hanalyze.Viz.Assets       (vegaJS, vegaLiteJS, vegaEmbedJS)
-import Hanalyze.Viz.MCMC         (mcmcDiagnostics, mcmcDiagnosticsMulti, autocorrPlot, pairScatter)
-import Hanalyze.Viz.ModelGraph   (buildMermaid)
-import Hanalyze.Viz.Core         (defaultConfig)
-
-
--- ---------------------------------------------------------------------------
--- Report data type
--- ---------------------------------------------------------------------------
-
--- | Inputs to the integrated MCMC HTML report.
-data MCMCReport = MCMCReport
-  { reportTitle    :: Text                -- ^ Page title.
-  , reportGraph    :: Maybe ModelGraph    -- ^ Optional model DAG.
-  , reportChain    :: Chain               -- ^ Representative chain
-                                          --   (used for autocorrelation
-                                          --   and pair scatter).
-  , reportChains   :: [Chain]             -- ^ All parallel chains
-                                          --   (empty enables single-chain mode).
-  , reportParams   :: [Text]              -- ^ Parameters to display.
-  , reportPairs    :: [(Text, Text)]      -- ^ Optional pair-scatter combinations.
-  , reportMaxLag   :: Int                 -- ^ Maximum autocorrelation lag.
-  }
-
--- | Build a default 'MCMCReport' from a title, chain and parameter list.
-defaultReport :: Text -> Chain -> [Text] -> MCMCReport
-defaultReport title_ chain params = MCMCReport
-  { reportTitle  = title_
-  , reportGraph  = Nothing
-  , reportChain  = chain
-  , reportChains = []
-  , reportParams = params
-  , reportPairs  = []
-  , reportMaxLag = 40
-  }
-
--- ---------------------------------------------------------------------------
--- Top-level renderer
--- ---------------------------------------------------------------------------
-
--- | Write the full integrated MCMC report to an HTML file.
-renderReport :: FilePath -> MCMCReport -> IO ()
-renderReport path rpt =
-  TIO.writeFile path (buildHtml rpt)
-
--- ---------------------------------------------------------------------------
--- HTML builder
--- ---------------------------------------------------------------------------
-
-buildHtml :: MCMCReport -> Text
-buildHtml rpt = T.unlines $
-  [ "<!DOCTYPE html>"
-  , "<html lang=\"ja\">"
-  , "<head>"
-  , "  <meta charset=\"utf-8\">"
-  , "  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
-  , "  <title>" <> reportTitle rpt <> "</title>"
-  , "  <script>" <> vegaJS      <> "</script>"
-  , "  <script>" <> vegaLiteJS  <> "</script>"
-  , "  <script>" <> vegaEmbedJS <> "</script>"
-  , "  <script src=\"https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js\"></script>"
-  , "  <style>"
-  , css
-  , "  </style>"
-  , "</head>"
-  , "<body>"
-  , nav rpt
-  , "<main>"
-  ] ++
-  maybe [] modelGraphSection (reportGraph rpt) ++
-  [ summarySection rpt
-  , diagnosticsSection rpt
-  , autocorrSection rpt
-  ] ++
-  pairSection rpt ++
-  [ "</main>"
-  , "<script>"
-  , "mermaid.initialize({ startOnLoad: true, theme: 'default' });"
-  , vegaEmbedJs rpt
-  , "document.querySelectorAll('.nav-link').forEach(a => {"
-  , "  a.addEventListener('click', e => {"
-  , "    e.preventDefault();"
-  , "    document.querySelector(a.getAttribute('href')).scrollIntoView({ behavior: 'smooth' });"
-  , "  });"
-  , "});"
-  , "</script>"
-  , "</body>"
-  , "</html>"
-  ]
-
--- ---------------------------------------------------------------------------
--- CSS
--- ---------------------------------------------------------------------------
-
-css :: Text
-css = T.unlines
-  [ "    * { box-sizing: border-box; margin: 0; padding: 0; }"
-  , "    body { font-family: 'Segoe UI', sans-serif; background: #f0f2f5; color: #333; }"
-  , "    nav { position: sticky; top: 0; z-index: 100; background: #2c3e50;"
-  , "          padding: 10px 24px; display: flex; gap: 20px; align-items: center; }"
-  , "    nav h1 { color: #ecf0f1; font-size: 1em; flex: 1; }"
-  , "    .nav-link { color: #bdc3c7; text-decoration: none; font-size: .85em; }"
-  , "    .nav-link:hover { color: #fff; }"
-  , "    main { max-width: 1100px; margin: 0 auto; padding: 30px 20px; }"
-  , "    section { background: white; border-radius: 10px; padding: 24px;"
-  , "              margin-bottom: 28px; box-shadow: 0 2px 8px rgba(0,0,0,.08); }"
-  , "    h2 { font-size: 1.1em; color: #2c3e50; margin-bottom: 16px;"
-  , "         border-bottom: 2px solid #e8ecf0; padding-bottom: 8px; }"
-  , "    .stat-grid { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 20px; }"
-  , "    .stat-box { background: #f8f9fa; border-radius: 8px; padding: 14px 20px;"
-  , "                min-width: 140px; text-align: center; }"
-  , "    .stat-box .label { font-size: .75em; color: #888; text-transform: uppercase; }"
-  , "    .stat-box .value { font-size: 1.4em; font-weight: 600; color: #2c3e50; }"
-  , "    table { width: 100%; border-collapse: collapse; font-size: .9em; }"
-  , "    th { background: #f0f2f5; text-align: right; padding: 8px 14px;"
-  , "         font-weight: 600; color: #555; }"
-  , "    th:first-child { text-align: left; }"
-  , "    td { padding: 7px 14px; border-bottom: 1px solid #f0f2f5; text-align: right; }"
-  , "    td:first-child { text-align: left; font-family: monospace; font-weight: 500; }"
-  , "    tr:last-child td { border-bottom: none; }"
-  , "    .vl-wrap { overflow-x: auto; }"
-  , "    .pair-grid { display: flex; flex-wrap: wrap; gap: 16px; }"
-  , "    .mermaid { text-align: center; }"
-  , "    .legend { margin-top: 12px; font-size: .82em; color: #666; }"
-  , "    .legend span { display: inline-block; width: 11px; height: 11px;"
-  , "                   border-radius: 2px; margin-right: 4px; vertical-align: middle; }"
-  ]
-
--- ---------------------------------------------------------------------------
--- Nav bar
--- ---------------------------------------------------------------------------
-
-nav :: MCMCReport -> Text
-nav rpt = T.unlines $
-  [ "<nav>"
-  , "  <h1>" <> reportTitle rpt <> "</h1>"
-  ] ++
-  maybe [] (const ["  <a class=\"nav-link\" href=\"#sec-graph\">Model Graph</a>"]) (reportGraph rpt) ++
-  [ "  <a class=\"nav-link\" href=\"#sec-summary\">Summary</a>"
-  , "  <a class=\"nav-link\" href=\"#sec-diagnostics\">Diagnostics</a>"
-  , "  <a class=\"nav-link\" href=\"#sec-autocorr\">Autocorrelation</a>"
-  ] ++
-  (if null (reportPairs rpt) then []
-   else ["  <a class=\"nav-link\" href=\"#sec-pairs\">Pair Plots</a>"]) ++
-  [ "</nav>" ]
-
--- ---------------------------------------------------------------------------
--- Model graph section
--- ---------------------------------------------------------------------------
-
-modelGraphSection :: ModelGraph -> [Text]
-modelGraphSection mg =
-  [ "<section id=\"sec-graph\">"
-  , "  <h2>Model Graph</h2>"
-  , "  <div class=\"mermaid\">"
-  , buildMermaid mg
-  , "  </div>"
-  , "  <div class=\"legend\">"
-  , "    <span style=\"background:#4C72B0\"></span>latent &nbsp;&nbsp;"
-  , "    <span style=\"background:#DD8844\"></span>observed"
-  , "  </div>"
-  , "</section>"
-  ]
-
--- ---------------------------------------------------------------------------
--- Summary section
--- ---------------------------------------------------------------------------
-
-summarySection :: MCMCReport -> Text
-summarySection rpt =
-  let chain  = reportChain rpt
-      params = reportParams rpt
-      total  = chainTotal chain
-      acc    = chainAccepted chain
-      rate   = if total == 0 then 0 else fromIntegral acc / fromIntegral total :: Double
-      nSamp  = length (chainSamples chain)
-
-      fmtD :: Int -> Double -> Text
-      fmtD dec v = T.pack (showF dec v)
-
-      showF :: Int -> Double -> String
-      showF 1 v = let s = show (round (v * 10) :: Int)
-                      (i, f) = splitAt (length s - 1) s
-                  in (if null i then "0" else i) ++ "." ++ f
-      showF _ v = let s = show (round v :: Int) in s
-
-      statBox lbl val = T.unlines
-        [ "    <div class=\"stat-box\">"
-        , "      <div class=\"label\">" <> lbl <> "</div>"
-        , "      <div class=\"value\">" <> val <> "</div>"
-        , "    </div>"
-        ]
-
-      get f p = maybe 0.0 id (f p chain)
-
-      multiChain = length (reportChains rpt) > 1
-      allChains  = if multiChain then reportChains rpt else [chain]
-
-      -- Hanalyze.Stat.Summary に統合 (Phase H6): mean/sd/HDI/ESS/R-hat を一括取得
-      rows = posteriorSummary params allChains
-
-      tableRow row =
-        let rhatCell = case srRhat row of
-              Nothing -> if multiChain then "<td>—</td>" else ""
-              Just r  -> "<td style=\"color:"
-                       <> (if r < 1.01 then "#2a9d2a" else "#cc2222")
-                       <> "\">" <> fmt4 r <> "</td>"
-        in T.unlines
-          [ "      <tr>"
-          , "        <td>" <> srName row <> "</td>"
-          , "        <td>" <> fmt4 (srMean row) <> "</td>"
-          , "        <td>" <> fmt4 (srSD   row) <> "</td>"
-          , "        <td>" <> fmt4 (srHdiLo row) <> "</td>"
-          , "        <td>" <> fmt4 (srHdiHi row) <> "</td>"
-          , "        <td>" <> T.pack (show (round (srEssV row) :: Int)) <> "</td>"
-          , rhatCell
-          , "      </tr>"
-          ]
-      rhatHeader = if multiChain then "<th>R-hat</th>" else ""
-
-  in T.unlines
-    [ "<section id=\"sec-summary\">"
-    , "  <h2>Posterior Summary</h2>"
-    , "  <div class=\"stat-grid\">"
-    , statBox "Samples"         (T.pack (show nSamp))
-    , statBox "Acceptance"      (fmtD 1 (rate * 100) <> "%")
-    , statBox "Chains"          (T.pack (show (length allChains)))
-    , statBox "Total Proposals" (T.pack (show total))
-    , "  </div>"
-    , "  <table>"
-    , "    <thead><tr>"
-    , "      <th>Parameter</th><th>Mean</th><th>SD</th>"
-    , "      <th>HDI 3%</th><th>HDI 97%</th><th>ESS (bulk)</th>" <> rhatHeader
-    , "    </tr></thead>"
-    , "    <tbody>"
-    , T.concat (map tableRow rows)
-    , "    </tbody>"
-    , "  </table>"
-    , "</section>"
-    ]
-
-fmt4 :: Double -> Text
-fmt4 v = T.pack (showFFloat4 v)
-
-showFFloat4 :: Double -> String
-showFFloat4 v
-  | isNaN v || isInfinite v = show v
-  | otherwise =
-      let scaled = round (v * 10000) :: Integer
-          (whole, frac) = divMod (abs scaled) 10000
-          sign = if v < 0 then "-" else ""
-      in sign ++ show whole ++ "." ++ pad4 (fromIntegral frac)
-  where
-    pad4 :: Int -> String
-    pad4 n = let s = show n in replicate (4 - length s) '0' ++ s
-
--- ---------------------------------------------------------------------------
--- Diagnostics section (trace + posterior hist)
--- ---------------------------------------------------------------------------
-
-diagnosticsSection :: MCMCReport -> Text
-diagnosticsSection rpt =
-  let cfg    = defaultConfig (reportTitle rpt <> " — Diagnostics")
-      chains = reportChains rpt
-      spec   = if length chains > 1
-               then mcmcDiagnosticsMulti cfg (reportParams rpt) chains
-               else mcmcDiagnostics      cfg (reportParams rpt) (reportChain rpt)
-      json   = decodeUtf8 . toStrict . encode . fromVL $ spec
-      subtitle = if length chains > 1
-                 then " (" <> T.pack (show (length chains)) <> " chains)"
-                 else ""
-  in T.unlines
-    [ "<section id=\"sec-diagnostics\">"
-    , "  <h2>MCMC Diagnostics (KDE &amp; Trace)" <> subtitle <> "</h2>"
-    , "  <div class=\"vl-wrap\">"
-    , "    <div id=\"vl-diagnostics\"></div>"
-    , "  </div>"
-    , "  <script>window.__vlDiag = " <> json <> ";</script>"
-    , "</section>"
-    ]
-
--- ---------------------------------------------------------------------------
--- Autocorrelation section
--- ---------------------------------------------------------------------------
-
-autocorrSection :: MCMCReport -> Text
-autocorrSection rpt =
-  let cfg   = defaultConfig (reportTitle rpt <> " — Autocorrelation")
-      spec  = autocorrPlot cfg (reportMaxLag rpt) (reportParams rpt) (reportChain rpt)
-      json  = decodeUtf8 . toStrict . encode . fromVL $ spec
-  in T.unlines
-    [ "<section id=\"sec-autocorr\">"
-    , "  <h2>Autocorrelation</h2>"
-    , "  <div class=\"vl-wrap\">"
-    , "    <div id=\"vl-autocorr\"></div>"
-    , "  </div>"
-    , "  <script>window.__vlAcf = " <> json <> ";</script>"
-    , "</section>"
-    ]
-
--- ---------------------------------------------------------------------------
--- Pair scatter section
--- ---------------------------------------------------------------------------
-
-pairSection :: MCMCReport -> [Text]
-pairSection rpt
-  | null (reportPairs rpt) = []
-  | otherwise =
-      [ "<section id=\"sec-pairs\">"
-      , "  <h2>Pair Scatter Plots</h2>"
-      , "  <div class=\"pair-grid\">"
-      ] ++
-      zipWith mkPairDiv [0 :: Int ..] (reportPairs rpt) ++
-      [ "  </div>"
-      , "</section>"
-      ]
-  where
-    mkPairDiv idx (xn, yn) =
-      let cfg  = defaultConfig (xn <> " vs " <> yn)
-          spec = pairScatter cfg xn yn (reportChain rpt)
-          json = decodeUtf8 . toStrict . encode . fromVL $ spec
-          divId = "vl-pair-" <> T.pack (show idx)
-      in T.unlines
-          [ "    <div id=\"" <> divId <> "\"></div>"
-          , "    <script>window.__vlPair" <> T.pack (show idx) <> " = " <> json <> ";</script>"
-          ]
-
--- ---------------------------------------------------------------------------
--- vegaEmbed JS (all plots in one script block)
--- ---------------------------------------------------------------------------
-
-vegaEmbedJs :: MCMCReport -> Text
-vegaEmbedJs rpt = T.unlines $
-  [ "vegaEmbed('#vl-diagnostics', window.__vlDiag, {renderer:'canvas',actions:false}).catch(console.error);"
-  , "vegaEmbed('#vl-autocorr',    window.__vlAcf,  {renderer:'canvas',actions:false}).catch(console.error);"
-  ] ++
-  zipWith mkEmbedCall [0 :: Int ..] (reportPairs rpt)
-  where
-    mkEmbedCall idx _ =
-      let divId = "#vl-pair-" <> T.pack (show idx)
-          varNm = "window.__vlPair" <> T.pack (show idx)
-      in "vegaEmbed('" <> divId <> "', " <> varNm <> ", {renderer:'canvas',actions:false}).catch(console.error);"
diff --git a/src/Hanalyze/Viz/ReportBuilder.hs b/src/Hanalyze/Viz/ReportBuilder.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/ReportBuilder.hs
+++ /dev/null
@@ -1,2540 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.ReportBuilder
--- Description : 全モデル/解析種別共通の合成型 HTML レポートビルダー (ReportSection / Reportable)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- | Compositional HTML report builder.
---
--- A unified report API across all model and analysis types: ridge, kernel,
--- spline, robust GP, Taguchi, regrid, and so on. Replaces the model-
--- specific 'Hanalyze.Viz.AnalysisReport'.
---
--- Design principles:
---
---   * 'ReportSection' is a sum type representing one HTML section.
---   * The caller (CLI or library user) builds a @[ReportSection]@.
---   * 'renderReport' lays the sections out into a single self-contained
---     HTML file (Vega-Lite assets bundled).
---   * The @Reportable@ typeclass generates default section sets from each
---     fit type.
---
--- 利用例:
---
--- @
--- import Hanalyze.Viz.ReportBuilder
--- renderReport "out.html" (defaultReportConfig "My Analysis")
---   [ secDataOverview df ["x"] "y"
---   , secModelOverview "Ridge regression" "y = β₀ + β₁x" Nothing
---   , secCoefficients [("β₀", 1.2), ("β₁", 2.4)] (Just ("R²", 0.96))
---   , secFitScatter "x" "y" xs ys (Just smooth)
---   , secResiduals fitted resids
---   ]
--- @
-module Hanalyze.Viz.ReportBuilder
-  ( -- * 設定
-    ReportConfig (..)
-  , defaultReportConfig
-    -- * Sections
-  , ReportSection (..)
-  , SmoothCurve (..)
-    -- * Section builders (smart constructors)
-  , secDataOverview
-  , secModelOverview
-  , secModelOverviewLink
-  , secModelOverviewExtras
-  , secKeyValue
-  , secCoefficients
-  , secFitScatter
-  , secResiduals
-  , secBarChart
-  , secVega
-  , secMermaid
-  , secTable
-  , secMarkdown
-  , secHtml
-  , secCollapsible
-  , secCard
-  , secStatRow
-    -- * Markdown-file ingestion (for appendices)
-  , secAppendixFromMd
-  , renderSimpleMarkdown
-    -- * MCMC and posterior diagnostics
-  , secMCMCDiagnostics
-  , secMCMCDiagnosticsMulti
-  , secMCMCAutocorr
-  , secMCMCPair
-  , secPosteriorSummary
-    -- * Model-comparison and diagnostic sections
-  , secComparisonTable
-  , secForestPlot
-  , secFeatureImportance
-  , secPPC
-    -- * Additional visualization sections
-  , secCalibration
-  , sec3DScatter
-  , secHeatmap
-    -- * Interpolation / regrid report
-  , InterpReport (..)
-  , defaultInterpReport
-  , secInterpolation
-    -- * Interactive prediction (LM / GLM)
-  , secInteractiveLM
-  , secInteractiveMulti
-  , InteractiveModel (..)
-    -- * Interactive prediction (multivariate RFF ridge)
-  , secInteractiveRFFMV
-  , InteractiveRFFMV (..)
-    -- * Interactive prediction (multi-output: 1 input → q output curves)
-  , secInteractiveMultiOut
-  , InteractiveMultiOut (..)
-  , InteractivePredictor (..)
-  , mkInteractiveMOLinear
-  , mkInteractiveMOKernelRBF
-    -- * Rendering
-  , renderReport
-    -- * Reportable typeclass
-  , Reportable (..)
-    -- * Specialized Vega-Lite helpers
-  , regPathSpec
-  , forestPlotSpec
-  , ppcSpec
-  , calibrationSpec
-  , scatter3DSpec
-  , heatmapSpec
-  , interpolationOverlaySpec
-  , densityProfileSpec
-  , idAlignmentSpec
-  ) where
-
-import Data.Aeson (encode)
-import Data.ByteString.Lazy (toStrict)
-import Data.List (sort, sortBy)
-import Data.Ord (Down (..), comparing)
-import Data.Text (Text)
-import qualified Data.Text    as T
-import qualified Data.Text.IO as TIO
-import Data.Text.Encoding (decodeUtf8)
-import Graphics.Vega.VegaLite hiding (filter, name)
-import qualified Graphics.Vega.VegaLite as VL
-import Numeric (showFFloat)
-import qualified Data.Vector as V
-import Text.Printf (printf)
-
-import qualified DataFrame.Operations.Core     as DX
-import qualified DataFrame.Internal.DataFrame as DXD
-import Hanalyze.DataIO.Convert (getDoubleVec, getTextVec)
-import Hanalyze.MCMC.Core (Chain)
-import qualified Hanalyze.Stat.MCMC as SM
-import Hanalyze.Viz.Assets (vegaJS, vegaLiteJS, vegaEmbedJS)
-import Hanalyze.Viz.Core (PlotConfig (..), defaultConfig)
-import qualified Hanalyze.Viz.MCMC as VM
-
--- ---------------------------------------------------------------------------
--- 設定
--- ---------------------------------------------------------------------------
-
--- | Top-level report configuration.
-data ReportConfig = ReportConfig
-  { rcTitle    :: Text   -- ^ Report heading (used as both heading and HTML @\<title\>@).
-  , rcSubtitle :: Text   -- ^ Subtitle (hidden when empty).
-  } deriving (Show)
-
--- | Build a 'ReportConfig' from just a title (no subtitle).
-defaultReportConfig :: Text -> ReportConfig
-defaultReportConfig t = ReportConfig t ""
-
--- ---------------------------------------------------------------------------
--- セクション型
--- ---------------------------------------------------------------------------
-
--- | A smooth curve with an optional confidence band.
-data SmoothCurve = SmoothCurve
-  { scXs    :: [Double]
-  , scYs    :: [Double]
-  , scLower :: [Double]   -- ^ Empty when no band is desired.
-  , scUpper :: [Double]
-  } deriving (Show, Eq)
-
--- | Interactive multivariate RFF-ridge prediction model.
---
--- Carries everything the browser's JavaScript needs to update the
--- prediction curve when the user moves a slider:
---
--- * For each @z@ in @mainGrid@:
---     @x_full[k] = (k == mainAxisIdx) ? z : sliderValues[k]@.
--- * @arg_j = b_j + Σ_k ω_jk · x_full[k]@.
--- * @ŷ(z) = Σ_j w_j · σ_f √(2/D) · cos(arg_j)@.
-data InteractiveRFFMV = InteractiveRFFMV
-  { irfXCols        :: [Text]            -- ^ All predictor names (length @p@).
-  , irfYCol         :: Text              -- ^ Response column name.
-  , irfXObs         :: [[Double]]        -- ^ Observed @x@ as @p × n@ (column-major).
-  , irfYObs         :: [Double]          -- ^ Observed @y@ (length @n@).
-  , irfGroups       :: [Text]            -- ^ Per-observation group labels for color coding (length @n@).
-  , irfMainAxis     :: Text              -- ^ Name of the column varied along the x axis (e.g. @\"z\"@).
-  , irfMainGrid     :: [Double]          -- ^ x-axis grid (e.g. 100 evenly-spaced @z@ values).
-  , irfSliders      :: [(Text, Double, Double, Double)]
-                                         -- ^ Slider definitions
-                                         --   @[(name, min, mid, max)]@,
-                                         --   one per non-main-axis column.
-  , irfOmegasRowMaj :: [Double]          -- ^ @p × D@ frequency matrix in row-major order.
-  , irfBs           :: [Double]          -- ^ Phases (length @D@).
-  , irfSigmaF       :: Double            -- ^ Signal SD @σ_f@.
-  , irfDim          :: Int               -- ^ Feature dimension @D@.
-  , irfP            :: Int               -- ^ Input dimension @p@.
-  , irfWeights      :: [Double]          -- ^ Ridge weights (length @D@).
-  , irfStdMu        :: Maybe [Double]    -- ^ Standardization @μ@ (length @p@). Used by
-                                         --   JS to convert raw inputs to standardized space.
-  , irfStdSd        :: Maybe [Double]    -- ^ Standardization @σ@ (length @p@).
-  } deriving (Show)
-
--- | Interactive predictor with one input @x@ and @q@ outputs
--- @y(z_1..z_q)@.
---
--- The plot's x-axis is the output grid @z@; the y-axis is @y@. Moving
--- the input @x@ slider re-evaluates all @q@ outputs and updates the
--- curve.
-data InteractiveMultiOut = InteractiveMultiOut
-  { imoXCol     :: Text                       -- ^ Input column name (e.g. @\"dose\"@).
-  , imoYCol     :: Text                       -- ^ Output name (e.g. @\"potential V\"@).
-  , imoOutAxis  :: Text                       -- ^ Output-axis label (e.g. @\"z [nm]\"@).
-  , imoOutGrid  :: [Double]                   -- ^ Output grid (length @q@).
-  , imoXObs     :: [Double]                   -- ^ Observed inputs (length @n@).
-  , imoYObs     :: [[Double]]                 -- ^ Observed @Y@ (@n × q@, row = sample).
-  , imoXSlider  :: (Double, Double, Double)   -- ^ Slider range @(min, mid, max)@.
-  , imoPred     :: InteractivePredictor       -- ^ Underlying predictor.
-  } deriving (Show)
-
--- | Interactive multi-output predictor. Extensible for future RFF / GP
--- variants.
-data InteractivePredictor
-  = -- | Linear: @ŷ_j(x) = β0_j + β1_j · x@.
-    PredLinearMO
-      { plmoIntercepts :: [Double]   -- ^ Per-output intercept (length @q@).
-      , plmoSlopes     :: [Double]   -- ^ Per-output slope (length @q@).
-      }
-    -- | 1D RBF kernel ridge:
-    --   @ŷ_j(x) = Σ_i exp(-(x - x_i)²/(2h²)) · α_{ij}@.
-  | PredKernelRBF1
-      { pkrXTrain :: [Double]        -- ^ Training inputs (length @n@).
-      , pkrAlpha  :: [[Double]]      -- ^ Per-output kernel coefficients
-                                     --   (@n × q@, row = sample).
-      , pkrH      :: Double          -- ^ Kernel bandwidth.
-      }
-  deriving (Show)
-
--- | Interactive single-input multivariate-LM/GLM model.
-data InteractiveModel = InteractiveModel
-  { imXCols     :: [Text]              -- ^ Predictor names (length @p@).
-  , imYCol      :: Text                -- ^ Response name.
-  , imXValues   :: [[Double]]          -- ^ Observed predictors (@n × p@).
-  , imYValues   :: [Double]            -- ^ Observed response.
-  , imIntercept :: Double              -- ^ Intercept @β₀@.
-  , imBetas     :: [Double]            -- ^ Slopes @[β₁, …, β_p]@.
-  , imLink      :: Text                -- ^ Link name: @\"identity\"@,
-                                       --   @\"log\"@, @\"logit\"@,
-                                       --   @\"sqrt\"@.
-  , imSlider    :: [(Double, Double, Double)]
-                                       -- ^ Per-column slider range
-                                       --   @(min, mid, max)@.
-  , imCISigma   :: Maybe Double        -- ^ Residual @σ̂@ (for the CI;
-                                       --   'Nothing' disables the CI).
-  } deriving (Show)
-
--- | A single report section. The renderer walks a @[ReportSection]@ and
--- emits the corresponding HTML block for each variant.
-data ReportSection
-  = -- | データ概要: 列ごとの型/N/min/max/mean/SD + ヒストグラム
-    SecDataOverview DXD.DataFrame [Text] Text
-    -- | モデル概要: タイトル / 数式 / 任意の追加 info-box [(label,value)] / Mermaid DAG
-  | SecModelOverview Text Text [(Text, Text)] (Maybe Text)
-    -- | 係数表: ラベル/値 + オプションの (R² ラベル, 値)
-  | SecCoefficients [(Text, Double)] (Maybe (Text, Double))
-    -- | 散布図 + 滑らか曲線 (信頼帯あれば描画)
-  | SecFitScatter Text Text [Double] [Double] (Maybe SmoothCurve)
-    -- | 残差プロット (fitted vs residuals + Predicted vs Actual)
-  | SecResiduals [Double] [Double]
-    -- | 棒グラフ (要因効果や lambda パスなど)
-  | SecBarChart Text [(Text, Double)]
-    -- | 任意の Vega-Lite チャート
-  | SecVega Text VegaLite
-    -- | Mermaid.js DAG
-  | SecMermaid Text
-    -- | 任意テーブル: ヘッダ / 行
-  | SecTable Text [Text] [[Text]]
-    -- | "key: value" 形式の小テーブル
-  | SecKeyValue Text [(Text, Text)]
-    -- | Markdown 風テキスト (実体は <p> 内 plain HTML)
-  | SecMarkdown Text Text
-    -- | raw HTML 本体 (escape hatch)
-  | SecHtml Text Text
-    -- | 単変数 LM/GLM の対話的予測 (スライダー + リアルタイム scatter)。
-    --   フィールド: title / xCol / yCol / xs / ys / smooth / (xSliderMin, xSliderMax)
-  | SecInteractiveLM Text Text Text [Double] [Double] SmoothCurve (Double, Double)
-    -- | 多変量対話的予測。主軸選択 dropdown + 各副軸 slider + 散布図。
-  | SecInteractiveMulti Text InteractiveModel
-    -- | 多変量 RFF Ridge の対話的予測。横軸固定 + 副軸スライダ + 散布図。
-  | SecInteractiveRFFMV Text InteractiveRFFMV
-    -- | 多出力対話的予測 (1 入力 → q 出力)。
-  | SecInteractiveMultiOut Text InteractiveMultiOut
-    -- | 折りたたみ可能なグループ。子セクションを 1 つの details で囲む。
-    --   フィールド: title / openByDefault / 子セクション
-  | SecCollapsible Text Bool [ReportSection]
-    -- | 淡い背景色の囲みカード。SecCollapsible の内部などで使い、
-    --   関連する図表をひとまとめにする (常に開いた状態)。
-  | SecCard Text [ReportSection]
-    -- | フラットな統計行 (section 包装なし)。
-    --   info-box が横並びになる stat-row。
-  | SecStatRow [(Text, Text)]
-
--- ---------------------------------------------------------------------------
--- ビルダ
--- ---------------------------------------------------------------------------
-
--- | Data-overview section (per-column type, summary stats, histogram).
-secDataOverview :: DXD.DataFrame -> [Text] -> Text -> ReportSection
-secDataOverview = SecDataOverview
-
--- | Model overview without any extra info boxes (e.g. plain LM).
-secModelOverview :: Text -> Text -> Maybe Text -> ReportSection
-secModelOverview ty fm mer = SecModelOverview ty fm [] mer
-
--- | Model overview with a link function (used by GLM, GLMM, etc.).
-secModelOverviewLink :: Text       -- ^ Model kind.
-                     -> Text       -- ^ Formula (HTML allowed).
-                     -> Text       -- ^ Link function (e.g. @\"log\"@,
-                                   --   @\"logit\"@, @\"identity\"@).
-                     -> Maybe Text -- ^ Optional Mermaid DAG.
-                     -> ReportSection
-secModelOverviewLink ty fm link mer =
-  SecModelOverview ty fm [("Link function", link)] mer
-
--- | Model overview with arbitrary additional info-box rows
--- (e.g. HBM sampler kind, GP kernel choice).
-secModelOverviewExtras :: Text             -- ^ Model kind.
-                       -> Text             -- ^ Formula (HTML allowed).
-                       -> [(Text, Text)]   -- ^ Extra @(label, value)@
-                                           --   info-box entries.
-                       -> Maybe Text       -- ^ Optional Mermaid DAG.
-                       -> ReportSection
-secModelOverviewExtras = SecModelOverview
-
--- | Free-form key-value table section.
-secKeyValue :: Text -> [(Text, Text)] -> ReportSection
-secKeyValue = SecKeyValue
-
--- | Coefficients table with an optional trailing @(label, value)@ row
--- (e.g. for R²).
-secCoefficients :: [(Text, Double)] -> Maybe (Text, Double) -> ReportSection
-secCoefficients = SecCoefficients
-
--- | Fit-vs-data scatter plot with an optional smooth curve overlay.
-secFitScatter :: Text -> Text -> [Double] -> [Double]
-              -> Maybe SmoothCurve -> ReportSection
-secFitScatter = SecFitScatter
-
--- | Residual diagnostic plot (residuals vs fitted).
-secResiduals :: [Double] -> [Double] -> ReportSection
-secResiduals = SecResiduals
-
--- | Bar-chart section.
-secBarChart :: Text -> [(Text, Double)] -> ReportSection
-secBarChart = SecBarChart
-
--- | Embed a raw 'VegaLite' spec as a section.
-secVega :: Text -> VegaLite -> ReportSection
-secVega = SecVega
-
--- | Embed a Mermaid-source diagram (rendered client-side).
-secMermaid :: Text -> ReportSection
-secMermaid = SecMermaid
-
--- | HTML table section: @secTable title headers rows@.
-secTable :: Text -> [Text] -> [[Text]] -> ReportSection
-secTable = SecTable
-
--- | Markdown section: rendered with a small built-in markdown subset.
-secMarkdown :: Text -> Text -> ReportSection
-secMarkdown = SecMarkdown
-
--- | Raw-HTML section. Trusted: emitted verbatim into the page.
-secHtml :: Text -> Text -> ReportSection
-secHtml = SecHtml
-
--- | Collapsible group. The 'Bool' controls the initial expanded state.
-secCollapsible :: Text -> Bool -> [ReportSection] -> ReportSection
-secCollapsible = SecCollapsible
-
--- | Light-shaded card group. Useful for clustering related plots inside
--- a regression-result section.
-secCard :: Text -> [ReportSection] -> ReportSection
-secCard = SecCard
-
--- | Flat key-value statistics row (no surrounding section box). Useful
--- to lay summary numbers between Cards.
-secStatRow :: [(Text, Text)] -> ReportSection
-secStatRow = SecStatRow
-
--- ---------------------------------------------------------------------------
--- Markdown appendix
--- ---------------------------------------------------------------------------
-
--- | Read the given markdown file, render it through the built-in
--- markdown subset
--- appendix セクションとして返す。
---
--- サポートする markdown 機能:
--- - 見出し: @# H1@, @## H2@, @### H3@
--- - 段落: 空行で区切られた連続行
--- - 箇条書き: @- item@
--- - インライン: @**bold**@, @*italic*@, @\`code\`@
--- - リンク: @[text](url)@
--- - インラインコード周辺は等幅フォント
-secAppendixFromMd :: Text -> FilePath -> IO ReportSection
-secAppendixFromMd title path = do
-  contents <- TIO.readFile path
-  let html  = renderSimpleMarkdown contents
-      icon  = "<span class=\"sec-icon\">&#128218;</span>"
-      tFull = icon <> " " <> title
-  -- 折りたたみ可能 section として返す (default open)
-  return (SecHtml title $ T.unlines
-    [ "<section class=\"collapsible-wrap appendix-md\">"
-    , "  <details open>"
-    , "    <summary><h2>" <> tFull <> "</h2></summary>"
-    , "    <div class=\"collapsible-body md-body\">"
-    , html
-    , "    </div>"
-    , "  </details>"
-    , "</section>"
-    ])
-
--- | 簡易 markdown → HTML 変換。フル機能ではない。
-renderSimpleMarkdown :: Text -> Text
-renderSimpleMarkdown txt =
-  let lns      = T.lines txt
-      blocks   = groupBlocks lns
-      htmlBlks = map renderBlock blocks
-  in T.intercalate "\n" htmlBlks
-
--- | 行群を「ブロック」に分割。空行で区切る。
-groupBlocks :: [Text] -> [[Text]]
-groupBlocks = filter (not . all T.null) . splitOn T.null
-  where
-    splitOn _ [] = []
-    splitOn p xs =
-      let (chunk, rest) = break p xs
-          rest' = dropWhile p rest
-      in chunk : splitOn p rest'
-
--- | ブロック (連続行のリスト) を HTML 化。
-renderBlock :: [Text] -> Text
-renderBlock []       = ""
-renderBlock ls@(l:_)
-  | "# "  `T.isPrefixOf` l =
-      "<h3>"  <> renderInline (T.drop 2 l) <> "</h3>"
-  | "## " `T.isPrefixOf` l =
-      "<h4>"  <> renderInline (T.drop 3 l) <> "</h4>"
-  | "### " `T.isPrefixOf` l =
-      "<h5>" <> renderInline (T.drop 4 l) <> "</h5>"
-  | all isListLine ls =
-      "<ul>" <> T.intercalate "\n"
-                 [ "<li>" <> renderInline (T.drop 2 li) <> "</li>"
-                 | li <- ls
-                 , let li' = T.stripStart li
-                 , let _ = li' ]  -- ダミー (li 自体を使う)
-             <> "</ul>"
-  | otherwise =
-      "<p>" <> renderInline (T.intercalate " " ls) <> "</p>"
-  where
-    isListLine x = "- " `T.isPrefixOf` T.stripStart x
-
--- | インラインフォーマット: bold/italic/code/link を順に置換。
--- 数式 ($...$, $$...$$) は MathJax が処理するため、ここでは触らずに保持。
--- ただし $...$ 内の '*' を italic と誤認しないよう、まず数式部分を退避してから処理する。
-renderInline :: Text -> Text
-renderInline txt =
-  let (chunks, maths) = extractMath txt
-      processed = applyLinks . applyCode . applyItalic . applyBold $ chunks
-  in restoreMath processed maths
-  where
-    applyBold t   = pairReplace "**" "<strong>" "</strong>" t
-    applyItalic t = pairReplace "*"  "<em>"     "</em>"     t
-    applyCode t   = pairReplace "`"  "<code>"   "</code>"   t
-    -- [text](url) → <a href="url">text</a>
-    applyLinks t = case T.breakOn "[" t of
-      (pre, "")   -> pre
-      (pre, rest) ->
-        case T.breakOn "](" (T.drop 1 rest) of
-          (lbl, "") -> pre <> rest
-          (lbl, rest1) ->
-            case T.breakOn ")" (T.drop 2 rest1) of
-              (url, "") -> pre <> rest
-              (url, rest2) ->
-                pre <> "<a href=\"" <> url <> "\">" <> lbl <> "</a>"
-                    <> applyLinks (T.drop 1 rest2)
-
--- | 開始/終了マーカーが交互に対になるとして置換。簡易版。
-pairReplace :: Text -> Text -> Text -> Text -> Text
-pairReplace marker startTag endTag txt = go txt True
-  where
-    go t inOpen =
-      case T.breakOn marker t of
-        (pre, "")   -> pre
-        (pre, rest) ->
-          let tag  = if inOpen then startTag else endTag
-              rest' = T.drop (T.length marker) rest
-          in pre <> tag <> go rest' (not inOpen)
-
--- | $...$ や $$...$$ の数式範囲を抽出してプレースホルダ "@@MATHn@@" に置換、
--- 元の数式テキストをリストで返す。
-extractMath :: Text -> (Text, [Text])
-extractMath = go 0 ""
-  where
-    go n acc t
-      | "$$" `T.isPrefixOf` t =
-          case T.breakOn "$$" (T.drop 2 t) of
-            (math, rest) | not (T.null rest) ->
-              let placeholder = "@@MATH" <> T.pack (show n) <> "@@"
-                  full = "$$" <> math <> "$$"
-                  (txt', maths) = go (n+1) (acc <> placeholder) (T.drop 2 rest)
-              in (txt', full : maths)
-            _ -> (acc <> t, [])
-      | "$" `T.isPrefixOf` t =
-          case T.breakOn "$" (T.drop 1 t) of
-            (math, rest) | not (T.null rest) ->
-              let placeholder = "@@MATH" <> T.pack (show n) <> "@@"
-                  full = "$" <> math <> "$"
-                  (txt', maths) = go (n+1) (acc <> placeholder) (T.drop 1 rest)
-              in (txt', full : maths)
-            _ -> (acc <> t, [])
-      | T.null t = (acc, [])
-      | otherwise =
-          let (chunk, rest) = T.break (== '$') t
-          in go n (acc <> chunk) rest
-
-restoreMath :: Text -> [Text] -> Text
-restoreMath txt maths = foldr replaceOne txt (zip [0::Int ..] maths)
-  where
-    replaceOne (i, m) acc =
-      T.replace ("@@MATH" <> T.pack (show i) <> "@@") m acc
-
--- ---------------------------------------------------------------------------
--- MCMC セクションビルダ (Hanalyze.Viz.MCMC のラッパ)
--- ---------------------------------------------------------------------------
-
--- | 単一チェーンの MCMC 診断 (KDE + トレース)。
-secMCMCDiagnostics :: Text       -- ^ セクションタイトル
-                   -> [Text]     -- ^ パラメータ名
-                   -> Chain
-                   -> ReportSection
-secMCMCDiagnostics title params chain =
-  SecVega title (VM.mcmcDiagnostics (defaultConfig title) params chain)
-
--- | 多チェーン MCMC 診断 (KDE 合算 + 色分けトレース)。
-secMCMCDiagnosticsMulti :: Text -> [Text] -> [Chain] -> ReportSection
-secMCMCDiagnosticsMulti title params chains =
-  SecVega title (VM.mcmcDiagnosticsMulti (defaultConfig title) params chains)
-
--- | 自己相関プロット。
-secMCMCAutocorr :: Text -> Int -> [Text] -> Chain -> ReportSection
-secMCMCAutocorr title maxLag params chain =
-  SecVega title (VM.autocorrPlot (defaultConfig title) maxLag params chain)
-
--- | ペアスキャッタープロット。
-secMCMCPair :: Text -> Text -> Text -> Chain -> ReportSection
-secMCMCPair title pa pb chain =
-  SecVega title (VM.pairScatter (defaultConfig title) pa pb chain)
-
--- | 事後要約テーブル (mean / SD / 2.5% / 97.5% / ESS / R-hat)。
--- 入力: パラメータごとに (name, mean, sd, q025, q975, ess, rhat)。
-secPosteriorSummary
-  :: Text                                                    -- title
-  -> [(Text, Double, Double, Double, Double, Double, Maybe Double)]
-  -> ReportSection
-secPosteriorSummary title rows =
-  let headers = ["パラメータ", "事後平均", "SD", "2.5%", "97.5%", "ESS", "R-hat"]
-      body    = [ [ p
-                  , T.pack (printf "%.4f" m)
-                  , T.pack (printf "%.4f" sd)
-                  , T.pack (printf "%.4f" lo)
-                  , T.pack (printf "%.4f" hi)
-                  , T.pack (printf "%.0f" ess)
-                  , maybe "—" (T.pack . printf "%.3f") rhat
-                  ]
-                | (p, m, sd, lo, hi, ess, rhat) <- rows ]
-  in SecTable title headers body
-
--- ---------------------------------------------------------------------------
--- モデル比較・診断セクション (Cycle 1)
--- ---------------------------------------------------------------------------
-
--- | モデル比較テーブル。'secTable' のラッパだが、
--- @mBest@ で 0-based 行 index を渡すと、その行をハイライト表示する。
--- WAIC / LOO / RMSE などを横並びにし最良モデルを強調するのに使う。
-secComparisonTable
-  :: Text         -- ^ タイトル
-  -> [Text]       -- ^ ヘッダ
-  -> [[Text]]     -- ^ 行
-  -> Maybe Int    -- ^ 最良行 index (0-based、'Nothing' でハイライトなし)
-  -> ReportSection
-secComparisonTable title headers rows mBest = case mBest of
-  Nothing  -> SecTable title headers rows
-  Just idx -> SecHtml title (renderComparisonHtml headers rows idx)
-
-renderComparisonHtml :: [Text] -> [[Text]] -> Int -> Text
-renderComparisonHtml headers rows bestIdx =
-  let hdr = "<tr>" <> T.concat ["<th>" <> h <> "</th>" | h <- headers] <> "</tr>"
-      mkRow i r =
-        let style | i == bestIdx =
-                      " style=\"background:#fff7d6;font-weight:600\""
-                  | otherwise = ""
-        in "<tr" <> style <> ">"
-           <> T.concat ["<td>" <> c <> "</td>" | c <- r]
-           <> "</tr>"
-      body = T.concat (zipWith mkRow [0 :: Int ..] rows)
-      legend = "<p style=\"margin-top:6px;font-size:.85em;color:#666\">"
-            <> "★ ハイライト行 = 最良 (黄色背景)</p>"
-  in "<table class=\"datatable\">" <> hdr <> body <> "</table>" <> legend
-
--- | Forest plot — 各パラメータの中央値 + 信用 (HDI/CI) 区間を横並び。
--- ベイズモデルの coefficient 比較や階層モデルの BLUP 表示に使う。
-secForestPlot
-  :: Text                                          -- ^ タイトル
-  -> [(Text, Double, Double, Double)]              -- ^ (label, lower, mean, upper)
-  -> ReportSection
-secForestPlot title rows = SecVega title (forestPlotSpec rows)
-
--- | 特徴量重要度バー — 値降順にソートして 'secBarChart' に渡す。
--- Random Forest / GBM の feature importance 表示用。
-secFeatureImportance :: Text -> [(Text, Double)] -> ReportSection
-secFeatureImportance title items =
-  SecBarChart title (sortBy (comparing (Down . snd)) items)
-
--- | Posterior Predictive Check — 観測データ密度 + 事後予測サンプルの密度を重ね描き。
--- 各 replicate の KDE を薄い線で、観測の KDE を太線で描画。
-secPPC
-  :: Text         -- ^ タイトル
-  -> [Double]     -- ^ 観測値 y_obs
-  -> [[Double]]   -- ^ 事後予測サンプル (replicate ごと、各長さ ~ length y_obs)
-  -> ReportSection
-secPPC title observed reps = SecVega title (ppcSpec observed reps)
-
--- | Calibration plot — 二値分類器の予測確率と観測頻度の対応図。
--- 入力データを 10 個のビン (`[0,0.1)..[0.9,1.0]`) に分割し、各ビンで
--- 予測確率の平均と観測 1 の頻度を計算し、対角線 (y = x) と重ねて描画。
--- 観測値は 0/1 (Bool 相当)。
-secCalibration
-  :: Text         -- ^ タイトル
-  -> [Double]     -- ^ 予測確率 p ∈ [0, 1]
-  -> [Double]     -- ^ 観測値 y ∈ {0, 1}
-  -> ReportSection
-secCalibration title pPred yObs =
-  SecVega title (calibrationSpec pPred yObs)
-
--- | 3D scatter (Vega-Lite は 3D 非対応のため、x/y 軸 + 色エンコード z で代用)。
--- z が連続なら viridis 系のグラデーション、離散ならカテゴリ色。
-sec3DScatter
-  :: Text         -- ^ セクションタイトル
-  -> Text         -- ^ x ラベル
-  -> Text         -- ^ y ラベル
-  -> Text         -- ^ z ラベル (色エンコード)
-  -> [Double] -> [Double] -> [Double]
-  -> ReportSection
-sec3DScatter title xL yL zL xs ys zs =
-  SecVega title (scatter3DSpec xL yL zL xs ys zs)
-
--- | 2D heatmap (rect mark + 値の色エンコード)。
--- 行ラベル × 列ラベルのグリッドに値を配置し、色強度で表現。
--- 例: 相関行列、混同行列、要因 × 水準の効果。
-secHeatmap
-  :: Text         -- ^ タイトル
-  -> [Text]       -- ^ 列ラベル
-  -> [Text]       -- ^ 行ラベル
-  -> [[Double]]   -- ^ 値 (rows × cols)
-  -> ReportSection
-secHeatmap title colLabels rowLabels values =
-  SecVega title (heatmapSpec colLabels rowLabels values)
-
--- ---------------------------------------------------------------------------
--- 対話的予測 (LM/GLM 単変数)
--- ---------------------------------------------------------------------------
-
--- | 単変数 LM/GLM の対話的予測セクション。
--- 与えられた x グリッド + 予測 y + バンドから埋め込み JS を生成し、
--- スライダーで予測点をリアルタイム移動できる scatter+chart を表示。
---
--- 引数:
---   * title         — セクション見出し
---   * xCol / yCol   — 軸ラベル
---   * xs / ys       — 観測データ
---   * sc            — グリッド + 予測曲線 (信頼帯付きなら band も描画)
---   * (xMin, xMax)  — スライダー範囲 (データ範囲 ±50% 推奨)
-secInteractiveLM
-  :: Text             -- title
-  -> Text             -- x 列名
-  -> Text             -- y 列名
-  -> [Double]         -- xs
-  -> [Double]         -- ys
-  -> SmoothCurve      -- 予測曲線 (信頼帯あれば band)
-  -> (Double, Double) -- スライダー範囲 (xMin, xMax)
-  -> ReportSection
-secInteractiveLM = SecInteractiveLM
-
--- | 多変量対話的予測 (主軸 dropdown + 副軸 slider + 散布図)。
-secInteractiveMulti :: Text -> InteractiveModel -> ReportSection
-secInteractiveMulti = SecInteractiveMulti
-
--- | 多変量 RFF Ridge の対話的予測セクション。
-secInteractiveRFFMV :: Text -> InteractiveRFFMV -> ReportSection
-secInteractiveRFFMV = SecInteractiveRFFMV
-
--- | 多出力対話的予測セクション (1 入力 → q 出力カーブ)。
-secInteractiveMultiOut :: Text -> InteractiveMultiOut -> ReportSection
-secInteractiveMultiOut = SecInteractiveMultiOut
-
--- | 線形多出力 fit から 'InteractiveMultiOut' を作る。
--- 入力: 列名・観測 x・観測 Y (n×q)・出力グリッド・intercepts (q)・slopes (q)・スライダ範囲
-mkInteractiveMOLinear
-  :: Text          -- xCol
-  -> Text          -- yCol
-  -> Text          -- outAxis label
-  -> [Double]      -- output grid (length q)
-  -> [Double]      -- observed x (length n)
-  -> [[Double]]    -- observed Y (n × q)
-  -> [Double]      -- intercepts (length q)
-  -> [Double]      -- slopes (length q)
-  -> (Double, Double, Double)   -- slider (min, mid, max)
-  -> InteractiveMultiOut
-mkInteractiveMOLinear xc yc oa grid xs ys ints slps slider =
-  InteractiveMultiOut xc yc oa grid xs ys slider (PredLinearMO ints slps)
-
--- | RBF Kernel Ridge 多出力 fit から 'InteractiveMultiOut' を作る。
--- alpha 行列は (n × q)、行 = sample。
-mkInteractiveMOKernelRBF
-  :: Text          -- xCol
-  -> Text          -- yCol
-  -> Text          -- outAxis label
-  -> [Double]      -- output grid (length q)
-  -> [Double]      -- observed x (length n)
-  -> [[Double]]    -- observed Y (n × q)
-  -> [Double]      -- training x (length n) — 通常は xObs と同じ
-  -> [[Double]]    -- alpha (n × q)
-  -> Double        -- bandwidth h
-  -> (Double, Double, Double)
-  -> InteractiveMultiOut
-mkInteractiveMOKernelRBF xc yc oa grid xs ys xtr alpha h slider =
-  InteractiveMultiOut xc yc oa grid xs ys slider (PredKernelRBF1 xtr alpha h)
-
--- ---------------------------------------------------------------------------
--- Reportable typeclass
--- ---------------------------------------------------------------------------
-
--- | フィット結果から既定セクション群を生成する型クラス。
--- ライブラリ利用者が `renderReport file cfg (toReport cfg df xCols yCol fit)` の
--- 形で簡潔に書ける。各モデル型 (RegFit / SplineFit / RobustGPFit 等) は
--- このクラスのインスタンスで既定セクションを定義する。
-class Reportable a where
-  toReport :: ReportConfig -> DXD.DataFrame -> [Text] -> Text -> a -> [ReportSection]
-
--- ---------------------------------------------------------------------------
--- レンダリング
--- ---------------------------------------------------------------------------
-
--- | 単一の自己完結 HTML ファイルとして書き出す。
-renderReport :: FilePath -> ReportConfig -> [ReportSection] -> IO ()
-renderReport path cfg sections =
-  TIO.writeFile path (buildHtml cfg sections)
-
-buildHtml :: ReportConfig -> [ReportSection] -> Text
-buildHtml cfg sections =
-  let pairs   = zip (map sectionId [0..]) sections
-      body    = T.intercalate "\n" [ renderSection sid s | (sid, s) <- pairs ]
-      scripts = T.intercalate "\n" [ sectionScript sid s | (sid, s) <- pairs ]
-      navBar  = mkNavBar cfg pairs
-  in T.unlines
-       [ "<!DOCTYPE html>"
-       , "<html lang=\"ja\">"
-       , "<head>"
-       , "<meta charset=\"utf-8\">"
-       , "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
-       , "<title>" <> rcTitle cfg <> "</title>"
-       , "<script>" <> vegaJS      <> "</script>"
-       , "<script>" <> vegaLiteJS  <> "</script>"
-       , "<script>" <> vegaEmbedJS <> "</script>"
-       , "<script src=\"https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js\"></script>"
-       , "<script>window.MathJax = { tex: {"
-       , "  inlineMath: [['$','$'], ['\\\\(','\\\\)']],"
-       , "  displayMath: [['$$','$$'], ['\\\\[','\\\\]']]"
-       , "}, svg: { fontCache: 'global' } };</script>"
-       , "<script src=\"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js\""
-       , "        async></script>"
-       , "<style>" <> css <> "</style>"
-       , "</head>"
-       , "<body>"
-       , navBar
-       , "<main>"
-       , body
-       , "</main>"
-       , "<script>"
-       , "mermaid.initialize({ startOnLoad: true, theme: 'default' });"
-       , scripts
-       , "document.querySelectorAll('.nav-link').forEach(a => {"
-       , "  a.addEventListener('click', e => {"
-       , "    const target = document.querySelector(a.getAttribute('href'));"
-       , "    if (target) { e.preventDefault();"
-       , "      target.scrollIntoView({ behavior: 'smooth' }); }"
-       , "  });"
-       , "});"
-       , "</script>"
-       , "</body>"
-       , "</html>"
-       ]
-
--- | ナビバーを構築。各 section のタイトルから生成。
-mkNavBar :: ReportConfig -> [(Text, ReportSection)] -> Text
-mkNavBar cfg pairs =
-  let links = [ "  <a class=\"nav-link\" href=\"#" <> sid <> "\">"
-                <> shortTitle s <> "</a>"
-              | (sid, s) <- pairs
-              , not (isInvisible s) ]
-  in T.unlines $
-       [ "<nav>"
-       , "  <h1>&#128202; " <> rcTitle cfg <> "</h1>"
-       ] ++ links ++ [ "</nav>" ]
-  where
-    isInvisible (SecHtml _ _) = False
-    isInvisible _             = False
-    shortTitle s = case s of
-      SecDataOverview {}     -> "データ"
-      SecModelOverview {}    -> "モデル"
-      SecCoefficients {}     -> "係数"
-      SecFitScatter {}       -> "散布図"
-      SecResiduals {}        -> "残差"
-      SecBarChart t _        -> if T.null t then "図表" else t
-      SecVega t _            -> if T.null t then "図表" else t
-      SecMermaid _           -> "DAG"
-      SecTable t _ _         -> if T.null t then "表" else t
-      SecKeyValue t _        -> if T.null t then "情報" else t
-      SecMarkdown t _        -> if T.null t then "備考" else t
-      SecHtml t _            -> if T.null t then "付録" else t
-      SecInteractiveLM {}    -> "対話的予測"
-      SecInteractiveMulti {} -> "対話的予測"
-      SecInteractiveRFFMV {} -> "対話的予測"
-      SecInteractiveMultiOut {} -> "対話的予測"
-      SecCollapsible t _ _   -> if T.null t then "詳細" else t
-      SecCard t _            -> if T.null t then "" else t
-      SecStatRow _           -> ""
-
-sectionId :: Int -> Text
-sectionId i = "sec_" <> T.pack (show i)
-
--- ---------------------------------------------------------------------------
--- セクション → HTML
--- ---------------------------------------------------------------------------
-
-renderSection :: Text -> ReportSection -> Text
-renderSection sid sec = case sec of
-  SecDataOverview df xs y     -> renderDataOverview sid df xs y
-  SecModelOverview ty fm extras mer -> renderModelOverview sid ty fm extras mer
-  SecCoefficients cs mr2      -> renderCoefficients sid cs mr2
-  SecFitScatter xc yc xs ys s -> renderFitScatter sid xc yc xs ys s
-  SecResiduals fit res        -> renderResiduals sid fit res
-  SecBarChart t vs            -> renderBarChart sid t vs
-  SecVega t _                 -> renderVegaPlaceholder sid t
-  SecMermaid m                -> renderMermaid sid m
-  SecTable t hs rs            -> renderTable sid t hs rs
-  SecKeyValue t kvs           -> renderKeyValue sid t kvs
-  SecMarkdown t txt           -> renderMarkdown sid t txt
-  -- SecHtml は <section> ラッパを付けず、生 HTML を <div id> で囲むのみ。
-  -- 利用側で <section> を含む完全な HTML を渡すことを想定 (secAppendixFromMd 等)。
-  SecHtml _ html              ->
-    "<div id=\"" <> sid <> "\" class=\"raw-section\">" <> html <> "</div>"
-  SecInteractiveLM t xc yc xs ys sc rng -> renderInteractiveLM sid t xc yc xs ys sc rng
-  SecInteractiveMulti t im   -> renderInteractiveMulti sid t im
-  SecInteractiveRFFMV t r    -> renderInteractiveRFFMV sid t r
-  SecInteractiveMultiOut t imo -> renderInteractiveMultiOut sid t imo
-  SecCollapsible t open children ->
-    renderCollapsible sid t open children
-  SecCard t children     -> renderCard sid t children
-  SecStatRow kvs         -> renderStatRow sid kvs
-
-wrapSection :: Text -> Text -> Text -> Text
-wrapSection sid title inner = T.unlines
-  [ "<section id=\"" <> sid <> "\">"
-  , if T.null title then "" else "  <h2>" <> title <> "</h2>"
-  , inner
-  , "</section>"
-  ]
-
--- | 折りたたみ可能な section 箱 (white bg、h2 をクリックで折りたたみ)。
--- データの特性 / モデル概要などで使う。
-collapsibleSection :: Text -> Text -> Bool -> Text -> Text
-collapsibleSection sid title open inner =
-  let attr = if open then " open" else ""
-  in T.unlines
-       [ "<section id=\"" <> sid <> "\" class=\"collapsible-wrap\">"
-       , "  <details" <> attr <> ">"
-       , "    <summary><h2>" <> title <> "</h2></summary>"
-       , "    <div class=\"collapsible-body\">"
-       , inner
-       , "    </div>"
-       , "  </details>"
-       , "</section>"
-       ]
-
--- データ概要 -----------------------------------------------------------------
-
--- | 列ごとの簡易分類: 数値列なら @NumCol [Double]@、Text 列なら @TxtCol [Text]@、
--- 取得不能なら 'NoCol'。ReportBuilder 内部のみで使用。
-data ColView = NumCol [Double] | TxtCol [Text] | NoCol
-
-classifyCol :: Text -> DXD.DataFrame -> ColView
-classifyCol c df = case getDoubleVec c df of
-  Just v  -> NumCol (V.toList v)
-  Nothing -> case getTextVec c df of
-    Just v  -> TxtCol (V.toList v)
-    Nothing -> NoCol
-
-renderDataOverview :: Text -> DXD.DataFrame -> [Text] -> Text -> Text
-renderDataOverview sid df xCols yCol =
-  let allCols  = xCols ++ [yCol]
-      relevant = [ (i, c, classifyCol c df) | (i, c) <- zip [0::Int ..] allCols ]
-      (n, _)   = DX.dimensions df
-      header   =
-        T.concat
-          [ "<tr>"
-          , "<th>列</th><th>型</th><th>N</th>"
-          , "<th>欠損</th>"
-          , "<th>最小</th><th>Q1</th><th>中央</th><th>Q3</th><th>最大</th>"
-          , "<th>平均</th><th>SD</th>"
-          , "<th>歪度</th><th>尖度</th>"
-          , "</tr>"
-          ]
-      rows = T.intercalate "\n" (map renderColRow relevant)
-      summary = "行数: <strong>" <> T.pack (show n)
-                <> "</strong>, 解析対象列: <strong>"
-                <> T.pack (show (length allCols)) <> "</strong>"
-      -- ヒストグラム (グループ全体で 1 つのトグル、各列は独立カード)
-      histBlocks = T.intercalate "\n"
-        [ "  <div class=\"hist-card\"><div class=\"hist-title\">" <> c
-          <> "</div><div class=\"vl-wrap\"><div id=\"hist_" <> sid
-          <> "_" <> T.pack (show i) <> "\"></div></div></div>"
-        | (i, c, NumCol _) <- relevant ]
-      title = "<span class=\"sec-icon\">&#128202;</span> データの特性"
-      body  = T.unlines
-        [ "<p class=\"sec-desc\">" <> summary <> "</p>"
-        , "<div class=\"table-scroll\"><table class=\"stats-table\">"
-        , "<thead>" <> header <> "</thead>"
-        , "<tbody>" <> rows <> "</tbody>"
-        , "</table></div>"
-        , "<details class=\"hist-toggle\"><summary>ヒストグラム (列ごと)</summary>"
-        , "<div class=\"hist-grid\">"
-        , histBlocks
-        , "</div>"
-        , "</details>"
-        ]
-  in collapsibleSection sid title True body
-  where
-    renderColRow (_, c, NumCol xs) =
-      let m  = length xs
-          ss = sort xs
-          mn = if m == 0 then 0 else minimum xs
-          mx = if m == 0 then 0 else maximum xs
-          mean = if m == 0 then 0 else sum xs / fromIntegral m
-          q1   = if m == 0 then 0 else ss !! (m `div` 4)
-          med  = if m == 0 then 0 else ss !! (m `div` 2)
-          q3   = if m == 0 then 0 else ss !! (3 * m `div` 4)
-          var  = if m <= 1 then 0
-                 else sum [(x - mean) ^ (2 :: Int) | x <- xs]
-                       / fromIntegral (m - 1)
-          sdv  = sqrt var
-          skew = if sdv <= 1e-12 then 0
-                 else sum [((x - mean) / sdv) ^ (3 :: Int) | x <- xs]
-                      / fromIntegral m
-          kurt = if sdv <= 1e-12 then 0
-                 else sum [((x - mean) / sdv) ^ (4 :: Int) | x <- xs]
-                      / fromIntegral m - 3
-      in "<tr>" <> T.intercalate ""
-           [ td c, td "numeric", td (T.pack (show m)), td "0"
-           , td (showD4 mn), td (showD4 q1), td (showD4 med)
-           , td (showD4 q3), td (showD4 mx)
-           , td (showD4 mean), td (showD4 sdv)
-           , td (showD4 skew), td (showD4 kurt)
-           ] <> "</tr>"
-    renderColRow (_, c, TxtCol xs) =
-      let m  = length xs
-          uniq = length (unique xs)
-      in "<tr>" <> T.intercalate ""
-           [ td c, td "text", td (T.pack (show m))
-           , td "0"
-           , td "—", td "—", td "—", td "—", td "—", td "—"
-           , td ("unique=" <> T.pack (show uniq))
-           , td "—"
-           ] <> "</tr>"
-    renderColRow (_, c, NoCol) =
-      "<tr><td>" <> c <> "</td><td colspan=12>(missing)</td></tr>"
-    td x = "<td>" <> x <> "</td>"
-    unique = foldr (\x acc -> if x `elem` acc then acc else x : acc) []
-
--- | データ概要セクションのスクリプト: 各 numeric 列のヒストグラムを embed。
-dataOverviewScript :: Text -> DXD.DataFrame -> [Text] -> Text -> Text
-dataOverviewScript sid df xCols yCol =
-  let allCols = xCols ++ [yCol]
-      pairs = [ (i, c, getDoubleVec c df)
-              | (i, c) <- zip [0::Int ..] allCols ]
-      embed i v =
-        let json = decodeUtf8 . toStrict . encode . fromVL $
-                    histogramSpec (allCols !! i) (V.toList v)
-        in "vegaEmbed('#hist_" <> sid <> "_" <> T.pack (show i)
-           <> "', " <> json <> ", {actions:false});"
-  in T.intercalate "\n"
-       [ embed i v | (i, _, Just v) <- pairs ]
-
--- | 単純なヒストグラム Vega-Lite spec。
-histogramSpec :: Text -> [Double] -> VegaLite
-histogramSpec col vals =
-  toVegaLite
-    [ dataFromColumns []
-        . dataColumn col (Numbers vals)
-        $ []
-    , mark Bar [MOpacity 0.85, MColor "#4C72B0"]
-    , encoding
-        . position X [PName col, PmType Quantitative,
-                      PBin [], PAxis [AxTitle col]]
-        . position Y [PAggregate Count, PmType Quantitative,
-                      PAxis [AxTitle "Count"]]
-        $ []
-    , width 320
-    , height 160
-    ]
-
--- モデル概要 -----------------------------------------------------------------
-
-renderModelOverview :: Text -> Text -> Text -> [(Text, Text)] -> Maybe Text -> Text
-renderModelOverview sid ty formula extras mer =
-  let merBlock = case mer of
-        Nothing -> ""
-        Just m  ->
-          T.unlines
-            [ "<h3>モデル構造 (DAG)</h3>"
-            , "<div class=\"mermaid-wrap\"><div class=\"mermaid\">"
-            , m
-            , "</div></div>"
-            ]
-      extraBox (lbl, val) = T.unlines
-        [ "  <div class=\"info-box\">"
-        , "    <div class=\"lbl\">" <> lbl <> "</div>"
-        , "    <div class=\"ival\">" <> val <> "</div>"
-        , "  </div>"
-        ]
-      extraBoxes = T.concat (map extraBox extras)
-  in collapsibleSection sid "<span class=\"sec-icon\">&#9878;</span> モデル概要" True $
-       T.unlines
-         [ "<div class=\"info-grid\">"
-         , "  <div class=\"info-box\">"
-         , "    <div class=\"lbl\">モデル種別</div>"
-         , "    <div class=\"ival\">" <> ty <> "</div>"
-         , "  </div>"
-         , extraBoxes
-         , "  <div class=\"info-box\" style=\"flex: 2\">"
-         , "    <div class=\"lbl\">数式</div>"
-         , "    <div class=\"ival\">" <> formula <> "</div>"
-         , "  </div>"
-         , "</div>"
-         , merBlock
-         ]
-
--- 係数表 -------------------------------------------------------------------
-
-renderCoefficients :: Text -> [(Text, Double)] -> Maybe (Text, Double) -> Text
-renderCoefficients sid coeffs mR2 =
-  let rows = T.intercalate "\n"
-        [ "<tr><td>" <> lbl <> "</td><td class=\"num\">"
-          <> showD4 v <> "</td></tr>"
-        | (lbl, v) <- coeffs ]
-      r2Row = case mR2 of
-        Just (lbl, v) ->
-          "<tfoot><tr><td><strong>" <> lbl <> "</strong></td><td class=\"num\"><strong>"
-          <> showD4 v <> "</strong></td></tr></tfoot>"
-        Nothing -> ""
-  in wrapSection sid "係数" $ T.unlines
-       [ "<table class=\"narrow\">"
-       , "<thead><tr><th>パラメータ</th><th>値</th></tr></thead>"
-       , "<tbody>" <> rows <> "</tbody>"
-       , r2Row
-       , "</table>"
-       ]
-
--- 散布図 + 滑らか曲線 -------------------------------------------------------
-
-renderFitScatter :: Text -> Text -> Text -> [Double] -> [Double]
-                 -> Maybe SmoothCurve -> Text
-renderFitScatter sid _xc _yc _xs _ys _msc =
-  wrapSection sid "散布図 + 適合曲線" $
-    "<div class=\"vl-wrap\"><div id=\"vl-" <> sid <> "\"></div></div>"
-
--- 残差 -----------------------------------------------------------------------
-
-renderResiduals :: Text -> [Double] -> [Double] -> Text
-renderResiduals sid _fitted _resids =
-  wrapSection sid "残差" $
-    "<div class=\"vl-wrap\"><div id=\"vl-" <> sid <> "\"></div></div>"
-
--- 棒グラフ -------------------------------------------------------------------
-
-renderBarChart :: Text -> Text -> [(Text, Double)] -> Text
-renderBarChart sid title _vs =
-  wrapSection sid title $
-    "<div class=\"vl-wrap\"><div id=\"vl-" <> sid <> "\"></div></div>"
-
--- 任意 Vega -----------------------------------------------------------------
-
-renderVegaPlaceholder :: Text -> Text -> Text
-renderVegaPlaceholder sid title =
-  wrapSection sid title $
-    "<div class=\"vl-wrap\"><div id=\"vl-" <> sid <> "\"></div></div>"
-
--- Mermaid -------------------------------------------------------------------
-
-renderMermaid :: Text -> Text -> Text
-renderMermaid sid m =
-  wrapSection sid "Model graph" $
-    "<div class=\"mermaid\">" <> m <> "</div>"
-
--- テーブル -------------------------------------------------------------------
-
-renderTable :: Text -> Text -> [Text] -> [[Text]] -> Text
-renderTable sid title hs rows =
-  let head' = T.intercalate "" ["<th>" <> h <> "</th>" | h <- hs]
-      body  = T.intercalate "\n"
-                [ "<tr>" <> T.intercalate "" ["<td>" <> c <> "</td>" | c <- r]
-                  <> "</tr>"
-                | r <- rows ]
-  in wrapSection sid title $ T.unlines
-       [ "<table>"
-       , "<thead><tr>" <> head' <> "</tr></thead>"
-       , "<tbody>" <> body <> "</tbody>"
-       , "</table>"
-       ]
-
--- KV ------------------------------------------------------------------------
-
-renderKeyValue :: Text -> Text -> [(Text, Text)] -> Text
-renderKeyValue sid title kvs =
-  let block = T.intercalate "\n"
-        [ "<div><span class=\"k\">" <> k <> "</span><span class=\"v\">"
-          <> v <> "</span></div>"
-        | (k, v) <- kvs ]
-  in wrapSection sid title $
-       "<div class=\"kv\">" <> block <> "</div>"
-
--- Markdown ------------------------------------------------------------------
-
-renderMarkdown :: Text -> Text -> Text -> Text
-renderMarkdown sid title txt =
-  wrapSection sid title $ "<p>" <> txt <> "</p>"
-
--- Interactive Multi (multivariate) -----------------------------------------
-
-renderInteractiveMulti :: Text -> Text -> InteractiveModel -> Text
-renderInteractiveMulti sid title im =
-  let xCols  = imXCols im
-      sliders = imSlider im
-      xCount = length xCols
-      sliderHtml = T.intercalate "\n"
-        [ T.unlines
-            [ "<div class=\"slider-row\">"
-            , "  <label>" <> col <> ":"
-            , "    <input type=\"range\" id=\"i-" <> sid <> "-s" <> T.pack (show i) <> "\""
-            , "      min=\"" <> showD4 mn <> "\""
-            , "      max=\"" <> showD4 mx <> "\""
-            , "      step=\"" <> showD4 ((mx - mn) / 200) <> "\""
-            , "      value=\"" <> showD4 mid <> "\""
-            , "      oninput=\"window.__updMulti_" <> sid <> "()\">"
-            , "    <span id=\"i-" <> sid <> "-s" <> T.pack (show i)
-              <> "-val\">" <> showD4 mid <> "</span>"
-            , "  </label>"
-            , "</div>"
-            ]
-        | (i, col, (mn, mid, mx)) <- zip3 [0::Int ..] xCols sliders ]
-      primaryDropdown = T.unlines
-        [ "<div class=\"slider-row\">"
-        , "  <label>Primary axis (chart x):"
-        , "    <select id=\"i-" <> sid <> "-primary\""
-        , "            onchange=\"window.__updMulti_" <> sid <> "()\">"
-        , T.intercalate "\n"
-            [ "      <option value=\"" <> T.pack (show i)
-              <> "\">" <> col <> "</option>"
-            | (i, col) <- zip [0::Int ..] xCols ]
-        , "    </select>"
-        , "  </label>"
-        , "</div>"
-        ]
-      _ = xCount
-      tFull = "<span class=\"sec-icon\">&#127919;</span> "
-              <> (if T.null title then "対話的予測" else title)
-  in collapsibleSection sid tFull True $
-       T.unlines
-         [ "<div class=\"interactive-multi\">"
-         , "  <div class=\"i-controls\">"
-         , primaryDropdown
-         , sliderHtml
-         , "    <div class=\"pred-output\">"
-         , "      <div><strong>Predicted " <> imYCol im <> ":</strong>"
-         , "        <span id=\"i-" <> sid <> "-yhat\">—</span></div>"
-         , "      <div class=\"band-readout\">95% CI:"
-         , "        <span id=\"i-" <> sid <> "-ci\">—</span></div>"
-         , "    </div>"
-         , "  </div>"
-         , "  <div class=\"i-chart\">"
-         , "    <div class=\"vl-wrap\"><div id=\"vl-" <> sid <> "\"></div></div>"
-         , "  </div>"
-         , "</div>"
-         ]
-
-interactiveMultiScript :: Text -> InteractiveModel -> Text
-interactiveMultiScript sid im =
-  let xCols   = imXCols im
-      yCol    = imYCol im
-      betas   = imBetas im
-      icpt    = imIntercept im
-      link    = imLink im
-      xVals   = imXValues im
-      yVals   = imYValues im
-      ciSigma = maybe 0 id (imCISigma im)
-      hasCI   = case imCISigma im of { Just s -> s > 0; _ -> False }
-      arrD xs = "[" <> T.intercalate "," (map showD4 xs) <> "]"
-      arrS xs = "[" <> T.intercalate "," (map (\s -> "\"" <> s <> "\"") xs) <> "]"
-      xMatJson =
-        "[" <> T.intercalate ","
-                  [ arrD row | row <- xVals ] <> "]"
-      yArrJson = arrD yVals
-      betasArr = arrD betas
-  in T.unlines
-       [ "(() => {"
-       , "  const xCols = " <> arrS xCols <> ";"
-       , "  const yCol  = \"" <> yCol <> "\";"
-       , "  const xMat  = " <> xMatJson <> ";"
-       , "  const yArr  = " <> yArrJson <> ";"
-       , "  const beta0 = " <> showD4 icpt <> ";"
-       , "  const betas = " <> betasArr <> ";"
-       , "  const link  = \"" <> link <> "\";"
-       , "  const sigma = " <> showD4 ciSigma <> ";"
-       , "  const hasCI = " <> (if hasCI then "true" else "false") <> ";"
-       , "  const invLink = (eta) => {"
-       , "    if (link === 'log')   return Math.exp(eta);"
-       , "    if (link === 'logit') return 1.0/(1.0+Math.exp(-eta));"
-       , "    if (link === 'sqrt')  return eta * eta;"
-       , "    return eta;"
-       , "  };"
-       , "  const predEta = (xs) => {"
-       , "    let e = beta0;"
-       , "    for (let i = 0; i < betas.length; i++) e += betas[i] * xs[i];"
-       , "    return e;"
-       , "  };"
-       , "  const sliderVals = () => xCols.map((_, i) =>"
-       , "    parseFloat(document.getElementById('i-" <> sid <> "-s' + i).value));"
-       , "  const primaryIdx = () =>"
-       , "    parseInt(document.getElementById('i-" <> sid <> "-primary').value);"
-       , "  let chartView = null;"
-       , "  const baseSpec = (pIdx, sliderXs) => {"
-       , "    const pCol = xCols[pIdx];"
-       , "    // primary 軸の min/max"
-       , "    let pMin = Infinity, pMax = -Infinity;"
-       , "    for (const row of xMat) {"
-       , "      if (row[pIdx] < pMin) pMin = row[pIdx];"
-       , "      if (row[pIdx] > pMax) pMax = row[pIdx];"
-       , "    }"
-       , "    const ext = (pMax - pMin) * 0.5;"
-       , "    pMin -= ext; pMax += ext;"
-       , "    // slider 範囲も外挿に含める"
-       , "    const sMin = parseFloat(document.getElementById('i-" <> sid <> "-s' + pIdx).min);"
-       , "    const sMax = parseFloat(document.getElementById('i-" <> sid <> "-s' + pIdx).max);"
-       , "    pMin = Math.min(pMin, sMin);"
-       , "    pMax = Math.max(pMax, sMax);"
-       , "    const N = 120;"
-       , "    const grid = [];"
-       , "    for (let i = 0; i < N; i++)"
-       , "      grid.push(pMin + i * (pMax - pMin) / (N - 1));"
-       , "    // 予測曲線: 副軸を slider 値で固定、primary を grid で動かす"
-       , "    const curve = grid.map(p => {"
-       , "      const xs = sliderXs.slice();"
-       , "      xs[pIdx] = p;"
-       , "      const eta = predEta(xs);"
-       , "      const y = invLink(eta);"
-       , "      return { gx: p, gy: y, lo: y - 1.96 * sigma, hi: y + 1.96 * sigma };"
-       , "    });"
-       , "    const obs = xMat.map((row, i) => ({ x: row[pIdx], y: yArr[i] }));"
-       , "    // 予測マーカー (slider 位置の現在予測値)"
-       , "    const curEta = predEta(sliderXs);"
-       , "    const curY   = invLink(curEta);"
-       , "    const predPoint = [{ px: sliderXs[pIdx], py: curY }];"
-       , "    const layers = ["
-       , "      { data: { values: obs },"
-       , "        mark: { type: 'point', opacity: 0.55, size: 50, color: '#5b8bbf' },"
-       , "        encoding: {"
-       , "          x: { field: 'x', type: 'quantitative', axis: { title: pCol } },"
-       , "          y: { field: 'y', type: 'quantitative', axis: { title: yCol } } } }"
-       , "    ];"
-       , "    if (hasCI) {"
-       , "      layers.push({"
-       , "        data: { values: curve },"
-       , "        mark: { type: 'area', opacity: 0.18, color: '#e74c3c' },"
-       , "        encoding: {"
-       , "          x: { field: 'gx', type: 'quantitative' },"
-       , "          y: { field: 'lo', type: 'quantitative' },"
-       , "          y2:{ field: 'hi' } } });"
-       , "    }"
-       , "    layers.push({"
-       , "      data: { values: curve },"
-       , "      mark: { type: 'line', color: '#e74c3c', strokeWidth: 2.5 },"
-       , "      encoding: {"
-       , "        x: { field: 'gx', type: 'quantitative' },"
-       , "        y: { field: 'gy', type: 'quantitative' } } });"
-       , "    // 予測マーカー (大きい赤丸)"
-       , "    layers.push({"
-       , "      data: { values: predPoint },"
-       , "      mark: { type: 'point', filled: true, size: 250, color: '#c0392b',"
-       , "              stroke: 'white', strokeWidth: 2 },"
-       , "      encoding: {"
-       , "        x: { field: 'px', type: 'quantitative' },"
-       , "        y: { field: 'py', type: 'quantitative' } } });"
-       , "    return { '$schema': 'https://vega.github.io/schema/vega-lite/v4.json',"
-       , "             layer: layers, width: 600, height: 320 };"
-       , "  };"
-       , "  window.__updMulti_" <> sid <> " = function() {"
-       , "    const xs = sliderVals();"
-       , "    xCols.forEach((_, i) => {"
-       , "      document.getElementById('i-" <> sid <> "-s' + i + '-val')"
-       , "        .textContent = xs[i].toFixed(3);"
-       , "    });"
-       , "    const eta = predEta(xs);"
-       , "    const yhat = invLink(eta);"
-       , "    document.getElementById('i-" <> sid <> "-yhat').textContent = yhat.toFixed(4);"
-       , "    if (hasCI) {"
-       , "      const lo = yhat - 1.96 * sigma;"
-       , "      const hi = yhat + 1.96 * sigma;"
-       , "      document.getElementById('i-" <> sid <> "-ci').textContent ="
-       , "        '[' + lo.toFixed(3) + ', ' + hi.toFixed(3) + ']';"
-       , "    } else {"
-       , "      document.getElementById('i-" <> sid <> "-ci').textContent = '—';"
-       , "    }"
-       , "    const pIdx = primaryIdx();"
-       , "    vegaEmbed('#vl-" <> sid <> "', baseSpec(pIdx, xs),"
-       , "              {actions:false}).then(r => { chartView = r.view; });"
-       , "  };"
-       , "  // 初期描画"
-       , "  window.__updMulti_" <> sid <> "();"
-       , "})();"
-       ]
-
--- Collapsible group ---------------------------------------------------------
-
-childId :: Text -> Int -> Text
-childId sid i = sid <> "_c" <> T.pack (show i)
-
-renderCollapsible :: Text -> Text -> Bool -> [ReportSection] -> Text
-renderCollapsible sid title open children =
-  let childHtml = T.intercalate "\n"
-        [ renderSection (childId sid i) c
-        | (i, c) <- zip [0::Int ..] children ]
-      attr = if open then " open" else ""
-  in T.unlines
-       [ "<section id=\"" <> sid <> "\" class=\"collapsible-wrap\">"
-       , "  <details" <> attr <> ">"
-       , "    <summary><h2>" <> title <> "</h2></summary>"
-       , "    <div class=\"collapsible-body\">"
-       , childHtml
-       , "    </div>"
-       , "  </details>"
-       , "</section>"
-       ]
-
--- | 淡い背景色のカード。子セクションの section ラッパは CSS で flat 化される。
-renderCard :: Text -> Text -> [ReportSection] -> Text
-renderCard sid title children =
-  let childHtml = T.intercalate "\n"
-        [ renderSection (childId sid i) c
-        | (i, c) <- zip [0::Int ..] children ]
-      titleHtml = if T.null title then ""
-                  else "  <h3 class=\"card-title\">" <> title <> "</h3>"
-  in T.unlines
-       [ "<div class=\"result-card\" id=\"" <> sid <> "\">"
-       , titleHtml
-       , childHtml
-       , "</div>"
-       ]
-
--- | フラットな統計行 (section box なし)。
-renderStatRow :: Text -> [(Text, Text)] -> Text
-renderStatRow sid kvs =
-  let boxes = T.intercalate "\n"
-        [ "  <div class=\"stat-box\">"
-          <> "<div class=\"lbl\">" <> k
-          <> "</div><div class=\"val\">" <> v <> "</div></div>"
-        | (k, v) <- kvs ]
-  in T.unlines
-       [ "<div class=\"stat-row\" id=\"" <> sid <> "\">"
-       , boxes
-       , "</div>"
-       ]
-
--- Interactive LM ------------------------------------------------------------
-
-renderInteractiveLM :: Text -> Text -> Text -> Text
-                    -> [Double] -> [Double]
-                    -> SmoothCurve -> (Double, Double) -> Text
-renderInteractiveLM sid title xc yc _xs _ys _sc (xMin, xMax) =
-  let mid  = (xMin + xMax) / 2
-      step = (xMax - xMin) / 200
-      tFull = "<span class=\"sec-icon\">&#127919;</span> "
-              <> (if T.null title then "対話的予測" else title)
-  in collapsibleSection sid tFull True $
-       T.unlines
-         [ "<div class=\"interactive-controls\">"
-         , "  <label>" <> xc <> ": "
-         , "    <input type=\"range\" id=\"i-" <> sid <> "-slider\""
-         , "      min=\"" <> showD4 xMin <> "\""
-         , "      max=\"" <> showD4 xMax <> "\""
-         , "      step=\"" <> showD4 step <> "\""
-         , "      value=\"" <> showD4 mid <> "\""
-         , "      oninput=\"window.__upd_" <> sid <> "(this.value)\">"
-         , "    <span id=\"i-" <> sid <> "-x\">" <> showD4 mid <> "</span>"
-         , "  </label>"
-         , "  <span class=\"pred-readout\"><strong>Predicted " <> yc
-           <> ":</strong> <span id=\"i-" <> sid <> "-y\">—</span>"
-         , "    <span id=\"i-" <> sid <> "-band\" class=\"band-readout\"></span></span>"
-         , "</div>"
-         , "<div class=\"vl-wrap\"><div id=\"vl-" <> sid <> "\"></div></div>"
-         ]
-
--- ---------------------------------------------------------------------------
--- Vega-Lite spec 埋め込みスクリプト
--- ---------------------------------------------------------------------------
-
-sectionScript :: Text -> ReportSection -> Text
-sectionScript sid sec = case sec of
-  SecFitScatter xc yc xs ys msc ->
-    embed sid (fitScatterSpec xc yc xs ys msc)
-  SecResiduals fitted resids ->
-    embed sid (residualsSpec fitted resids)
-  SecBarChart t vs ->
-    embed sid (barChartSpec t vs)
-  SecVega _ spec ->
-    embed sid spec
-  SecInteractiveLM _ xc yc xs ys sc _ ->
-    interactiveLMScript sid xc yc xs ys sc
-  SecInteractiveMulti _ im ->
-    interactiveMultiScript sid im
-  SecInteractiveRFFMV _ r ->
-    interactiveRFFMVScript sid r
-  SecInteractiveMultiOut _ imo ->
-    interactiveMultiOutScript sid imo
-  SecCollapsible _ _ children ->
-    T.intercalate "\n"
-      [ sectionScript (childId sid i) child
-      | (i, child) <- zip [0::Int ..] children ]
-  SecCard _ children ->
-    T.intercalate "\n"
-      [ sectionScript (childId sid i) child
-      | (i, child) <- zip [0::Int ..] children ]
-  SecDataOverview df xCols yCol ->
-    dataOverviewScript sid df xCols yCol
-  _ -> ""
-  where
-    embed s spec =
-      let json = decodeUtf8 . toStrict . encode . fromVL $ spec
-      in "vegaEmbed('#vl-" <> s <> "', " <> json <> ", {actions:false});"
-
--- | Interactive LM の JS: scatter+曲線を描画し、スライダーで予測点を更新。
--- グリッド (sc) で線形補間して予測値を計算する (モデル係数を JS に渡さなくて済む)。
-interactiveLMScript :: Text -> Text -> Text -> [Double] -> [Double]
-                    -> SmoothCurve -> Text
-interactiveLMScript sid xc yc xs ys sc =
-  let gridX = scXs sc
-      gridY = scYs sc
-      gridLo = scLower sc
-      gridHi = scUpper sc
-      hasBand = not (null gridLo) && length gridLo == length gridX
-      arr xs0 = "[" <> T.intercalate "," (map showD4 xs0) <> "]"
-      arrObs xs0 ys0 = "[" <> T.intercalate ","
-        [ "{\"x\":" <> showD4 x <> ",\"y\":" <> showD4 y <> "}"
-        | (x, y) <- zip xs0 ys0 ] <> "]"
-  in T.unlines
-       [ "(() => {"
-       , "  const gx = " <> arr gridX <> ";"
-       , "  const gy = " <> arr gridY <> ";"
-       , "  const gl = " <> arr (if hasBand then gridLo else []) <> ";"
-       , "  const gh = " <> arr (if hasBand then gridHi else []) <> ";"
-       , "  const obs = " <> arrObs xs ys <> ";"
-       , "  const xc = \"" <> xc <> "\";"
-       , "  const yc = \"" <> yc <> "\";"
-       , "  const hasBand = gl.length > 0;"
-       , "  const interp = (x, xs, ys) => {"
-       , "    if (xs.length === 0) return null;"
-       , "    if (x <= xs[0]) return ys[0];"
-       , "    if (x >= xs[xs.length-1]) return ys[ys.length-1];"
-       , "    for (let i = 1; i < xs.length; i++) {"
-       , "      if (x <= xs[i]) {"
-       , "        const t = (x - xs[i-1]) / (xs[i] - xs[i-1]);"
-       , "        return ys[i-1] + t * (ys[i] - ys[i-1]);"
-       , "      }"
-       , "    }"
-       , "    return ys[ys.length-1];"
-       , "  };"
-       , "  const buildSpec = (curX) => {"
-       , "    const curY = interp(curX, gx, gy);"
-       , "    const layers = ["
-       , "      { data: { values: obs },"
-       , "        mark: { type: 'point', opacity: 0.55, size: 50, color: '#5b8bbf' },"
-       , "        encoding: {"
-       , "          x: { field: 'x', type: 'quantitative', axis: { title: xc } },"
-       , "          y: { field: 'y', type: 'quantitative', axis: { title: yc } } } }"
-       , "    ];"
-       , "    if (hasBand) {"
-       , "      const bandData = gx.map((x, i) => ({ gx: x, lo: gl[i], hi: gh[i] }));"
-       , "      layers.push({"
-       , "        data: { values: bandData },"
-       , "        mark: { type: 'area', opacity: 0.18, color: '#e74c3c' },"
-       , "        encoding: {"
-       , "          x: { field: 'gx', type: 'quantitative' },"
-       , "          y: { field: 'lo', type: 'quantitative' },"
-       , "          y2:{ field: 'hi' } } });"
-       , "    }"
-       , "    const lineData = gx.map((x, i) => ({ gx: x, gy: gy[i] }));"
-       , "    layers.push({"
-       , "      data: { values: lineData },"
-       , "      mark: { type: 'line', color: '#e74c3c', strokeWidth: 2.5 },"
-       , "      encoding: {"
-       , "        x: { field: 'gx', type: 'quantitative' },"
-       , "        y: { field: 'gy', type: 'quantitative' } } });"
-       , "    layers.push({"
-       , "      data: { values: [{ px: curX, py: curY }] },"
-       , "      mark: { type: 'point', filled: true, size: 250, color: '#c0392b',"
-       , "              stroke: 'white', strokeWidth: 2 },"
-       , "      encoding: {"
-       , "        x: { field: 'px', type: 'quantitative' },"
-       , "        y: { field: 'py', type: 'quantitative' } } });"
-       , "    return { '$schema': 'https://vega.github.io/schema/vega-lite/v4.json',"
-       , "             layer: layers, width: 600, height: 320 };"
-       , "  };"
-       , "  window.__upd_" <> sid <> " = function(v) {"
-       , "    const x = parseFloat(v);"
-       , "    document.getElementById('i-" <> sid <> "-x').textContent = x.toFixed(3);"
-       , "    const y = interp(x, gx, gy);"
-       , "    document.getElementById('i-" <> sid <> "-y').textContent ="
-       , "      y === null ? '—' : y.toFixed(4);"
-       , "    if (hasBand) {"
-       , "      const lo = interp(x, gx, gl);"
-       , "      const hi = interp(x, gx, gh);"
-       , "      document.getElementById('i-" <> sid <> "-band').textContent ="
-       , "        ' [' + lo.toFixed(3) + ', ' + hi.toFixed(3) + ']';"
-       , "    }"
-       , "    vegaEmbed('#vl-" <> sid <> "', buildSpec(x), {actions:false});"
-       , "  };"
-       , "  // 初期表示"
-       , "  const initX = (gx[0] + gx[gx.length-1]) / 2;"
-       , "  window.__upd_" <> sid <> "(initX);"
-       , "})();"
-       ]
-
-fitScatterSpec :: Text -> Text -> [Double] -> [Double]
-               -> Maybe SmoothCurve -> VegaLite
-fitScatterSpec xc yc xs ys msc =
-  let scatterLayer = asSpec
-        [ dataFromColumns []
-            . dataColumn xc (Numbers xs)
-            . dataColumn yc (Numbers ys)
-            $ []
-        , mark Point [MOpacity 0.7, MSize 50, MColor "#4C72B0"]
-        , encoding
-            . position X [PName xc, PmType Quantitative,
-                          PAxis [AxTitle xc]]
-            . position Y [PName yc, PmType Quantitative,
-                          PAxis [AxTitle yc]]
-            $ []
-        ]
-      smoothLayers = case msc of
-        Nothing -> []
-        Just sc ->
-          let lineLayer = asSpec
-                [ dataFromColumns []
-                    . dataColumn "x_grid" (Numbers (scXs sc))
-                    . dataColumn "y_fit"  (Numbers (scYs sc))
-                    $ []
-                , mark Line [MColor "#DD5566", MStrokeWidth 2.5]
-                , encoding
-                    . position X [PName "x_grid", PmType Quantitative]
-                    . position Y [PName "y_fit",  PmType Quantitative]
-                    $ []
-                ]
-              hasBand = not (null (scLower sc)) && not (null (scUpper sc))
-                          && length (scLower sc) == length (scXs sc)
-              bandLayer
-                | hasBand = [asSpec
-                  [ dataFromColumns []
-                      . dataColumn "x_grid" (Numbers (scXs sc))
-                      . dataColumn "lo" (Numbers (scLower sc))
-                      . dataColumn "hi" (Numbers (scUpper sc))
-                      $ []
-                  , mark Area [MOpacity 0.2, MColor "#DD5566"]
-                  , encoding
-                      . position X  [PName "x_grid", PmType Quantitative]
-                      . position Y  [PName "lo", PmType Quantitative]
-                      . position Y2 [PName "hi"]
-                      $ []
-                  ]]
-                | otherwise = []
-          in bandLayer ++ [lineLayer]
-  in toVegaLite
-       [ layer (scatterLayer : smoothLayers)
-       , width 600
-       , height 320
-       ]
-
-residualsSpec :: [Double] -> [Double] -> VegaLite
-residualsSpec fitted resids =
-  toVegaLite
-    [ dataFromColumns []
-        . dataColumn "fitted"  (Numbers fitted)
-        . dataColumn "residual" (Numbers resids)
-        $ []
-    , mark Point [MOpacity 0.7, MSize 50, MColor "#4C72B0"]
-    , encoding
-        . position X [PName "fitted",  PmType Quantitative,
-                      PAxis [AxTitle "Fitted"]]
-        . position Y [PName "residual", PmType Quantitative,
-                      PAxis [AxTitle "Residual"]]
-        $ []
-    , width 600
-    , height 280
-    ]
-
--- | Regularization path (lambda 対 各係数) をログスケール x 軸の多線グラフで描く。
--- 入力: 係数ラベル + (λ, 係数ベクトル) のリスト。intercept は除外推奨。
-regPathSpec
-  :: [Text]                    -- ^ 係数ラベル (length = 係数数)
-  -> [(Double, [Double])]      -- ^ (λ, [coef])
-  -> VegaLite
-regPathSpec labels path =
-  let -- long format: 各 (λ, label, value) を平坦化
-      rows = [ (lam, lbl, val)
-             | (lam, coefs) <- path
-             , (lbl, val)   <- zip labels coefs ]
-      lams   = [ lam | (lam, _, _) <- rows ]
-      lbls   = [ lbl | (_, lbl, _) <- rows ]
-      vals   = [ val | (_, _, val) <- rows ]
-  in toVegaLite
-       [ dataFromColumns []
-           . dataColumn "lambda"      (Numbers lams)
-           . dataColumn "coefficient" (Strings lbls)
-           . dataColumn "value"       (Numbers vals)
-           $ []
-       , mark Line [MStrokeWidth 2.2, MOpacity 0.9]
-       , encoding
-           . position X [PName "lambda", PmType Quantitative,
-                         PScale [SType ScLog],
-                         PAxis [AxTitle "λ (log scale)"]]
-           . position Y [PName "value", PmType Quantitative,
-                         PAxis [AxTitle "Coefficient"]]
-           . color [MName "coefficient", MmType Nominal,
-                    MScale [SScheme "tableau10" []],
-                    MLegend [LTitle "feature"]]
-           $ []
-       , width 640
-       , height 320
-       ]
-
-barChartSpec :: Text -> [(Text, Double)] -> VegaLite
-barChartSpec _title vs =
-  let labels = map fst vs
-      values = map snd vs
-  in toVegaLite
-       [ dataFromColumns []
-           . dataColumn "label" (Strings labels)
-           . dataColumn "value" (Numbers values)
-           $ []
-       , mark Bar [MColor "#4C72B0", MOpacity 0.85]
-       , encoding
-           . position X [PName "label", PmType Nominal,
-                         PAxis [AxTitle "", AxLabelAngle (-30)],
-                         PSort []]
-           . position Y [PName "value", PmType Quantitative,
-                         PAxis [AxTitle ""]]
-           $ []
-       , widthStep 40
-       , height 220
-       ]
-
--- | Forest plot — 各パラメータの中央値 (点) と HDI/CI (横棒)。
-forestPlotSpec :: [(Text, Double, Double, Double)] -> VegaLite
-forestPlotSpec rows =
-  let names = [n | (n, _, _, _) <- rows]
-      means = [m | (_, _, m, _) <- rows]
-      los   = [l | (_, l, _, _) <- rows]
-      his   = [h | (_, _, _, h) <- rows]
-  in toVegaLite
-       [ dataFromColumns []
-           . dataColumn "param" (Strings names)
-           . dataColumn "mean"  (Numbers means)
-           . dataColumn "lo"    (Numbers los)
-           . dataColumn "hi"    (Numbers his)
-           $ []
-       , layer
-           [ asSpec
-               [ mark Rule [MStrokeWidth 2.4, MColor "#4C72B0"]
-               , encoding
-                   . position Y  [PName "param", PmType Nominal,
-                                  PAxis [AxTitle ""]]
-                   . position X  [PName "lo", PmType Quantitative,
-                                  PAxis [AxTitle "推定値"]]
-                   . position X2 [PName "hi"]
-                   $ []
-               ]
-           , asSpec
-               [ mark Circle [MSize 110, MColor "#1e3a5c", MOpacity 0.95]
-               , encoding
-                   . position Y [PName "param", PmType Nominal]
-                   . position X [PName "mean", PmType Quantitative]
-                   $ []
-               ]
-           ]
-       , width 540
-       , heightStep 28
-       ]
-
--- | Posterior Predictive Check — 観測 KDE + 各 replicate KDE 重ね描き。
-ppcSpec :: [Double] -> [[Double]] -> VegaLite
-ppcSpec observed reps =
-  let nGrid = 200
-      obsKde   = SM.kde nGrid observed
-      repKdes  = [ SM.kde nGrid r | r <- reps, not (null r) ]
-      obsRows  = [ (x, y, "観測 (y_obs)" :: Text, 0 :: Int) | (x, y) <- obsKde ]
-      repRows  = [ (x, y, "事後予測", j)
-                 | (j, kd) <- zip [1 :: Int ..] repKdes
-                 , (x, y)  <- kd ]
-      rows     = obsRows ++ repRows
-      xs    = [ x  | (x, _, _, _) <- rows ]
-      ys    = [ y  | (_, y, _, _) <- rows ]
-      grps  = [ g  | (_, _, g, _) <- rows ]
-      idx   = [ T.pack ("rep_" <> show k) | (_, _, _, k) <- rows ]
-  in toVegaLite
-       [ dataFromColumns []
-           . dataColumn "x"     (Numbers xs)
-           . dataColumn "y"     (Numbers ys)
-           . dataColumn "group" (Strings grps)
-           . dataColumn "rep"   (Strings idx)
-           $ []
-       , layer
-           [ asSpec
-               [ transform . VL.filter (FExpr "datum.group === '事後予測'") $ []
-               , mark Line [MStrokeWidth 0.7, MOpacity 0.25, MColor "#888"]
-               , encoding
-                   . position X [PName "x", PmType Quantitative,
-                                 PAxis [AxTitle "y"]]
-                   . position Y [PName "y", PmType Quantitative,
-                                 PAxis [AxTitle "密度"]]
-                   . detail [DName "rep", DmType Nominal]
-                   $ []
-               ]
-           , asSpec
-               [ transform . VL.filter (FExpr "datum.group === '観測 (y_obs)'") $ []
-               , mark Line [MStrokeWidth 2.4, MColor "#1e3a5c"]
-               , encoding
-                   . position X [PName "x", PmType Quantitative]
-                   . position Y [PName "y", PmType Quantitative]
-                   $ []
-               ]
-           ]
-       , width 640
-       , height 280
-       ]
-
--- | Calibration spec: 10 ビンに分割し (mean p, observed freq) を点 + 対角線で描画。
-calibrationSpec :: [Double] -> [Double] -> VegaLite
-calibrationSpec pPred yObs =
-  let pairs = zip pPred yObs
-      bin p
-        | p >= 1.0  = 9
-        | p <= 0.0  = 0
-        | otherwise = max 0 (min 9 (floor (p * 10) :: Int))
-      bins = [0 .. 9 :: Int]
-      perBin =
-        [ let inB = [ (p, y) | (p, y) <- pairs, bin p == k ]
-              n   = length inB
-              mP  = if n == 0 then fromIntegral k / 10 + 0.05
-                    else sum (map fst inB) / fromIntegral n
-              mY  = if n == 0 then 0
-                    else sum (map snd inB) / fromIntegral n
-          in (k, n, mP, mY)
-        | k <- bins ]
-      nonEmpty = [ (mP, mY, n) | (_, n, mP, mY) <- perBin, n > 0 ]
-      meanPs = [ p | (p, _, _) <- nonEmpty ]
-      meanYs = [ y | (_, y, _) <- nonEmpty ]
-      counts = [ fromIntegral n :: Double | (_, _, n) <- nonEmpty ]
-      diagXs = [0, 1] :: [Double]
-      diagYs = [0, 1] :: [Double]
-  in toVegaLite
-       [ layer
-           [ asSpec
-               [ dataFromColumns []
-                   . dataColumn "x" (Numbers diagXs)
-                   . dataColumn "y" (Numbers diagYs)
-                   $ []
-               , mark Line [MStrokeWidth 1.2, MColor "#888", MStrokeDash [4, 4]]
-               , encoding
-                   . position X [PName "x", PmType Quantitative,
-                                 PScale [SDomain (DNumbers [0, 1])],
-                                 PAxis [AxTitle "予測確率 (mean)"]]
-                   . position Y [PName "y", PmType Quantitative,
-                                 PScale [SDomain (DNumbers [0, 1])],
-                                 PAxis [AxTitle "観測頻度"]]
-                   $ []
-               ]
-           , asSpec
-               [ dataFromColumns []
-                   . dataColumn "p"     (Numbers meanPs)
-                   . dataColumn "y"     (Numbers meanYs)
-                   . dataColumn "count" (Numbers counts)
-                   $ []
-               , mark Circle [MOpacity 0.85, MColor "#1e3a5c"]
-               , encoding
-                   . position X [PName "p", PmType Quantitative]
-                   . position Y [PName "y", PmType Quantitative]
-                   . size [MName "count", MmType Quantitative,
-                           MLegend [LTitle "n"]]
-                   $ []
-               ]
-           ]
-       , width 480
-       , height 380
-       ]
-
--- | 3D scatter (z は色エンコード)。
-scatter3DSpec :: Text -> Text -> Text -> [Double] -> [Double] -> [Double]
-              -> VegaLite
-scatter3DSpec xL yL zL xs ys zs =
-  toVegaLite
-    [ dataFromColumns []
-        . dataColumn xL (Numbers xs)
-        . dataColumn yL (Numbers ys)
-        . dataColumn zL (Numbers zs)
-        $ []
-    , mark Circle [MSize 80, MOpacity 0.85]
-    , encoding
-        . position X [PName xL, PmType Quantitative,
-                      PAxis [AxTitle xL]]
-        . position Y [PName yL, PmType Quantitative,
-                      PAxis [AxTitle yL]]
-        . color [MName zL, MmType Quantitative,
-                 MScale [SScheme "viridis" []],
-                 MLegend [LTitle zL]]
-        $ []
-    , width 560
-    , height 380
-    ]
-
--- | 2D heatmap (rect + 色エンコード)。
-heatmapSpec :: [Text] -> [Text] -> [[Double]] -> VegaLite
-heatmapSpec colLabels rowLabels values =
-  let rows = [ (rLbl, cLbl, v)
-             | (rLbl, rowVals) <- zip rowLabels values
-             , (cLbl, v)       <- zip colLabels rowVals ]
-      rs   = [ r | (r, _, _) <- rows ]
-      cs   = [ c | (_, c, _) <- rows ]
-      vs   = [ v | (_, _, v) <- rows ]
-  in toVegaLite
-       [ dataFromColumns []
-           . dataColumn "row" (Strings rs)
-           . dataColumn "col" (Strings cs)
-           . dataColumn "val" (Numbers vs)
-           $ []
-       , mark Rect [MStroke "#fff", MStrokeWidth 0.5]
-       , encoding
-           . position X [PName "col", PmType Nominal,
-                         PAxis [AxTitle "", AxLabelAngle (-30)]]
-           . position Y [PName "row", PmType Nominal,
-                         PAxis [AxTitle ""]]
-           . color [MName "val", MmType Quantitative,
-                    MScale [SScheme "viridis" []],
-                    MLegend [LTitle "値"]]
-           $ []
-       , width 520
-       , height 380
-       ]
-
--- ---------------------------------------------------------------------------
--- 数値フォーマット
--- ---------------------------------------------------------------------------
-
-showD4 :: Double -> Text
-showD4 d = T.pack (showFFloat (Just 4) d "")
-
--- ---------------------------------------------------------------------------
--- CSS
--- ---------------------------------------------------------------------------
-
-css :: Text
-css = T.unlines
-  [ "* { box-sizing: border-box; margin: 0; padding: 0; }"
-  , "body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5;"
-  , "       color: #333; line-height: 1.6; }"
-  , "nav { position: sticky; top: 0; z-index: 100; background: #1e3a5c;"
-  , "      padding: 10px 28px; display: flex; gap: 18px; align-items: center;"
-  , "      box-shadow: 0 2px 6px rgba(0,0,0,.25); flex-wrap: wrap; }"
-  , "nav h1 { color: #ecf0f1; font-size: 1em; font-weight: 600; flex: 1; min-width: 250px; }"
-  , ".nav-link { color: #9ab; text-decoration: none; font-size: .82em; white-space: nowrap; }"
-  , ".nav-link:hover { color: #fff; }"
-  , "main { max-width: 1160px; margin: 0 auto; padding: 32px 20px; }"
-  , "section { background: white; border-radius: 12px; padding: 26px 28px;"
-  , "          margin-bottom: 28px; box-shadow: 0 2px 10px rgba(0,0,0,.07); }"
-  , "h2 { font-size: 1.05em; font-weight: 700; color: #1e3a5c; margin-bottom: 18px;"
-  , "     border-bottom: 2px solid #e4e9f0; padding-bottom: 8px; }"
-  , "h3 { font-size: .92em; font-weight: 600; color: #2a5298; margin: 18px 0 10px; }"
-  , "table { width: 100%; border-collapse: collapse; font-size: .88em; margin-bottom: 8px; }"
-  , "table.narrow { max-width: 480px; }"
-  , "thead tr { background: #f0f4f8; }"
-  , "th { padding: 8px 14px; text-align: left; font-weight: 600; color: #444; }"
-  , "td { padding: 7px 14px; border-bottom: 1px solid #f0f2f5; font-family: monospace; }"
-  , "td:first-child { font-family: inherit; font-weight: 500; }"
-  , "tr:last-child td { border-bottom: none; }"
-  , "tfoot td { border-top: 2px solid #ddd; }"
-  , ".num { font-family: monospace; }"
-  , ".vl-wrap { overflow-x: auto; margin-bottom: 8px; }"
-  , ".kv { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; }"
-  , ".kv > div { background: #f7f9fc; border: 1px solid #e4e9f0; border-radius: 10px;"
-  , "            padding: 12px 16px; min-width: 140px; text-align: center;"
-  , "            display: flex; flex-direction: column; }"
-  , ".kv .k { font-size: .7em; color: #888; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 4px; }"
-  , ".kv .v { font-size: 1.2em; font-weight: 700; color: #1e3a5c; }"
-  , ".sec-icon { font-size: 1.1em; margin-right: 6px; }"
-  , ".sec-desc { font-size: .88em; color: #666; margin-bottom: 16px; }"
-  , ".info-grid { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }"
-  , ".info-box { background: #f7f9fc; border: 1px solid #e4e9f0; border-radius: 10px;"
-  , "            padding: 12px 18px; min-width: 180px; flex: 1; }"
-  , ".info-box .lbl { font-size: .72em; color: #888; text-transform: uppercase; letter-spacing: .04em; margin-bottom: 4px; }"
-  , ".info-box .ival { font-size: .95em; font-weight: 600; color: #1e3a5c; }"
-  , ".mermaid-wrap { background:#f7fafc; border-radius:8px; padding:24px;"
-  , "                margin:12px 0; text-align:center; overflow-x:auto; }"
-  , ".mermaid-wrap .mermaid { display:inline-block; min-width:320px; min-height:200px;"
-  , "                         font-family:'Segoe UI',sans-serif; line-height:1.4; }"
-  , ".mermaid-wrap .mermaid svg { max-width:100%; height:auto; min-height:240px; }"
-  , ".hist-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));"
-  , "             gap: 14px; margin-top: 12px; }"
-  , ".hist-card { background: #f7f9fc; border: 1px solid #e4e9f0; border-radius: 8px;"
-  , "             padding: 10px; }"
-  , ".hist-title { font-weight: 600; color: #1e3a5c; margin-bottom: 6px; font-size: .9em; }"
-  , "p { line-height: 1.6; color: #444; font-size: .92em; }"
-  , ".interactive-controls { margin-bottom: 16px; padding: 16px 18px;"
-  , "                         background: #f7f9fc; border: 1px solid #e4e9f0;"
-  , "                         border-radius: 10px; }"
-  , ".interactive-controls input[type='range'] { width: 360px; vertical-align: middle;"
-  , "                                            margin: 0 10px; accent-color: #1e3a5c; }"
-  , ".interactive-controls label { display: block; margin-bottom: 8px; font-size: .9em; }"
-  , ".pred-readout { font-size: 1em; }"
-  , ".pred-readout strong { color: #1e3a5c; }"
-  , ".band-readout { color: #888; font-size: .9em; }"
-  , "details { margin: 8px 0; }"
-  , "details summary { cursor: pointer; padding: 10px 14px;"
-  , "                  background: #f0f4f8; border-radius: 8px;"
-  , "                  font-weight: 600; color: #1e3a5c; user-select: none; }"
-  , "details summary h2 { display: inline; font-size: 1.05em; border: none;"
-  , "                     padding: 0; margin: 0; color: inherit; }"
-  , "details[open] summary { background: #dce6f0; }"
-  , "details summary::-webkit-details-marker { color: #888; }"
-  -- Collapsible は通常の section と同じ白背景・影付きの箱として表示。
-  -- summary の h2 は通常の h2 と同じスタイルにし、右に折りたたみ三角を付ける。
-  , ".collapsible-wrap > details > summary { list-style: none; cursor: pointer;"
-  , "                                        padding: 0; background: transparent;"
-  , "                                        margin: 0; }"
-  , ".collapsible-wrap > details > summary::-webkit-details-marker { display: none; }"
-  , ".collapsible-wrap > details > summary > h2 { display: block;"
-  , "  font-size: 1.05em; font-weight: 700; color: #1e3a5c;"
-  , "  margin: 0; border-bottom: 2px solid #e4e9f0; padding-bottom: 8px; }"
-  , ".collapsible-wrap > details[open] > summary > h2 { margin-bottom: 18px; }"
-  , ".collapsible-wrap > details > summary > h2::after { content: '\\25BC';"
-  , "  font-size: .7em; margin-left: 10px; color: #888;"
-  , "  transition: transform .2s; display: inline-block; }"
-  , ".collapsible-wrap > details:not([open]) > summary > h2::after {"
-  , "  transform: rotate(-90deg); }"
-  , ".collapsible-body { padding: 0; }"
-  , ".collapsible-body > section { background: transparent; border: none;"
-  , "                              box-shadow: none; padding: 6px 0; margin: 0; }"
-  , ".collapsible-body > section > h2 { display: none; }"
-  , ".collapsible-body > .table-scroll { margin: 0; }"
-  , ".collapsible-body > .info-grid { margin-top: 0; }"
-  -- Card (淡い背景の囲み)
-  , ".result-card { background: #f7f9fc; border: 1px solid #e4e9f0;"
-  , "               border-radius: 10px; padding: 14px 16px; margin: 12px 0; }"
-  , ".result-card .card-title { font-weight: 600; color: #1e3a5c;"
-  , "                           margin-bottom: 10px; font-size: .98em;"
-  , "                           border-bottom: 1px solid #dde6ee; padding-bottom: 6px; }"
-  , ".result-card section { background: transparent; border: none;"
-  , "                       box-shadow: none; padding: 0; margin: 0; }"
-  , ".result-card section > h2 { display: none; }"
-  -- Stat row (Card 間のフラットな統計バー)
-  , ".stat-row { display: flex; gap: 12px; flex-wrap: wrap;"
-  , "            margin: 14px 0; }"
-  , ".stat-row .stat-box { background: white; border: 1px solid #d6dde6;"
-  , "                      border-radius: 8px; padding: 10px 14px;"
-  , "                      min-width: 110px; flex: 1; text-align: center; }"
-  , ".stat-row .lbl { font-size: .7em; color: #888; text-transform: uppercase;"
-  , "                 letter-spacing: .04em; margin-bottom: 4px; }"
-  , ".stat-row .val { font-size: 1.1em; font-weight: 700; color: #1e3a5c;"
-  , "                 font-family: monospace; }"
-  , ".stats-card, .hist-card-group { margin: 10px 0; }"
-  , ".stats-card[open] summary, .hist-card-group[open] summary { background: #d6e4f0; }"
-  , ".hist-card { border: 1px solid #e0e6ee; border-radius: 6px;"
-  , "             padding: 4px 8px; margin: 6px 0; }"
-  , ".hist-card summary { background: transparent; padding: 4px 0; }"
-  , ".hist-card summary strong { color: #2c3e50; }"
-  , ".table-scroll { overflow-x: auto; }"
-  , ".stats-table { font-size: .85em; }"
-  , ".stats-table th, .stats-table td { padding: 5px 10px; }"
-  , ".interactive-multi { display: grid; grid-template-columns: 280px 1fr;"
-  , "                     gap: 20px; align-items: start; }"
-  , ".interactive-multi .i-controls { background: #f8f9fa;"
-  , "                                  border-radius: 8px; padding: 14px; }"
-  , ".interactive-multi .slider-row { margin-bottom: 10px; }"
-  , ".interactive-multi .slider-row label { display: block; font-size: .9em; }"
-  , ".interactive-multi input[type='range'] { width: 100%; vertical-align: middle; }"
-  , ".interactive-multi select { width: 100%; padding: 4px; }"
-  , ".interactive-multi .pred-output { margin-top: 14px; padding-top: 12px;"
-  , "                                  border-top: 1px solid #ddd; font-size: .95em; }"
-  , ".interactive-multi .pred-output strong { color: #2c3e50; }"
-  , "@media (max-width: 700px) {"
-  , "  .interactive-multi { grid-template-columns: 1fr; }"
-  , "}"
-  , ".raw-section { margin-bottom: 28px; }"
-  , ".appendix-md.collapsible-wrap { background: white; }"
-  , ".md-body h3 { font-size: 1em; color: #2c3e50; margin: 12px 0 6px; }"
-  , ".md-body h4 { font-size: .95em; color: #34495e; margin: 10px 0 4px; }"
-  , ".md-body h5 { font-size: .9em; color: #555; margin: 8px 0 4px; }"
-  , ".md-body p { margin: 8px 0; }"
-  , ".md-body ul { margin: 6px 0 6px 20px; }"
-  , ".md-body code { background: #eef2f7; padding: 1px 5px; border-radius: 3px;"
-  , "                font-family: monospace; font-size: .92em; }"
-  , ".md-body strong { color: #2c3e50; }"
-  , ".md-body a { color: #2980b9; text-decoration: none; }"
-  , ".md-body a:hover { text-decoration: underline; }"
-  ]
-
--- ---------------------------------------------------------------------------
--- Interactive RFF MV (多変量 RFF Ridge の対話的予測) -----------------------
--- ---------------------------------------------------------------------------
-
-renderInteractiveRFFMV :: Text -> Text -> InteractiveRFFMV -> Text
-renderInteractiveRFFMV sid title r =
-  let sliderHtml = T.intercalate "\n"
-        [ T.unlines
-            [ "<div class=\"slider-row\">"
-            , "  <label>" <> col <> ":"
-            , "    <input type=\"range\" id=\"i-" <> sid <> "-s" <> T.pack (show i) <> "\""
-            , "      min=\"" <> showD4 mn <> "\""
-            , "      max=\"" <> showD4 mx <> "\""
-            , "      step=\"" <> showD4 ((mx - mn) / 200) <> "\""
-            , "      value=\"" <> showD4 mid <> "\""
-            , "      oninput=\"window.__updRFFMV_" <> sid <> "()\">"
-            , "    <span id=\"i-" <> sid <> "-s" <> T.pack (show i)
-              <> "-val\">" <> showD4 mid <> "</span>"
-            , "  </label>"
-            , "</div>"
-            ]
-        | (i, (col, mn, mid, mx)) <- zip [0::Int ..] (irfSliders r) ]
-      tFull = "<span class=\"sec-icon\">&#127919;</span> "
-              <> (if T.null title then "対話的予測" else title)
-  in collapsibleSection sid tFull True $
-       T.unlines
-         [ "<div class=\"interactive-multi\">"
-         , "  <div class=\"i-controls\">"
-         , "    <div class=\"slider-row\"><em>主軸: " <> irfMainAxis r
-            <> " (横軸固定。副軸を以下のスライダで動かすと予測曲線が更新されます)</em></div>"
-         , sliderHtml
-         , "  </div>"
-         , "  <div class=\"i-chart\">"
-         , "    <div class=\"vl-wrap\"><div id=\"vl-" <> sid <> "\"></div></div>"
-         , "  </div>"
-         , "</div>"
-         ]
-
-interactiveRFFMVScript :: Text -> InteractiveRFFMV -> Text
-interactiveRFFMVScript sid r =
-  let mainAxis  = irfMainAxis r
-      yCol      = irfYCol r
-      xColsAll  = irfXCols r
-      mainIdx   = case [ i | (i, c) <- zip [0::Int ..] xColsAll, c == mainAxis ] of
-                    (i:_) -> i
-                    []    -> 0
-      sliderCols = [ c | c <- xColsAll, c /= mainAxis ]
-      sliderIdx = [ i | (i, c) <- zip [0::Int ..] xColsAll, c /= mainAxis ]
-      arrD xs = "[" <> T.intercalate "," (map showD4 xs) <> "]"
-      arrS xs = "[" <> T.intercalate "," (map (\s -> "\"" <> s <> "\"") xs) <> "]"
-      omegasArr = arrD (irfOmegasRowMaj r)
-      bsArr     = arrD (irfBs r)
-      wArr      = arrD (irfWeights r)
-      muArr  = case irfStdMu r of { Just xs -> arrD xs; Nothing -> "null" }
-      sdArr  = case irfStdSd r of { Just xs -> arrD xs; Nothing -> "null" }
-      xObsJson  =
-        "[" <> T.intercalate ","
-                  [ arrD col | col <- irfXObs r ] <> "]"
-      yObsJson  = arrD (irfYObs r)
-      groupsJson = arrS (irfGroups r)
-      mainGridJson = arrD (irfMainGrid r)
-      sliderColsJson = arrS sliderCols
-      sliderIdxJson  = "[" <> T.intercalate "," (map (T.pack . show) sliderIdx) <> "]"
-  in T.unlines
-       [ "(() => {"
-       , "  const sid       = \"" <> sid <> "\";"
-       , "  const xCols     = " <> arrS xColsAll <> ";"
-       , "  const yCol      = \"" <> yCol <> "\";"
-       , "  const mainAxis  = \"" <> mainAxis <> "\";"
-       , "  const mainIdx   = " <> T.pack (show mainIdx) <> ";"
-       , "  const sliderCols = " <> sliderColsJson <> ";"
-       , "  const sliderIdx  = " <> sliderIdxJson <> ";"
-       , "  const omegas   = " <> omegasArr <> ";"  -- length p*D, row-major
-       , "  const bs       = " <> bsArr <> ";"
-       , "  const sigmaF   = " <> showD4 (irfSigmaF r) <> ";"
-       , "  const Ddim     = " <> T.pack (show (irfDim r)) <> ";"
-       , "  const pDim     = " <> T.pack (show (irfP r)) <> ";"
-       , "  const weights  = " <> wArr <> ";"
-       , "  const xObs     = " <> xObsJson <> ";"  -- p arrays (each n)
-       , "  const yObs     = " <> yObsJson <> ";"
-       , "  const groups   = " <> groupsJson <> ";"
-       , "  const mainGrid = " <> mainGridJson <> ";"
-       , "  const coef     = sigmaF * Math.sqrt(2 / Ddim);"
-       , "  const stdMu    = " <> muArr <> ";"
-       , "  const stdSd    = " <> sdArr <> ";"
-       , "  function standardize(xVec) {"
-       , "    if (stdMu === null) return xVec;"
-       , "    return xVec.map((v, k) => (v - stdMu[k]) / stdSd[k]);"
-       , "  }"
-       , "  function predictY(xVecRaw) {"
-       , "    const xVec = standardize(xVecRaw);"
-       , "    let y = 0;"
-       , "    for (let j = 0; j < Ddim; j++) {"
-       , "      let arg = bs[j];"
-       , "      for (let k = 0; k < pDim; k++) {"
-       , "        arg += omegas[k * Ddim + j] * xVec[k];"
-       , "      }"
-       , "      y += weights[j] * coef * Math.cos(arg);"
-       , "    }"
-       , "    return y;"
-       , "  }"
-       , "  function readSliders() {"
-       , "    const vals = new Array(pDim).fill(0);"
-       , "    for (let s = 0; s < sliderCols.length; s++) {"
-       , "      const el = document.getElementById('i-' + sid + '-s' + s);"
-       , "      const v  = parseFloat(el.value);"
-       , "      vals[sliderIdx[s]] = v;"
-       , "      const lbl = document.getElementById('i-' + sid + '-s' + s + '-val');"
-       , "      if (lbl) lbl.textContent = (Math.round(v*1000)/1000).toString();"
-       , "    }"
-       , "    return vals;"
-       , "  }"
-       , "  function buildSpec() {"
-       , "    const sliders = readSliders();"
-       , "    // 観測点 (固定)"
-       , "    const obs = [];"
-       , "    const n = yObs.length;"
-       , "    for (let i = 0; i < n; i++) {"
-       , "      obs.push({ z: xObs[mainIdx][i], y: yObs[i], group: groups[i] });"
-       , "    }"
-       , "    // 予測曲線 (現在のスライダ値で)"
-       , "    const pred = [];"
-       , "    for (const z of mainGrid) {"
-       , "      const xVec = sliders.slice();"
-       , "      xVec[mainIdx] = z;"
-       , "      pred.push({ z: z, yhat: predictY(xVec) });"
-       , "    }"
-       , "    return {"
-       , "      $schema: 'https://vega.github.io/schema/vega-lite/v5.json',"
-       , "      width: 720, height: 480,"
-       , "      layer: ["
-       , "        { data: { values: obs },"
-       , "          mark: { type: 'point', filled: true, opacity: 0.6 },"
-       , "          encoding: {"
-       , "            x: { field: 'z', type: 'quantitative', title: mainAxis },"
-       , "            y: { field: 'y', type: 'quantitative', title: yCol },"
-       , "            color: { field: 'group', type: 'nominal' },"
-       , "            tooltip: ["
-       , "              { field: 'group' }, { field: 'z' }, { field: 'y' }"
-       , "            ]"
-       , "          } },"
-       , "        { data: { values: pred },"
-       , "          mark: { type: 'line', strokeWidth: 3, color: '#333' },"
-       , "          encoding: {"
-       , "            x: { field: 'z', type: 'quantitative' },"
-       , "            y: { field: 'yhat', type: 'quantitative' }"
-       , "          } }"
-       , "      ]"
-       , "    };"
-       , "  }"
-       , "  function update() {"
-       , "    const spec = buildSpec();"
-       , "    if (window.vegaEmbed) {"
-       , "      window.vegaEmbed('#vl-' + sid, spec, { actions: false });"
-       , "    }"
-       , "  }"
-       , "  window['__updRFFMV_' + sid] = update;"
-       , "  setTimeout(update, 0);"
-       , "})();"
-       ]
-
--- ---------------------------------------------------------------------------
--- 多出力対話的予測 (1 入力 → q 出力)
--- ---------------------------------------------------------------------------
-
-renderInteractiveMultiOut :: Text -> Text -> InteractiveMultiOut -> Text
-renderInteractiveMultiOut sid title imo =
-  let (mn, mid, mx) = imoXSlider imo
-      tFull = "<span class=\"sec-icon\">&#127919;</span> "
-              <> (if T.null title then "対話的予測" else title)
-  in collapsibleSection sid tFull True $
-       T.unlines
-         [ "<div class=\"interactive-multi\">"
-         , "  <div class=\"i-controls\">"
-         , "    <div class=\"slider-row\"><em>入力 " <> imoXCol imo
-            <> " を動かすと " <> imoYCol imo <> "(" <> imoOutAxis imo
-            <> ") の予測曲線が更新されます</em></div>"
-         , "    <div class=\"slider-row\">"
-         , "      <label><b>" <> imoXCol imo <> "</b>:"
-         , "        <input type=\"range\" id=\"i-" <> sid <> "-x\""
-         , "          min=\"" <> showD4 mn <> "\""
-         , "          max=\"" <> showD4 mx <> "\""
-         , "          step=\"" <> showD4 ((mx - mn) / 200) <> "\""
-         , "          value=\"" <> showD4 mid <> "\""
-         , "          oninput=\"window.__updMO_" <> sid <> "()\">"
-         , "        <span id=\"i-" <> sid <> "-x-val\">" <> showD4 mid <> "</span>"
-         , "      </label>"
-         , "    </div>"
-         , "  </div>"
-         , "  <div class=\"i-chart\">"
-         , "    <div class=\"vl-wrap\"><div id=\"vl-" <> sid <> "\"></div></div>"
-         , "  </div>"
-         , "</div>"
-         ]
-
-interactiveMultiOutScript :: Text -> InteractiveMultiOut -> Text
-interactiveMultiOutScript sid imo =
-  let arrD xs = "[" <> T.intercalate "," (map showD4 xs) <> "]"
-      arr2D xss = "[" <> T.intercalate "," (map arrD xss) <> "]"
-      gridArr = arrD (imoOutGrid imo)
-      xObsArr = arrD (imoXObs imo)
-      yObsArr = arr2D (imoYObs imo)
-      predBlock = case imoPred imo of
-        PredLinearMO ints slps -> T.unlines
-          [ "  const model = 'linear-mo';"
-          , "  const intercepts = " <> arrD ints <> ";"
-          , "  const slopes     = " <> arrD slps <> ";"
-          , "  function predict(x) {"
-          , "    const out = new Array(intercepts.length);"
-          , "    for (let j = 0; j < intercepts.length; j++)"
-          , "      out[j] = intercepts[j] + slopes[j] * x;"
-          , "    return out;"
-          , "  }"
-          ]
-        PredKernelRBF1 xtr alpha h -> T.unlines
-          [ "  const model = 'kernel-rbf-1d';"
-          , "  const xTrain = " <> arrD xtr <> ";"
-          , "  const alpha  = " <> arr2D alpha <> ";"  -- n × q
-          , "  const hBand  = " <> showD4 h <> ";"
-          , "  function predict(x) {"
-          , "    const n = xTrain.length;"
-          , "    const q = alpha[0].length;"
-          , "    const out = new Array(q).fill(0);"
-          , "    for (let i = 0; i < n; i++) {"
-          , "      const u = (x - xTrain[i]) / hBand;"
-          , "      const k = Math.exp(-0.5 * u * u) / Math.sqrt(2 * Math.PI);"
-          , "      const row = alpha[i];"
-          , "      for (let j = 0; j < q; j++) out[j] += k * row[j];"
-          , "    }"
-          , "    return out;"
-          , "  }"
-          ]
-  in T.unlines
-       [ "(() => {"
-       , "  const sid = \"" <> sid <> "\";"
-       , "  const xCol = \"" <> imoXCol imo <> "\";"
-       , "  const yCol = \"" <> imoYCol imo <> "\";"
-       , "  const outAxis = \"" <> imoOutAxis imo <> "\";"
-       , "  const outGrid = " <> gridArr <> ";"
-       , "  const xObs    = " <> xObsArr <> ";"
-       , "  const yObs    = " <> yObsArr <> ";"
-       , predBlock
-       , "  function buildSpec() {"
-       , "    const slider = document.getElementById('i-' + sid + '-x');"
-       , "    const x = parseFloat(slider.value);"
-       , "    const lbl = document.getElementById('i-' + sid + '-x-val');"
-       , "    if (lbl) lbl.textContent = (Math.round(x*1000)/1000).toString();"
-       , "    const yPred = predict(x);"
-       , "    const predData = outGrid.map((z, j) => ({ z: z, y: yPred[j] }));"
-       , "    const obsData = [];"
-       , "    for (let i = 0; i < xObs.length; i++) {"
-       , "      const lab = xCol + '=' + xObs[i].toFixed(2);"
-       , "      for (let j = 0; j < outGrid.length; j++) {"
-       , "        obsData.push({ z: outGrid[j], y: yObs[i][j], src: lab });"
-       , "      }"
-       , "    }"
-       , "    return {"
-       , "      $schema: 'https://vega.github.io/schema/vega-lite/v5.json',"
-       , "      width: 760, height: 420,"
-       , "      layer: ["
-       , "        { data: { values: obsData },"
-       , "          mark: { type: 'circle', size: 18, opacity: 0.35 },"
-       , "          encoding: {"
-       , "            x: { field: 'z', type: 'quantitative', title: outAxis },"
-       , "            y: { field: 'y', type: 'quantitative', title: yCol },"
-       , "            color: { field: 'src', type: 'nominal', title: 'observed', legend: null }"
-       , "          } },"
-       , "        { data: { values: predData },"
-       , "          mark: { type: 'line', strokeWidth: 3, color: '#d62728' },"
-       , "          encoding: {"
-       , "            x: { field: 'z', type: 'quantitative' },"
-       , "            y: { field: 'y', type: 'quantitative' }"
-       , "          } }"
-       , "      ]"
-       , "    };"
-       , "  }"
-       , "  function update() {"
-       , "    const spec = buildSpec();"
-       , "    if (window.vegaEmbed) {"
-       , "      window.vegaEmbed('#vl-' + sid, spec, { actions: false });"
-       , "    }"
-       , "  }"
-       , "  window['__updMO_' + sid] = update;"
-       , "  setTimeout(update, 0);"
-       , "})();"
-       ]
-
--- ---------------------------------------------------------------------------
--- 補間 / regrid レポート (Phase G4)
--- ---------------------------------------------------------------------------
-
--- | regrid 結果を可視化するためのデータ。
---
--- R1-R7 は必須情報、R8-R10 はオプション (空リスト/Nothing で非表示)。
--- 'Hanalyze.DataIO.Preprocess.RegridResult' から構築する想定だが、
--- セクション側ではプリミティブ型のみで受けて柔軟性を保つ。
-data InterpReport = InterpReport
-  { irTitle         :: !Text
-  , irInterpKind    :: !Text                       -- ^ "Linear" | "NaturalSpline" | "PCHIP"
-  , irGridKind      :: !Text                       -- ^ "Uniform" | "Adaptive"
-  , irN             :: !Int                        -- ^ 出力 grid 点数
-  , irZBoundsMode   :: !Text                       -- ^ "intersect" | "union"
-  , irZMin          :: !Double
-  , irZMax          :: !Double
-  , irPerIdObserved :: ![(Text, [(Double, Double)])]
-                          -- ^ id ごとの元観測点 [(z, y)]
-  , irPerIdInterpY  :: ![(Text, [(Double, Double)])]
-                          -- ^ id ごとの (z_grid, y_interp) (R2 ライン用)
-  , irGrid          :: ![Double]                   -- ^ 共通 grid (R3 spacing 用)
-  , irDensity       :: ![(Double, Double)]         -- ^ (z, peak |dy/dz|) — adaptive 時のみ
-  , irPerIdSummary  :: ![(Text, Int, Double, Double, Double, Double, Double)]
-                          -- ^ (id, n_obs, zmin, zmax, extrap_below, extrap_above, residual_max)
-                          -- R4 用
-    -- R8-R10 オプション
-  , irExtraEnabled  :: !Bool                       -- ^ True で R8-R10 を出力
-  , irPerIdYRange   :: ![(Text, Double, Double, Double, Double)]
-                          -- ^ (id, ymin_orig, ymax_orig, ymin_grid, ymax_grid) — R10 用
-  } deriving (Show)
-
--- | 最低限のフィールドだけ埋めた InterpReport (テスト/ダミー用)。
-defaultInterpReport :: Text -> InterpReport
-defaultInterpReport t = InterpReport
-  { irTitle         = t
-  , irInterpKind    = "Linear"
-  , irGridKind      = "Uniform"
-  , irN             = 0
-  , irZBoundsMode   = "intersect"
-  , irZMin          = 0
-  , irZMax          = 1
-  , irPerIdObserved = []
-  , irPerIdInterpY  = []
-  , irGrid          = []
-  , irDensity       = []
-  , irPerIdSummary  = []
-  , irExtraEnabled  = False
-  , irPerIdYRange   = []
-  }
-
--- | 補間 / regrid のレポートセクションを構築。
---
--- 出力構造:
---
--- * Card "Regrid summary"
---   - R1: パラメタテーブル (KeyValue)
---   - R4: id ごとの観測点数 / z レンジ / 外挿距離 / 残差表 (Table)
---   - R6: 外挿警告テーブル (該当 id のみ; 0 件なら省略)
---   - R7: id 間 z アラインメント dot plot (Vega)
---   - R2: 補間オーバーレイ small multiples (Vega)
---   - R3: adaptive 時のみ density(z) + grid spacing (Vega)
---   - R5: 補間残差サマリ (R4 と統合済)
---   - (オプション) R8: id ごとの観測点数 bar chart
---   - (オプション) R9: 単調性チェック (PCHIP 以外、簡易判定)
---   - (オプション) R10: y レンジ比較表
-secInterpolation :: InterpReport -> ReportSection
-secInterpolation ir =
-  let -- R1 params
-      r1 = secKeyValue "Parameters"
-             [ ("Interpolation",  irInterpKind ir)
-             , ("Grid",           irGridKind ir)
-             , ("Grid points (N)", T.pack (show (irN ir)))
-             , ("Z bounds mode",  irZBoundsMode ir)
-             , ("Effective zmin", T.pack (showFFloat (Just 4) (irZMin ir) ""))
-             , ("Effective zmax", T.pack (showFFloat (Just 4) (irZMax ir) ""))
-             , ("Number of ids",  T.pack (show (length (irPerIdSummary ir))))
-             ]
-      -- R4 per-id summary table
-      fmt n x = T.pack (showFFloat (Just n) x "")
-      r4Rows = [ [ i, T.pack (show n), fmt 4 zmn, fmt 4 zmx
-                 , fmt 4 eb, fmt 4 ea, fmt 4 res ]
-               | (i, n, zmn, zmx, eb, ea, res) <- irPerIdSummary ir ]
-      r4 = secTable "Per-id summary"
-             ["id", "n_observed", "z_min", "z_max"
-             , "extrap_below", "extrap_above", "interp_residual_max"]
-             r4Rows
-      -- R6 extrapolation warning (only ids with extrap > 0)
-      r6Rows = [ [ i, fmt 4 eb, fmt 4 ea ]
-               | (i, _, _, _, eb, ea, _) <- irPerIdSummary ir
-               , eb > 1e-12 || ea > 1e-12 ]
-      r6 = if null r6Rows
-             then Nothing
-             else Just (secTable "Extrapolation warnings"
-                          ["id", "extrap_below", "extrap_above"]
-                          r6Rows)
-      -- R7 id-z alignment dot plot
-      r7 = secVega "Z alignment across ids" (idAlignmentSpec ir)
-      -- R2 interpolation overlay (small multiples)
-      r2 = secVega "Interpolation overlay (per id)" (interpolationOverlaySpec ir)
-      -- R3 density profile (adaptive only)
-      r3 = if null (irDensity ir)
-             then Nothing
-             else Just (secVega "Adaptive density profile" (densityProfileSpec ir))
-      -- R8 obs count bar (extra)
-      r8 = if irExtraEnabled ir
-             then Just (secBarChart "Observation count per id"
-                         [ (i, fromIntegral n)
-                         | (i, n, _, _, _, _, _) <- irPerIdSummary ir ])
-             else Nothing
-      -- R10 y-range comparison (extra)
-      r10Rows = [ [ i, fmt 4 yo0, fmt 4 yo1, fmt 4 yg0, fmt 4 yg1
-                  , fmt 4 (yg0 - yo0), fmt 4 (yg1 - yo1) ]
-                | (i, yo0, yo1, yg0, yg1) <- irPerIdYRange ir ]
-      r10 = if irExtraEnabled ir && not (null r10Rows)
-              then Just (secTable
-                          "Y range: original vs interpolated"
-                          ["id", "y_min_orig", "y_max_orig"
-                          , "y_min_grid", "y_max_grid"
-                          , "Δ_min", "Δ_max"]
-                          r10Rows)
-              else Nothing
-      -- R9 monotonicity check (extra; skip for PCHIP since guaranteed)
-      r9 = if irExtraEnabled ir && irInterpKind ir /= "PCHIP"
-             then
-               let nonMono =
-                     [ i
-                     | (i, ys) <- irPerIdInterpY ir
-                     , let vs = map snd ys
-                     , let asc = and (zipWith (<=) vs (tail vs))
-                     , let desc = and (zipWith (>=) vs (tail vs))
-                     , not asc && not desc
-                       -- かつ 元データが単調なら警告
-                     , let obs = Prelude.lookup i (irPerIdObserved ir)
-                     , case obs of
-                         Just ps ->
-                           let os = map snd ps
-                           in and (zipWith (<=) os (tail os))
-                              || and (zipWith (>=) os (tail os))
-                         Nothing -> False
-                     ]
-               in if null nonMono
-                    then Nothing
-                    else Just (secMarkdown "Monotonicity warning"
-                                ("Non-monotone interpolation curves "
-                                 <> "(observed data was monotone): "
-                                 <> T.intercalate ", " nonMono))
-             else Nothing
-      sections = [r1, r4]
-              ++ maybe [] (:[]) r6
-              ++ [r7, r2]
-              ++ maybe [] (:[]) r3
-              ++ maybe [] (:[]) r8
-              ++ maybe [] (:[]) r9
-              ++ maybe [] (:[]) r10
-  in secCard (irTitle ir) sections
-
--- | R2: 補間オーバーレイ — id ごとに facet 化 (small multiples)。
--- 元観測点を dot、補間曲線を line で重ね描き (kind 列で区別)。
-interpolationOverlaySpec :: InterpReport -> VegaLite
-interpolationOverlaySpec ir =
-  let mkObsRows = concat
-        [ [ dataRow [ ("id", Str i), ("z", Number z), ("y", Number y)
-                    , ("kind", Str "obs") ] []
-          | (z, y) <- pts ]
-        | (i, pts) <- irPerIdObserved ir ]
-      mkLineRows = concat
-        [ [ dataRow [ ("id", Str i), ("z", Number z), ("y", Number y)
-                    , ("kind", Str "interp") ] []
-          | (z, y) <- ys ]
-        | (i, ys) <- irPerIdInterpY ir ]
-      datValues = dataFromRows [] (concat (mkObsRows ++ mkLineRows))
-      enc = encoding
-            . position X [PName "z", PmType Quantitative]
-            . position Y [PName "y", PmType Quantitative]
-            . color [MName "kind", MmType Nominal
-                   , MScale [SDomain (DStrings ["obs", "interp"])
-                           , SRange (RStrings ["#d62728", "#1f77b4"])]]
-            . VL.shape [MName "kind", MmType Nominal]
-      facetCfg = facetFlow [FName "id", FmType Nominal, FHeader [HTitle ""]]
-      spec = asSpec
-        [ mark Point [MOpacity 0.7]
-        , (enc [])
-        ]
-  in toVegaLite
-       [ datValues
-       , columns 3
-       , facetCfg
-       , specification spec
-       , VL.width 200, VL.height 150
-       ]
-
--- | R3: adaptive density(z) を line で表示し、その下に grid 点を rule (vertical) で重ねる。
-densityProfileSpec :: InterpReport -> VegaLite
-densityProfileSpec ir =
-  let densRows  = [ dataRow [("z", Number z), ("density", Number d)] []
-                  | (z, d) <- irDensity ir ]
-      gridRows  = [ dataRow [("z", Number z)] [] | z <- irGrid ir ]
-      densSpec  = asSpec
-        [ dataFromRows [] (concat densRows)
-        , mark Line [MStrokeWidth 2, MColor "#2ca02c"]
-        , (encoding . position X [PName "z", PmType Quantitative]
-                   . position Y [PName "density", PmType Quantitative
-                               , PAxis [AxTitle "peak |dy/dz|"]]) []
-        ]
-      gridSpec  = asSpec
-        [ dataFromRows [] (concat gridRows)
-        , mark Rule [MStrokeWidth 1, MColor "#ff7f0e", MOpacity 0.4]
-        , (encoding . position X [PName "z", PmType Quantitative]) []
-        ]
-  in toVegaLite
-       [ layer [densSpec, gridSpec]
-       , VL.width 600, VL.height 200
-       ]
-
--- | R7: id ごとの z 観測点を縦並びの dot plot で表示 (z レンジ揃え目視確認)。
-idAlignmentSpec :: InterpReport -> VegaLite
-idAlignmentSpec ir =
-  let rows = concat
-        [ [ dataRow [("id", Str i), ("z", Number z)] [] | (z, _) <- pts ]
-        | (i, pts) <- irPerIdObserved ir ]
-      enc  = encoding
-             . position X [PName "z", PmType Quantitative]
-             . position Y [PName "id", PmType Nominal]
-  in toVegaLite
-       [ dataFromRows [] (concat rows)
-       , mark Tick [MOpacity 0.7, MColor "#4c78a8"]
-       , (enc [])
-       , VL.width 600
-       , VL.height 200
-       ]
diff --git a/src/Hanalyze/Viz/ReportInstances.hs b/src/Hanalyze/Viz/ReportInstances.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/ReportInstances.hs
+++ /dev/null
@@ -1,1255 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.ReportInstances
--- Description : 各種フィット結果型に対する Reportable インスタンス集
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
-{-# OPTIONS_GHC -Wno-orphans #-}
--- | 'Hanalyze.Viz.ReportBuilder.Reportable' instances for the various fit types.
---
--- Importing this module (purely for its instances) lets a user pass any
--- supported fit result directly to 'renderReport':
---
--- @
--- import Hanalyze.Model.Regularized
--- import Hanalyze.Viz.ReportBuilder
--- import Hanalyze.Viz.ReportInstances ()
---
--- main = do
---   let fit = fitRegularized (L2 0.1) xMat yVec
---       cfg = defaultReportConfig "Ridge demo"
---   renderReport "out.html" cfg (toReport cfg df ["x"] "y" fit)
--- @
---
--- 提供されるインスタンス:
--- - 'RegFit'         (Hanalyze.Model.Regularized) — 正則化線形回帰
--- - 'SplineFit'      (Hanalyze.Model.Spline)      — B-spline / Natural cubic
--- - 'KernelRidgeFit' (Hanalyze.Model.KernelRegression)      — Kernel Ridge regression
--- - 'RFFRidgeFit'    (Hanalyze.Model.RFF)         — Random Fourier Features Ridge
--- - 'RobustGPFit'    (Hanalyze.Model.GPRobust)    — ロバスト GP
---
--- LM/GLM/GLMM/GP/HBM は当面 'Hanalyze.Viz.AnalysisReport' (非推奨) 経由。
--- ReportBuilder 化が次の課題。
-module Hanalyze.Viz.ReportInstances
-  ( LMReport (..)
-  , GLMReport (..)
-  , RFReport (..)
-  , GLMMReport (..)
-  , GPReport (..)
-  , HBMLinearReport (..)
-  , HBMReport (..)
-  , HBMRibbon (..)
-  , RFFMVReport (..)
-  ) where
-
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.List (sortBy)
-import qualified Data.Vector as V
-import qualified Numeric.LinearAlgebra as LA
-import Text.Printf (printf)
-
-import qualified DataFrame.Internal.DataFrame as DXD
-import Hanalyze.DataIO.Convert (getDoubleVec, getMaybeTextVec)
-import qualified Numeric.LinearAlgebra as LA2
-import qualified Hanalyze.Stat.Standardize as Std
-import qualified Hanalyze.Stat.NumberFormat as NF
-import Hanalyze.Viz.Scatter   (scatterWithGroups)
-import Hanalyze.Viz.Core      (defaultConfig, PlotConfig (..))
-import Hanalyze.Model.Core      (FitResult, coeffList, fittedList, residualsV, rSquared1)
-import Hanalyze.Model.LM        (SmoothFit (..))
-import Hanalyze.Model.GLM       (Family (..), LinkFn (..))
-import Hanalyze.Model.Regularized (RegFit (..), Penalty (..), predictRegularized)
-import Hanalyze.Model.Spline     (SplineFit (..), SplineKind (..), predictSpline, sfBeta)
-import Hanalyze.Model.KernelRegression     (KernelRidgeFit (..), predictKernelRidge)
-import Hanalyze.Model.RFF        (RFFRidgeFit (..), predictRFFRidge, rffrFeatures,
-                         rffSigmaF, rffLengthScale, rffOmegas,
-                         RFFRidgeFitMV (..), RFFFeaturesMV (..),
-                         predictRFFRidgeMV)
-import Hanalyze.Model.GP         (GPParams (..))
-import Hanalyze.Model.GPRobust   (RobustGPFit (..), RobustLikelihood (..))
-import Hanalyze.Model.Quantile   (QRFit (..))
-import Hanalyze.Model.GAM        (GAMFit (..), predictGAMComponent)
-import Hanalyze.Model.RandomForest (RandomForest (..), featureImportance)
-import qualified Hanalyze.Model.GLMM as GLMM
-import qualified Hanalyze.Model.GP   as GP
-import qualified Hanalyze.MCMC.Core  as MC
-import Hanalyze.Viz.ReportBuilder
-
--- ---------------------------------------------------------------------------
--- 内部ユーティリティ
--- ---------------------------------------------------------------------------
-
--- | x グリッド (データの min/max から 100 点)。
-xGridFromVec :: V.Vector Double -> [Double]
-xGridFromVec v
-  | V.null v  = []
-  | otherwise =
-      let lo = V.minimum v
-          hi = V.maximum v
-      in [ lo + fromIntegral i * (hi - lo) / 99 | i <- [0 .. 99 :: Int] ]
-
--- | DataFrame から x 列 (1 つ) を numeric vector で取り出す。
-firstNumericVec :: [Text] -> DXD.DataFrame -> Maybe (V.Vector Double)
-firstNumericVec []     _  = Nothing
-firstNumericVec (c:_)  df = getDoubleVec c df
-
-penaltyName :: Penalty -> Text
-penaltyName p = case p of
-  NoPen          -> "OLS"
-  L2 _           -> "Ridge (L2)"
-  L1 _           -> "Lasso (L1)"
-  ElasticNet _ _ -> "Elastic Net"
-
-penaltyKVs :: Penalty -> [(Text, Text)]
-penaltyKVs p = case p of
-  NoPen           -> [("Penalty", "OLS")]
-  L2 lam          -> [("Penalty", "L2 (Ridge)"), ("λ", T.pack (printf "%g" lam))]
-  L1 lam          -> [("Penalty", "L1 (Lasso)"), ("λ", T.pack (printf "%g" lam))]
-  ElasticNet l1 l2 ->
-    [ ("Penalty", "ElasticNet")
-    , ("λ₁ (L1)", T.pack (printf "%g" l1))
-    , ("λ₂ (L2)", T.pack (printf "%g" l2))
-    ]
-
-splineKindName :: SplineKind -> Text
-splineKindName (BSpline k) = "B-spline (degree " <> T.pack (show k) <> ")"
-splineKindName NaturalCubic = "Natural cubic spline"
-
--- ---------------------------------------------------------------------------
--- RegFit (Regularized)
--- ---------------------------------------------------------------------------
-
-instance Reportable RegFit where
-  toReport _cfg df xCols yCol fit =
-    let beta     = LA.toList (rfBeta fit)
-        labels   = "intercept" : xCols
-        coeffs   = zip labels beta
-        nonZero  = rfNonZero fit
-        n        = length beta
-        modelLbl = penaltyName (rfPenalty fit)
-        formula  = yCol <> " ~ "
-                   <> T.intercalate " + " ("β₀" : xCols)
-        residuals = LA.toList (rfResid fit)
-        fitted    = LA.toList (rfYHat fit)
-
-        -- 1 変数なら scatter + fit
-        scatterSec = case (xCols, firstNumericVec xCols df,
-                           getDoubleVec yCol df) of
-          ([xc1], Just xVec, Just yVec) ->
-            let grid = xGridFromVec xVec
-                gridMat = LA.fromColumns
-                            [ LA.konst 1 (length grid)
-                            , LA.fromList grid ]
-                gridY = LA.toList (predictRegularized fit gridMat)
-                smooth = SmoothCurve grid gridY [] []
-                _ = xc1
-            in [secFitScatter xc1 yCol (V.toList xVec) (V.toList yVec)
-                  (Just smooth)]
-          _ -> []
-    in [ secDataOverview df xCols yCol
-       , secModelOverview modelLbl formula Nothing
-       , secCoefficients coeffs (Just ("R²", rfR2 fit))
-       , secKeyValue "Fit summary" $
-           penaltyKVs (rfPenalty fit) ++
-           [ ("|β| > 1e-8",
-              T.pack (show nonZero) <> " / " <> T.pack (show n))
-           ]
-       ] ++ scatterSec ++
-       [ secResiduals fitted residuals ]
-
--- ---------------------------------------------------------------------------
--- SplineFit
--- ---------------------------------------------------------------------------
-
-instance Reportable SplineFit where
-  toReport _cfg df xCols yCol fit =
-    case (xCols, firstNumericVec xCols df, getDoubleVec yCol df) of
-      ([xc], Just xVec, Just yVec) ->
-        let kindLbl = splineKindName (sfKind fit)
-            grid    = xGridFromVec xVec
-            gridY   = V.toList (predictSpline fit (V.fromList grid))
-            smooth  = SmoothCurve grid gridY [] []
-            ys      = V.toList yVec
-            yhat    = V.toList (predictSpline fit xVec)
-            beta    = LA.toList (sfBeta fit)
-            knots   = sfKnots fit
-            formula = yCol <> " ~ s(" <> xc <> "; " <> T.pack (show (length knots))
-                      <> " knots)"
-        in [ secDataOverview df [xc] yCol
-           , secModelOverview kindLbl formula Nothing
-           , secKeyValue "Fit summary"
-               [ ("Kind",  kindLbl)
-               , ("Knots", T.pack (show (length knots)))
-               , ("Coefficients", T.pack (show (length beta)))
-               ]
-           , secFitScatter xc yCol (V.toList xVec) ys (Just smooth)
-           , secResiduals yhat (zipWith (-) ys yhat)
-           ]
-      _ -> [secDataOverview df xCols yCol
-           , secModelOverview "Spline" "(needs single numeric x and y)" Nothing
-           ]
-
--- ---------------------------------------------------------------------------
--- KernelRidgeFit
--- ---------------------------------------------------------------------------
-
-instance Reportable KernelRidgeFit where
-  toReport _cfg df xCols yCol fit =
-    case (xCols, firstNumericVec xCols df, getDoubleVec yCol df) of
-      ([xc], Just xVec, Just yVec) ->
-        let grid    = xGridFromVec xVec
-            gridV   = V.fromList grid
-            gridY   = V.toList (predictKernelRidge fit gridV)
-            smooth  = SmoothCurve grid gridY [] []
-            ys      = V.toList yVec
-            yhat    = V.toList (predictKernelRidge fit xVec)
-            formula = yCol <> " ~ K_h(" <> xc <> ", ·)ᵀ α"
-        in [ secDataOverview df [xc] yCol
-           , secModelOverview "Kernel Ridge regression" formula Nothing
-           , secKeyValue "Fit summary"
-               [ ("Kernel",    T.pack (show (krKernel fit)))
-               , ("Bandwidth", T.pack (printf "%.4f" (krH fit)))
-               , ("Lambda",    T.pack (printf "%g" (krLambda fit)))
-               , ("Train size",T.pack (show (V.length (krXs fit))))
-               ]
-           , secFitScatter xc yCol (V.toList xVec) ys (Just smooth)
-           , secResiduals yhat (zipWith (-) ys yhat)
-           ]
-      _ -> [secDataOverview df xCols yCol
-           , secModelOverview "Kernel Ridge" "(needs single numeric x and y)"
-                              Nothing
-           ]
-
--- ---------------------------------------------------------------------------
--- RFFRidgeFit
--- ---------------------------------------------------------------------------
-
-instance Reportable RFFRidgeFit where
-  toReport _cfg df xCols yCol fit =
-    case (xCols, firstNumericVec xCols df, getDoubleVec yCol df) of
-      ([xc], Just xVec, Just yVec) ->
-        let feats   = rffrFeatures fit
-            grid    = xGridFromVec xVec
-            gridY   = predictRFFRidge fit grid
-            smooth  = SmoothCurve grid gridY [] []
-            ys      = V.toList yVec
-            yhat    = predictRFFRidge fit (V.toList xVec)
-            d       = V.length (rffOmegas feats)
-            formula = yCol <> " ~ φ(" <> xc <> ")ᵀ w   (D=" <> T.pack (show d) <> ")"
-            ellLbl  = T.pack (printf "%.4f" (rffLengthScale feats))
-            sfLbl   = T.pack (printf "%.4f" (rffSigmaF feats))
-        in [ secDataOverview df [xc] yCol
-           , secModelOverview "RFF Ridge regression" formula Nothing
-           , secKeyValue "Fit summary"
-               [ ("Features (D)", T.pack (show d))
-               , ("Length scale ℓ", ellLbl)
-               , ("Signal σ_f",     sfLbl)
-               , ("Lambda",         T.pack (printf "%g" (rffrLambda fit)))
-               ]
-           , secFitScatter xc yCol (V.toList xVec) ys (Just smooth)
-           , secResiduals yhat (zipWith (-) ys yhat)
-           ]
-      _ -> [secDataOverview df xCols yCol
-           , secModelOverview "RFF Ridge" "(needs single numeric x and y)"
-                              Nothing
-           ]
-
--- ---------------------------------------------------------------------------
--- RobustGPFit
--- ---------------------------------------------------------------------------
-
-instance Reportable RobustGPFit where
-  toReport _cfg df xCols yCol fit =
-    let likLbl = case rgpLik fit of
-          RGaussian s -> "Gaussian (σ_n=" <> T.pack (printf "%.3f" s) <> ")"
-          RStudentT nu s -> "StudentT (ν=" <> T.pack (printf "%g" nu)
-                            <> ", σ=" <> T.pack (printf "%.3f" s) <> ")"
-          RCauchy g      -> "Cauchy (γ=" <> T.pack (printf "%.3f" g) <> ")"
-        params = rgpParams fit
-        formula = yCol <> " | f ~ " <> likLbl
-                  <> ",   f ~ GP(0, K(" <> T.intercalate "," xCols <> "))"
-    in [ secDataOverview df xCols yCol
-       , secModelOverview "Robust Gaussian Process" formula Nothing
-       , secKeyValue "Fit summary"
-           [ ("Kernel", T.pack (show (rgpKernel fit)))
-           , ("Likelihood", likLbl)
-           , ("Length scale", T.pack (printf "%.4f" (gpLengthScale params)))
-           , ("Signal σ_f²", T.pack (printf "%.4f" (gpSignalVar params)))
-           , ("IRLS iterations", T.pack (show (rgpIters fit)))
-           , ("Train size", T.pack (show (length (rgpTrainX fit))))
-           ]
-       ]
-
--- ---------------------------------------------------------------------------
--- LM / GLM (axis-1 C, Phase 1)
--- ---------------------------------------------------------------------------
-
--- | Wrapper to drive a @Reportable@ instance for a linear-model fit.
---
--- Bundles the information needed by the single-predictor LM @Reportable@
--- instance. Passing @lmrSmooth = Just sf@ overlays a smooth curve with
--- its confidence band on the scatter plot.
---
--- For multi-predictor LMs (two or more @xCols@), the scatter+smooth
--- view is omitted; 'secInteractiveMulti' provides a primary-axis
--- dropdown plus secondary-axis sliders for prediction.
-data LMReport = LMReport
-  { lmrFit    :: FitResult
-  , lmrSmooth :: Maybe SmoothFit
-  } deriving Show
-
--- | Wrapper to drive a @Reportable@ instance for a GLM fit.
-data GLMReport = GLMReport
-  { glmrFit    :: FitResult
-  , glmrFamily :: Family
-  , glmrLink   :: LinkFn
-  , glmrSmooth :: Maybe SmoothFit
-  } deriving Show
-
--- | Display name of a 'LinkFn'.
-linkLabel :: LinkFn -> Text
-linkLabel Identity = "identity"
-linkLabel Log      = "log"
-linkLabel Logit    = "logit"
-linkLabel Sqrt     = "sqrt"
-
--- | Display name of a 'Family'.
-familyLabel :: Family -> Text
-familyLabel Gaussian = "Gaussian"
-familyLabel Binomial = "Binomial"
-familyLabel Poisson  = "Poisson"
-
--- | 残差から σ_hat / RMSE / max|r| を作る。
-residStats :: [Double] -> Int -> (Double, Double, Double)
-residStats resid p =
-  let n        = length resid
-      sumSq    = sum [ r * r | r <- resid ]
-      sigmaHat = sqrt (sumSq / fromIntegral (max 1 (n - p)))
-      rmse     = sqrt (sumSq / fromIntegral (max 1 n))
-      maxAbs   = maximum (0 : map abs resid)
-  in (sigmaHat, rmse, maxAbs)
-
--- | smoothFit → SmoothCurve への変換 (空 Smooth は空カーブ)。
-smoothFitToCurve :: Maybe SmoothFit -> SmoothCurve
-smoothFitToCurve Nothing   = SmoothCurve [] [] [] []
-smoothFitToCurve (Just sf) = SmoothCurve (sfX sf) (sfFit sf) (sfLower sf) (sfUpper sf)
-
--- | xCols + xVecs から InteractiveModel を構築 (LM/GLM 共通)。
-mkInteractive :: [Text] -> Text -> [V.Vector Double] -> [Double]
-              -> Double -> [Double] -> Text -> Maybe Double
-              -> InteractiveModel
-mkInteractive xCols yCol xVecs ys b0 betas link mSigma =
-  let n        = length ys
-      xRows    = [ [ xv V.! i | xv <- xVecs ] | i <- [0 .. n - 1] ]
-      mkSlider xv =
-        let lo = if V.null xv then 0 else V.minimum xv
-            hi = if V.null xv then 1 else V.maximum xv
-            ext = (hi - lo) * 0.5
-        in (lo - ext, (lo + hi) / 2, hi + ext)
-  in InteractiveModel
-       { imXCols     = xCols
-       , imYCol      = yCol
-       , imXValues   = xRows
-       , imYValues   = ys
-       , imIntercept = b0
-       , imBetas     = betas
-       , imLink      = link
-       , imSlider    = map mkSlider xVecs
-       , imCISigma   = mSigma
-       }
-
--- | 数式: y = β₀ + β₁ x_1 + ... + β_p x_p
-linearFormula :: Text -> [Text] -> Text
-linearFormula yCol xCols =
-  yCol <> " ~ "
-       <> T.intercalate " + "
-            ("β₀" : [ "β" <> T.pack (show (i :: Int)) <> " · " <> x
-                   | (i, x) <- zip [1 ..] xCols ])
-
-instance Reportable LMReport where
-  toReport _cfg df xCols yCol (LMReport fit mSmooth) =
-    let beta    = coeffList fit
-        coefLabels = "β₀ (intercept)"
-                   : [ "β" <> T.pack (show (i :: Int)) <> " (" <> x <> ")"
-                     | (i, x) <- zip [1 ..] xCols ]
-        coeffs   = zip coefLabels beta
-        fitted   = fittedList fit
-        resid    = LA.toList (residualsV fit)
-        p        = length beta
-        (sigmaH, rmse, maxAbs) = residStats resid p
-
-        xVecs    = [ v | c <- xCols, Just v <- [getDoubleVec c df] ]
-        yVecMb   = getDoubleVec yCol df
-
-        smoothC  = smoothFitToCurve mSmooth
-
-        scatterCard = case (xCols, xVecs, yVecMb) of
-          ([xc], [xv], Just yv)
-            | length xVecs == length xCols ->
-                [ secCard "散布図 + 回帰線"
-                    [ secFitScatter xc yCol (V.toList xv) (V.toList yv)
-                        (Just smoothC) ] ]
-          _ -> []
-
-        interactiveSec
-          | length xVecs == length xCols, not (null xVecs)
-          , Just yv <- yVecMb =
-              let im = mkInteractive xCols yCol xVecs (V.toList yv)
-                                     (head beta) (drop 1 beta)
-                                     "identity" (Just sigmaH)
-              in [secInteractiveMulti "対話的予測" im]
-          | otherwise = []
-
-        formula =
-          "$" <> linearFormula yCol xCols <> "$<br>"
-          <> "$\\varepsilon_i \\sim \\text{Normal}(0, \\sigma^2)$"
-
-        statRow =
-          secStatRow
-            [ ("R²",         T.pack (printf "%.4f" (rSquared1 fit)))
-            , ("方法",       "OLS (QR)")
-            , ("σ_hat",      T.pack (printf "%.4f" sigmaH))
-            , ("RMSE",       T.pack (printf "%.4f" rmse))
-            , ("最大絶対残差", T.pack (printf "%.4f" maxAbs))
-            ]
-
-        resultSec =
-          secCollapsible "<span class=\"sec-icon\">&#128200;</span> 回帰結果" True
-            ([ statRow
-             , secCard "係数" [secCoefficients coeffs (Just ("R²", rSquared1 fit))]
-             ]
-             ++ scatterCard
-             ++ [ secCard "残差プロット" [secResiduals fitted resid] ])
-
-    in [ secDataOverview df xCols yCol
-       , secModelOverview "LM" formula Nothing
-       , resultSec
-       ] ++ interactiveSec
-
-instance Reportable GLMReport where
-  toReport _cfg df xCols yCol (GLMReport fit fam lk mSmooth) =
-    let beta    = coeffList fit
-        coefLabels = "β₀ (intercept)"
-                   : [ "β" <> T.pack (show (i :: Int)) <> " (" <> x <> ")"
-                     | (i, x) <- zip [1 ..] xCols ]
-        coeffs   = zip coefLabels beta
-        fitted   = fittedList fit
-        resid    = LA.toList (residualsV fit)
-        p        = length beta
-        (sigmaH, rmse, maxAbs) = residStats resid p
-
-        xVecs    = [ v | c <- xCols, Just v <- [getDoubleVec c df] ]
-        yVecMb   = getDoubleVec yCol df
-
-        smoothC  = smoothFitToCurve mSmooth
-
-        modelType = "GLM(" <> familyLabel fam <> ")"
-        linkTxt   = linkLabel lk
-
-        formula = case fam of
-          Poisson  -> "$" <> yCol <> "_i \\sim \\text{Poisson}(\\lambda_i)$<br>"
-                      <> "$\\log \\lambda_i = "
-                      <> T.intercalate " + "
-                           ("\\beta_0" : [ "\\beta_" <> T.pack (show (i :: Int))
-                                            <> " " <> x <> "_i"
-                                          | (i, x) <- zip [1 ..] xCols ])
-                      <> "$"
-          Binomial -> "$" <> yCol <> "_i \\sim \\text{Binomial}(n_i, p_i)$<br>"
-                      <> "$\\text{logit}(p_i) = \\beta_0 + \\sum \\beta_j x_{ij}$"
-          Gaussian -> "$" <> linearFormula yCol xCols <> "$<br>"
-                      <> "$\\varepsilon_i \\sim \\text{Normal}(0, \\sigma^2)$"
-
-        scatterCard = case (xCols, xVecs, yVecMb) of
-          ([xc], [xv], Just yv)
-            | length xVecs == length xCols ->
-                [ secCard "散布図 + 回帰線"
-                    [ secFitScatter xc yCol (V.toList xv) (V.toList yv)
-                        (Just smoothC) ] ]
-          _ -> []
-
-        interactiveSec
-          | length xVecs == length xCols, not (null xVecs)
-          , Just yv <- yVecMb =
-              let im = mkInteractive xCols yCol xVecs (V.toList yv)
-                                     (head beta) (drop 1 beta)
-                                     linkTxt Nothing
-              in [secInteractiveMulti "対話的予測" im]
-          | otherwise = []
-
-        r2Label = case fam of
-          Gaussian -> "R²"
-          _        -> "McFadden R²"
-
-        statRow =
-          secStatRow
-            [ (r2Label,     T.pack (printf "%.4f" (rSquared1 fit)))
-            , ("方法",       "IRLS")
-            , ("σ_hat",      T.pack (printf "%.4f" sigmaH))
-            , ("RMSE",       T.pack (printf "%.4f" rmse))
-            , ("最大絶対残差", T.pack (printf "%.4f" maxAbs))
-            ]
-
-        resultSec =
-          secCollapsible "<span class=\"sec-icon\">&#128200;</span> 回帰結果" True
-            ([ statRow
-             , secCard "係数" [secCoefficients coeffs (Just (r2Label, rSquared1 fit))]
-             ]
-             ++ scatterCard
-             ++ [ secCard "残差プロット" [secResiduals fitted resid] ])
-
-    in [ secDataOverview df xCols yCol
-       , secModelOverviewLink modelType formula linkTxt Nothing
-       , resultSec
-       ] ++ interactiveSec
-
--- ---------------------------------------------------------------------------
--- Quantile Regression (axis-1 B)
--- ---------------------------------------------------------------------------
-
-instance Reportable QRFit where
-  toReport _cfg df xCols yCol fit =
-    let beta    = LA.toList (qfBeta fit)
-        coefLabels = "intercept"
-                   : [ "β" <> T.pack (show (i :: Int)) <> " (" <> x <> ")"
-                     | (i, x) <- zip [1 ..] xCols ]
-        coeffs   = zip coefLabels beta
-        fitted   = LA.toList (qfYHat fit)
-        resid    = LA.toList (qfResid fit)
-        p        = length beta
-        (_sigmaH, rmse, maxAbs) = residStats resid p
-        tau      = qfTau fit
-        formula  = "$Q_{\\tau=" <> T.pack (printf "%.2f" tau)
-                   <> "}(" <> yCol <> " | x) = "
-                   <> T.intercalate " + "
-                        ("\\beta_0" : [ "\\beta_" <> T.pack (show (i :: Int))
-                                          <> " " <> x
-                                       | (i, x) <- zip [1 ..] xCols ])
-                   <> "$"
-        statRow =
-          secStatRow
-            [ ("τ",            T.pack (printf "%.2f" tau))
-            , ("Pseudo R¹",    T.pack (printf "%.4f" (qfR1 fit)))
-            , ("Pinball loss", T.pack (printf "%.4f" (qfPinball fit)))
-            , ("反復",         T.pack (show (qfIters fit)))
-            , ("RMSE",         T.pack (printf "%.4f" rmse))
-            , ("最大絶対残差", T.pack (printf "%.4f" maxAbs))
-            ]
-        scatterCard = case (xCols, firstNumericVec xCols df, getDoubleVec yCol df) of
-          ([xc], Just xv, Just yv) ->
-            -- 単変数: yHat を x ソート順で線として描く
-            let pairs = zip (V.toList xv) fitted
-                sorted = sortByFst pairs
-                smooth = SmoothCurve (map fst sorted) (map snd sorted) [] []
-            in [ secCard "散布図 + 推定 τ-分位点線"
-                   [ secFitScatter xc yCol (V.toList xv) (V.toList yv)
-                       (Just smooth) ] ]
-          _ -> []
-        resultSec =
-          secCollapsible "<span class=\"sec-icon\">&#128200;</span> 回帰結果" True
-            ([ statRow
-             , secCard "係数" [secCoefficients coeffs (Just ("Pseudo R¹", qfR1 fit))]
-             ]
-             ++ scatterCard
-             ++ [ secCard "残差プロット" [secResiduals fitted resid] ])
-    in [ secDataOverview df xCols yCol
-       , secModelOverview "Quantile Regression" formula Nothing
-       , resultSec
-       ]
-
-sortByFst :: Ord a => [(a, b)] -> [(a, b)]
-sortByFst = sortBy (\(a, _) (b, _) -> compare a b)
-
--- ---------------------------------------------------------------------------
--- GAM (axis-1 B)
--- ---------------------------------------------------------------------------
-
-instance Reportable GAMFit where
-  toReport _cfg df xCols yCol fit =
-    let fitted = LA.toList (gamYHat fit)
-        resid  = LA.toList (gamResid fit)
-        n      = length fitted
-        p      = sum [ LA.size b | b <- gamBetas fit ]
-        (_sigmaH, rmse, maxAbs) = residStats resid p
-        statRow =
-          secStatRow
-            [ ("R²",        T.pack (printf "%.4f" (gamR2 fit)))
-            , ("Degree",    T.pack (show (gamDegree fit)))
-            , ("Knots",     T.pack (show (length (head (gamKnots fit ++ [[]])))))
-            , ("λ (Ridge)", T.pack (printf "%g" (gamLambda fit)))
-            , ("RMSE",      T.pack (printf "%.4f" rmse))
-            , ("最大絶対残差", T.pack (printf "%.4f" maxAbs))
-            ]
-        formula = "$" <> yCol <> "_i = \\beta_0 + \\sum_j s_j("
-                  <> T.intercalate ", " xCols <> ")_i + \\varepsilon_i$"
-
-        -- 各特徴の partial effect: s_j(x_j) を smooth として可視化
-        partialCards =
-          [ let mxVec = getDoubleVec x df
-            in case mxVec of
-                 Just xv ->
-                   let xsRaw = V.toList xv
-                       sorted = sortByFst (zip xsRaw [0 :: Int ..])
-                       xsS    = map fst sorted
-                       grid   = V.fromList xsS
-                       sjV    = predictGAMComponent fit (j - 1) grid
-                       sjList = V.toList sjV
-                       partRes = [ resid !! i + (sjList !! k)
-                                 | (k, (_, i)) <- zip [0 ..] sorted ]
-                       smooth = SmoothCurve xsS sjList [] []
-                   in secCard ("Partial effect: s(" <> x <> ")")
-                        [ secFitScatter x ("s(" <> x <> ")")
-                            xsS partRes (Just smooth) ]
-                 Nothing -> secMarkdown ("Partial effect: " <> x)
-                              ("(列 " <> x <> " が DataFrame に見つかりません)")
-          | (j, x) <- zip [1 :: Int ..] xCols, n > 0 ]
-
-        resultSec =
-          secCollapsible "<span class=\"sec-icon\">&#128200;</span> 回帰結果" True
-            ([ statRow ]
-             ++ partialCards
-             ++ [ secCard "残差プロット" [secResiduals fitted resid] ])
-    in [ secDataOverview df xCols yCol
-       , secModelOverview "GAM" formula Nothing
-       , resultSec
-       ]
-
--- ---------------------------------------------------------------------------
--- Random Forest (axis-1 B)
--- ---------------------------------------------------------------------------
-
--- | Wrapper to drive a @Reportable@ instance for a random-forest fit.
---
--- 'RandomForest' itself does not store fitted values, so the user must
--- supply training-set predictions (and the corresponding observed
--- values, for R²).
-data RFReport = RFReport
-  { rfrModel :: RandomForest
-  , rfrYHat  :: V.Vector Double   -- ^ Training-set predictions.
-  , rfrYObs  :: V.Vector Double   -- ^ Training-set observations (for R²).
-  } deriving Show
-
-instance Reportable RFReport where
-  toReport _cfg df xCols yCol (RFReport rf yHatV yObsV) =
-    let yHat   = V.toList yHatV
-        yObs   = V.toList yObsV
-        resid  = zipWith (-) yObs yHat
-        n      = length yObs
-        meanY  = if n == 0 then 0 else sum yObs / fromIntegral n
-        ssTot  = sum [ (y - meanY) ^ (2 :: Int) | y <- yObs ]
-        ssRes  = sum [ r * r | r <- resid ]
-        r2     = if ssTot > 0 then 1 - ssRes / ssTot else 0
-        (_sigmaH, rmse, maxAbs) = residStats resid 1
-
-        importVec = featureImportance rf
-        importPairs =
-          [ (lbl, importVec V.! (i - 1))
-          | (i, lbl) <- zip [1 ..] xCols
-          , i - 1 < V.length importVec ]
-
-        formula = "$\\hat{y}(x) = \\frac{1}{T} \\sum_{t=1}^{T} \\text{Tree}_t(x)$ "
-                  <> "(T = bagged regression trees)"
-
-        statRow =
-          secStatRow
-            [ ("R² (train)",  T.pack (printf "%.4f" r2))
-            , ("Trees",       T.pack (show (length (rfTreesV rf))))
-            , ("Features",    T.pack (show (rfNFeatures rf)))
-            , ("RMSE",        T.pack (printf "%.4f" rmse))
-            , ("最大絶対残差", T.pack (printf "%.4f" maxAbs))
-            ]
-
-        importanceCard =
-          secCard "Feature importance" [ secFeatureImportance "" importPairs ]
-
-        resultSec =
-          secCollapsible "<span class=\"sec-icon\">&#128200;</span> 回帰結果" True
-            [ statRow
-            , importanceCard
-            , secCard "残差プロット" [secResiduals yHat resid]
-            ]
-    in [ secDataOverview df xCols yCol
-       , secModelOverview "Random Forest (regression)" formula Nothing
-       , resultSec
-       ]
-
--- ---------------------------------------------------------------------------
--- GLMM (axis-1 C, Phase A残)
--- ---------------------------------------------------------------------------
-
--- | GLMM (LME / non-Gaussian GLMM) レポート用ラッパ。
-data GLMMReport = GLMMReport
-  { glmmrResult   :: GLMM.GLMMResult
-  , glmmrFamily   :: Family
-  , glmmrLink     :: LinkFn
-  , glmmrGroupCol :: Text
-  } deriving Show
-
-instance Reportable GLMMReport where
-  toReport _cfg df xCols yCol (GLMMReport gr fam lk grpCol) =
-    let fixed   = GLMM.glmmFixed gr
-        beta    = coeffList fixed
-        coefLabels = "β₀ (intercept)"
-                   : [ "β" <> T.pack (show (i :: Int)) <> " (" <> x <> ")"
-                     | (i, x) <- zip [1 ..] xCols ]
-        coeffs   = zip coefLabels beta
-        fitted   = fittedList fixed
-        resid    = LA.toList (residualsV fixed)
-        p        = length beta
-        (_sigmaH, rmse, maxAbs) = residStats resid p
-
-        groups   = V.toList (GLMM.glmmGroups gr)
-        blups    = V.toList (GLMM.glmmBLUPs  gr)
-        blupRows = [ [g, T.pack (printf "%+.4f" u)] | (g, u) <- zip groups blups ]
-
-        xVecs    = [ v | c <- xCols, Just v <- [getDoubleVec c df] ]
-        yVecMb   = getDoubleVec yCol df
-
-        modelType = case fam of
-          Gaussian -> "LME (linear mixed effects)"
-          _        -> "GLMM(" <> familyLabel fam <> ")"
-        linkTxt  = linkLabel lk
-
-        formula =
-          "$" <> yCol <> "_{ij} = \\beta_0 + \\sum \\beta_j x_{ij} + u_j "
-          <> "+ \\varepsilon_{ij}$<br>"
-          <> "$u_j \\sim \\text{Normal}(0, \\sigma^2_u),\\quad "
-          <> "\\varepsilon_{ij} \\sim \\text{Normal}(0, \\sigma^2)$"
-
-        interactiveSec
-          | length xVecs == length xCols, not (null xVecs)
-          , Just yv <- yVecMb =
-              let im = mkInteractive xCols yCol xVecs (V.toList yv)
-                                     (head beta) (drop 1 beta)
-                                     linkTxt (Just (sqrt (GLMM.glmmResidVar gr)))
-              in [secInteractiveMulti
-                    "対話的予測 (固定効果のみ、ランダム効果 = 0)" im]
-          | otherwise = []
-
-        statRow =
-          secStatRow
-            [ ("周辺 R²",  T.pack (printf "%.4f" (rSquared1 fixed)))
-            , ("σ²_u",     T.pack (printf "%.4f" (GLMM.glmmRandVar gr)))
-            , ("σ²",       T.pack (printf "%.4f" (GLMM.glmmResidVar gr)))
-            , ("ICC",      T.pack (printf "%.4f" (GLMM.glmmICC gr)))
-            , ("RMSE",     T.pack (printf "%.4f" rmse))
-            , ("最大絶対残差", T.pack (printf "%.4f" maxAbs))
-            ]
-
-        resultSec =
-          secCollapsible "<span class=\"sec-icon\">&#128200;</span> 回帰結果" True
-            [ statRow
-            , secCard "固定効果"
-                [secCoefficients coeffs (Just ("周辺 R²", rSquared1 fixed))]
-            , secCard ("BLUP (" <> grpCol <> " 別ランダム切片)")
-                [secTable "" ["グループ", "u_j"] blupRows]
-            , secCard "残差プロット" [secResiduals fitted resid]
-            ]
-    in [ secDataOverview df xCols yCol
-       , secModelOverviewLink modelType formula linkTxt Nothing
-       , resultSec
-       ] ++ interactiveSec
-
--- ---------------------------------------------------------------------------
--- GP (axis-1 C, Phase A残)
--- ---------------------------------------------------------------------------
-
--- | GP レポート用ラッパ。
---
--- `gprResult` は予測グリッド (`gprGridX`) 上の事後平均と 95% 信用帯を保持。
--- ライブラリ利用者は `Hanalyze.Model.GP.fitGP` で外挿域も含めた grid を渡すと
--- 対話的予測の信頼帯がそのまま使える。
-data GPReport = GPReport
-  { gprKernel  :: GP.Kernel
-  , gprParams  :: GP.GPParams
-  , gprResult  :: GP.GPResult
-  , gprGridX   :: [Double]
-  , gprTrainX  :: [Double]
-  , gprTrainY  :: [Double]
-  , gprLML     :: Double
-  } deriving Show
-
-instance Reportable GPReport where
-  toReport _cfg df xCols yCol rep =
-    let xs   = gprTrainX rep
-        ys   = gprTrainY rep
-        params = gprParams rep
-        kern   = gprKernel rep
-        gridX  = gprGridX rep
-        res    = gprResult rep
-        lml    = gprLML rep
-
-        smooth = SmoothCurve gridX (GP.gpMean res) (GP.gpLower res) (GP.gpUpper res)
-
-        -- 学習点での残差: 観測 vs 各 x の事後平均
-        yHat   = GP.gpMean (GP.fitGP (GP.GPModel kern params) xs ys xs)
-        resid  = zipWith (-) ys yHat
-        (_sigmaH, rmse, maxAbs) = residStats resid 1
-
-        kernLbl = T.pack (show kern)
-        formula =
-          "$f \\sim \\text{GP}(0, k(x, x'))$<br>"
-          <> "$y_i = f_i + \\varepsilon_i,\\quad "
-          <> "\\varepsilon_i \\sim \\text{Normal}(0, \\sigma_n^2)$"
-
-        statRow =
-          secStatRow
-            [ ("ℓ",    T.pack (printf "%.4f" (GP.gpLengthScale params)))
-            , ("σ_f²", T.pack (printf "%.4f" (GP.gpSignalVar params)))
-            , ("σ_n²", T.pack (printf "%.4f" (GP.gpNoiseVar params)))
-            , ("LML",  T.pack (printf "%.2f"  lml))
-            , ("RMSE", T.pack (printf "%.4f" rmse))
-            , ("最大絶対残差", T.pack (printf "%.4f" maxAbs))
-            ]
-
-        resultSec =
-          secCollapsible "<span class=\"sec-icon\">&#128200;</span> 回帰結果" True
-            [ statRow
-            , secCard "ハイパーパラメータ (周辺尤度最大化で推定)"
-                [ secCoefficients
-                    [ ("ℓ (length scale)",      GP.gpLengthScale params)
-                    , ("σ_f² (signal variance)", GP.gpSignalVar params)
-                    , ("σ_n² (noise variance)",  GP.gpNoiseVar params)
-                    ]
-                    (Just ("log p(y|X,θ)", lml))
-                ]
-            , secCard "残差プロット" [secResiduals yHat resid]
-            ]
-
-        sliderRange = case xs of
-          [] -> (0, 1)
-          _  ->
-            let lo = minimum xs
-                hi = maximum xs
-                ext = (hi - lo) * 0.5
-            in (lo - ext, hi + ext)
-
-        xc = case xCols of { (c:_) -> c; _ -> "x" }
-
-    in [ secDataOverview df xCols yCol
-       , secModelOverviewExtras "GP" formula
-           [("カーネル", kernLbl)] Nothing
-       , resultSec
-       , secInteractiveLM "対話的予測" xc yCol xs ys smooth sliderRange
-       ]
-
--- ---------------------------------------------------------------------------
--- HBM (Bayesian Linear Regression) (axis-1 C, Phase A残)
--- ---------------------------------------------------------------------------
-
--- | ベイズ単回帰 (`y ~ Normal(α + β x, σ)`) の HBM レポート用ラッパ。
---
--- 一般的な HBM (任意の構造) は section を直接構築するか、用途別ラッパを別途定義する。
--- ここでは「α + β·x」という最も典型的なパターンに特化。
-data HBMLinearReport = HBMLinearReport
-  { hbmrChain     :: MC.Chain
-  , hbmrXs        :: [Double]
-  , hbmrYs        :: [Double]
-  , hbmrAlphaName :: Text     -- ^ 例: "alpha"
-  , hbmrBetaName  :: Text     -- ^ 例: "beta"
-  , hbmrSigmaName :: Text     -- ^ 例: "sigma"
-  , hbmrGraph     :: Maybe Text  -- ^ Mermaid DAG (`Hanalyze.Viz.ModelGraph` で構築)
-  }
-
--- | x の各点での α + β·x の事後分位点 (中央値, 2.5%, 97.5%)。
-hbmRibbonAt :: [Double] -> [Double] -> [Double] -> ([Double], [Double], [Double])
-hbmRibbonAt grid alphas betas =
-  let qsAt p s =
-        let n = length s
-        in if n == 0 then 0 else s !! min (n - 1) (max 0 (floor (p * fromIntegral n)))
-      atX x =
-        let s  = sortByList (zipWith (\a b -> a + b * x) alphas betas)
-        in (qsAt 0.5 s, qsAt 0.025 s, qsAt 0.975 s)
-      preds = map atX grid
-      (m, lo, hi) = unzip3 preds
-  in (m, lo, hi)
-
-sortByList :: Ord a => [a] -> [a]
-sortByList = sortBy compare
-
-instance Reportable HBMLinearReport where
-  toReport _cfg df xCols yCol rep =
-    let chain   = hbmrChain rep
-        xs      = hbmrXs rep
-        ys      = hbmrYs rep
-        aName   = hbmrAlphaName rep
-        bName   = hbmrBetaName  rep
-        sName   = hbmrSigmaName rep
-        params  = [aName, bName, sName]
-
-        alphas  = MC.chainVals aName chain
-        betas   = MC.chainVals bName chain
-        sigmas  = MC.chainVals sName chain
-        aMean   = mean0 alphas
-        bMean   = mean0 betas
-        sMean   = mean0 sigmas
-
-        fitted  = [ aMean + bMean * x | x <- xs ]
-        resid   = zipWith (-) ys fitted
-        n       = length ys
-        meanY   = if n == 0 then 0 else sum ys / fromIntegral n
-        ssTot   = sum [ (y - meanY) ^ (2 :: Int) | y <- ys ]
-        ssRes   = sum [ r * r | r <- resid ]
-        r2      = if ssTot > 1e-12 then 1 - ssRes / ssTot else 0
-        (_sH, rmse, maxAbs) = residStats resid 2
-
-        xMin    = if null xs then 0 else minimum xs
-        xMax    = if null xs then 1 else maximum xs
-        ext     = (xMax - xMin) * 0.5
-        gMin    = xMin - ext
-        gMax    = xMax + ext
-        grid    = if null xs then []
-                  else [ gMin + i * (gMax - gMin) / 99 | i <- [0 .. 99] ]
-        (mid, lo, hi) = hbmRibbonAt grid alphas betas
-        smooth  = SmoothCurve grid mid lo hi
-
-        formula =
-          "$" <> yCol <> "_i \\sim \\text{Normal}(\\alpha + \\beta x_i, \\sigma)$<br>"
-          <> "$\\alpha \\sim \\text{Normal}(0, 10),\\ "
-          <> "\\beta \\sim \\text{Normal}(0, 10),\\ "
-          <> "\\sigma \\sim \\text{Exponential}(1)$"
-
-        accept = MC.chainAccepted chain
-        total  = max 1 (MC.chainTotal chain)
-        accRate :: Double
-        accRate = fromIntegral accept / fromIntegral total
-
-        statRow =
-          secStatRow
-            [ ("R²",         T.pack (printf "%.4f" r2))
-            , ("サンプル数", T.pack (show total))
-            , ("受容率",     T.pack (printf "%.1f%%" (accRate * 100)))
-            , ("RMSE",       T.pack (printf "%.4f" rmse))
-            , ("最大絶対残差", T.pack (printf "%.4f" maxAbs))
-            ]
-
-        coeffsCard = secCard "事後平均係数"
-          [ secCoefficients
-              [ ("α (intercept)", aMean)
-              , ("β (slope)",      bMean)
-              , ("σ",              sMean)
-              ]
-              (Just ("R²", r2))
-          ]
-
-        diagCard = secCard "MCMC 診断"
-          [ secMCMCDiagnostics "Posterior + trace" params chain
-          , secMCMCAutocorr   "自己相関 (max lag 40)" 40 params chain
-          , secMCMCPair        "ペア散布 (α, β)" aName bName chain
-          ]
-
-        residCard = secCard "残差プロット" [ secResiduals fitted resid ]
-
-        resultSec =
-          secCollapsible "<span class=\"sec-icon\">&#128200;</span> 回帰結果" True
-            [ statRow
-            , coeffsCard
-            , diagCard
-            , residCard
-            ]
-
-        xc = case xCols of { (c:_) -> c; _ -> "x" }
-
-    in [ secDataOverview df xCols yCol
-       , secModelOverviewExtras "HBM(NUTS)" formula
-           [("サンプラー", "NUTS")] (hbmrGraph rep)
-       , resultSec
-       , secInteractiveLM "対話的予測 (信用区間付)" xc yCol xs ys smooth (gMin, gMax)
-       ]
-
-mean0 :: [Double] -> Double
-mean0 [] = 0
-mean0 xs = sum xs / fromIntegral (length xs)
-
--- ---------------------------------------------------------------------------
--- HBM (一般) - multi-x / 非線形対応の汎用ラッパ (Cycle 7)
--- ---------------------------------------------------------------------------
-
--- | 単変数 x 上の予測リボン (中央値 + 信用区間)。
---
--- 任意の HBM (非線形を含む) に対しユーザー側で事後ドローから計算したものを渡す。
--- 'HBMReport' に含めると散布図 + リボン + 対話的予測 (信用帯付き) が描かれる。
-data HBMRibbon = HBMRibbon
-  { hribXCol :: Text          -- ^ x 軸ラベル (列名)
-  , hribXObs :: [Double]      -- ^ 学習データ x
-  , hribYObs :: [Double]      -- ^ 学習データ y
-  , hribGrid :: [Double]      -- ^ 予測グリッド X (推奨: ±50% 外挿)
-  , hribMid  :: [Double]      -- ^ 各グリッド点での事後中央値
-  , hribLow  :: [Double]      -- ^ 各グリッド点での 2.5% 分位
-  , hribHigh :: [Double]      -- ^ 各グリッド点での 97.5% 分位
-  } deriving Show
-
--- | HBM (一般) レポート用ラッパ。multi-x / 非線形 / 任意の構造に対応。
---
--- 'HBMLinearReport' は @α + β·x@ という線形 HBM に特化したショートカット。
--- 一般のモデルでは 'HBMReport' に以下の情報をユーザー側で集約して渡す:
---
--- * `hbmrChainG` — MCMC チェーン (診断プロット用)
--- * `hbmrPostSummaryG` — 事後要約 (mean/SD/quantile/ESS/R-hat) を直接指定
--- * `hbmrYHatG` — 学習データへの予測値 (例: 事後中央値による予測)
--- * `hbmrRibbonG` — 単変数 x 上の予測リボン (省略可)
--- * `hbmrPairsG` — 興味のあるパラメータペア散布
---
--- @
--- let postRows =
---       [ ("alpha", aMean, aSD, aQ025, aQ975, aESS, Just aRhat)
---       , ...
---       ]
---     rep = HBMReport { hbmrChainG = chain, hbmrParamsG = ["alpha","beta","sigma"]
---                     , hbmrFormulaG = "$y_i \\sim ...$"
---                     , hbmrSamplerG = "NUTS"
---                     , hbmrModelTypeG = "HBM(NUTS)"
---                     , hbmrGraphG = Just dag
---                     , hbmrPostSummaryG = postRows
---                     , hbmrYObsG = ys, hbmrYHatG = yHat
---                     , hbmrRibbonG = Just ribbon
---                     , hbmrPairsG = [("alpha","beta")]
---                     }
--- renderReport "out.html" cfg (toReport cfg df xCols yCol rep)
--- @
-data HBMReport = HBMReport
-  { hbmrChainG       :: MC.Chain
-  , hbmrParamsG      :: [Text]
-  , hbmrFormulaG     :: Text
-  , hbmrSamplerG     :: Text
-  , hbmrModelTypeG   :: Text
-  , hbmrGraphG       :: Maybe Text
-  , hbmrPostSummaryG ::
-      [(Text, Double, Double, Double, Double, Double, Maybe Double)]
-  , hbmrYObsG        :: [Double]
-  , hbmrYHatG        :: [Double]
-  , hbmrRibbonG      :: Maybe HBMRibbon
-  , hbmrPairsG       :: [(Text, Text)]
-  }
-
-instance Reportable HBMReport where
-  toReport _cfg df xCols yCol rep =
-    let chain    = hbmrChainG rep
-        params   = hbmrParamsG rep
-        ys       = hbmrYObsG rep
-        yHat     = hbmrYHatG rep
-        resid    = zipWith (-) ys yHat
-        n        = length ys
-        meanY    = if n == 0 then 0 else sum ys / fromIntegral n
-        ssTot    = sum [ (y - meanY) ^ (2 :: Int) | y <- ys ]
-        ssRes    = sum [ r * r | r <- resid ]
-        r2       = if ssTot > 1e-12 then 1 - ssRes / ssTot else 0
-        nP       = length params
-        (_sH, rmse, maxAbs) = residStats resid (max 1 nP)
-
-        accept   = MC.chainAccepted chain
-        total    = max 1 (MC.chainTotal chain)
-        accRate :: Double
-        accRate  = fromIntegral accept / fromIntegral total
-
-        statRow =
-          secStatRow
-            [ ("R²",         T.pack (printf "%.4f" r2))
-            , ("サンプル数", T.pack (show total))
-            , ("受容率",     T.pack (printf "%.1f%%" (accRate * 100)))
-            , ("RMSE",       T.pack (printf "%.4f" rmse))
-            , ("最大絶対残差", T.pack (printf "%.4f" maxAbs))
-            ]
-
-        postCard = secCard "事後要約"
-          [ secPosteriorSummary "" (hbmrPostSummaryG rep) ]
-
-        diagSecs =
-          [ secMCMCDiagnostics "Posterior + trace" params chain
-          , secMCMCAutocorr "自己相関 (max lag 40)" 40 params chain
-          ]
-          ++ [ secMCMCPair ("ペア散布 (" <> a <> ", " <> b <> ")") a b chain
-             | (a, b) <- hbmrPairsG rep ]
-        diagCard = secCard "MCMC 診断" diagSecs
-
-        residCard = secCard "残差プロット" [ secResiduals yHat resid ]
-
-        resultSec =
-          secCollapsible "<span class=\"sec-icon\">&#128200;</span> 回帰結果" True
-            [ statRow, postCard, diagCard, residCard ]
-
-        -- 単変数の予測リボンセクション (オプション)
-        ribbonSecs = case hbmrRibbonG rep of
-          Nothing -> []
-          Just rb ->
-            let smooth = SmoothCurve (hribGrid rb) (hribMid rb)
-                                     (hribLow rb)  (hribHigh rb)
-                gMin = if null (hribGrid rb) then 0 else minimum (hribGrid rb)
-                gMax = if null (hribGrid rb) then 1 else maximum (hribGrid rb)
-            in [ secInteractiveLM "対話的予測 (信用区間付)"
-                   (hribXCol rb) yCol
-                   (hribXObs rb) (hribYObs rb)
-                   smooth (gMin, gMax) ]
-
-    in [ secDataOverview df xCols yCol
-       , secModelOverviewExtras (hbmrModelTypeG rep) (hbmrFormulaG rep)
-           [("サンプラー", hbmrSamplerG rep)] (hbmrGraphG rep)
-       , resultSec
-       ] ++ ribbonSecs
-
--- ---------------------------------------------------------------------------
--- RFFMVReport — 多変量 RFF Ridge (Phase B-RFF)
--- ---------------------------------------------------------------------------
-
--- | 多変量 RFF Ridge のレポート。`rfmvGroup` 列で色分けし、`rfmvXAxis` 列
--- (xCols のいずれか) を横軸にして観測点 + 予測曲線を描く。
-data RFFMVReport = RFFMVReport
-  { rfmvFit          :: RFFRidgeFitMV
-  , rfmvGroup        :: Text
-  , rfmvXAxis        :: Text
-  , rfmvInteractive  :: Bool
-    -- ^ True なら 'secInteractiveRFFMV' (スライダ + リアルタイム JS 予測) を含める
-  , rfmvStandardizer :: Maybe Std.Standardizer
-    -- ^ fit 時に X を標準化したときの μ/σ。Nothing なら未標準化。
-    --   plot や JS 予測時はこれで raw → 標準化変換を行う。
-  } deriving (Show)
-
-instance Reportable RFFMVReport where
-  toReport _cfg df xCols yCol r =
-    case (mapM (`getDoubleVec` df) xCols, getDoubleVec yCol df,
-          getMaybeTextVec (rfmvGroup r) df) of
-      (Just xVecs, Just yVec, Just gv) ->
-        let cols    = map V.toList xVecs
-            ys      = V.toList yVec
-            groups  = [ maybe "" id g | g <- V.toList gv ]
-            xMatRaw = LA2.fromColumns (map LA2.fromList cols)
-            -- fit は標準化空間で行われたので、観測点も標準化空間に投げる
-            stdr    = case rfmvStandardizer r of
-                        Just s  -> s
-                        Nothing -> Std.identityStandardizer (length xCols)
-            xMatObs = Std.applyStandardizer stdr xMatRaw
-            yhat    = predictRFFRidgeMV (rfmvFit r) xMatObs
-            sse     = sum (zipWith (\a b -> (a-b)*(a-b)) ys yhat)
-            sst     = let m = sum ys / fromIntegral (max 1 (length ys))
-                      in sum [(y - m)*(y - m) | y <- ys]
-            r2      = if sst < 1e-12 then 0 else 1 - sse / sst
-            n       = length ys
-            rmse    = sqrt (sse / fromIntegral (max 1 n))
-            feats   = rffrmvFeatures (rfmvFit r)
-            d       = LA2.cols (rffmvOmegas feats)
-            ellLbl  = NF.fmtNumT (rffmvLengthScale feats)
-            sfLbl   = NF.fmtNumT (rffmvSigmaF feats)
-            lamLbl  = NF.fmtNumT (rffrmvLambda (rfmvFit r))
-            xColIdx = case [ i | (i, c) <- zip [0..] xCols, c == rfmvXAxis r ] of
-                        (i:_) -> i
-                        []    -> 0
-            xValuesAll = cols !! xColIdx
-            xMin = minimum xValuesAll
-            xMax = maximum xValuesAll
-            ngrid = 100
-            xGrid = [ xMin + fromIntegral i * (xMax - xMin) / fromIntegral (ngrid - 1)
-                    | i <- [0 .. ngrid - 1] ]
-            ptData = zip3 groups xValuesAll ys
-            uniqGroups = uniq2 groups
-            rowsForGroup g = [ i | (i, gg) <- zip [0..] groups, gg == g ]
-            repValues g = [ (cols !! j) !! head (rowsForGroup g)
-                          | j <- [0 .. length xCols - 1] ]
-            mkLineData g =
-              let rep = repValues g
-                  makeRow t =
-                    [ if j == xColIdx then t else rep !! j
-                    | j <- [0 .. length xCols - 1] ]
-                  xMatRawGrid = LA2.fromLists [ makeRow t | t <- xGrid ]
-                  xMatStdGrid = Std.applyStandardizer stdr xMatRawGrid
-                  ys'  = predictRFFRidgeMV (rfmvFit r) xMatStdGrid
-              in [ (g, t, y') | (t, y') <- zip xGrid ys' ]
-            lnData = concatMap mkLineData uniqGroups
-            plotCfg = (defaultConfig
-                        (yCol <> " by " <> rfmvGroup r
-                          <> " — RFF Ridge (multivariate)"))
-                       { plotWidth = 720, plotHeight = 480 }
-            vega = scatterWithGroups plotCfg (rfmvXAxis r) yCol ptData lnData
-            xJoined = T.intercalate ", " xCols
-            -- 完全な数式 (MathJax)。φ の中身、Ridge 形、ω/b の事前を明示。
-            formula = T.unlines
-              [ "$$"
-              , "\\hat{y}(x) = \\sum_{j=1}^{D} w_j\\, \\varphi_j(x), \\qquad"
-              , "\\varphi_j(x) = \\sigma_f \\sqrt{\\tfrac{2}{D}}"
-              , "\\, \\cos\\!\\bigl(\\boldsymbol{\\omega}_j^{\\top} x + b_j\\bigr)"
-              , "$$"
-              , "$$"
-              , "x = (\\mathrm{" <> T.replace ", " "},\\,\\mathrm{" xJoined
-                <> "})^{\\top} \\in \\mathbb{R}^{p}, \\quad p="
-                <> T.pack (show (length xCols))
-                <> ", \\quad D=" <> T.pack (show d) <> "."
-              , "$$"
-              , "$$"
-              , "\\boldsymbol{\\omega}_j \\sim \\mathcal{N}\\!\\left(\\mathbf{0},\\, \\ell^{-2} I_p\\right),"
-              , "\\quad b_j \\sim \\mathrm{Uniform}(0, 2\\pi),"
-              , "\\quad \\ell = " <> ellLbl
-                <> ",\\ \\sigma_f = " <> sfLbl <> "."
-              , "$$"
-              , "$$"
-              , "\\boldsymbol{w} = \\arg\\min_{w}\\,\\bigl\\| y - \\Phi w \\bigr\\|^2 + \\lambda\\,\\|w\\|^2"
-              , " \\;=\\; (\\Phi^{\\top}\\Phi + \\lambda I_D)^{-1} \\Phi^{\\top} y,"
-              , "\\quad \\lambda = " <> lamLbl <> " \\;(=\\sigma_n^2)."
-              , "$$"
-              , "ここで $\\Phi \\in \\mathbb{R}^{n \\times D}$ は $i$ 行目が $\\varphi(x_i)^{\\top}$。"
-              , "標準化 ON のときは $x$ を $(x-\\mu)/\\sigma$ してから $\\varphi$ に投入する。"
-              ]
-            -- インタラクティブセクション (スライダで副軸を変えると JS が予測を再計算)
-            sliderRows = mkSliders xCols xColIdx cols
-            omegasRowMaj =
-              concat [ LA2.toList (LA2.flatten (rffmvOmegas feats)) ]
-              -- LA.flatten は row-major なので OK
-            iSection
-              | rfmvInteractive r =
-                  [ secInteractiveRFFMV "対話的予測 (副軸スライダ)"
-                      InteractiveRFFMV
-                        { irfXCols       = xCols
-                        , irfYCol        = yCol
-                        , irfXObs        = cols
-                        , irfYObs        = ys
-                        , irfGroups      = groups
-                        , irfMainAxis    = rfmvXAxis r
-                        , irfMainGrid    = xGrid
-                        , irfSliders     = sliderRows
-                        , irfOmegasRowMaj = omegasRowMaj
-                        , irfBs          = V.toList (rffmvBs feats)
-                        , irfSigmaF      = rffmvSigmaF feats
-                        , irfDim         = d
-                        , irfP           = length xCols
-                        , irfWeights     = LA2.toList (rffrmvWeights (rfmvFit r))
-                        , irfStdMu       = fmap Std.stMu (rfmvStandardizer r)
-                        , irfStdSd       = fmap Std.stSd (rfmvStandardizer r)
-                        }
-                  ]
-              | otherwise = []
-        in [ secDataOverview df xCols yCol
-           , secModelOverview "Multivariate RFF Ridge" formula Nothing
-           , secKeyValue "Fit summary"
-               [ ("Features (D)",       T.pack (show d))
-               , ("Length scale ℓ",     ellLbl)
-               , ("Signal σ_f",         sfLbl)
-               , ("Ridge λ (=σ_n²)",    lamLbl)
-               , ("Standardize",
-                   maybe "OFF" (const "ON") (rfmvStandardizer r))
-               , ("R²",                 NF.fmtNumT r2)
-               , ("RMSE",               NF.fmtNumT rmse)
-               , ("n",                  T.pack (show n))
-               ]
-           , secVega ("予測曲線 + 観測点 (" <> rfmvGroup r <> " で色分け)") vega
-           ] ++ iSection ++
-           [ secResiduals yhat (zipWith (-) ys yhat) ]
-      _ -> [ secDataOverview df xCols yCol
-           , secModelOverview "Multivariate RFF Ridge"
-               "(必要な列が取得できません: x_i, y, group の数値/Text 列を確認してください)"
-               Nothing
-           ]
-
-uniq2 :: Ord a => [a] -> [a]
-uniq2 []     = []
-uniq2 (x:xs) = x : uniq2 (filter (/= x) xs)
-
--- | 副軸 (= 横軸以外) について (列名, min, mid, max) のスライダ情報を作る。
-mkSliders :: [Text] -> Int -> [[Double]] -> [(Text, Double, Double, Double)]
-mkSliders xCols mainIdx cols =
-  [ (xCols !! j, minimum c, mid c, maximum c)
-  | (j, c) <- zip [0..] cols
-  , j /= mainIdx
-  ]
-  where
-    mid xs = let s = sortBy compare xs
-                 n = length s
-             in if n == 0 then 0 else s !! (n `div` 2)
diff --git a/src/Hanalyze/Viz/Scatter.hs b/src/Hanalyze/Viz/Scatter.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/Scatter.hs
+++ /dev/null
@@ -1,460 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.Scatter
--- Description : 散布図とその重ね描き (回帰直線/平滑化・グループ別・予測 vs 実測)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- | Scatter plots and overlays.
---
--- Provides plain scatter, scatter-with-fit-line ('scatterWithLM' /
--- 'scatterWithSmooth'), grouped scatter and predicted-vs-actual
--- diagnostic plots.
-module Hanalyze.Viz.Scatter
-  ( scatterPlot
-  , scatterPlotFile
-  , scatterWithLM
-  , scatterWithLMFile
-  , scatterWithLMCI
-  , scatterWithLMCIFile
-  , scatterWithSmooth
-  , scatterWithSmoothFile
-  , scatterMultiY
-  , scatterMultiYFile
-  , scatterWithGroups
-  , scatterWithGroupsFile
-  , predictedVsActual
-  , predictedVsActualFile
-    -- * 130: PlotData ベースの汎用 spec API (HPotfire Vega 移行用)
-  , scatterSpec
-  ) where
-
-import qualified DataFrame.Internal.DataFrame as DXD
-import Hanalyze.DataIO.Convert (getDoubleVec)
-import Hanalyze.Model.Core  (FitResult, fittedList)
-import Hanalyze.Model.LM    (CIBand (..), SmoothFit (..))
-import Hanalyze.Viz.Core       (PlotConfig (..), OutputFormat, writeSpec)
-import Hanalyze.Viz.PlotData   (PlotData, numericColumn, textColumn)
-
-import Data.List (sortBy)
-import Data.Ord (comparing)
-import Data.Text (Text)
-import qualified Data.Vector as V
-import Graphics.Vega.VegaLite
-
--- | Build a Vega-Lite scatter plot spec from two numeric columns.
-scatterPlot :: PlotConfig -> DXD.DataFrame -> Text -> Text -> VegaLite
-scatterPlot cfg df xCol yCol =
-  toVegaLite
-    [ title (plotTitle cfg) []
-    , dataSpec
-    , mark Point [MTooltip TTEncoding]
-    , encSpec
-    , width  (plotWidth  cfg)
-    , height (plotHeight cfg)
-    ]
-  where
-    xVals   = maybe [] V.toList (getDoubleVec xCol df)
-    yVals   = maybe [] V.toList (getDoubleVec yCol df)
-    dataSpec = dataFromColumns []
-               . dataColumn xCol (Numbers xVals)
-               . dataColumn yCol (Numbers yVals)
-               $ []
-    encSpec  = encoding
-               . position X [PName xCol, PmType Quantitative, PAxis [AxTitle xCol]]
-               . position Y [PName yCol, PmType Quantitative, PAxis [AxTitle yCol]]
-               $ []
-
--- | Render 'scatterPlot' to a file via 'writeSpec'.
-scatterPlotFile :: OutputFormat -> FilePath -> PlotConfig -> DXD.DataFrame -> Text -> Text -> IO ()
-scatterPlotFile fmt path cfg df xCol yCol =
-  writeSpec fmt path (scatterPlot cfg df xCol yCol)
-
--- | Scatter plot with a fitted regression line overlaid.
-scatterWithLM :: PlotConfig -> DXD.DataFrame -> Text -> Text -> FitResult -> VegaLite
-scatterWithLM cfg df xCol yCol res =
-  toVegaLite
-    [ title (plotTitle cfg) []
-    , layer [pointLayer, lineLayer]
-    , width  (plotWidth  cfg)
-    , height (plotHeight cfg)
-    ]
-  where
-    xVals  = maybe [] V.toList (getDoubleVec xCol df)
-    yVals  = maybe [] V.toList (getDoubleVec yCol df)
-    pairs  = sortBy (comparing fst) (zip xVals (fittedList res))
-    xLine  = map fst pairs
-    yLine  = map snd pairs
-
-    pointLayer = asSpec
-      [ dataFromColumns []
-          . dataColumn xCol (Numbers xVals)
-          . dataColumn yCol (Numbers yVals)
-          $ []
-      , mark Point [MTooltip TTEncoding]
-      , encoding
-          . position X [PName xCol, PmType Quantitative, PAxis [AxTitle xCol]]
-          . position Y [PName yCol, PmType Quantitative, PAxis [AxTitle yCol]]
-          $ []
-      ]
-
-    lineLayer = asSpec
-      [ dataFromColumns []
-          . dataColumn xCol     (Numbers xLine)
-          . dataColumn "fitted" (Numbers yLine)
-          $ []
-      , mark Line [MColor "red", MStrokeWidth 2.0]
-      , encoding
-          . position X [PName xCol,     PmType Quantitative]
-          . position Y [PName "fitted", PmType Quantitative]
-          $ []
-      ]
-
--- | Render 'scatterWithLM' to a file via 'writeSpec'.
-scatterWithLMFile :: OutputFormat -> FilePath -> PlotConfig -> DXD.DataFrame -> Text -> Text -> FitResult -> IO ()
-scatterWithLMFile fmt path cfg df xCol yCol res =
-  writeSpec fmt path (scatterWithLM cfg df xCol yCol res)
-
--- | Scatter plot with regression line and confidence band (training-point CI).
-scatterWithLMCI :: PlotConfig -> DXD.DataFrame -> Text -> Text -> FitResult -> CIBand -> VegaLite
-scatterWithLMCI cfg df xCol yCol res ci =
-  toVegaLite
-    [ title (plotTitle cfg) []
-    , layer [ciLayer, lineLayer, pointLayer]
-    , width  (plotWidth  cfg)
-    , height (plotHeight cfg)
-    ]
-  where
-    xVals = maybe [] V.toList (getDoubleVec xCol df)
-    yVals = maybe [] V.toList (getDoubleVec yCol df)
-
-    sorted4 = sortBy (comparing (\(x,_,_,_) -> x))
-                [ (x, f, l, u)
-                | ((x,f),(l,u)) <-
-                    zip (zip xVals (fittedList res))
-                        (zip (lowerBound ci) (upperBound ci))
-                ]
-    xSorted = [x | (x,_,_,_) <- sorted4]
-    fSorted = [f | (_,f,_,_) <- sorted4]
-    lSorted = [l | (_,_,l,_) <- sorted4]
-    uSorted = [u | (_,_,_,u) <- sorted4]
-
-    pointLayer = asSpec
-      [ dataFromColumns []
-          . dataColumn xCol (Numbers xVals)
-          . dataColumn yCol (Numbers yVals)
-          $ []
-      , mark Point [MTooltip TTEncoding]
-      , encoding
-          . position X [PName xCol, PmType Quantitative, PAxis [AxTitle xCol]]
-          . position Y [PName yCol, PmType Quantitative, PAxis [AxTitle yCol]]
-          $ []
-      ]
-    lineLayer = asSpec
-      [ dataFromColumns []
-          . dataColumn xCol     (Numbers xSorted)
-          . dataColumn "fitted" (Numbers fSorted)
-          $ []
-      , mark Line [MColor "red", MStrokeWidth 2.0]
-      , encoding
-          . position X [PName xCol,     PmType Quantitative]
-          . position Y [PName "fitted", PmType Quantitative]
-          $ []
-      ]
-    ciLayer = asSpec
-      [ dataFromColumns []
-          . dataColumn xCol    (Numbers xSorted)
-          . dataColumn "lower" (Numbers lSorted)
-          . dataColumn "upper" (Numbers uSorted)
-          $ []
-      , mark Area [MOpacity 0.15, MColor "red"]
-      , encoding
-          . position X  [PName xCol,    PmType Quantitative]
-          . position Y  [PName "lower", PmType Quantitative]
-          . position Y2 [PName "upper"]
-          $ []
-      ]
-
--- | Render 'scatterWithLMCI' to a file via 'writeSpec'.
-scatterWithLMCIFile :: OutputFormat -> FilePath -> PlotConfig -> DXD.DataFrame -> Text -> Text -> FitResult -> CIBand -> IO ()
-scatterWithLMCIFile fmt path cfg df xCol yCol res ci =
-  writeSpec fmt path (scatterWithLMCI cfg df xCol yCol res ci)
-
--- | Scatter plot with smooth fitted curve.
--- Renders a CI/PI band when sfHasBand is True.
--- Shows an optional equation subtitle under the chart title.
-scatterWithSmooth :: PlotConfig -> Maybe Text -> DXD.DataFrame -> Text -> Text -> SmoothFit -> VegaLite
-scatterWithSmooth cfg mEquation df xCol yCol sf =
-  toVegaLite
-    [ title (plotTitle cfg) titleOpts
-    , layer layers
-    , width  (plotWidth  cfg)
-    , height (plotHeight cfg)
-    ]
-  where
-    xVals = maybe [] V.toList (getDoubleVec xCol df)
-    yVals = maybe [] V.toList (getDoubleVec yCol df)
-
-    titleOpts = case mEquation of
-      Just eq -> [TSubtitle eq, TSubtitleFontSize 11, TSubtitleColor "#555"]
-      Nothing -> []
-
-    pointLayer = asSpec
-      [ dataFromColumns []
-          . dataColumn xCol (Numbers xVals)
-          . dataColumn yCol (Numbers yVals)
-          $ []
-      , mark Point [MTooltip TTEncoding]
-      , encoding
-          . position X [PName xCol, PmType Quantitative, PAxis [AxTitle xCol]]
-          . position Y [PName yCol, PmType Quantitative, PAxis [AxTitle yCol]]
-          $ []
-      ]
-
-    lineLayer = asSpec
-      [ dataFromColumns []
-          . dataColumn xCol     (Numbers (sfX sf))
-          . dataColumn "fitted" (Numbers (sfFit sf))
-          $ []
-      , mark Line [MColor "red", MStrokeWidth 2.0]
-      , encoding
-          . position X [PName xCol,     PmType Quantitative]
-          . position Y [PName "fitted", PmType Quantitative]
-          $ []
-      ]
-
-    ciLayer = asSpec
-      [ dataFromColumns []
-          . dataColumn xCol    (Numbers (sfX sf))
-          . dataColumn "lower" (Numbers (sfLower sf))
-          . dataColumn "upper" (Numbers (sfUpper sf))
-          $ []
-      , mark Area [MOpacity 0.15, MColor "red"]
-      , encoding
-          . position X  [PName xCol,    PmType Quantitative]
-          . position Y  [PName "lower", PmType Quantitative]
-          . position Y2 [PName "upper"]
-          $ []
-      ]
-
-    layers = (if sfHasBand sf then [ciLayer] else []) ++ [lineLayer, pointLayer]
-
--- | Render 'scatterWithSmooth' to a file via 'writeSpec'.
-scatterWithSmoothFile :: OutputFormat -> FilePath -> PlotConfig -> Maybe Text -> DXD.DataFrame -> Text -> Text -> SmoothFit -> IO ()
-scatterWithSmoothFile fmt path cfg mEq df xCol yCol sf =
-  writeSpec fmt path (scatterWithSmooth cfg mEq df xCol yCol sf)
-
--- | Scatter plot with multiple y columns as color-coded series (no regression).
-scatterMultiY :: PlotConfig -> DXD.DataFrame -> Text -> [Text] -> VegaLite
-scatterMultiY cfg df xCol yCols =
-  toVegaLite
-    [ title (plotTitle cfg) []
-    , dataSpec
-    , transform
-        . foldAs yCols "series" "value"
-        $ []
-    , mark Point [MTooltip TTEncoding]
-    , encoding
-        . position X [PName xCol,    PmType Quantitative, PAxis [AxTitle xCol]]
-        . position Y [PName "value", PmType Quantitative, PAxis [AxTitle "value"]]
-        . color [MName "series", MmType Nominal]
-        $ []
-    , width  (plotWidth  cfg)
-    , height (plotHeight cfg)
-    ]
-  where
-    xVals = maybe [] V.toList (getDoubleVec xCol df)
-    yData = foldr (\col f -> dataColumn col (Numbers (maybe [] V.toList (getDoubleVec col df))) . f)
-                  id yCols
-
-    dataSpec = dataFromColumns []
-               . dataColumn xCol (Numbers xVals)
-               . yData
-               $ []
-
--- | Render 'scatterMultiY' to a file via 'writeSpec'.
-scatterMultiYFile :: OutputFormat -> FilePath -> PlotConfig -> DXD.DataFrame -> Text -> [Text] -> IO ()
-scatterMultiYFile fmt path cfg df xCol yCols =
-  writeSpec fmt path (scatterMultiY cfg df xCol yCols)
-
--- | Predicted vs Actual diagnostic plot.
-predictedVsActual :: PlotConfig -> [Double] -> [Double] -> VegaLite
-predictedVsActual cfg actuals preds =
-  toVegaLite
-    [ title (plotTitle cfg) []
-    , layer [identityLayer, pointLayer]
-    , width  (plotWidth  cfg)
-    , height (plotHeight cfg)
-    ]
-  where
-    resids = zipWith (-) actuals preds
-    lo     = minimum (actuals ++ preds)
-    hi     = maximum (actuals ++ preds)
-
-    pointLayer = asSpec
-      [ dataFromColumns []
-          . dataColumn "actual"    (Numbers actuals)
-          . dataColumn "predicted" (Numbers preds)
-          . dataColumn "residual"  (Numbers resids)
-          $ []
-      , mark Point [MTooltip TTEncoding]
-      , encoding
-          . position X [PName "actual",    PmType Quantitative, PAxis [AxTitle "Actual"]]
-          . position Y [PName "predicted", PmType Quantitative, PAxis [AxTitle "Predicted"]]
-          $ []
-      ]
-
-    identityLayer = asSpec
-      [ dataFromColumns []
-          . dataColumn "ix" (Numbers [lo, hi])
-          . dataColumn "iy" (Numbers [lo, hi])
-          $ []
-      , mark Line [MColor "gray", MStrokeWidth 1.5, MStrokeDash [6, 4]]
-      , encoding
-          . position X [PName "ix", PmType Quantitative]
-          . position Y [PName "iy", PmType Quantitative]
-          $ []
-      ]
-
--- | Render 'predictedVsActual' to a file via 'writeSpec'.
-predictedVsActualFile :: OutputFormat -> FilePath -> PlotConfig -> [Double] -> [Double] -> IO ()
-predictedVsActualFile fmt path cfg actuals preds =
-  writeSpec fmt path (predictedVsActual cfg actuals preds)
-
--- | Scatter with per-group conditional fitted lines (LME / GLMM).
--- Points are colour-coded by group; one fitted line per group shares the same colour scheme.
--- ptData: (group, x, y) for raw observations
--- lnData: (group, x, ŷ) for smooth conditional fits (grid-evaluated)
-scatterWithGroups
-  :: PlotConfig
-  -> Text
-  -> Text
-  -> [(Text, Double, Double)]
-  -> [(Text, Double, Double)]
-  -> VegaLite
-scatterWithGroups cfg xCol yCol ptData lnData =
-  toVegaLite
-    [ title (plotTitle cfg) []
-    , layer [lineLayer, pointLayer]
-    , width  (plotWidth  cfg)
-    , height (plotHeight cfg)
-    ]
-  where
-    (ptGrps, ptXs, ptYs) = unzip3 ptData
-    (lnGrps, lnXs, lnYs) = unzip3 lnData
-
-    pointLayer = asSpec
-      [ dataFromColumns []
-          . dataColumn xCol    (Numbers ptXs)
-          . dataColumn yCol    (Numbers ptYs)
-          . dataColumn "group" (Strings ptGrps)
-          $ []
-      , mark Point [MTooltip TTEncoding]
-      , encoding
-          . position X [PName xCol,  PmType Quantitative, PAxis [AxTitle xCol]]
-          . position Y [PName yCol,  PmType Quantitative, PAxis [AxTitle yCol]]
-          . color [MName "group", MmType Nominal]
-          $ []
-      ]
-
-    lineLayer = asSpec
-      [ dataFromColumns []
-          . dataColumn xCol     (Numbers lnXs)
-          . dataColumn "fitted" (Numbers lnYs)
-          . dataColumn "group"  (Strings lnGrps)
-          $ []
-      , mark Line [MStrokeWidth 2.0]
-      , encoding
-          . position X [PName xCol,     PmType Quantitative]
-          . position Y [PName "fitted", PmType Quantitative]
-          . color [MName "group", MmType Nominal]
-          $ []
-      ]
-
--- | Render 'scatterWithGroups' to a file via 'writeSpec'.
-scatterWithGroupsFile
-  :: OutputFormat -> FilePath -> PlotConfig -> Text -> Text
-  -> [(Text, Double, Double)] -> [(Text, Double, Double)] -> IO ()
-scatterWithGroupsFile fmt path cfg xCol yCol ptData lnData =
-  writeSpec fmt path (scatterWithGroups cfg xCol yCol ptData lnData)
-
--- ---------------------------------------------------------------------------
--- 130: PlotData ベースの汎用 spec API
--- ---------------------------------------------------------------------------
-
--- | Build a Vega-Lite scatter spec from a 'PlotData' source.
---
--- The third argument is an optional grouping column used for colour
--- encoding. If the column lives in @pdText@ it is treated as nominal,
--- if in @pdNumeric@ as quantitative; if absent, no colour encoding is
--- emitted. 'plotColorScheme' / 'plotFacetColumn' / 'plotLegendPos' on
--- 'PlotConfig' are honoured.
-scatterSpec
-  :: PlotConfig
-  -> (Text, Text)        -- ^ (xCol, yCol)
-  -> Maybe Text          -- ^ optional colour / group column
-  -> PlotData
-  -> VegaLite
-scatterSpec cfg (xCol, yCol) mColor pd =
-  toVegaLite
-    [ title (plotTitle cfg) []
-    , dataSpec
-    , mark Point [MTooltip TTEncoding]
-    , encSpec
-    , width  (plotWidth  cfg)
-    , height (plotHeight cfg)
-    ]
-  where
-    xVals = maybe [] V.toList (numericColumn xCol pd)
-    yVals = maybe [] V.toList (numericColumn yCol pd)
-
-    mColorTextVals = mColor >>= \c -> V.toList <$> textColumn    c pd
-    mColorNumVals  = mColor >>= \c -> V.toList <$> numericColumn c pd
-    mFacetVals     = plotFacetColumn cfg
-                       >>= \c -> V.toList <$> textColumn c pd
-
-    addColorCol cols = case (mColor, mColorTextVals, mColorNumVals) of
-      (Just c, Just txts, _)        -> dataColumn c (Strings txts) cols
-      (Just c, Nothing,   Just nms) -> dataColumn c (Numbers nms)  cols
-      _                             -> cols
-    addFacetCol cols = case (plotFacetColumn cfg, mFacetVals) of
-      (Just c, Just txts) -> dataColumn c (Strings txts) cols
-      _                   -> cols
-
-    dataSpec = dataFromColumns []
-                . dataColumn xCol (Numbers xVals)
-                . dataColumn yCol (Numbers yVals)
-                . addColorCol
-                . addFacetCol
-                $ []
-
-    schemeOpts = case plotColorScheme cfg of
-      Just sch -> [MScale [SScheme sch []]]
-      Nothing  -> []
-    legendOpts = case plotLegendPos cfg of
-      Just "none" -> [MLegend []]
-      Just pos    -> [MLegend [LOrient (parseLegendOrient pos)]]
-      Nothing     -> []
-
-    addColorEnc encs = case (mColor, mColorTextVals, mColorNumVals) of
-      (Just c, Just _, _) ->
-        color ([MName c, MmType Nominal]      ++ schemeOpts ++ legendOpts) encs
-      (Just c, Nothing, Just _) ->
-        color ([MName c, MmType Quantitative] ++ schemeOpts ++ legendOpts) encs
-      _ -> encs
-    addFacetEnc encs = case plotFacetColumn cfg of
-      Just c  -> column [FName c, FmType Nominal] encs
-      Nothing -> encs
-
-    encSpec = encoding
-                . position X [PName xCol, PmType Quantitative, PAxis [AxTitle xCol]]
-                . position Y [PName yCol, PmType Quantitative, PAxis [AxTitle yCol]]
-                . addColorEnc
-                . addFacetEnc
-                $ []
-
-    parseLegendOrient "right"  = LORight
-    parseLegendOrient "left"   = LOLeft
-    parseLegendOrient "top"    = LOTop
-    parseLegendOrient "bottom" = LOBottom
-    parseLegendOrient _        = LORight
diff --git a/src/Hanalyze/Viz/Taguchi.hs b/src/Hanalyze/Viz/Taguchi.hs
deleted file mode 100644
--- a/src/Hanalyze/Viz/Taguchi.hs
+++ /dev/null
@@ -1,261 +0,0 @@
--- |
--- Module      : Hanalyze.Viz.Taguchi
--- Description : 田口メソッド解析結果の HTML レポート (SN 比・主効果・最適水準)
--- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
--- License     : BSD-3-Clause
---
-{-# LANGUAGE OverloadedStrings #-}
--- | HTML report for Taguchi-method analysis.
---
--- Bundles the results of 'Hanalyze.Design.Taguchi.analyzeSN' / @optimalLevels@ /
--- @predictSN@ into a single self-contained HTML file:
---
---   * Summary: array name, SN type, run count, predicted SN.
---   * Per-run SN bar chart.
---   * Per-factor main-effects bars (one bar per level).
---   * Best-level table.
-module Hanalyze.Viz.Taguchi
-  ( TaguchiReport (..)
-  , renderTaguchiReport
-  ) where
-
-import Data.Aeson (encode)
-import Data.ByteString.Lazy (toStrict)
-import Data.Text (Text)
-import qualified Data.Text    as T
-import qualified Data.Text.IO as TIO
-import Data.Text.Encoding (decodeUtf8)
-import Graphics.Vega.VegaLite
-import Text.Printf (printf)
-
-import qualified Hanalyze.Design.Orthogonal as OA
-import qualified Hanalyze.Design.Taguchi    as TG
-import           Hanalyze.Viz.Assets        (vegaJS, vegaLiteJS, vegaEmbedJS)
-
--- ---------------------------------------------------------------------------
--- Report data type
--- ---------------------------------------------------------------------------
-
--- | Inputs needed to render a Taguchi-method HTML report.
-data TaguchiReport = TaguchiReport
-  { trTitle     :: Text                                  -- ^ Report heading.
-  , trArrayName :: Text                                  -- ^ Orthogonal-array
-                                                         --   name (e.g. @\"L9(3^4)\"@).
-  , trSNType    :: TG.SNType                             -- ^ SN-ratio type.
-  , trPerRunSN  :: [Double]                              -- ^ Per-run SN ratios.
-  , trEffects   :: [TG.FactorEffect]                     -- ^ Per-factor effects.
-  , trOptimal   :: [(Text, OA.LevelValue, Double)]       -- ^ Best level per factor.
-  , trPredicted :: Double                                -- ^ Predicted SN ratio.
-  }
-
--- ---------------------------------------------------------------------------
--- Top-level renderer
--- ---------------------------------------------------------------------------
-
--- | Write the rendered HTML report to the given path.
-renderTaguchiReport :: FilePath -> TaguchiReport -> IO ()
-renderTaguchiReport path tr = TIO.writeFile path (buildHtml tr)
-
--- ---------------------------------------------------------------------------
--- HTML
--- ---------------------------------------------------------------------------
-
-buildHtml :: TaguchiReport -> Text
-buildHtml tr = T.unlines
-  [ "<!DOCTYPE html>"
-  , "<html lang=\"ja\">"
-  , "<head>"
-  , "  <meta charset=\"utf-8\">"
-  , "  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
-  , "  <title>" <> trTitle tr <> "</title>"
-  , "  <script>" <> vegaJS      <> "</script>"
-  , "  <script>" <> vegaLiteJS  <> "</script>"
-  , "  <script>" <> vegaEmbedJS <> "</script>"
-  , "  <style>" <> css <> "</style>"
-  , "</head>"
-  , "<body>"
-  , "<header><h1>" <> trTitle tr <> "</h1></header>"
-  , "<main>"
-  , summarySection tr
-  , perRunSection tr
-  , factorEffectsSection tr
-  , optimumSection tr
-  , "</main>"
-  , "<script>" <> embedScript tr <> "</script>"
-  , "</body>"
-  , "</html>"
-  ]
-
--- ---------------------------------------------------------------------------
--- Sections
--- ---------------------------------------------------------------------------
-
-summarySection :: TaguchiReport -> Text
-summarySection tr = T.unlines
-  [ "<section>"
-  , "  <h2>Summary</h2>"
-  , "  <div class=\"stat-grid\">"
-  , statBox "Array"        (trArrayName tr)
-  , statBox "SN type"      (TG.snTypeName (trSNType tr))
-  , statBox "Inner runs"   (T.pack (show (length (trPerRunSN tr))))
-  , statBox "Predicted SN" (T.pack (printf "%.3f dB" (trPredicted tr)))
-  , "  </div>"
-  , "</section>"
-  ]
-  where
-    statBox lbl val = T.unlines
-      [ "  <div class=\"stat-box\">"
-      , "    <div class=\"label\">" <> lbl <> "</div>"
-      , "    <div class=\"value\">" <> val <> "</div>"
-      , "  </div>"
-      ]
-
-perRunSection :: TaguchiReport -> Text
-perRunSection _ = T.unlines
-  [ "<section>"
-  , "  <h2>SN ratio per run</h2>"
-  , "  <div class=\"vl-wrap\"><div id=\"vl-perrun\"></div></div>"
-  , "</section>"
-  ]
-
-factorEffectsSection :: TaguchiReport -> Text
-factorEffectsSection tr = T.unlines
-  [ "<section>"
-  , "  <h2>Factor effects (mean SN per level)</h2>"
-  , "  <div class=\"effects-grid\">"
-  , T.intercalate "\n"
-      [ "    <div class=\"effect-card\">"
-        <> "<h3>" <> TG.feFactor fe <> "</h3>"
-        <> "<div id=\"vl-factor-" <> T.pack (show i) <> "\"></div>"
-        <> "</div>"
-      | (i, fe) <- zip [0::Int ..] (trEffects tr) ]
-  , "  </div>"
-  , "</section>"
-  ]
-
-optimumSection :: TaguchiReport -> Text
-optimumSection tr = T.unlines
-  [ "<section>"
-  , "  <h2>Optimal levels (max mean SN)</h2>"
-  , "  <table>"
-  , "    <thead><tr><th>Factor</th><th>Best level</th><th>Mean SN (dB)</th></tr></thead>"
-  , "    <tbody>"
-  , T.intercalate "\n"
-      [ "      <tr><td>" <> f
-        <> "</td><td>" <> levelToText lvl
-        <> "</td><td>" <> T.pack (printf "%.3f" eta)
-        <> "</td></tr>"
-      | (f, lvl, eta) <- trOptimal tr ]
-  , "    </tbody>"
-  , "  </table>"
-  , "  <p class=\"note\">Predicted SN at this combination "
-    <> "(additive main-effects model): "
-    <> "<strong>" <> T.pack (printf "%.3f dB" (trPredicted tr))
-    <> "</strong></p>"
-  , "</section>"
-  ]
-  where
-    levelToText (OA.LText t) = t
-    levelToText (OA.LNumeric d)
-      | d == fromIntegral (round d :: Integer) = T.pack (show (round d :: Integer))
-      | otherwise                              = T.pack (printf "%g" d)
-
--- ---------------------------------------------------------------------------
--- Vega-Lite specs (embedded as JS)
--- ---------------------------------------------------------------------------
-
-embedScript :: TaguchiReport -> Text
-embedScript tr =
-  let perRunJSON = vlJson (perRunSpec tr)
-      effectsJS  = T.intercalate "\n"
-        [ "vegaEmbed('#vl-factor-" <> T.pack (show i) <> "', "
-          <> vlJson (factorSpec fe) <> ", {actions:false});"
-        | (i, fe) <- zip [0::Int ..] (trEffects tr) ]
-  in T.unlines
-       [ "vegaEmbed('#vl-perrun', " <> perRunJSON <> ", {actions:false});"
-       , effectsJS
-       ]
-
-vlJson :: VegaLite -> Text
-vlJson = decodeUtf8 . toStrict . encode . fromVL
-
--- | Per-run SN-ratio bar chart.
-perRunSpec :: TaguchiReport -> VegaLite
-perRunSpec tr =
-  let n = length (trPerRunSN tr)
-      runs = [ T.pack (show (i :: Int)) | i <- [1 .. n] ]
-  in toVegaLite
-       [ dataFromColumns []
-           . dataColumn "Run" (Strings runs)
-           . dataColumn "SN"  (Numbers (trPerRunSN tr))
-           $ []
-       , mark Bar [MColor "#4C72B0", MOpacity 0.85]
-       , encoding
-           . position X [PName "Run", PmType Ordinal,
-                         PAxis [AxTitle "Inner Run"], PSort []]
-           . position Y [PName "SN",  PmType Quantitative,
-                         PAxis [AxTitle "SN ratio (dB)"]]
-           $ []
-       , width 600
-       , height 220
-       ]
-
--- | Bar chart of per-level SN ratio for a single factor.
-factorSpec :: TG.FactorEffect -> VegaLite
-factorSpec fe =
-  let lvls = map levelToShort (TG.feLevels fe)
-      sns  = TG.feSNByLevel fe
-  in toVegaLite
-       [ dataFromColumns []
-           . dataColumn "level" (Strings lvls)
-           . dataColumn "SN"    (Numbers sns)
-           $ []
-       , mark Bar [MColor "#DD7755", MOpacity 0.85]
-       , encoding
-           . position X [PName "level", PmType Nominal,
-                         PAxis [AxTitle "Level", AxLabelAngle 0],
-                         PSort []]
-           . position Y [PName "SN", PmType Quantitative,
-                         PAxis [AxTitle "Mean SN (dB)"]]
-           $ []
-       , width 240
-       , height 180
-       ]
-  where
-    levelToShort (OA.LText t) = t
-    levelToShort (OA.LNumeric d)
-      | d == fromIntegral (round d :: Integer) = T.pack (show (round d :: Integer))
-      | otherwise                              = T.pack (printf "%g" d)
-
--- ---------------------------------------------------------------------------
--- CSS
--- ---------------------------------------------------------------------------
-
-css :: Text
-css = T.unlines
-  [ "* { box-sizing: border-box; margin: 0; padding: 0; }"
-  , "body { font-family: 'Segoe UI', sans-serif; background: #f0f2f5; color: #333; }"
-  , "header { background: #2c3e50; color: #ecf0f1; padding: 18px 30px; }"
-  , "header h1 { font-size: 1.2em; font-weight: 600; }"
-  , "main { max-width: 1100px; margin: 0 auto; padding: 30px 20px; }"
-  , "section { background: white; border-radius: 10px; padding: 24px;"
-  , "          margin-bottom: 28px; box-shadow: 0 2px 8px rgba(0,0,0,.08); }"
-  , "h2 { font-size: 1.1em; color: #2c3e50; margin-bottom: 16px;"
-  , "     border-bottom: 2px solid #e8ecf0; padding-bottom: 8px; }"
-  , "h3 { font-size: .95em; color: #555; margin-bottom: 8px; }"
-  , ".stat-grid { display: flex; gap: 16px; flex-wrap: wrap; }"
-  , ".stat-box { background: #f8f9fa; border-radius: 8px; padding: 14px 20px;"
-  , "            min-width: 160px; text-align: center; }"
-  , ".stat-box .label { font-size: .75em; color: #888; text-transform: uppercase; }"
-  , ".stat-box .value { font-size: 1.25em; font-weight: 600; color: #2c3e50; margin-top: 4px; }"
-  , ".effects-grid { display: flex; flex-wrap: wrap; gap: 16px; }"
-  , ".effect-card { flex: 1 1 260px; min-width: 260px; }"
-  , "table { width: 100%; border-collapse: collapse; font-size: .9em; }"
-  , "th { background: #f0f2f5; text-align: right; padding: 8px 14px;"
-  , "     font-weight: 600; color: #555; }"
-  , "th:first-child { text-align: left; }"
-  , "td { padding: 7px 14px; border-bottom: 1px solid #f0f2f5; text-align: right; }"
-  , "td:first-child { text-align: left; font-family: monospace; }"
-  , ".vl-wrap { overflow-x: auto; }"
-  , ".note { margin-top: 14px; font-size: .9em; color: #666; }"
-  ]
diff --git a/test/Hanalyze/Design/WorkflowSpec.hs b/test/Hanalyze/Design/WorkflowSpec.hs
--- a/test/Hanalyze/Design/WorkflowSpec.hs
+++ b/test/Hanalyze/Design/WorkflowSpec.hs
@@ -1,11 +1,10 @@
-{-# LANGUAGE CPP               #-}
 {-# LANGUAGE OverloadedStrings #-}
 -- | Phase 78.A/B: DOE ワークフロー (設計オブジェクト + runsheet + designModel)。
 --
--- 大半は standalone (flag plot-integration off・upstream portable) で build/run できるが、
--- 一部の診断テスト (tracesOf / MultiVarModel の事後予測帯) は plot 連携層
--- (Hanalyze.Plot.*) に依存するため @PLOT_INTEGRATION@ CPP で囲む
--- (= flag plot-integration on のときだけ compile)。
+-- standalone (upstream portable) で build/run できるテストのみを置く。
+-- plot 連携層 (Hanalyze.Plot.*) 依存の診断テスト (tracesOf /
+-- MultiVarModel の事後予測帯) は Phase 106.4 で hanalyze-plot-test
+-- (hanalyze-plot/test-plot/Spec.hs) へ移行した。
 module Hanalyze.Design.WorkflowSpec (spec) where
 
 import qualified Data.Text as T
@@ -22,17 +21,8 @@
 import           Test.Hspec
 import           Hanalyze.Design.Workflow
 import           Hanalyze.Fit             (designModel, designModelGP, defaultGP, gpMulti, GPConfig (..), (|->), ranIntercept, ranSlope, designHBMProgram, designModelHBM, DesignHBMFit (..), multiOutput, modelFor)
-#ifdef PLOT_INTEGRATION
-import           Hanalyze.Plot.Bayes      (tracesOf)
-#endif
 import           Hanalyze.Model.HBM       (sampleNames, ModelP)
 import           Hanalyze.Model.Wrappers  (MultiLMModel (..), GPRegModelN (..), GPMethod (..), HyperStrategy (..), defaultHBM, HBMConfig (..))
-#ifdef PLOT_INTEGRATION
--- ModelFrame/VarRole は MultiVarModel 事後予測帯テスト (guarded) 専用ゆえ一緒に囲む。
-import           Hanalyze.Model.Formula.Frame (ModelFrame (..), VarRole (..))
-import           Hanalyze.Plot.Core       (MultiVarModel (..))
-import           Hanalyze.Plot.ML         ()  -- instance MultiVarModel DesignHBMFit
-#endif
 import           Hanalyze.Model.GP        (GPParams (..), Kernel (..))
 import           Hanalyze.Model.Core      (rSquared1)
 import           Hanalyze.Model.Formula.RFormula (parseRFormula)
@@ -191,23 +181,6 @@
           bTemp = mean0 (drawsFor "temp" m)
       bTemp `shouldSatisfy` (\b -> abs (b - 3) < 1.0)
 
-#ifdef PLOT_INTEGRATION
-    -- Phase 78.J: designModelHBM の学習済 HBM を dhfModel で露出し、 診断抽出子
-    -- (tracesOf / dagOf 等) に渡せる (DesignHBMFit から診断が出せる)。
-    -- ※ tracesOf は plot 連携層 (Hanalyze.Plot.Bayes)・plot-integration 限定。
-    it "designModelHBM: dhfModel で診断 (tracesOf) が出せる" $ do
-      let temps  = [-1, 1, -1, 1, -1, 1, -1, 1] :: [Double]
-          lots   = ["A","A","A","A","B","B","B","B"] :: [T.Text]
-          ys     = [ 2 + 3 * t + (if l == "A" then -1 else 1) | (t, l) <- zip temps lots ]
-          df     = DX.insertColumn "lot"  (DX.fromList lots)
-                 $ DX.insertColumn "temp" (DX.fromList temps)
-                 $ DX.insertColumn "y"    (DX.fromList ys)
-                 $ DX.empty
-          plan   = factorialDesign [contFactor "temp" (-1, 1)]
-          m      = df |-> designModelHBM defaultHBM plan [ranIntercept "lot"] "y"
-      length (tracesOf (dhfModel m)) `shouldSatisfy` (> 0)   -- param ごとの trace が出る
-#endif
-
     -- Phase 78.G-f 回帰: NA 行 drop と群 idx の行ずれ (code review 指摘)。
     -- 'modelFrame' (DropRows) は formula 関与列 (temp/y) に NA を含む行を落として
     -- designX/ys を作るが、 群 idx (prepRE) が raw df (NA 除去前・行数不変) から
@@ -314,40 +287,6 @@
         (mkFrameP (\l -> if l=="A" then 5 else 1)
            |-> designModelHBM (tinyCfg 100) planP [ranSlope ["temp"] "lot"] "y")
       True `shouldBe` True
-
-#ifdef PLOT_INTEGRATION
-    -- Phase 78.G-f Task 4: profiler/contour が designModelHBM に載る事後予測帯。
-    -- designMatrixF (dhfFormula m) ef を直接叩き、 fit 時と同じ列順の設計行列を作る
-    -- ( evalFrameAt 相当の helper は無いので eval ModelFrame を手組みする)。
-    -- ★mfParams (合成パラメータ名 "_p0"/"_p1"…) は formula 内部表現なので手で推測せず、
-    --   訓練済 'dhfFrame' (= mvFrame m) を土台に mfRoles/mfNRows だけ差し替える
-    --   (本番の Core.hs evalFrame と同じ据え置き方)。
-    -- ※ MultiVarModel (mvFrame/mvEvalFrame) は plot 連携層・plot-integration 限定。
-    it "MultiVarModel DesignHBMFit は事後予測帯を返す" $ do
-      let temps  = [-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1] :: [Double]
-          lots   = ["A","A","A","A","A","A","B","B","B","B","B","B"] :: [T.Text]
-          noise  = [0.05, -0.03, 0.02, -0.04, 0.01, -0.02, 0.03, -0.01, 0.04, -0.05, 0.02, -0.03]
-          lotShift l = if l == "A" then (-1.0) else 1.0
-          ys     = [ 2 + 3 * t + lotShift l + e
-                   | (t, l, e) <- zip3 temps lots noise ]
-          mkFrameHBM = DX.insertColumn "lot"  (DX.fromList lots)
-                     $ DX.insertColumn "temp" (DX.fromList temps)
-                     $ DX.insertColumn "y"    (DX.fromList ys)
-                     $ DX.empty
-          plan = factorialDesign [contFactor "temp" (-1, 1)]
-          m    = mkFrameHBM |-> designModelHBM defaultHBM plan [ranIntercept "lot"] "y"
-          ef   = (mvFrame m)
-                   { mfRoles  = [ ("y",    RoleResponse (V.fromList [0, 0, 0]))
-                                , ("temp", RoleContinuous (V.fromList [-1, 0, 1]))
-                                ]
-                   , mfNRows  = 3
-                   }
-          (mu, band) = mvEvalFrame m 0.95 ef
-      length mu `shouldBe` 3
-      band `shouldSatisfy` isJust
-      -- 中心 μ は temp とともに増加 (真の傾き ≈ 3)。
-      (last mu - head mu) `shouldSatisfy` (> 3)
-#endif
 
     -- Phase 78.G-f Task 5: multiOutput が designModelHBM とも対称に使えること
     -- (LM/GP と同じ「カレー化 spec (Text -> spec) を渡せば複数応答へ一括適用」 が
